diff --git a/.changeset/sync-sdk-with-api.md b/.changeset/sync-sdk-with-api.md new file mode 100644 index 0000000..89ee1b4 --- /dev/null +++ b/.changeset/sync-sdk-with-api.md @@ -0,0 +1,5 @@ +--- +'scope3': major +--- + +Sync SDK with current OpenAPI specification. Removes stale storefront-only and deprecated resources (agents, billing/storefront, inventory-sources, notifications, readiness, bundles, signals, sales-agents, conversion-events, creative-sets). Adds all missing buyer endpoints (discovery, accounts, planning-briefs, moderation, storefronts, audit-logs, notification-preferences). Replaces typed campaign creation methods with generic create/update/delete. Fixes schema generation regex escaping bug. diff --git a/package.json b/package.json index 25be53b..b0bfb58 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "scope3", - "version": "2.1.0", + "version": "3.0.0", "description": "Scope3 SDK - REST and MCP client for the Agentic Platform", "engines": { "node": ">=18" diff --git a/scripts/generate-schemas.ts b/scripts/generate-schemas.ts index 73f4d4f..cdcb195 100644 --- a/scripts/generate-schemas.ts +++ b/scripts/generate-schemas.ts @@ -71,6 +71,17 @@ function postProcessSchemas(filePath: string) { content = content.replace(/\\&/g, '&'); + // Fix regex literals that break Prettier's parser. + // openapi-zod-client double-escapes backslashes before slashes (e.g. \\/) which Prettier + // interprets as "backslash literal" + "end of regex". Replace \\/ with \/ inside .regex() calls. + content = content.replace( + /\.regex\(\/([^)]+)\/([gimsuy]*)\)/g, + (_match, body: string, flags: string) => { + const fixed = body.replace(/\\\\\//g, '\\/'); + return `.regex(/${fixed}/${flags})`; + } + ); + // Normalize schema names to PascalCase const nameRenames: Array<[RegExp, string]> = []; const namePattern = /^const ([a-z]\w*_\w+|[a-z]\w+) = /gm; diff --git a/src/__tests__/cli/commands/storefront.test.ts b/src/__tests__/cli/commands/storefront.test.ts index 55786b4..0106a37 100644 --- a/src/__tests__/cli/commands/storefront.test.ts +++ b/src/__tests__/cli/commands/storefront.test.ts @@ -1,158 +1,3 @@ -import { Command } from 'commander'; -import { storefrontCommand } from '../../../cli/commands/storefront'; -import * as utils from '../../../cli/utils'; -import * as format from '../../../cli/format'; - -jest.mock('../../../cli/utils'); -jest.mock('../../../cli/format'); - -const mockCreateClient = utils.createClient as jest.MockedFunction; -const mockFormatOutput = format.formatOutput as jest.MockedFunction; -const mockPrintError = format.printError as jest.MockedFunction; -const mockPrintSuccess = format.printSuccess as jest.MockedFunction; - -describe('storefront command', () => { - let program: Command; - let mockClient: { - storefront: { - get: jest.Mock; - create: jest.Mock; - update: jest.Mock; - delete: jest.Mock; - }; - agents: { - list: jest.Mock; - get: jest.Mock; - }; - }; - const originalExit = process.exit; - - beforeEach(() => { - mockClient = { - storefront: { - get: jest.fn(), - create: jest.fn(), - update: jest.fn(), - delete: jest.fn(), - }, - agents: { - list: jest.fn(), - get: jest.fn(), - }, - }; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - mockCreateClient.mockReturnValue(mockClient as any); - process.exit = jest.fn() as never; - - program = new Command(); - program.option('--api-key ', 'API key'); - program.option('--format ', 'Output format', 'table'); - program.option('--persona ', 'Persona', 'storefront'); - program.addCommand(storefrontCommand); - }); - - afterEach(() => { - jest.restoreAllMocks(); - process.exit = originalExit; - }); - - describe('get', () => { - it('should call client.storefront.get', async () => { - mockClient.storefront.get.mockResolvedValue({ - data: { platformId: 'plat-1', name: 'Test' }, - }); - - await program.parseAsync(['node', 'test', 'storefront', 'get', '--api-key', 'sk-test']); - - expect(mockClient.storefront.get).toHaveBeenCalled(); - expect(mockFormatOutput).toHaveBeenCalled(); - }); - }); - - describe('create', () => { - it('should call client.storefront.create with required args', async () => { - mockClient.storefront.create.mockResolvedValue({ - data: { platformId: 'plat-1', name: 'My Store' }, - }); - - await program.parseAsync([ - 'node', - 'test', - 'storefront', - 'create', - '--api-key', - 'sk-test', - '--platform-id', - 'plat-1', - '--name', - 'My Store', - ]); - - expect(mockClient.storefront.create).toHaveBeenCalledWith( - expect.objectContaining({ - platformId: 'plat-1', - name: 'My Store', - }) - ); - expect(mockPrintSuccess).toHaveBeenCalled(); - }); - }); - - describe('delete', () => { - it('should call client.storefront.delete', async () => { - mockClient.storefront.delete.mockResolvedValue(undefined); - - await program.parseAsync(['node', 'test', 'storefront', 'delete', '--api-key', 'sk-test']); - - expect(mockClient.storefront.delete).toHaveBeenCalled(); - expect(mockPrintSuccess).toHaveBeenCalled(); - }); - }); - - describe('error handling', () => { - it('should print error and exit on failure', async () => { - mockClient.storefront.get.mockRejectedValue(new Error('Unauthorized')); - - await program.parseAsync(['node', 'test', 'storefront', 'get', '--api-key', 'sk-test']); - - expect(mockPrintError).toHaveBeenCalledWith('Unauthorized'); - expect(process.exit).toHaveBeenCalledWith(1); - }); - }); - - describe('agents subcommand', () => { - it('should call client.agents.list', async () => { - mockClient.agents.list.mockResolvedValue({ data: [] }); - - await program.parseAsync([ - 'node', - 'test', - 'storefront', - 'agents', - 'list', - '--api-key', - 'sk-test', - ]); - - expect(mockClient.agents.list).toHaveBeenCalled(); - expect(mockFormatOutput).toHaveBeenCalled(); - }); - - it('should call client.agents.get with agentId', async () => { - mockClient.agents.get.mockResolvedValue({ data: { id: 'agent-1' } }); - - await program.parseAsync([ - 'node', - 'test', - 'storefront', - 'agents', - 'get', - 'agent-1', - '--api-key', - 'sk-test', - ]); - - expect(mockClient.agents.get).toHaveBeenCalledWith('agent-1'); - }); - }); +describe('removed', () => { + it.todo('resource removed in v3'); }); diff --git a/src/__tests__/client.test.ts b/src/__tests__/client.test.ts index bc5fd1a..a790509 100644 --- a/src/__tests__/client.test.ts +++ b/src/__tests__/client.test.ts @@ -3,10 +3,7 @@ */ import { Scope3Client } from '../client'; -import { ConversionEventsResource } from '../resources/conversion-events'; -import { CreativeSetsResource } from '../resources/creative-sets'; import { TestCohortsResource } from '../resources/test-cohorts'; -import { BundleProductsResource } from '../resources/products'; import { TasksResource } from '../resources/tasks'; import { PropertyListChecksResource } from '../resources/property-lists'; import { EventSourcesResource } from '../resources/event-sources'; @@ -16,6 +13,14 @@ import { AudiencesResource } from '../resources/audiences'; import { SyndicationResource } from '../resources/syndication'; import { PropertyListsResource } from '../resources/property-lists'; import { CreativesResource } from '../resources/creatives'; +import { DiscoveryResource } from '../resources/discovery'; +import { AccountsResource } from '../resources/accounts'; +import { NotificationPreferencesResource } from '../resources/notification-preferences'; +import { ModerationResource } from '../resources/moderation'; +import { StorefrontsResource } from '../resources/storefronts'; +import { AuditLogsResource } from '../resources/audit-logs'; +import { PlanningBriefsResource } from '../resources/planning-briefs'; +import { BuyerBillingResource } from '../resources/billing'; jest.mock('../skill', () => ({ fetchSkillMd: jest.fn(), @@ -97,8 +102,13 @@ describe('Scope3Client', () => { }); describe('buyer persona resources', () => { + let client: Scope3Client; + + beforeEach(() => { + client = new Scope3Client({ apiKey: 'test-key', persona: 'buyer' }); + }); + it('should have advertisers resource', () => { - const client = new Scope3Client({ apiKey: 'test-key', persona: 'buyer' }); expect(client.advertisers).toBeDefined(); expect(typeof client.advertisers.list).toBe('function'); expect(typeof client.advertisers.get).toBe('function'); @@ -107,178 +117,73 @@ describe('Scope3Client', () => { expect(typeof client.advertisers.delete).toBe('function'); }); - it('should have campaigns resource with type-specific methods', () => { - const client = new Scope3Client({ apiKey: 'test-key', persona: 'buyer' }); + it('should have campaigns resource with generic CRUD and actions', () => { expect(client.campaigns).toBeDefined(); expect(typeof client.campaigns.list).toBe('function'); expect(typeof client.campaigns.get).toBe('function'); - expect(typeof client.campaigns.createDiscovery).toBe('function'); - expect(typeof client.campaigns.updateDiscovery).toBe('function'); - expect(typeof client.campaigns.createPerformance).toBe('function'); - expect(typeof client.campaigns.updatePerformance).toBe('function'); - expect(typeof client.campaigns.createAudience).toBe('function'); + expect(typeof client.campaigns.create).toBe('function'); + expect(typeof client.campaigns.update).toBe('function'); + expect(typeof client.campaigns.delete).toBe('function'); expect(typeof client.campaigns.execute).toBe('function'); expect(typeof client.campaigns.pause).toBe('function'); }); - it('should have bundles resource', () => { - const client = new Scope3Client({ apiKey: 'test-key', persona: 'buyer' }); - expect(client.bundles).toBeDefined(); - expect(typeof client.bundles.create).toBe('function'); - expect(typeof client.bundles.discoverProducts).toBe('function'); - expect(typeof client.bundles.browseProducts).toBe('function'); - expect(typeof client.bundles.products).toBe('function'); - }); - - it('should return products resource for bundle with list, add, remove', () => { - const client = new Scope3Client({ apiKey: 'test-key', persona: 'buyer' }); - const products = client.bundles.products('bundle-123'); - expect(products).toBeDefined(); - expect(typeof products.list).toBe('function'); - expect(typeof products.add).toBe('function'); - expect(typeof products.remove).toBe('function'); - }); - - it('should have signals resource', () => { - const client = new Scope3Client({ apiKey: 'test-key', persona: 'buyer' }); - expect(client.signals).toBeDefined(); - expect(typeof client.signals.discover).toBe('function'); - expect(typeof client.signals.list).toBe('function'); - }); - it('should have reporting resource', () => { - const client = new Scope3Client({ apiKey: 'test-key', persona: 'buyer' }); expect(client.reporting).toBeDefined(); expect(typeof client.reporting.get).toBe('function'); }); - it('should have salesAgents resource', () => { - const client = new Scope3Client({ apiKey: 'test-key', persona: 'buyer' }); - expect(client.salesAgents).toBeDefined(); - expect(typeof client.salesAgents.list).toBe('function'); - expect(typeof client.salesAgents.registerAccount).toBe('function'); - }); - it('should have tasks resource', () => { - const client = new Scope3Client({ apiKey: 'test-key', persona: 'buyer' }); expect(client.tasks).toBeDefined(); expect(client.tasks).toBeInstanceOf(TasksResource); expect(typeof client.tasks.get).toBe('function'); }); it('should have propertyListChecks resource', () => { - const client = new Scope3Client({ apiKey: 'test-key', persona: 'buyer' }); expect(client.propertyListChecks).toBeDefined(); expect(client.propertyListChecks).toBeInstanceOf(PropertyListChecksResource); expect(typeof client.propertyListChecks.check).toBe('function'); expect(typeof client.propertyListChecks.getReport).toBe('function'); }); - }); - describe('storefront persona resources', () => { - it('should have storefront resource', () => { - const client = new Scope3Client({ apiKey: 'test-key', persona: 'storefront' }); - expect(client.storefront).toBeDefined(); - expect(typeof client.storefront.get).toBe('function'); - expect(typeof client.storefront.create).toBe('function'); - expect(typeof client.storefront.update).toBe('function'); - expect(typeof client.storefront.delete).toBe('function'); + it('should have discovery resource', () => { + expect(client.discovery).toBeDefined(); + expect(client.discovery).toBeInstanceOf(DiscoveryResource); }); - it('should have inventorySources resource', () => { - const client = new Scope3Client({ apiKey: 'test-key', persona: 'storefront' }); - expect(client.inventorySources).toBeDefined(); - expect(typeof client.inventorySources.list).toBe('function'); - expect(typeof client.inventorySources.get).toBe('function'); - expect(typeof client.inventorySources.create).toBe('function'); - expect(typeof client.inventorySources.update).toBe('function'); - expect(typeof client.inventorySources.delete).toBe('function'); + it('should have accounts resource', () => { + expect(client.accounts).toBeDefined(); + expect(client.accounts).toBeInstanceOf(AccountsResource); }); - it('should have agents resource', () => { - const client = new Scope3Client({ apiKey: 'test-key', persona: 'storefront' }); - expect(client.agents).toBeDefined(); - expect(typeof client.agents.list).toBe('function'); - expect(typeof client.agents.get).toBe('function'); - expect(typeof client.agents.update).toBe('function'); + it('should have notificationPreferences resource', () => { + expect(client.notificationPreferences).toBeDefined(); + expect(client.notificationPreferences).toBeInstanceOf(NotificationPreferencesResource); }); - it('should have readiness resource', () => { - const client = new Scope3Client({ apiKey: 'test-key', persona: 'storefront' }); - expect(client.readiness).toBeDefined(); - expect(typeof client.readiness.check).toBe('function'); + it('should have moderation resource', () => { + expect(client.moderation).toBeDefined(); + expect(client.moderation).toBeInstanceOf(ModerationResource); }); - it('should have billing resource', () => { - const client = new Scope3Client({ apiKey: 'test-key', persona: 'storefront' }); - expect(client.billing).toBeDefined(); - expect(typeof client.billing.get).toBe('function'); - expect(typeof client.billing.connect).toBe('function'); - expect(typeof client.billing.status).toBe('function'); - expect(typeof client.billing.transactions).toBe('function'); - expect(typeof client.billing.payouts).toBe('function'); - expect(typeof client.billing.onboardingUrl).toBe('function'); - }); - - it('should have notifications resource', () => { - const client = new Scope3Client({ apiKey: 'test-key', persona: 'storefront' }); - expect(client.notifications).toBeDefined(); - expect(typeof client.notifications.list).toBe('function'); - expect(typeof client.notifications.markAsRead).toBe('function'); - expect(typeof client.notifications.acknowledge).toBe('function'); - expect(typeof client.notifications.markAllAsRead).toBe('function'); - }); - - it('should NOT have advertisers', () => { - const client = new Scope3Client({ apiKey: 'test-key', persona: 'storefront' }); - expect(() => client.advertisers).toThrow( - 'advertisers is only available with the buyer persona' - ); + it('should have storefronts resource', () => { + expect(client.storefronts).toBeDefined(); + expect(client.storefronts).toBeInstanceOf(StorefrontsResource); }); - it('should NOT have campaigns', () => { - const client = new Scope3Client({ apiKey: 'test-key', persona: 'storefront' }); - expect(() => client.campaigns).toThrow('campaigns is only available with the buyer persona'); - }); - }); - - describe('buyer persona cannot access storefront resources', () => { - it('should throw when accessing storefront', () => { - const client = new Scope3Client({ apiKey: 'test-key', persona: 'buyer' }); - expect(() => client.storefront).toThrow( - 'storefront is only available with the storefront persona' - ); + it('should have auditLogs resource', () => { + expect(client.auditLogs).toBeDefined(); + expect(client.auditLogs).toBeInstanceOf(AuditLogsResource); }); - it('should throw when accessing inventorySources', () => { - const client = new Scope3Client({ apiKey: 'test-key', persona: 'buyer' }); - expect(() => client.inventorySources).toThrow( - 'inventorySources is only available with the storefront persona' - ); + it('should have planningBriefs resource', () => { + expect(client.planningBriefs).toBeDefined(); + expect(client.planningBriefs).toBeInstanceOf(PlanningBriefsResource); }); - it('should throw when accessing agents', () => { - const client = new Scope3Client({ apiKey: 'test-key', persona: 'buyer' }); - expect(() => client.agents).toThrow('agents is only available with the storefront persona'); - }); - - it('should throw when accessing readiness', () => { - const client = new Scope3Client({ apiKey: 'test-key', persona: 'buyer' }); - expect(() => client.readiness).toThrow( - 'readiness is only available with the storefront persona' - ); - }); - - it('should throw when accessing billing', () => { - const client = new Scope3Client({ apiKey: 'test-key', persona: 'buyer' }); - expect(() => client.billing).toThrow('billing is only available with the storefront persona'); - }); - - it('should throw when accessing notifications', () => { - const client = new Scope3Client({ apiKey: 'test-key', persona: 'buyer' }); - expect(() => client.notifications).toThrow( - 'notifications is only available with the storefront persona' - ); + it('should have billing resource', () => { + expect(client.billing).toBeDefined(); + expect(client.billing).toBeInstanceOf(BuyerBillingResource); }); }); @@ -377,7 +282,6 @@ describe('Scope3Client', () => { await expect(client.getSkill()).rejects.toThrow('Network error'); - // After error, cache should be cleared — next call should retry mockFetchSkillMd.mockResolvedValue('recovered markdown'); mockParseSkillMd.mockReturnValue(fakeParsed); @@ -387,18 +291,6 @@ describe('Scope3Client', () => { expect(result).toEqual(fakeParsed); }); - it('should pass correct params for storefront persona', async () => { - const sfClient = new Scope3Client({ apiKey: 'test-key', persona: 'storefront' }); - mockFetchSkillMd.mockResolvedValue('markdown'); - mockParseSkillMd.mockReturnValue({ ...fakeParsed, name: 'scope3-agentic-storefront' }); - - await sfClient.getSkill(); - - expect(mockFetchSkillMd).toHaveBeenCalledWith( - expect.objectContaining({ persona: 'storefront' }) - ); - }); - it('should pass correct params for custom version', async () => { const v1Client = new Scope3Client({ apiKey: 'test-key', persona: 'buyer', version: 'v1' }); mockFetchSkillMd.mockResolvedValue('markdown'); @@ -436,32 +328,6 @@ describe('Scope3Client', () => { client = new Scope3Client({ apiKey: 'test-key', persona: 'buyer' }); }); - it('conversionEvents() returns a ConversionEventsResource', () => { - const resource = client.advertisers.conversionEvents('adv-123'); - expect(resource).toBeInstanceOf(ConversionEventsResource); - }); - - it('conversionEvents() has list, get, create, update methods', () => { - const resource = client.advertisers.conversionEvents('adv-123'); - expect(typeof resource.list).toBe('function'); - expect(typeof resource.get).toBe('function'); - expect(typeof resource.create).toBe('function'); - expect(typeof resource.update).toBe('function'); - }); - - it('creativeSets() returns a CreativeSetsResource', () => { - const resource = client.advertisers.creativeSets('adv-456'); - expect(resource).toBeInstanceOf(CreativeSetsResource); - }); - - it('creativeSets() has list, create, addAsset, removeAsset methods', () => { - const resource = client.advertisers.creativeSets('adv-456'); - expect(typeof resource.list).toBe('function'); - expect(typeof resource.create).toBe('function'); - expect(typeof resource.addAsset).toBe('function'); - expect(typeof resource.removeAsset).toBe('function'); - }); - it('testCohorts() returns a TestCohortsResource', () => { const resource = client.advertisers.testCohorts('adv-789'); expect(resource).toBeInstanceOf(TestCohortsResource); @@ -474,14 +340,8 @@ describe('Scope3Client', () => { }); it('returns a new resource instance each call (not cached)', () => { - const a = client.advertisers.conversionEvents('adv-123'); - const b = client.advertisers.conversionEvents('adv-123'); - expect(a).not.toBe(b); - }); - - it('returns different resources for different advertiser IDs', () => { - const a = client.advertisers.conversionEvents('adv-1'); - const b = client.advertisers.conversionEvents('adv-2'); + const a = client.advertisers.testCohorts('adv-123'); + const b = client.advertisers.testCohorts('adv-123'); expect(a).not.toBe(b); }); @@ -542,47 +402,5 @@ describe('Scope3Client', () => { expect(a).not.toBe(b); }); }); - - describe('bundles sub-resources', () => { - let client: Scope3Client; - - beforeEach(() => { - client = new Scope3Client({ apiKey: 'test-key', persona: 'buyer' }); - }); - - it('products() returns a BundleProductsResource', () => { - const resource = client.bundles.products('bundle-123'); - expect(resource).toBeInstanceOf(BundleProductsResource); - }); - - it('products() has list, add, remove methods', () => { - const resource = client.bundles.products('bundle-123'); - expect(typeof resource.list).toBe('function'); - expect(typeof resource.add).toBe('function'); - expect(typeof resource.remove).toBe('function'); - }); - - it('returns a new resource instance each call', () => { - const a = client.bundles.products('bundle-123'); - const b = client.bundles.products('bundle-123'); - expect(a).not.toBe(b); - }); - }); - - describe('storefront persona cannot access buyer sub-resources', () => { - it('should throw when accessing advertisers sub-resources', () => { - const client = new Scope3Client({ apiKey: 'test-key', persona: 'storefront' }); - expect(() => client.advertisers.conversionEvents('adv-1')).toThrow( - 'advertisers is only available with the buyer persona' - ); - }); - - it('should throw when accessing bundles sub-resources', () => { - const client = new Scope3Client({ apiKey: 'test-key', persona: 'storefront' }); - expect(() => client.bundles.products('bundle-1')).toThrow( - 'bundles is only available with the buyer persona' - ); - }); - }); }); }); diff --git a/src/__tests__/resources/advertisers.test.ts b/src/__tests__/resources/advertisers.test.ts index c5653e6..8b95646 100644 --- a/src/__tests__/resources/advertisers.test.ts +++ b/src/__tests__/resources/advertisers.test.ts @@ -4,6 +4,13 @@ import { AdvertisersResource } from '../../resources/advertisers'; import type { BaseAdapter } from '../../adapters/base'; +import { TestCohortsResource } from '../../resources/test-cohorts'; +import { EventSourcesResource } from '../../resources/event-sources'; +import { MeasurementDataResource } from '../../resources/measurement-data'; +import { CatalogsResource } from '../../resources/catalogs'; +import { AudiencesResource } from '../../resources/audiences'; +import { SyndicationResource } from '../../resources/syndication'; +import { PropertyListsResource } from '../../resources/property-lists'; describe('AdvertisersResource', () => { let mockAdapter: jest.Mocked; @@ -101,20 +108,81 @@ describe('AdvertisersResource', () => { }); }); - describe('sub-resources', () => { - it('should return conversionEvents resource for advertiser', () => { - const convEvents = resource.conversionEvents('adv-123'); - expect(convEvents).toBeDefined(); + describe('validateDataDeliveryCredential', () => { + it('should call adapter with correct path', async () => { + mockAdapter.request.mockResolvedValue({ data: { valid: true } }); + + await resource.validateDataDeliveryCredential('adv-123', 'my-cred'); + + expect(mockAdapter.request).toHaveBeenCalledWith( + 'POST', + '/advertisers/adv-123/data-delivery-credentials/my-cred/validate' + ); + }); + }); + + describe('listAvailableAccounts', () => { + it('should call adapter with correct path', async () => { + mockAdapter.request.mockResolvedValue({ data: [] }); + + await resource.listAvailableAccounts('adv-123'); + + expect(mockAdapter.request).toHaveBeenCalledWith( + 'GET', + '/advertisers/adv-123/accounts/available' + ); }); + }); + + describe('updateAccountReportingBucket', () => { + it('should call adapter with correct path and body', async () => { + const data = { bucket: 's3://reports' }; + mockAdapter.request.mockResolvedValue({ data: {} }); + + await resource.updateAccountReportingBucket('adv-123', 'link-456', data); - it('should return creativeSets resource for advertiser', () => { - const creativeSets = resource.creativeSets('adv-123'); - expect(creativeSets).toBeDefined(); + expect(mockAdapter.request).toHaveBeenCalledWith( + 'PUT', + '/advertisers/adv-123/accounts/link-456/reporting-bucket', + data + ); }); + }); + describe('sub-resources', () => { it('should return testCohorts resource for advertiser', () => { const testCohorts = resource.testCohorts('adv-123'); - expect(testCohorts).toBeDefined(); + expect(testCohorts).toBeInstanceOf(TestCohortsResource); + }); + + it('should return eventSources resource for advertiser', () => { + const eventSources = resource.eventSources('adv-123'); + expect(eventSources).toBeInstanceOf(EventSourcesResource); + }); + + it('should return measurementData resource for advertiser', () => { + const measurementData = resource.measurementData('adv-123'); + expect(measurementData).toBeInstanceOf(MeasurementDataResource); + }); + + it('should return catalogs resource for advertiser', () => { + const catalogs = resource.catalogs('adv-123'); + expect(catalogs).toBeInstanceOf(CatalogsResource); + }); + + it('should return audiences resource for advertiser', () => { + const audiences = resource.audiences('adv-123'); + expect(audiences).toBeInstanceOf(AudiencesResource); + }); + + it('should return syndication resource for advertiser', () => { + const syndication = resource.syndication('adv-123'); + expect(syndication).toBeInstanceOf(SyndicationResource); + }); + + it('should return propertyLists resource for advertiser', () => { + const propertyLists = resource.propertyLists('adv-123'); + expect(propertyLists).toBeInstanceOf(PropertyListsResource); }); }); }); diff --git a/src/__tests__/resources/agents.test.ts b/src/__tests__/resources/agents.test.ts index 50a1a19..0106a37 100644 --- a/src/__tests__/resources/agents.test.ts +++ b/src/__tests__/resources/agents.test.ts @@ -1,123 +1,3 @@ -/** - * Tests for AgentsResource - */ - -import { AgentsResource } from '../../resources/agents'; -import type { BaseAdapter } from '../../adapters/base'; - -describe('AgentsResource', () => { - let mockAdapter: jest.Mocked; - let resource: AgentsResource; - - beforeEach(() => { - mockAdapter = { - baseUrl: 'https://api.test.com', - version: 'v2', - persona: 'storefront' as const, - debug: false, - validate: false, - request: jest.fn(), - connect: jest.fn(), - disconnect: jest.fn(), - }; - resource = new AgentsResource(mockAdapter); - }); - - describe('list', () => { - it('should call adapter with correct path and no params', async () => { - mockAdapter.request.mockResolvedValue({ items: [] }); - - await resource.list(); - - expect(mockAdapter.request).toHaveBeenCalledWith('GET', '/agents', undefined, { - params: { - type: undefined, - status: undefined, - relationship: undefined, - }, - }); - }); - - it('should pass filter params when provided', async () => { - mockAdapter.request.mockResolvedValue({ items: [] }); - - await resource.list({ - type: 'SALES', - status: 'ACTIVE', - relationship: 'CONNECTED', - }); - - expect(mockAdapter.request).toHaveBeenCalledWith('GET', '/agents', undefined, { - params: { - type: 'SALES', - status: 'ACTIVE', - relationship: 'CONNECTED', - }, - }); - }); - }); - - describe('get', () => { - it('should call adapter with correct path', async () => { - mockAdapter.request.mockResolvedValue({ id: 'agent-1', name: 'Test Agent' }); - - await resource.get('agent-1'); - - expect(mockAdapter.request).toHaveBeenCalledWith('GET', '/agents/agent-1'); - }); - }); - - describe('update', () => { - it('should call adapter with correct path and body', async () => { - const input = { name: 'Updated Agent' }; - mockAdapter.request.mockResolvedValue({ id: 'agent-1', name: 'Updated Agent' }); - - await resource.update('agent-1', input); - - expect(mockAdapter.request).toHaveBeenCalledWith('PATCH', '/agents/agent-1', input); - }); - }); - - describe('authorizeOAuth', () => { - it('should call adapter with correct path', async () => { - mockAdapter.request.mockResolvedValue({ authorizationUrl: 'https://oauth.example.com' }); - - await resource.authorizeOAuth('agent-1'); - - expect(mockAdapter.request).toHaveBeenCalledWith( - 'POST', - '/agents/agent-1/oauth/authorize', - {} - ); - }); - }); - - describe('authorizeAccountOAuth', () => { - it('should call adapter with correct path', async () => { - mockAdapter.request.mockResolvedValue({ authorizationUrl: 'https://oauth.example.com' }); - - await resource.authorizeAccountOAuth('agent-1'); - - expect(mockAdapter.request).toHaveBeenCalledWith( - 'POST', - '/agents/agent-1/accounts/oauth/authorize', - {} - ); - }); - }); - - describe('exchangeOAuthCode', () => { - it('should call adapter with correct path and body', async () => { - const input = { code: 'auth-code-123', state: 'state-456' }; - mockAdapter.request.mockResolvedValue({ success: true }); - - await resource.exchangeOAuthCode('agent-1', input); - - expect(mockAdapter.request).toHaveBeenCalledWith( - 'POST', - '/agents/agent-1/oauth/callback', - input - ); - }); - }); +describe('removed', () => { + it.todo('resource removed in v3'); }); diff --git a/src/__tests__/resources/billing.test.ts b/src/__tests__/resources/billing.test.ts index f2fee7c..f6b5b21 100644 --- a/src/__tests__/resources/billing.test.ts +++ b/src/__tests__/resources/billing.test.ts @@ -1,77 +1,41 @@ /** - * Tests for BillingResource + * Tests for BuyerBillingResource */ -import { BillingResource } from '../../resources/billing'; +import { BuyerBillingResource } from '../../resources/billing'; import type { BaseAdapter } from '../../adapters/base'; -describe('BillingResource', () => { +describe('BuyerBillingResource', () => { let mockAdapter: jest.Mocked; - let resource: BillingResource; + let resource: BuyerBillingResource; beforeEach(() => { mockAdapter = { baseUrl: 'https://api.test.com', version: 'v2', - persona: 'storefront' as const, + persona: 'buyer' as const, debug: false, validate: false, request: jest.fn(), connect: jest.fn(), disconnect: jest.fn(), }; - resource = new BillingResource(mockAdapter); + resource = new BuyerBillingResource(mockAdapter); }); - describe('get', () => { + describe('listInvoices', () => { it('should call adapter with correct path', async () => { - mockAdapter.request.mockResolvedValue({ billing: null }); - await resource.get(); - expect(mockAdapter.request).toHaveBeenCalledWith('GET', '/billing'); - }); - }); - - describe('connect', () => { - it('should call adapter with correct path', async () => { - mockAdapter.request.mockResolvedValue({ onboardingUrl: 'https://stripe.com/...' }); - await resource.connect(); - expect(mockAdapter.request).toHaveBeenCalledWith('POST', '/billing/connect'); - }); - }); - - describe('status', () => { - it('should call adapter with correct path', async () => { - mockAdapter.request.mockResolvedValue({ verified: true }); - await resource.status(); - expect(mockAdapter.request).toHaveBeenCalledWith('GET', '/billing/status'); - }); - }); - - describe('transactions', () => { - it('should call adapter with correct path and params', async () => { - mockAdapter.request.mockResolvedValue({ data: [] }); - await resource.transactions({ limit: 10, starting_after: 'txn_abc' }); - expect(mockAdapter.request).toHaveBeenCalledWith('GET', '/billing/transactions', undefined, { - params: { limit: 10, starting_after: 'txn_abc' }, - }); - }); - }); - - describe('payouts', () => { - it('should call adapter with correct path and params', async () => { mockAdapter.request.mockResolvedValue({ data: [] }); - await resource.payouts({ limit: 25 }); - expect(mockAdapter.request).toHaveBeenCalledWith('GET', '/billing/payouts', undefined, { - params: { limit: 25, starting_after: undefined }, - }); + await resource.listInvoices(); + expect(mockAdapter.request).toHaveBeenCalledWith('GET', '/billing/invoices'); }); }); - describe('onboardingUrl', () => { + describe('listPendingItems', () => { it('should call adapter with correct path', async () => { - mockAdapter.request.mockResolvedValue({ url: 'https://stripe.com/...' }); - await resource.onboardingUrl(); - expect(mockAdapter.request).toHaveBeenCalledWith('GET', '/billing/onboard'); + mockAdapter.request.mockResolvedValue({ data: [] }); + await resource.listPendingItems(); + expect(mockAdapter.request).toHaveBeenCalledWith('GET', '/billing/pending-invoice-items'); }); }); }); diff --git a/src/__tests__/resources/bundles.test.ts b/src/__tests__/resources/bundles.test.ts index b45e38b..0106a37 100644 --- a/src/__tests__/resources/bundles.test.ts +++ b/src/__tests__/resources/bundles.test.ts @@ -1,116 +1,3 @@ -/** - * Tests for BundlesResource - */ - -import { BundlesResource } from '../../resources/bundles'; -import { BundleProductsResource } from '../../resources/products'; -import type { BaseAdapter } from '../../adapters/base'; - -function createMockAdapter(overrides?: Partial): jest.Mocked { - return { - baseUrl: 'https://api.test.com', - version: 'v2', - persona: 'buyer' as const, - debug: false, - validate: false, - request: jest.fn(), - connect: jest.fn(), - disconnect: jest.fn(), - ...overrides, - } as jest.Mocked; -} - -describe('BundlesResource', () => { - let mockAdapter: jest.Mocked; - let resource: BundlesResource; - - beforeEach(() => { - mockAdapter = createMockAdapter(); - resource = new BundlesResource(mockAdapter); - }); - - describe('create', () => { - it('should call adapter with correct path and body', async () => { - const input = { advertiserId: 'adv-1', channels: ['display'] }; - mockAdapter.request.mockResolvedValue({ bundleId: 'b-1' }); - - await resource.create(input); - - expect(mockAdapter.request).toHaveBeenCalledWith('POST', '/bundles', input); - }); - }); - - describe('discoverProducts', () => { - it('should call adapter with correct path and query params', async () => { - mockAdapter.request.mockResolvedValue({ groups: [] }); - - await resource.discoverProducts('bundle-123', { - groupLimit: 5, - groupOffset: 0, - productsPerGroup: 10, - productOffset: 0, - publisherDomain: 'example.com', - salesAgentIds: 'sa-1', - salesAgentNames: 'Agent One', - }); - - expect(mockAdapter.request).toHaveBeenCalledWith( - 'GET', - '/bundles/bundle-123/discover-products', - undefined, - { - params: { - groupLimit: 5, - groupOffset: 0, - productsPerGroup: 10, - productOffset: 0, - publisherDomain: 'example.com', - salesAgentIds: 'sa-1', - salesAgentNames: 'Agent One', - }, - } - ); - }); - - it('should call adapter with no params when none provided', async () => { - mockAdapter.request.mockResolvedValue({ groups: [] }); - - await resource.discoverProducts('bundle-456'); - - expect(mockAdapter.request).toHaveBeenCalledWith( - 'GET', - '/bundles/bundle-456/discover-products', - undefined, - { - params: { - groupLimit: undefined, - groupOffset: undefined, - productsPerGroup: undefined, - productOffset: undefined, - publisherDomain: undefined, - salesAgentIds: undefined, - salesAgentNames: undefined, - }, - } - ); - }); - }); - - describe('browseProducts', () => { - it('should call adapter with correct path and body', async () => { - const input = { advertiserId: 'adv-1', channels: ['display'] }; - mockAdapter.request.mockResolvedValue({ groups: [], bundleId: 'b-auto' }); - - await resource.browseProducts(input); - - expect(mockAdapter.request).toHaveBeenCalledWith('POST', '/bundles/discover-products', input); - }); - }); - - describe('products', () => { - it('should return a BundleProductsResource instance', () => { - const productsResource = resource.products('bundle-789'); - expect(productsResource).toBeInstanceOf(BundleProductsResource); - }); - }); +describe('removed', () => { + it.todo('resource removed in v3'); }); diff --git a/src/__tests__/resources/campaigns.test.ts b/src/__tests__/resources/campaigns.test.ts index 9e64fce..35b05cf 100644 --- a/src/__tests__/resources/campaigns.test.ts +++ b/src/__tests__/resources/campaigns.test.ts @@ -40,7 +40,7 @@ describe('CampaignsResource', () => { }); }); - it('should pass filter params including type', async () => { + it('should pass filter params', async () => { mockAdapter.request.mockResolvedValue({ items: [], total: 0 }); await resource.list({ advertiserId: 'adv-123', status: 'ACTIVE', type: 'discovery' }); @@ -59,7 +59,7 @@ describe('CampaignsResource', () => { describe('get', () => { it('should call adapter with correct path', async () => { - mockAdapter.request.mockResolvedValue({ id: 'camp-123', name: 'Test Campaign' }); + mockAdapter.request.mockResolvedValue({ data: { id: 'camp-123', name: 'Test Campaign' } }); await resource.get('camp-123'); @@ -67,105 +67,100 @@ describe('CampaignsResource', () => { }); }); - describe('createDiscovery', () => { - it('should call adapter with POST /campaigns/discovery', async () => { + describe('create', () => { + it('should call adapter with POST /campaigns', async () => { const input = { advertiserId: 'adv-123', name: 'Q1 Campaign', - bundleId: 'bundle-456', + type: 'discovery' as const, flightDates: { startDate: '2025-01-01', endDate: '2025-03-31' }, budget: { total: 50000, currency: 'USD' }, }; - mockAdapter.request.mockResolvedValue({ id: 'camp-123', ...input }); + mockAdapter.request.mockResolvedValue({ data: { id: 'camp-123', ...input } }); - await resource.createDiscovery(input); + await resource.create(input); - expect(mockAdapter.request).toHaveBeenCalledWith('POST', '/campaigns/discovery', input); + expect(mockAdapter.request).toHaveBeenCalledWith('POST', '/campaigns', input); }); }); - describe('updateDiscovery', () => { - it('should call adapter with PUT /campaigns/discovery/{id}', async () => { - mockAdapter.request.mockResolvedValue({ id: 'camp-123', name: 'Updated' }); + describe('update', () => { + it('should call adapter with PUT /campaigns/{id}', async () => { + mockAdapter.request.mockResolvedValue({ data: { id: 'camp-123', name: 'Updated' } }); - await resource.updateDiscovery('camp-123', { name: 'Updated' }); + await resource.update('camp-123', { name: 'Updated' }); - expect(mockAdapter.request).toHaveBeenCalledWith('PUT', '/campaigns/discovery/camp-123', { + expect(mockAdapter.request).toHaveBeenCalledWith('PUT', '/campaigns/camp-123', { name: 'Updated', }); }); }); - describe('createPerformance', () => { - it('should call adapter with POST /campaigns/performance', async () => { - const input = { - advertiserId: 'adv-123', - name: 'Q1 ROAS', - flightDates: { startDate: '2025-01-01', endDate: '2025-03-31' }, - budget: { total: 100000, currency: 'USD' }, - performanceConfig: { objective: 'ROAS' as const, goals: { targetRoas: 4.0 } }, - }; - - mockAdapter.request.mockResolvedValue({ id: 'camp-456', ...input }); + describe('delete', () => { + it('should call adapter with DELETE /campaigns/{id}', async () => { + mockAdapter.request.mockResolvedValue(undefined); - await resource.createPerformance(input); + await resource.delete('camp-123'); - expect(mockAdapter.request).toHaveBeenCalledWith('POST', '/campaigns/performance', input); + expect(mockAdapter.request).toHaveBeenCalledWith('DELETE', '/campaigns/camp-123'); }); }); - describe('updatePerformance', () => { - it('should call adapter with PUT /campaigns/performance/{id}', async () => { - const update = { performanceConfig: { goals: { targetRoas: 5.0 } } }; - mockAdapter.request.mockResolvedValue({ id: 'camp-456' }); + describe('execute', () => { + it('should call execute endpoint', async () => { + mockAdapter.request.mockResolvedValue({ data: { success: true } }); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - await resource.updatePerformance('camp-456', update as any); + await resource.execute('camp-123'); - expect(mockAdapter.request).toHaveBeenCalledWith( - 'PUT', - '/campaigns/performance/camp-456', - update - ); + expect(mockAdapter.request).toHaveBeenCalledWith('POST', '/campaigns/camp-123/execute'); }); }); - describe('createAudience', () => { - it('should call adapter with POST /campaigns/audience', async () => { - const input = { - advertiserId: 'adv-123', - name: 'Audience Campaign', - flightDates: { startDate: '2025-01-01', endDate: '2025-03-31' }, - budget: { total: 25000, currency: 'USD' }, - signals: ['signal-1'], - }; + describe('pause', () => { + it('should call pause endpoint', async () => { + mockAdapter.request.mockResolvedValue({ data: { id: 'camp-123', status: 'PAUSED' } }); - mockAdapter.request.mockResolvedValue({ id: 'camp-789', ...input }); + await resource.pause('camp-123'); - await resource.createAudience(input); + expect(mockAdapter.request).toHaveBeenCalledWith('POST', '/campaigns/camp-123/pause'); + }); + }); + + describe('autoSelectProducts', () => { + it('should call auto-select-products endpoint', async () => { + mockAdapter.request.mockResolvedValue({ data: { selected: 5 } }); + + await resource.autoSelectProducts('camp-123'); - expect(mockAdapter.request).toHaveBeenCalledWith('POST', '/campaigns/audience', input); + expect(mockAdapter.request).toHaveBeenCalledWith( + 'POST', + '/campaigns/camp-123/auto-select-products', + undefined + ); }); }); - describe('execute', () => { - it('should call execute endpoint', async () => { - mockAdapter.request.mockResolvedValue({ success: true, executedAt: '2025-01-01' }); + describe('getMediaBuyStatus', () => { + it('should call media-buy-status endpoint', async () => { + mockAdapter.request.mockResolvedValue({ data: { status: 'ACTIVE' } }); - await resource.execute('camp-123'); + await resource.getMediaBuyStatus('camp-123'); - expect(mockAdapter.request).toHaveBeenCalledWith('POST', '/campaigns/camp-123/execute'); + expect(mockAdapter.request).toHaveBeenCalledWith( + 'GET', + '/campaigns/camp-123/media-buy-status' + ); }); }); - describe('pause', () => { - it('should call pause endpoint', async () => { - mockAdapter.request.mockResolvedValue({ id: 'camp-123', status: 'PAUSED' }); + describe('getProducts', () => { + it('should call products endpoint', async () => { + mockAdapter.request.mockResolvedValue({ data: { products: [] } }); - await resource.pause('camp-123'); + await resource.getProducts('camp-123'); - expect(mockAdapter.request).toHaveBeenCalledWith('POST', '/campaigns/camp-123/pause'); + expect(mockAdapter.request).toHaveBeenCalledWith('GET', '/campaigns/camp-123/products'); }); }); }); diff --git a/src/__tests__/resources/conversion-events.test.ts b/src/__tests__/resources/conversion-events.test.ts index ec065ba..0106a37 100644 --- a/src/__tests__/resources/conversion-events.test.ts +++ b/src/__tests__/resources/conversion-events.test.ts @@ -1,88 +1,3 @@ -/** - * Tests for ConversionEventsResource - */ - -import { ConversionEventsResource } from '../../resources/conversion-events'; -import type { BaseAdapter } from '../../adapters/base'; - -describe('ConversionEventsResource', () => { - let mockAdapter: jest.Mocked; - let resource: ConversionEventsResource; - const advertiserId = 'adv-123'; - - beforeEach(() => { - mockAdapter = { - baseUrl: 'https://api.test.com', - version: 'v2', - persona: 'buyer' as const, - debug: false, - validate: false, - request: jest.fn(), - connect: jest.fn(), - disconnect: jest.fn(), - }; - resource = new ConversionEventsResource(mockAdapter, advertiserId); - }); - - describe('constructor', () => { - it('should accept adapter and advertiserId', () => { - expect(resource).toBeDefined(); - }); - }); - - describe('list', () => { - it('should call adapter with correct path', async () => { - mockAdapter.request.mockResolvedValue({ items: [] }); - - await resource.list(); - - expect(mockAdapter.request).toHaveBeenCalledWith( - 'GET', - '/advertisers/adv-123/conversion-events' - ); - }); - }); - - describe('get', () => { - it('should call adapter with correct path', async () => { - mockAdapter.request.mockResolvedValue({ id: 'evt-1' }); - - await resource.get('evt-1'); - - expect(mockAdapter.request).toHaveBeenCalledWith( - 'GET', - '/advertisers/adv-123/conversion-events/evt-1' - ); - }); - }); - - describe('create', () => { - it('should call adapter with correct path and body', async () => { - const input = { name: 'Purchase', type: 'PURCHASE' as const }; - mockAdapter.request.mockResolvedValue({ id: 'evt-1', name: 'Purchase' }); - - await resource.create(input); - - expect(mockAdapter.request).toHaveBeenCalledWith( - 'POST', - '/advertisers/adv-123/conversion-events', - input - ); - }); - }); - - describe('update', () => { - it('should call adapter with correct path and body', async () => { - const input = { name: 'Updated Purchase' }; - mockAdapter.request.mockResolvedValue({ id: 'evt-1', name: 'Updated Purchase' }); - - await resource.update('evt-1', input); - - expect(mockAdapter.request).toHaveBeenCalledWith( - 'PUT', - '/advertisers/adv-123/conversion-events/evt-1', - input - ); - }); - }); +describe('removed', () => { + it.todo('resource removed in v3'); }); diff --git a/src/__tests__/resources/creative-sets.test.ts b/src/__tests__/resources/creative-sets.test.ts index b121c53..0106a37 100644 --- a/src/__tests__/resources/creative-sets.test.ts +++ b/src/__tests__/resources/creative-sets.test.ts @@ -1,83 +1,3 @@ -/** - * Tests for CreativeSetsResource - */ - -import { CreativeSetsResource } from '../../resources/creative-sets'; -import type { BaseAdapter } from '../../adapters/base'; - -describe('CreativeSetsResource', () => { - let mockAdapter: jest.Mocked; - let resource: CreativeSetsResource; - const advertiserId = 'adv-123'; - - beforeEach(() => { - mockAdapter = { - baseUrl: 'https://api.test.com', - version: 'v2', - persona: 'buyer' as const, - debug: false, - validate: false, - request: jest.fn(), - connect: jest.fn(), - disconnect: jest.fn(), - }; - resource = new CreativeSetsResource(mockAdapter, advertiserId); - }); - - describe('list', () => { - it('should call adapter with correct path', async () => { - mockAdapter.request.mockResolvedValue({ items: [] }); - - await resource.list(); - - expect(mockAdapter.request).toHaveBeenCalledWith('GET', '/advertisers/adv-123/creative-sets'); - }); - }); - - describe('create', () => { - it('should call adapter with correct path and body', async () => { - const input = { name: 'Holiday Set', type: 'DISPLAY' }; - mockAdapter.request.mockResolvedValue({ id: 'cs-1', name: 'Holiday Set' }); - - await resource.create(input); - - expect(mockAdapter.request).toHaveBeenCalledWith( - 'POST', - '/advertisers/adv-123/creative-sets', - input - ); - }); - }); - - describe('addAsset', () => { - it('should call adapter with correct path and body', async () => { - const input = { - assetUrl: 'https://cdn.example.com/image.png', - name: 'Banner', - type: 'IMAGE', - }; - mockAdapter.request.mockResolvedValue({ id: 'asset-1' }); - - await resource.addAsset('cs-1', input); - - expect(mockAdapter.request).toHaveBeenCalledWith( - 'POST', - '/advertisers/adv-123/creative-sets/cs-1/assets', - input - ); - }); - }); - - describe('removeAsset', () => { - it('should call adapter with correct path', async () => { - mockAdapter.request.mockResolvedValue(undefined); - - await resource.removeAsset('cs-1', 'asset-1'); - - expect(mockAdapter.request).toHaveBeenCalledWith( - 'DELETE', - '/advertisers/adv-123/creative-sets/cs-1/assets/asset-1' - ); - }); - }); +describe('removed', () => { + it.todo('resource removed in v3'); }); diff --git a/src/__tests__/resources/event-sources.test.ts b/src/__tests__/resources/event-sources.test.ts index 26a2b50..0b6f811 100644 --- a/src/__tests__/resources/event-sources.test.ts +++ b/src/__tests__/resources/event-sources.test.ts @@ -23,19 +23,6 @@ describe('EventSourcesResource', () => { resource = new EventSourcesResource(mockAdapter, 'adv-123'); }); - describe('sync', () => { - it('should call adapter with correct path and body', async () => { - const data = { sources: [{ name: 'pixel' }] }; - mockAdapter.request.mockResolvedValue({ synced: 1 }); - await resource.sync(data); - expect(mockAdapter.request).toHaveBeenCalledWith( - 'POST', - '/advertisers/adv-123/event-sources/sync', - data - ); - }); - }); - describe('list', () => { it('should call adapter with correct path', async () => { mockAdapter.request.mockResolvedValue([]); @@ -44,51 +31,40 @@ describe('EventSourcesResource', () => { }); }); - describe('create', () => { + describe('sync', () => { it('should call adapter with correct path and body', async () => { - const data = { name: 'new-source', type: 'pixel' }; - mockAdapter.request.mockResolvedValue({ id: 'es-1' }); - await resource.create(data); + const data = { sources: [{ name: 'pixel' }] }; + mockAdapter.request.mockResolvedValue({ synced: 1 }); + await resource.sync(data); expect(mockAdapter.request).toHaveBeenCalledWith( 'POST', - '/advertisers/adv-123/event-sources', + '/advertisers/adv-123/event-sources/sync', data ); }); }); - describe('get', () => { + describe('getEventSummary', () => { it('should call adapter with correct path', async () => { - mockAdapter.request.mockResolvedValue({ id: 'es-1' }); - await resource.get('es-1'); + mockAdapter.request.mockResolvedValue({ data: {} }); + await resource.getEventSummary(); expect(mockAdapter.request).toHaveBeenCalledWith( 'GET', - '/advertisers/adv-123/event-sources/es-1' + '/advertisers/adv-123/events/summary' ); }); }); - describe('update', () => { + describe('logEvent', () => { it('should call adapter with correct path and body', async () => { - const data = { name: 'updated-source' }; - mockAdapter.request.mockResolvedValue({ id: 'es-1', name: 'updated-source' }); - await resource.update('es-1', data); + const data = { events: [{ eventType: 'conversion', value: 100 }] }; + mockAdapter.request.mockResolvedValue({ data: { success: true } }); + await resource.logEvent(data); expect(mockAdapter.request).toHaveBeenCalledWith( - 'PUT', - '/advertisers/adv-123/event-sources/es-1', + 'POST', + '/advertisers/adv-123/log-event', data ); }); }); - - describe('delete', () => { - it('should call adapter with correct path', async () => { - mockAdapter.request.mockResolvedValue(undefined); - await resource.delete('es-1'); - expect(mockAdapter.request).toHaveBeenCalledWith( - 'DELETE', - '/advertisers/adv-123/event-sources/es-1' - ); - }); - }); }); diff --git a/src/__tests__/resources/inventory-sources.test.ts b/src/__tests__/resources/inventory-sources.test.ts index 8ebf778..0106a37 100644 --- a/src/__tests__/resources/inventory-sources.test.ts +++ b/src/__tests__/resources/inventory-sources.test.ts @@ -1,75 +1,3 @@ -/** - * Tests for InventorySourcesResource - */ - -import { InventorySourcesResource } from '../../resources/inventory-sources'; -import type { BaseAdapter } from '../../adapters/base'; - -describe('InventorySourcesResource', () => { - let mockAdapter: jest.Mocked; - let resource: InventorySourcesResource; - - beforeEach(() => { - mockAdapter = { - baseUrl: 'https://api.test.com', - version: 'v2', - persona: 'storefront' as const, - debug: false, - validate: false, - request: jest.fn(), - connect: jest.fn(), - disconnect: jest.fn(), - }; - resource = new InventorySourcesResource(mockAdapter); - }); - - describe('list', () => { - it('should call adapter with correct path', async () => { - mockAdapter.request.mockResolvedValue([]); - await resource.list(); - expect(mockAdapter.request).toHaveBeenCalledWith('GET', '/inventory-sources'); - }); - }); - - describe('get', () => { - it('should call adapter with correct path', async () => { - mockAdapter.request.mockResolvedValue({ sourceId: 'src-1' }); - await resource.get('src-1'); - expect(mockAdapter.request).toHaveBeenCalledWith('GET', '/inventory-sources/src-1'); - }); - }); - - describe('create', () => { - it('should call adapter with correct path and body', async () => { - const input = { - sourceId: 'snap-agent', - name: 'Snap Agent', - executionType: 'agent' as const, - type: 'SALES' as const, - endpointUrl: 'https://agent.example.com', - protocol: 'MCP' as const, - authenticationType: 'API_KEY' as const, - }; - mockAdapter.request.mockResolvedValue({ sourceId: 'snap-agent', agentId: 'snap_abc' }); - await resource.create(input); - expect(mockAdapter.request).toHaveBeenCalledWith('POST', '/inventory-sources', input); - }); - }); - - describe('update', () => { - it('should call adapter with correct path and body', async () => { - const input = { status: 'active' }; - mockAdapter.request.mockResolvedValue({ sourceId: 'src-1', status: 'active' }); - await resource.update('src-1', input); - expect(mockAdapter.request).toHaveBeenCalledWith('PUT', '/inventory-sources/src-1', input); - }); - }); - - describe('delete', () => { - it('should call adapter with correct path', async () => { - mockAdapter.request.mockResolvedValue(undefined); - await resource.delete('src-1'); - expect(mockAdapter.request).toHaveBeenCalledWith('DELETE', '/inventory-sources/src-1'); - }); - }); +describe('removed', () => { + it.todo('resource removed in v3'); }); diff --git a/src/__tests__/resources/notifications.test.ts b/src/__tests__/resources/notifications.test.ts index 0ad2f91..0106a37 100644 --- a/src/__tests__/resources/notifications.test.ts +++ b/src/__tests__/resources/notifications.test.ts @@ -1,94 +1,3 @@ -/** - * Tests for NotificationsResource - */ - -import { NotificationsResource } from '../../resources/notifications'; -import type { BaseAdapter } from '../../adapters/base'; - -describe('NotificationsResource', () => { - let mockAdapter: jest.Mocked; - let resource: NotificationsResource; - - beforeEach(() => { - mockAdapter = { - baseUrl: 'https://api.test.com', - version: 'v2', - persona: 'storefront' as const, - debug: false, - validate: false, - request: jest.fn(), - connect: jest.fn(), - disconnect: jest.fn(), - }; - resource = new NotificationsResource(mockAdapter); - }); - - describe('list', () => { - it('should call adapter with correct path and no params', async () => { - mockAdapter.request.mockResolvedValue([]); - await resource.list(); - expect(mockAdapter.request).toHaveBeenCalledWith('GET', '/notifications', undefined, { - params: { - unreadOnly: undefined, - brandAgentId: undefined, - types: undefined, - limit: undefined, - offset: undefined, - }, - }); - }); - - it('should pass filter params when provided', async () => { - mockAdapter.request.mockResolvedValue([]); - await resource.list({ unreadOnly: true, limit: 20 }); - expect(mockAdapter.request).toHaveBeenCalledWith('GET', '/notifications', undefined, { - params: { - unreadOnly: true, - brandAgentId: undefined, - types: undefined, - limit: 20, - offset: undefined, - }, - }); - }); - }); - - describe('markAsRead', () => { - it('should call adapter with correct path', async () => { - mockAdapter.request.mockResolvedValue(undefined); - await resource.markAsRead('notif-1'); - expect(mockAdapter.request).toHaveBeenCalledWith('POST', '/notifications/notif-1/read'); - }); - }); - - describe('acknowledge', () => { - it('should call adapter with correct path', async () => { - mockAdapter.request.mockResolvedValue(undefined); - await resource.acknowledge('notif-1'); - expect(mockAdapter.request).toHaveBeenCalledWith( - 'POST', - '/notifications/notif-1/acknowledge' - ); - }); - }); - - describe('markAllAsRead', () => { - it('should call adapter with correct path and no body', async () => { - mockAdapter.request.mockResolvedValue(undefined); - await resource.markAllAsRead(); - expect(mockAdapter.request).toHaveBeenCalledWith( - 'POST', - '/notifications/read-all', - undefined - ); - }); - - it('should pass brandAgentId when provided', async () => { - mockAdapter.request.mockResolvedValue(undefined); - await resource.markAllAsRead(123); - expect(mockAdapter.request).toHaveBeenCalledWith('POST', '/notifications/read-all', { - brandAgentId: 123, - }); - }); - }); +describe('removed', () => { + it.todo('resource removed in v3'); }); diff --git a/src/__tests__/resources/products.test.ts b/src/__tests__/resources/products.test.ts index 013922d..0106a37 100644 --- a/src/__tests__/resources/products.test.ts +++ b/src/__tests__/resources/products.test.ts @@ -1,99 +1,3 @@ -/** - * Tests for BundleProductsResource - */ - -import { BundleProductsResource } from '../../resources/products'; -import type { BaseAdapter } from '../../adapters/base'; - -describe('BundleProductsResource', () => { - let mockAdapter: jest.Mocked; - let resource: BundleProductsResource; - - beforeEach(() => { - mockAdapter = { - baseUrl: 'https://api.test.com', - version: 'v2', - persona: 'buyer' as const, - debug: false, - validate: false, - request: jest.fn(), - connect: jest.fn(), - disconnect: jest.fn(), - }; - resource = new BundleProductsResource(mockAdapter, 'bundle-123'); - }); - - describe('list', () => { - it('should call adapter with GET /bundles/{bundleId}/products', async () => { - mockAdapter.request.mockResolvedValue({ - bundleId: 'bundle-123', - products: [], - totalProducts: 0, - }); - - await resource.list(); - - expect(mockAdapter.request).toHaveBeenCalledWith('GET', '/bundles/bundle-123/products'); - }); - }); - - describe('add', () => { - it('should call adapter with POST /bundles/{bundleId}/products and products array', async () => { - const input = { - products: [ - { - productId: 'prod-1', - salesAgentId: 'agent-1', - groupId: 'group-0', - groupName: 'Publisher A', - cpm: 12.5, - budget: 5000, - }, - ], - }; - mockAdapter.request.mockResolvedValue({ - bundleId: 'bundle-123', - products: input.products, - totalProducts: 1, - }); - - await resource.add(input); - - expect(mockAdapter.request).toHaveBeenCalledWith( - 'POST', - '/bundles/bundle-123/products', - input - ); - }); - }); - - describe('remove', () => { - it('should call adapter with DELETE /bundles/{bundleId}/products and productIds', async () => { - const input = { productIds: ['prod-1', 'prod-2'] }; - mockAdapter.request.mockResolvedValue(undefined); - - await resource.remove(input); - - expect(mockAdapter.request).toHaveBeenCalledWith( - 'DELETE', - '/bundles/bundle-123/products', - input - ); - }); - }); - - describe('scoping', () => { - it('should use the correct bundle ID in all paths', async () => { - const resource2 = new BundleProductsResource(mockAdapter, 'different-bundle'); - mockAdapter.request.mockResolvedValue({ - bundleId: 'different-bundle', - products: [], - totalProducts: 0, - }); - - await resource2.list(); - - expect(mockAdapter.request).toHaveBeenCalledWith('GET', '/bundles/different-bundle/products'); - }); - }); +describe('removed', () => { + it.todo('resource removed in v3'); }); diff --git a/src/__tests__/resources/readiness.test.ts b/src/__tests__/resources/readiness.test.ts index 474aee6..0106a37 100644 --- a/src/__tests__/resources/readiness.test.ts +++ b/src/__tests__/resources/readiness.test.ts @@ -1,37 +1,3 @@ -/** - * Tests for ReadinessResource - */ - -import { ReadinessResource } from '../../resources/readiness'; -import type { BaseAdapter } from '../../adapters/base'; - -describe('ReadinessResource', () => { - let mockAdapter: jest.Mocked; - let resource: ReadinessResource; - - beforeEach(() => { - mockAdapter = { - baseUrl: 'https://api.test.com', - version: 'v2', - persona: 'storefront' as const, - debug: false, - validate: false, - request: jest.fn(), - connect: jest.fn(), - disconnect: jest.fn(), - }; - resource = new ReadinessResource(mockAdapter); - }); - - describe('check', () => { - it('should call adapter with correct path', async () => { - mockAdapter.request.mockResolvedValue({ - platformId: 'acme', - status: 'ready', - checks: [], - }); - await resource.check(); - expect(mockAdapter.request).toHaveBeenCalledWith('GET', '/readiness'); - }); - }); +describe('removed', () => { + it.todo('resource removed in v3'); }); diff --git a/src/__tests__/resources/sales-agents.test.ts b/src/__tests__/resources/sales-agents.test.ts index 826d03c..0106a37 100644 --- a/src/__tests__/resources/sales-agents.test.ts +++ b/src/__tests__/resources/sales-agents.test.ts @@ -1,171 +1,3 @@ -/** - * Tests for SalesAgentsResource - */ - -import { SalesAgentsResource } from '../../resources/sales-agents'; -import type { BaseAdapter } from '../../adapters/base'; - -// Mock the validation module -jest.mock('../../validation', () => ({ - shouldValidateInput: jest.fn(), - shouldValidateResponse: jest.fn(), - validateInput: jest.fn(), - validateResponse: jest.fn(), -})); - -// Mock the schemas registry -jest.mock('../../schemas/registry', () => ({ - salesAgentSchemas: { - listResponse: { parse: jest.fn() }, - registerAccountInput: { parse: jest.fn() }, - accountResponse: { parse: jest.fn() }, - }, -})); - -import { - shouldValidateInput, - shouldValidateResponse, - validateInput, - validateResponse, -} from '../../validation'; -import { salesAgentSchemas } from '../../schemas/registry'; - -const mockShouldValidateInput = shouldValidateInput as jest.Mock; -const mockShouldValidateResponse = shouldValidateResponse as jest.Mock; -const mockValidateInput = validateInput as jest.Mock; -const mockValidateResponse = validateResponse as jest.Mock; - -function createMockAdapter(overrides?: Partial): jest.Mocked { - return { - baseUrl: 'https://api.test.com', - version: 'v2', - persona: 'buyer' as const, - debug: false, - validate: false, - request: jest.fn(), - connect: jest.fn(), - disconnect: jest.fn(), - ...overrides, - } as jest.Mocked; -} - -describe('SalesAgentsResource', () => { - let mockAdapter: jest.Mocked; - let resource: SalesAgentsResource; - - beforeEach(() => { - mockAdapter = createMockAdapter(); - resource = new SalesAgentsResource(mockAdapter); - jest.clearAllMocks(); - }); - - describe('list', () => { - it('should call adapter with correct path and no params', async () => { - mockAdapter.request.mockResolvedValue({ items: [] }); - mockShouldValidateResponse.mockReturnValue(false); - - await resource.list(); - - expect(mockAdapter.request).toHaveBeenCalledWith('GET', '/sales-agents', undefined, { - params: { - status: undefined, - relationship: undefined, - name: undefined, - limit: undefined, - offset: undefined, - }, - }); - }); - - it('should pass filter params when provided', async () => { - mockAdapter.request.mockResolvedValue({ items: [] }); - mockShouldValidateResponse.mockReturnValue(false); - - await resource.list({ - status: 'ACTIVE', - relationship: 'CONNECTED', - name: 'Test Agent', - limit: 10, - offset: 20, - }); - - expect(mockAdapter.request).toHaveBeenCalledWith('GET', '/sales-agents', undefined, { - params: { - status: 'ACTIVE', - relationship: 'CONNECTED', - name: 'Test Agent', - limit: 10, - offset: 20, - }, - }); - }); - - it('should validate response when validation is enabled', async () => { - const validatingAdapter = createMockAdapter({ validate: true }); - const validatingResource = new SalesAgentsResource(validatingAdapter); - - const mockResult = { items: [{ id: 'sa-1' }] }; - const validatedResult = { items: [{ id: 'sa-1', validated: true }] }; - validatingAdapter.request.mockResolvedValue(mockResult); - mockShouldValidateResponse.mockReturnValue(true); - mockValidateResponse.mockReturnValue(validatedResult); - - const result = await validatingResource.list(); - - expect(mockShouldValidateResponse).toHaveBeenCalledWith(true); - expect(mockValidateResponse).toHaveBeenCalledWith(salesAgentSchemas.listResponse, mockResult); - expect(result).toBe(validatedResult); - }); - }); - - describe('registerAccount', () => { - it('should call adapter with correct path and body', async () => { - const input = { advertiserId: 'adv-1', accountIdentifier: 'acc-ident-1' }; - mockAdapter.request.mockResolvedValue({ id: 'acc-1' }); - mockShouldValidateInput.mockReturnValue(false); - mockShouldValidateResponse.mockReturnValue(false); - - await resource.registerAccount('agent-123', input); - - expect(mockAdapter.request).toHaveBeenCalledWith( - 'POST', - '/sales-agents/agent-123/accounts', - input - ); - }); - - it('should validate input and response when validation is enabled', async () => { - const validatingAdapter = createMockAdapter({ validate: true }); - const validatingResource = new SalesAgentsResource(validatingAdapter); - - const input = { advertiserId: 'adv-1', accountIdentifier: 'acc-ident-1' }; - const validatedInput = { - advertiserId: 'adv-1', - accountIdentifier: 'acc-ident-1', - validated: true, - }; - const mockResult = { id: 'acc-1' }; - const validatedResult = { id: 'acc-1', validated: true }; - - mockShouldValidateInput.mockReturnValue(true); - mockShouldValidateResponse.mockReturnValue(true); - mockValidateInput.mockReturnValue(validatedInput); - validatingAdapter.request.mockResolvedValue(mockResult); - mockValidateResponse.mockReturnValue(validatedResult); - - const result = await validatingResource.registerAccount('agent-123', input); - - expect(mockValidateInput).toHaveBeenCalledWith(salesAgentSchemas.registerAccountInput, input); - expect(validatingAdapter.request).toHaveBeenCalledWith( - 'POST', - '/sales-agents/agent-123/accounts', - validatedInput - ); - expect(mockValidateResponse).toHaveBeenCalledWith( - salesAgentSchemas.accountResponse, - mockResult - ); - expect(result).toBe(validatedResult); - }); - }); +describe('removed', () => { + it.todo('resource removed in v3'); }); diff --git a/src/__tests__/resources/signals.test.ts b/src/__tests__/resources/signals.test.ts index f4a57be..0106a37 100644 --- a/src/__tests__/resources/signals.test.ts +++ b/src/__tests__/resources/signals.test.ts @@ -1,58 +1,3 @@ -/** - * Tests for SignalsResource - */ - -import { SignalsResource } from '../../resources/signals'; -import type { BaseAdapter } from '../../adapters/base'; - -describe('SignalsResource', () => { - let mockAdapter: jest.Mocked; - let resource: SignalsResource; - - beforeEach(() => { - mockAdapter = { - baseUrl: 'https://api.test.com', - version: 'v2', - persona: 'buyer' as const, - debug: false, - validate: false, - request: jest.fn(), - connect: jest.fn(), - disconnect: jest.fn(), - }; - resource = new SignalsResource(mockAdapter); - }); - - describe('discover', () => { - it('should call adapter with correct path and body', async () => { - const input = { filters: { catalogTypes: ['PRODUCT'] } }; - mockAdapter.request.mockResolvedValue([{ id: 'sig-1' }]); - - await resource.discover(input); - - expect(mockAdapter.request).toHaveBeenCalledWith('POST', '/campaign/signals/discover', input); - }); - - it('should call adapter with no data when none provided', async () => { - mockAdapter.request.mockResolvedValue([]); - - await resource.discover(); - - expect(mockAdapter.request).toHaveBeenCalledWith( - 'POST', - '/campaign/signals/discover', - undefined - ); - }); - }); - - describe('list', () => { - it('should call adapter with correct path', async () => { - mockAdapter.request.mockResolvedValue([{ id: 'sig-1' }, { id: 'sig-2' }]); - - await resource.list(); - - expect(mockAdapter.request).toHaveBeenCalledWith('GET', '/signals'); - }); - }); +describe('removed', () => { + it.todo('resource removed in v3'); }); diff --git a/src/__tests__/resources/storefront.test.ts b/src/__tests__/resources/storefront.test.ts index 6d773e0..0106a37 100644 --- a/src/__tests__/resources/storefront.test.ts +++ b/src/__tests__/resources/storefront.test.ts @@ -1,59 +1,3 @@ -/** - * Tests for StorefrontResource - */ - -import { StorefrontResource } from '../../resources/storefront'; -import type { BaseAdapter } from '../../adapters/base'; - -describe('StorefrontResource', () => { - let mockAdapter: jest.Mocked; - let resource: StorefrontResource; - - beforeEach(() => { - mockAdapter = { - baseUrl: 'https://api.test.com', - version: 'v2', - persona: 'storefront' as const, - debug: false, - validate: false, - request: jest.fn(), - connect: jest.fn(), - disconnect: jest.fn(), - }; - resource = new StorefrontResource(mockAdapter); - }); - - describe('get', () => { - it('should call adapter with correct path', async () => { - mockAdapter.request.mockResolvedValue({ platformId: 'acme' }); - await resource.get(); - expect(mockAdapter.request).toHaveBeenCalledWith('GET', ''); - }); - }); - - describe('create', () => { - it('should call adapter with correct path and body', async () => { - const input = { platformId: 'acme', name: 'Acme Media' }; - mockAdapter.request.mockResolvedValue({ platformId: 'acme', name: 'Acme Media' }); - await resource.create(input); - expect(mockAdapter.request).toHaveBeenCalledWith('POST', '', input); - }); - }); - - describe('update', () => { - it('should call adapter with correct path and body', async () => { - const input = { name: 'Updated Name', enabled: true }; - mockAdapter.request.mockResolvedValue({ platformId: 'acme', name: 'Updated Name' }); - await resource.update(input); - expect(mockAdapter.request).toHaveBeenCalledWith('PUT', '', input); - }); - }); - - describe('delete', () => { - it('should call adapter with correct path', async () => { - mockAdapter.request.mockResolvedValue(undefined); - await resource.delete(); - expect(mockAdapter.request).toHaveBeenCalledWith('DELETE', ''); - }); - }); +describe('removed', () => { + it.todo('resource removed in v3'); }); diff --git a/src/cli/commands/bundles.ts b/src/cli/commands/bundles.ts index cb62167..e69de29 100644 --- a/src/cli/commands/bundles.ts +++ b/src/cli/commands/bundles.ts @@ -1,228 +0,0 @@ -/** - * Bundle commands - manage media bundles and product discovery - */ - -import { Command } from 'commander'; -import { createClient, GlobalOptions, parseJsonArg } from '../utils'; -import { formatOutput, printError, printSuccess, OutputFormat } from '../format'; -import type { - CreateBundleInput, - DiscoverProductsParams, - BrowseProductsInput, - AddBundleProductsInput, - BundleProductInput, - RemoveBundleProductsInput, -} from '../../types'; - -export const bundlesCommand = new Command('bundles').description( - 'Manage media bundles and product discovery' -); - -/** - * Create a bundle - */ -bundlesCommand - .command('create') - .description('Create a new media bundle') - .requiredOption('--advertiser-id ', 'Advertiser ID') - .option('--channels ', 'Comma-separated list of channels (e.g., ctv,display)') - .option('--countries ', 'Comma-separated list of country codes (e.g., US,CA)') - .option('--brief ', 'Campaign brief/description') - .option('--budget ', 'Budget amount') - .option('--start-date ', 'Flight start date (ISO format)') - .option('--end-date ', 'Flight end date (ISO format)') - .action(async (options, cmd) => { - try { - const globalOpts = cmd.optsWithGlobals() as GlobalOptions; - const client = createClient(globalOpts); - - const data: CreateBundleInput = { - advertiserId: options.advertiserId, - }; - - if (options.channels) { - data.channels = options.channels.split(',').map((c: string) => c.trim()); - } - if (options.countries) { - data.countries = options.countries.split(',').map((c: string) => c.trim()); - } - if (options.brief) { - data.brief = options.brief; - } - if (options.budget) { - data.budget = parseFloat(options.budget); - } - if (options.startDate && options.endDate) { - data.flightDates = { - startDate: options.startDate, - endDate: options.endDate, - }; - } - - const result = await client.bundles.create(data); - formatOutput(result, globalOpts.format as OutputFormat); - printSuccess(`Created bundle: ${result.data.bundleId}`); - } catch (error) { - printError(error instanceof Error ? error.message : 'Unknown error'); - process.exit(1); - } - }); - -/** - * Discover products for a bundle - */ -bundlesCommand - .command('discover-products ') - .description('Discover available products for a bundle') - .option('--group-limit ', 'Max groups to return (default: 10, max: 50)') - .option('--group-offset ', 'Groups to skip for pagination') - .option('--products-per-group ', 'Products per group (default: 5, max: 50)') - .option('--publisher-domain ', 'Filter by publisher domain') - .action(async (bundleId: string, options, cmd) => { - try { - const globalOpts = cmd.optsWithGlobals() as GlobalOptions; - const client = createClient(globalOpts); - - const params: DiscoverProductsParams = {}; - - if (options.groupLimit) { - params.groupLimit = parseInt(options.groupLimit, 10); - } - if (options.groupOffset) { - params.groupOffset = parseInt(options.groupOffset, 10); - } - if (options.productsPerGroup) { - params.productsPerGroup = parseInt(options.productsPerGroup, 10); - } - if (options.publisherDomain) { - params.publisherDomain = options.publisherDomain; - } - - const result = await client.bundles.discoverProducts(bundleId, params); - formatOutput(result, globalOpts.format as OutputFormat); - } catch (error) { - printError(error instanceof Error ? error.message : 'Unknown error'); - process.exit(1); - } - }); - -/** - * Browse products without an existing bundle - */ -bundlesCommand - .command('browse-products') - .description('Browse available products without creating a bundle') - .requiredOption('--advertiser-id ', 'Advertiser ID') - .option('--channels ', 'Comma-separated list of channels') - .option('--countries ', 'Comma-separated list of country codes') - .option('--brief ', 'Campaign brief for context') - .option('--publisher-domain ', 'Filter by publisher domain') - .action(async (options, cmd) => { - try { - const globalOpts = cmd.optsWithGlobals() as GlobalOptions; - const client = createClient(globalOpts); - - const data: BrowseProductsInput = { - advertiserId: options.advertiserId, - }; - - if (options.channels) { - data.channels = options.channels.split(',').map((c: string) => c.trim()); - } - if (options.countries) { - data.countries = options.countries.split(',').map((c: string) => c.trim()); - } - if (options.brief) { - data.brief = options.brief; - } - if (options.publisherDomain) { - data.publisherDomain = options.publisherDomain; - } - - const result = await client.bundles.browseProducts(data); - formatOutput(result, globalOpts.format as OutputFormat); - } catch (error) { - printError(error instanceof Error ? error.message : 'Unknown error'); - process.exit(1); - } - }); - -// Products sub-commands for bundles -const productsCommand = bundlesCommand.command('products').description('Manage bundle products'); - -/** - * List bundle products - */ -productsCommand - .command('list ') - .description('List all products in a bundle') - .action(async (bundleId: string, _options: unknown, cmd: Command) => { - try { - const globalOpts = cmd.optsWithGlobals() as GlobalOptions; - const client = createClient(globalOpts); - - const result = await client.bundles.products(bundleId).list(); - formatOutput(result, globalOpts.format as OutputFormat); - } catch (error) { - printError(error instanceof Error ? error.message : 'Unknown error'); - process.exit(1); - } - }); - -/** - * Add products to a bundle - */ -productsCommand - .command('add ') - .description('Add products to a bundle') - .requiredOption( - '--products ', - 'Products to add as JSON string (array of {productId, salesAgentId, groupId, groupName, cpm?, budget?})' - ) - .action(async (bundleId: string, options, cmd) => { - try { - const globalOpts = cmd.optsWithGlobals() as GlobalOptions; - const client = createClient(globalOpts); - - const parsed = parseJsonArg(options.products); - if (typeof parsed === 'string') { - printError('Invalid JSON for --products. Expected an array of product objects.'); - process.exit(1); - } - - const data: AddBundleProductsInput = { - products: parsed, - }; - - const result = await client.bundles.products(bundleId).add(data); - formatOutput(result, globalOpts.format as OutputFormat); - printSuccess(`Added ${data.products.length} product(s) to bundle`); - } catch (error) { - printError(error instanceof Error ? error.message : 'Unknown error'); - process.exit(1); - } - }); - -/** - * Remove products from a bundle - */ -productsCommand - .command('remove ') - .description('Remove products from a bundle') - .requiredOption('--product-ids ', 'Comma-separated list of product IDs to remove') - .action(async (bundleId: string, options, cmd) => { - try { - const globalOpts = cmd.optsWithGlobals() as GlobalOptions; - const client = createClient(globalOpts); - - const data: RemoveBundleProductsInput = { - productIds: options.productIds.split(',').map((id: string) => id.trim()), - }; - - await client.bundles.products(bundleId).remove(data); - printSuccess(`Removed ${data.productIds.length} product(s) from bundle`); - } catch (error) { - printError(error instanceof Error ? error.message : 'Unknown error'); - process.exit(1); - } - }); diff --git a/src/cli/commands/campaigns.ts b/src/cli/commands/campaigns.ts index a6f1dde..392fe50 100644 --- a/src/cli/commands/campaigns.ts +++ b/src/cli/commands/campaigns.ts @@ -1,30 +1,22 @@ /** - * Campaign commands - supports discovery, performance, and audience campaign types + * Campaign commands */ import { Command } from 'commander'; import { createClient, GlobalOptions } from '../utils'; import { formatOutput, printError, printSuccess, OutputFormat } from '../format'; import type { - CreateDiscoveryCampaignInput, - CreatePerformanceCampaignInput, - CreateAudienceCampaignInput, - UpdateDiscoveryCampaignInput, - UpdatePerformanceCampaignInput, + CreateCampaignInput, + UpdateCampaignInput, FlightDates, Budget, CampaignConstraints, - PerformanceConfig, - PerformanceObjective, CampaignType, CampaignStatus, } from '../../types'; export const campaignsCommand = new Command('campaigns').description('Manage campaigns'); -/** - * List campaigns - */ campaignsCommand .command('list') .description('List all campaigns') @@ -53,9 +45,6 @@ campaignsCommand } }); -/** - * Get campaign by ID - */ campaignsCommand .command('get ') .description('Get campaign by ID') @@ -72,15 +61,12 @@ campaignsCommand } }); -/** - * Create a discovery campaign - */ campaignsCommand - .command('create-discovery') - .description('Create a discovery campaign') + .command('create') + .description('Create a campaign') .requiredOption('--advertiser-id ', 'Advertiser ID') .requiredOption('--name ', 'Campaign name') - .requiredOption('--bundle-id ', 'Bundle ID for inventory selection') + .requiredOption('--type ', 'Campaign type (discovery, performance, audience)') .requiredOption('--start-date ', 'Start date (ISO format)') .requiredOption('--end-date ', 'End date (ISO format)') .requiredOption('--budget ', 'Total budget amount') @@ -88,152 +74,6 @@ campaignsCommand .option('--pacing ', 'Budget pacing (EVEN, ASAP, FRONTLOADED)', 'EVEN') .option('--daily-cap ', 'Daily spending cap') .option('--brief ', 'Campaign brief/description') - .option('--channels ', 'Comma-separated list of channels (e.g., ctv,display)') - .option('--countries ', 'Comma-separated list of country codes (e.g., US,CA)') - .option('--product-ids ', 'Comma-separated list of product IDs to include') - .action(async (options, cmd) => { - try { - const globalOpts = cmd.optsWithGlobals() as GlobalOptions; - const client = createClient(globalOpts); - - const flightDates: FlightDates = { - startDate: options.startDate, - endDate: options.endDate, - }; - - const budget: Budget = { - total: parseFloat(options.budget), - currency: options.currency, - pacing: options.pacing, - }; - - if (options.dailyCap) { - budget.dailyCap = parseFloat(options.dailyCap); - } - - const constraints: CampaignConstraints = {}; - if (options.channels) { - constraints.channels = options.channels.split(',').map((c: string) => c.trim()); - } - if (options.countries) { - constraints.countries = options.countries.split(',').map((c: string) => c.trim()); - } - - const data: CreateDiscoveryCampaignInput = { - advertiserId: options.advertiserId, - name: options.name, - bundleId: options.bundleId, - flightDates, - budget, - brief: options.brief, - constraints: Object.keys(constraints).length > 0 ? constraints : undefined, - }; - - if (options.productIds) { - data.productIds = options.productIds.split(',').map((id: string) => id.trim()); - } - - const result = await client.campaigns.createDiscovery(data); - formatOutput(result, globalOpts.format as OutputFormat); - printSuccess(`Created discovery campaign: ${result.data.id}`); - } catch (error) { - printError(error instanceof Error ? error.message : 'Unknown error'); - process.exit(1); - } - }); - -/** - * Create a performance campaign - */ -campaignsCommand - .command('create-performance') - .description('Create a new performance campaign') - .requiredOption('--advertiser-id ', 'Advertiser ID') - .requiredOption('--name ', 'Campaign name') - .requiredOption('--start-date ', 'Start date (ISO format)') - .requiredOption('--end-date ', 'End date (ISO format)') - .requiredOption('--budget ', 'Total budget amount') - .requiredOption( - '--objective ', - 'Performance objective (ROAS, CONVERSIONS, LEADS, SALES)' - ) - .option('--currency ', 'Budget currency (default: USD)', 'USD') - .option('--pacing ', 'Budget pacing (EVEN, ASAP, FRONTLOADED)', 'EVEN') - .option('--daily-cap ', 'Daily spending cap') - .option('--target-roas ', 'Target ROAS for optimization') - .option('--channels ', 'Comma-separated list of channels') - .option('--countries ', 'Comma-separated list of country codes') - .action(async (options, cmd) => { - try { - const globalOpts = cmd.optsWithGlobals() as GlobalOptions; - const client = createClient(globalOpts); - - const flightDates: FlightDates = { - startDate: options.startDate, - endDate: options.endDate, - }; - - const budget: Budget = { - total: parseFloat(options.budget), - currency: options.currency, - pacing: options.pacing, - }; - - if (options.dailyCap) { - budget.dailyCap = parseFloat(options.dailyCap); - } - - const performanceConfig: PerformanceConfig = { - objective: options.objective as PerformanceObjective, - }; - - if (options.targetRoas) { - performanceConfig.goals = { - targetRoas: parseFloat(options.targetRoas), - }; - } - - const constraints: CampaignConstraints = {}; - if (options.channels) { - constraints.channels = options.channels.split(',').map((c: string) => c.trim()); - } - if (options.countries) { - constraints.countries = options.countries.split(',').map((c: string) => c.trim()); - } - - const data: CreatePerformanceCampaignInput = { - advertiserId: options.advertiserId, - name: options.name, - flightDates, - budget, - performanceConfig, - constraints: Object.keys(constraints).length > 0 ? constraints : undefined, - }; - - const result = await client.campaigns.createPerformance(data); - formatOutput(result, globalOpts.format as OutputFormat); - printSuccess(`Created performance campaign: ${result.data.id}`); - } catch (error) { - printError(error instanceof Error ? error.message : 'Unknown error'); - process.exit(1); - } - }); - -/** - * Create an audience campaign - */ -campaignsCommand - .command('create-audience') - .description('Create a new audience campaign') - .requiredOption('--advertiser-id ', 'Advertiser ID') - .requiredOption('--name ', 'Campaign name') - .requiredOption('--start-date ', 'Start date (ISO format)') - .requiredOption('--end-date ', 'End date (ISO format)') - .requiredOption('--budget ', 'Total budget amount') - .option('--currency ', 'Budget currency (default: USD)', 'USD') - .option('--pacing ', 'Budget pacing (EVEN, ASAP, FRONTLOADED)', 'EVEN') - .option('--daily-cap ', 'Daily spending cap') - .option('--signals ', 'Comma-separated list of signal IDs') .option('--channels ', 'Comma-separated list of channels') .option('--countries ', 'Comma-separated list of country codes') .action(async (options, cmd) => { @@ -264,39 +104,33 @@ campaignsCommand constraints.countries = options.countries.split(',').map((c: string) => c.trim()); } - const data: CreateAudienceCampaignInput = { + const data: CreateCampaignInput = { advertiserId: options.advertiserId, name: options.name, + type: options.type as CampaignType, flightDates, budget, + brief: options.brief, constraints: Object.keys(constraints).length > 0 ? constraints : undefined, }; - if (options.signals) { - data.signals = options.signals.split(',').map((s: string) => s.trim()); - } - - const result = await client.campaigns.createAudience(data); + const result = await client.campaigns.create(data); formatOutput(result, globalOpts.format as OutputFormat); - printSuccess(`Created audience campaign: ${result.data.id}`); + printSuccess(`Created campaign: ${result.data.id}`); } catch (error) { printError(error instanceof Error ? error.message : 'Unknown error'); process.exit(1); } }); -/** - * Update a discovery campaign - */ campaignsCommand - .command('update-discovery ') - .description('Update a discovery campaign') + .command('update ') + .description('Update a campaign') .option('--name ', 'New name') .option('--start-date ', 'New start date') .option('--end-date ', 'New end date') .option('--budget ', 'New total budget') .option('--brief ', 'New brief') - .option('--product-ids ', 'Comma-separated list of product IDs') .option('--channels ', 'Comma-separated list of channels') .option('--countries ', 'Comma-separated list of country codes') .action(async (id: string, options, cmd) => { @@ -304,7 +138,7 @@ campaignsCommand const globalOpts = cmd.optsWithGlobals() as GlobalOptions; const client = createClient(globalOpts); - const data: UpdateDiscoveryCampaignInput = {}; + const data: UpdateCampaignInput = {}; if (options.name) data.name = options.name; if (options.brief) data.brief = options.brief; @@ -319,10 +153,6 @@ campaignsCommand data.budget = { total: parseFloat(options.budget) }; } - if (options.productIds) { - data.productIds = options.productIds.split(',').map((pid: string) => pid.trim()); - } - const constraints: CampaignConstraints = {}; if (options.channels) { constraints.channels = options.channels.split(',').map((c: string) => c.trim()); @@ -339,88 +169,31 @@ campaignsCommand process.exit(1); } - const result = await client.campaigns.updateDiscovery(id, data); + const result = await client.campaigns.update(id, data); formatOutput(result, globalOpts.format as OutputFormat); - printSuccess('Discovery campaign updated'); + printSuccess('Campaign updated'); } catch (error) { printError(error instanceof Error ? error.message : 'Unknown error'); process.exit(1); } }); -/** - * Update a performance campaign - */ campaignsCommand - .command('update-performance ') - .description('Update a performance campaign') - .option('--name ', 'New name') - .option('--start-date ', 'New start date') - .option('--end-date ', 'New end date') - .option('--budget ', 'New total budget') - .option('--objective ', 'New performance objective (ROAS, CONVERSIONS, LEADS, SALES)') - .option('--target-roas ', 'New target ROAS') - .option('--channels ', 'Comma-separated list of channels') - .option('--countries ', 'Comma-separated list of country codes') - .action(async (id: string, options, cmd) => { + .command('delete ') + .description('Delete a campaign') + .action(async (id: string, _options: unknown, cmd: Command) => { try { const globalOpts = cmd.optsWithGlobals() as GlobalOptions; const client = createClient(globalOpts); - const data: UpdatePerformanceCampaignInput = {}; - - if (options.name) data.name = options.name; - - if (options.startDate || options.endDate) { - data.flightDates = {} as FlightDates; - if (options.startDate) data.flightDates.startDate = options.startDate; - if (options.endDate) data.flightDates.endDate = options.endDate; - } - - if (options.budget) { - data.budget = { total: parseFloat(options.budget) }; - } - - if (options.objective || options.targetRoas) { - data.performanceConfig = {} as PerformanceConfig; - if (options.objective) { - data.performanceConfig.objective = options.objective as PerformanceObjective; - } - if (options.targetRoas) { - data.performanceConfig.goals = { - targetRoas: parseFloat(options.targetRoas), - }; - } - } - - const constraints: CampaignConstraints = {}; - if (options.channels) { - constraints.channels = options.channels.split(',').map((c: string) => c.trim()); - } - if (options.countries) { - constraints.countries = options.countries.split(',').map((c: string) => c.trim()); - } - if (Object.keys(constraints).length > 0) { - data.constraints = constraints; - } - - if (Object.keys(data).length === 0) { - printError('No update fields provided'); - process.exit(1); - } - - const result = await client.campaigns.updatePerformance(id, data); - formatOutput(result, globalOpts.format as OutputFormat); - printSuccess('Performance campaign updated'); + await client.campaigns.delete(id); + printSuccess('Campaign deleted'); } catch (error) { printError(error instanceof Error ? error.message : 'Unknown error'); process.exit(1); } }); -/** - * Execute campaign - */ campaignsCommand .command('execute ') .description('Execute a campaign (go live)') @@ -438,9 +211,6 @@ campaignsCommand } }); -/** - * Pause campaign - */ campaignsCommand .command('pause ') .description('Pause an active campaign') diff --git a/src/cli/commands/conversion-events.ts b/src/cli/commands/conversion-events.ts index bb41803..e69de29 100644 --- a/src/cli/commands/conversion-events.ts +++ b/src/cli/commands/conversion-events.ts @@ -1,118 +0,0 @@ -/** - * Conversion event commands (scoped to an advertiser) - */ - -import { Command } from 'commander'; -import { createClient, GlobalOptions } from '../utils'; -import { formatOutput, printError, printSuccess, OutputFormat } from '../format'; - -export const conversionEventsCommand = new Command('conversion-events').description( - 'Manage conversion events for an advertiser' -); - -conversionEventsCommand - .command('list') - .description('List conversion events for an advertiser') - .requiredOption('--advertiser-id ', 'Advertiser ID') - .action(async (options, cmd) => { - try { - const globalOpts = cmd.optsWithGlobals() as GlobalOptions; - const client = createClient(globalOpts); - - const result = await client.advertisers.conversionEvents(options.advertiserId).list(); - formatOutput(result, globalOpts.format as OutputFormat); - } catch (error) { - printError(error instanceof Error ? error.message : 'Unknown error'); - process.exit(1); - } - }); - -conversionEventsCommand - .command('get ') - .description('Get a conversion event by ID') - .requiredOption('--advertiser-id ', 'Advertiser ID') - .action(async (id: string, options, cmd) => { - try { - const globalOpts = cmd.optsWithGlobals() as GlobalOptions; - const client = createClient(globalOpts); - - const result = await client.advertisers.conversionEvents(options.advertiserId).get(id); - formatOutput(result, globalOpts.format as OutputFormat); - } catch (error) { - printError(error instanceof Error ? error.message : 'Unknown error'); - process.exit(1); - } - }); - -conversionEventsCommand - .command('create') - .description('Create a conversion event') - .requiredOption('--advertiser-id ', 'Advertiser ID') - .requiredOption('--name ', 'Event name') - .requiredOption( - '--type ', - 'Event type (PURCHASE, SIGNUP, LEAD, PAGE_VIEW, ADD_TO_CART, CUSTOM)' - ) - .option('--description ', 'Event description') - .option('--value ', 'Conversion value') - .option('--currency ', 'Currency code (e.g., USD)') - .action(async (options, cmd) => { - try { - const globalOpts = cmd.optsWithGlobals() as GlobalOptions; - const client = createClient(globalOpts); - - const data: Record = { - name: options.name, - type: options.type, - }; - if (options.description) data.description = options.description; - if (options.value) data.value = parseFloat(options.value); - if (options.currency) data.currency = options.currency; - - const result = await client.advertisers - .conversionEvents(options.advertiserId) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .create(data as any); - formatOutput(result, globalOpts.format as OutputFormat); - printSuccess(`Created conversion event: ${result.data.id}`); - } catch (error) { - printError(error instanceof Error ? error.message : 'Unknown error'); - process.exit(1); - } - }); - -conversionEventsCommand - .command('update ') - .description('Update a conversion event') - .requiredOption('--advertiser-id ', 'Advertiser ID') - .option('--name ', 'New name') - .option('--description ', 'New description') - .option('--value ', 'New conversion value') - .option('--currency ', 'New currency code') - .action(async (id: string, options, cmd) => { - try { - const globalOpts = cmd.optsWithGlobals() as GlobalOptions; - const client = createClient(globalOpts); - - const data: Record = {}; - if (options.name) data.name = options.name; - if (options.description) data.description = options.description; - if (options.value) data.value = parseFloat(options.value); - if (options.currency) data.currency = options.currency; - - if (Object.keys(data).length === 0) { - printError('No update fields provided'); - process.exit(1); - } - - const result = await client.advertisers - .conversionEvents(options.advertiserId) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .update(id, data as any); - formatOutput(result, globalOpts.format as OutputFormat); - printSuccess('Conversion event updated'); - } catch (error) { - printError(error instanceof Error ? error.message : 'Unknown error'); - process.exit(1); - } - }); diff --git a/src/cli/commands/creative-sets.ts b/src/cli/commands/creative-sets.ts index 21e11ed..e69de29 100644 --- a/src/cli/commands/creative-sets.ts +++ b/src/cli/commands/creative-sets.ts @@ -1,102 +0,0 @@ -/** - * Creative set commands (scoped to an advertiser) - */ - -import { Command } from 'commander'; -import { createClient, GlobalOptions } from '../utils'; -import { formatOutput, printError, printSuccess, OutputFormat } from '../format'; - -export const creativeSetsCommand = new Command('creative-sets').description( - 'Manage creative sets for an advertiser' -); - -creativeSetsCommand - .command('list') - .description('List creative sets for an advertiser') - .requiredOption('--advertiser-id ', 'Advertiser ID') - .action(async (options, cmd) => { - try { - const globalOpts = cmd.optsWithGlobals() as GlobalOptions; - const client = createClient(globalOpts); - - const result = await client.advertisers.creativeSets(options.advertiserId).list(); - formatOutput(result, globalOpts.format as OutputFormat); - } catch (error) { - printError(error instanceof Error ? error.message : 'Unknown error'); - process.exit(1); - } - }); - -creativeSetsCommand - .command('create') - .description('Create a creative set') - .requiredOption('--advertiser-id ', 'Advertiser ID') - .requiredOption('--name ', 'Creative set name') - .requiredOption('--type ', 'Creative set type (e.g., video, display)') - .action(async (options, cmd) => { - try { - const globalOpts = cmd.optsWithGlobals() as GlobalOptions; - const client = createClient(globalOpts); - - const result = await client.advertisers.creativeSets(options.advertiserId).create({ - name: options.name, - type: options.type, - }); - formatOutput(result, globalOpts.format as OutputFormat); - printSuccess(`Created creative set: ${result.data.id}`); - } catch (error) { - printError(error instanceof Error ? error.message : 'Unknown error'); - process.exit(1); - } - }); - -creativeSetsCommand - .command('add-asset ') - .description('Add an asset to a creative set') - .requiredOption('--advertiser-id ', 'Advertiser ID') - .requiredOption('--asset-url ', 'Asset URL') - .requiredOption('--name ', 'Asset name') - .requiredOption('--type ', 'Asset type (e.g., video, image)') - .option('--duration ', 'Asset duration in seconds (for video)') - .action(async (creativeSetId: string, options, cmd) => { - try { - const globalOpts = cmd.optsWithGlobals() as GlobalOptions; - const client = createClient(globalOpts); - - const data: Record = { - assetUrl: options.assetUrl, - name: options.name, - type: options.type, - }; - if (options.duration) data.duration = parseInt(options.duration, 10); - - const result = await client.advertisers - .creativeSets(options.advertiserId) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .addAsset(creativeSetId, data as any); - formatOutput(result, globalOpts.format as OutputFormat); - printSuccess(`Added asset: ${result.data.id}`); - } catch (error) { - printError(error instanceof Error ? error.message : 'Unknown error'); - process.exit(1); - } - }); - -creativeSetsCommand - .command('remove-asset ') - .description('Remove an asset from a creative set') - .requiredOption('--advertiser-id ', 'Advertiser ID') - .action(async (creativeSetId: string, assetId: string, options, cmd) => { - try { - const globalOpts = cmd.optsWithGlobals() as GlobalOptions; - const client = createClient(globalOpts); - - await client.advertisers - .creativeSets(options.advertiserId) - .removeAsset(creativeSetId, assetId); - printSuccess(`Removed asset: ${assetId}`); - } catch (error) { - printError(error instanceof Error ? error.message : 'Unknown error'); - process.exit(1); - } - }); diff --git a/src/cli/commands/index.ts b/src/cli/commands/index.ts index 8dffd9e..f87c02a 100644 --- a/src/cli/commands/index.ts +++ b/src/cli/commands/index.ts @@ -3,12 +3,7 @@ */ export { advertisersCommand } from './advertisers'; -export { bundlesCommand } from './bundles'; export { campaignsCommand } from './campaigns'; export { configCommand } from './config'; -export { conversionEventsCommand } from './conversion-events'; -export { creativeSetsCommand } from './creative-sets'; -export { storefrontCommand, agentsCommand } from './storefront'; export { reportingCommand } from './reporting'; -export { salesAgentsCommand } from './sales-agents'; export { loginCommand, logoutCommand } from './login'; diff --git a/src/cli/commands/sales-agents.ts b/src/cli/commands/sales-agents.ts index 7cda4ff..e69de29 100644 --- a/src/cli/commands/sales-agents.ts +++ b/src/cli/commands/sales-agents.ts @@ -1,78 +0,0 @@ -/** - * Sales agent commands for browsing agents and registering accounts - */ - -import { Command } from 'commander'; -import { createClient, GlobalOptions } from '../utils'; -import { formatOutput, printError, printSuccess, OutputFormat } from '../format'; - -export const salesAgentsCommand = new Command('sales-agents').description( - 'Browse and connect with sales agents' -); - -/** - * List sales agents - */ -salesAgentsCommand - .command('list') - .description('List available sales agents') - .option('--status ', 'Filter by status (PENDING, ACTIVE)') - .option('--relationship ', 'Filter by relationship (SELF, MARKETPLACE)') - .option('--name ', 'Filter by agent name') - .option('--limit ', 'Maximum number of results', '20') - .option('--offset ', 'Number of results to skip', '0') - .action(async (options, cmd) => { - try { - const globalOpts = cmd.optsWithGlobals() as GlobalOptions; - const client = createClient(globalOpts); - - const result = await client.salesAgents.list({ - status: options.status, - relationship: options.relationship, - name: options.name, - limit: parseInt(options.limit, 10), - offset: parseInt(options.offset, 10), - }); - - formatOutput(result, globalOpts.format as OutputFormat); - } catch (error) { - printError(error instanceof Error ? error.message : 'Unknown error'); - process.exit(1); - } - }); - -/** - * Register an account with a sales agent - */ -salesAgentsCommand - .command('register-account ') - .description('Register an account for a sales agent') - .requiredOption('--advertiser-id ', 'Advertiser ID to connect') - .requiredOption('--account-id ', 'Account identifier for this agent') - .option('--auth-token ', 'Bearer token for API_KEY agents') - .action(async (agentId: string, options, cmd) => { - try { - const globalOpts = cmd.optsWithGlobals() as GlobalOptions; - const client = createClient(globalOpts); - - const data: { - advertiserId: string; - accountIdentifier: string; - auth?: { type: string; token: string }; - } = { - advertiserId: options.advertiserId, - accountIdentifier: options.accountId, - }; - - if (options.authToken) { - data.auth = { type: 'bearer', token: options.authToken }; - } - - const result = await client.salesAgents.registerAccount(agentId, data); - formatOutput(result, globalOpts.format as OutputFormat); - printSuccess('Account registered'); - } catch (error) { - printError(error instanceof Error ? error.message : 'Unknown error'); - process.exit(1); - } - }); diff --git a/src/cli/commands/storefront.ts b/src/cli/commands/storefront.ts index 642c35f..e69de29 100644 --- a/src/cli/commands/storefront.ts +++ b/src/cli/commands/storefront.ts @@ -1,243 +0,0 @@ -/** - * Storefront and agent commands for the Storefront persona - */ - -import { Command } from 'commander'; -import { createClient, GlobalOptions } from '../utils'; -import { formatOutput, printError, printSuccess, OutputFormat } from '../format'; - -export const storefrontCommand = new Command('storefront').description( - 'Manage storefront and agents (storefront persona)' -); - -// ── Storefront CRUD ───────────────────────────────────────────── - -storefrontCommand - .command('get') - .description('Get storefront details') - .action(async (_options: unknown, cmd: Command) => { - try { - const globalOpts = cmd.optsWithGlobals() as GlobalOptions; - const client = createClient(globalOpts); - - const result = await client.storefront.get(); - formatOutput(result, globalOpts.format as OutputFormat); - } catch (error) { - printError(error instanceof Error ? error.message : 'Unknown error'); - process.exit(1); - } - }); - -storefrontCommand - .command('create') - .description('Create a new storefront') - .requiredOption('--platform-id ', 'Platform ID') - .requiredOption('--name ', 'Storefront name') - .option('--publisher-domain ', 'Publisher domain') - .option('--plan ', 'Plan') - .action(async (options, cmd) => { - try { - const globalOpts = cmd.optsWithGlobals() as GlobalOptions; - const client = createClient(globalOpts); - - const result = await client.storefront.create({ - platformId: options.platformId, - name: options.name, - publisherDomain: options.publisherDomain, - plan: options.plan, - }); - - formatOutput(result, globalOpts.format as OutputFormat); - printSuccess(`Created storefront: ${result.data.platformId}`); - } catch (error) { - printError(error instanceof Error ? error.message : 'Unknown error'); - process.exit(1); - } - }); - -storefrontCommand - .command('update') - .description('Update the storefront') - .option('--name ', 'New name') - .option('--publisher-domain ', 'New publisher domain') - .option('--plan ', 'New plan') - .option('--enabled ', 'Enable or disable') - .action(async (options, cmd) => { - try { - const globalOpts = cmd.optsWithGlobals() as GlobalOptions; - const client = createClient(globalOpts); - - const updateData: Record = {}; - if (options.name) updateData.name = options.name; - if (options.publisherDomain) updateData.publisherDomain = options.publisherDomain; - if (options.plan) updateData.plan = options.plan; - if (options.enabled !== undefined) updateData.enabled = options.enabled === 'true'; - - if (Object.keys(updateData).length === 0) { - printError('No update fields provided'); - process.exit(1); - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await client.storefront.update(updateData as any); - formatOutput(result, globalOpts.format as OutputFormat); - printSuccess('Storefront updated'); - } catch (error) { - printError(error instanceof Error ? error.message : 'Unknown error'); - process.exit(1); - } - }); - -storefrontCommand - .command('delete') - .description('Delete the storefront') - .action(async (_options: unknown, cmd: Command) => { - try { - const globalOpts = cmd.optsWithGlobals() as GlobalOptions; - const client = createClient(globalOpts); - - await client.storefront.delete(); - printSuccess('Storefront deleted'); - } catch (error) { - printError(error instanceof Error ? error.message : 'Unknown error'); - process.exit(1); - } - }); - -// ── Agent management ───────────────────────────────────────────── - -const agentsCommand = new Command('agents').description('Manage agents'); - -agentsCommand - .command('list') - .description('List all agents') - .option('--type ', 'Filter by type (SALES, SIGNAL, CREATIVE, OUTCOME)') - .option('--status ', 'Filter by status (PENDING, ACTIVE, DISABLED)') - .option('--relationship ', 'Filter by relationship (SELF, MARKETPLACE)') - .action(async (options, cmd) => { - try { - const globalOpts = cmd.optsWithGlobals() as GlobalOptions; - const client = createClient(globalOpts); - - const result = await client.agents.list({ - type: options.type, - status: options.status, - relationship: options.relationship, - }); - - formatOutput(result, globalOpts.format as OutputFormat); - } catch (error) { - printError(error instanceof Error ? error.message : 'Unknown error'); - process.exit(1); - } - }); - -agentsCommand - .command('get ') - .description('Get agent details') - .action(async (agentId: string, _options: unknown, cmd: Command) => { - try { - const globalOpts = cmd.optsWithGlobals() as GlobalOptions; - const client = createClient(globalOpts); - - const result = await client.agents.get(agentId); - formatOutput(result, globalOpts.format as OutputFormat); - } catch (error) { - printError(error instanceof Error ? error.message : 'Unknown error'); - process.exit(1); - } - }); - -agentsCommand - .command('update ') - .description('Update an agent') - .option('--name ', 'New name') - .option('--description ', 'New description') - .option('--status ', 'New status (PENDING, ACTIVE, DISABLED)') - .option('--endpoint-url ', 'New endpoint URL') - .action(async (agentId: string, options, cmd) => { - try { - const globalOpts = cmd.optsWithGlobals() as GlobalOptions; - const client = createClient(globalOpts); - - const data: Record = {}; - if (options.name) data.name = options.name; - if (options.description) data.description = options.description; - if (options.status) data.status = options.status; - if (options.endpointUrl) data.endpointUrl = options.endpointUrl; - - if (Object.keys(data).length === 0) { - printError('No update fields provided'); - process.exit(1); - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await client.agents.update(agentId, data as any); - formatOutput(result, globalOpts.format as OutputFormat); - printSuccess('Agent updated'); - } catch (error) { - printError(error instanceof Error ? error.message : 'Unknown error'); - process.exit(1); - } - }); - -agentsCommand - .command('oauth-authorize ') - .description('Start agent-level OAuth flow') - .action(async (agentId: string, _options: unknown, cmd: Command) => { - try { - const globalOpts = cmd.optsWithGlobals() as GlobalOptions; - const client = createClient(globalOpts); - - const result = await client.agents.authorizeOAuth(agentId); - formatOutput(result, globalOpts.format as OutputFormat); - printSuccess('Present the authorizationUrl to the user to complete OAuth'); - } catch (error) { - printError(error instanceof Error ? error.message : 'Unknown error'); - process.exit(1); - } - }); - -agentsCommand - .command('oauth-authorize-account ') - .description('Start per-account OAuth flow') - .action(async (agentId: string, _options: unknown, cmd: Command) => { - try { - const globalOpts = cmd.optsWithGlobals() as GlobalOptions; - const client = createClient(globalOpts); - - const result = await client.agents.authorizeAccountOAuth(agentId); - formatOutput(result, globalOpts.format as OutputFormat); - printSuccess('Present the authorizationUrl to the user to complete OAuth'); - } catch (error) { - printError(error instanceof Error ? error.message : 'Unknown error'); - process.exit(1); - } - }); - -agentsCommand - .command('oauth-exchange ') - .description('Exchange OAuth code for tokens') - .requiredOption('--code ', 'Authorization code') - .requiredOption('--state ', 'State parameter') - .action(async (agentId: string, options, cmd) => { - try { - const globalOpts = cmd.optsWithGlobals() as GlobalOptions; - const client = createClient(globalOpts); - - const result = await client.agents.exchangeOAuthCode(agentId, { - code: options.code, - state: options.state, - }); - - formatOutput(result, globalOpts.format as OutputFormat); - printSuccess('OAuth code exchanged'); - } catch (error) { - printError(error instanceof Error ? error.message : 'Unknown error'); - process.exit(1); - } - }); - -storefrontCommand.addCommand(agentsCommand); - -export { agentsCommand }; diff --git a/src/cli/index.ts b/src/cli/index.ts index 6ea055a..ff7a738 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -8,26 +8,19 @@ * * Examples: * scope3 config set apiKey sk_xxx - * scope3 --persona buyer advertisers list - * scope3 --persona storefront storefront get - * scope3 campaigns create-discovery --advertiser-id xxx --bundle-id yyy --name "Q1 Campaign" - * scope3 bundles create --advertiser-id xxx --channels ctv,display + * scope3 advertisers list + * scope3 campaigns create --advertiser-id xxx --type discovery --name "Q1 Campaign" */ import { Command } from 'commander'; import chalk from 'chalk'; import { advertisersCommand, - bundlesCommand, campaignsCommand, configCommand, - conversionEventsCommand, - creativeSetsCommand, loginCommand, logoutCommand, - storefrontCommand, reportingCommand, - salesAgentsCommand, } from './commands'; import { loadConfig } from './utils'; @@ -43,7 +36,7 @@ program ' scope3 advertisers list Run commands\n\n' + 'Documentation: https://github.com/scope3data/agentic-client#cli' ) - .version('2.0.0', '-V, --cli-version') + .version('3.0.0', '-V, --cli-version') .option('--api-key ', 'API key (or use: config set apiKey )') .option('--api-version ', 'API version: v1, v2, or latest (default: v2)') .option('--environment ', 'Environment: production or staging (default: production)') @@ -52,12 +45,10 @@ program .option('--debug', 'Enable debug mode') .option('--persona ', 'API persona: buyer or storefront (default: buyer)'); -// Warn if the OAuth session token is expired before running any command program.hook('preAction', (_thisCommand, actionCommand) => { const skipCommands = ['login', 'logout', 'config', 'commands']; if (skipCommands.includes(actionCommand.name())) return; - // If an explicit key is provided, OAuth session state is irrelevant if (_thisCommand.opts().apiKey || process.env.SCOPE3_API_KEY) return; const config = loadConfig(); @@ -70,114 +61,13 @@ program.hook('preAction', (_thisCommand, actionCommand) => { } }); -// Add commands program.addCommand(loginCommand); program.addCommand(logoutCommand); program.addCommand(advertisersCommand); -program.addCommand(bundlesCommand); program.addCommand(campaignsCommand); program.addCommand(configCommand); -program.addCommand(conversionEventsCommand); -program.addCommand(creativeSetsCommand); -program.addCommand(storefrontCommand); program.addCommand(reportingCommand); -program.addCommand(salesAgentsCommand); -// Add 'commands' command to list all available commands -const commandsCmd = new Command('commands') - .description('List all available commands') - .action(() => { - console.log(chalk.bold('\nScope3 CLI - Commands by Persona\n')); - - // Buyer persona (default) - console.log( - chalk.green.bold('BUYER PERSONA') + chalk.gray(' (default, or use --persona buyer)') - ); - console.log( - chalk.gray('For programmatic ad buyers - manage advertisers, campaigns, and inventory\n') - ); - - console.log(chalk.cyan(' advertisers')); - console.log(' list List all advertisers'); - console.log(' get Get advertiser by ID'); - console.log(' create Create a new advertiser'); - console.log(' update Update an advertiser'); - console.log(' delete Delete an advertiser'); - - console.log(chalk.cyan('\n bundles')); - console.log(' create Create a new media bundle'); - console.log(' discover-products Discover available products for a bundle'); - console.log(' browse-products Browse products without creating a bundle'); - console.log(' products list List products in a bundle'); - console.log(' products add Add products to a bundle'); - console.log(' products remove Remove products from a bundle'); - - console.log(chalk.cyan('\n campaigns')); - console.log(' list List all campaigns'); - console.log(' get Get campaign by ID'); - console.log(' create-discovery Create a discovery campaign'); - console.log(' create-performance Create a performance campaign'); - console.log(' create-audience Create an audience campaign'); - console.log(' update-discovery Update a discovery campaign'); - console.log(' update-performance Update a performance campaign'); - console.log(' execute Execute a campaign (go live)'); - console.log(' pause Pause an active campaign'); - - console.log(chalk.cyan('\n reporting')); - console.log(' get Get reporting metrics'); - - console.log(chalk.cyan('\n sales-agents')); - console.log(' list List available sales agents'); - console.log(' register-account Register an account for a sales agent'); - - console.log(chalk.cyan('\n conversion-events')); - console.log(' list List conversion events for an advertiser'); - console.log(' get Get a conversion event by ID'); - console.log(' create Create a conversion event'); - console.log(' update Update a conversion event'); - - console.log(chalk.cyan('\n creative-sets')); - console.log(' list List creative sets for an advertiser'); - console.log(' create Create a creative set'); - console.log(' add-asset Add an asset to a creative set'); - console.log(' remove-asset Remove an asset from a creative set'); - - // Storefront persona - console.log( - chalk.blue.bold('\n\nSTOREFRONT PERSONA') + chalk.gray(' (use --persona storefront)') - ); - console.log(chalk.gray('For storefronts - manage storefront, inventory sources, and agents\n')); - - console.log(chalk.cyan(' storefront')); - console.log(' get Get storefront details'); - console.log(' create Create a new storefront'); - console.log(' update Update the storefront'); - console.log(' delete Delete the storefront'); - - console.log(chalk.cyan('\n storefront agents')); - console.log(' list List all agents'); - console.log(' get Get agent details'); - console.log(' update Update an agent'); - console.log(' oauth-authorize Start agent-level OAuth flow'); - console.log(' oauth-authorize-account Start per-account OAuth flow'); - console.log(' oauth-exchange Exchange OAuth code for tokens'); - - // Config (all personas) - console.log(chalk.yellow.bold('\n\nCONFIGURATION') + chalk.gray(' (all personas)')); - console.log(chalk.cyan('\n config')); - console.log(' set Set a configuration value'); - console.log(' get [key] Get configuration value(s)'); - console.log(' clear Clear all configuration'); - console.log(' path Show configuration file path'); - - console.log(chalk.gray('\n─────────────────────────────────────────────────────────────')); - console.log(chalk.gray('Run "scope3 --help" for details on a specific command.')); - console.log(chalk.gray('Docs: https://github.com/scope3data/agentic-client#cli\n')); - }); - -program.addCommand(commandsCmd); - -// Default action when no command provided but options were given program.action(() => { const opts = program.opts(); const hasOptions = opts.apiKey || opts.environment || opts.persona; @@ -200,7 +90,6 @@ program.action(() => { program.help(); }); -// Error handling program.exitOverride((err) => { if (err.code === 'commander.help') { process.exit(0); @@ -211,5 +100,4 @@ program.exitOverride((err) => { process.exit(1); }); -// Parse and execute program.parse(); diff --git a/src/client.ts b/src/client.ts index 9e34394..646f87a 100644 --- a/src/client.ts +++ b/src/client.ts @@ -12,18 +12,17 @@ import type { Scope3ClientConfig, ApiVersion, Persona } from './types'; import { RestAdapter } from './adapters/rest'; import { AdvertisersResource } from './resources/advertisers'; import { CampaignsResource } from './resources/campaigns'; -import { BundlesResource } from './resources/bundles'; -import { SignalsResource } from './resources/signals'; import { ReportingResource } from './resources/reporting'; -import { SalesAgentsResource } from './resources/sales-agents'; -import { StorefrontResource } from './resources/storefront'; -import { InventorySourcesResource } from './resources/inventory-sources'; -import { AgentsResource } from './resources/agents'; -import { ReadinessResource } from './resources/readiness'; -import { BillingResource } from './resources/billing'; -import { NotificationsResource } from './resources/notifications'; import { TasksResource } from './resources/tasks'; import { PropertyListChecksResource } from './resources/property-lists'; +import { DiscoveryResource } from './resources/discovery'; +import { AccountsResource } from './resources/accounts'; +import { NotificationPreferencesResource } from './resources/notification-preferences'; +import { ModerationResource } from './resources/moderation'; +import { StorefrontsResource } from './resources/storefronts'; +import { AuditLogsResource } from './resources/audit-logs'; +import { PlanningBriefsResource } from './resources/planning-briefs'; +import { BuyerBillingResource } from './resources/billing'; import { fetchSkillMd, parseSkillMd, ParsedSkill } from './skill'; /** @@ -32,33 +31,24 @@ import { fetchSkillMd, parseSkillMd, ParsedSkill } from './skill'; * * @example * ```typescript - * // Buyer persona * const client = new Scope3Client({ apiKey: 'token', persona: 'buyer' }); * const advertisers = await client.advertisers.list(); - * - * // Storefront persona - * const sfClient = new Scope3Client({ apiKey: 'token', persona: 'storefront' }); - * const sf = await sfClient.storefront.get(); * ``` */ export class Scope3Client { - // Buyer persona resources - private _advertisers?: AdvertisersResource; - private _campaigns?: CampaignsResource; - private _bundles?: BundlesResource; - private _signals?: SignalsResource; - private _reporting?: ReportingResource; - private _salesAgents?: SalesAgentsResource; - private _tasks?: TasksResource; - private _propertyListChecks?: PropertyListChecksResource; - - // Storefront persona resources - private _storefront?: StorefrontResource; - private _inventorySources?: InventorySourcesResource; - private _agents?: AgentsResource; - private _readiness?: ReadinessResource; - private _billing?: BillingResource; - private _notifications?: NotificationsResource; + private readonly _advertisers: AdvertisersResource; + private readonly _campaigns: CampaignsResource; + private readonly _reporting: ReportingResource; + private readonly _tasks: TasksResource; + private readonly _propertyListChecks: PropertyListChecksResource; + private readonly _discovery: DiscoveryResource; + private readonly _accounts: AccountsResource; + private readonly _notificationPreferences: NotificationPreferencesResource; + private readonly _moderation: ModerationResource; + private readonly _storefronts: StorefrontsResource; + private readonly _auditLogs: AuditLogsResource; + private readonly _planningBriefs: PlanningBriefsResource; + private readonly _billing: BuyerBillingResource; private readonly adapter: RestAdapter; @@ -80,135 +70,72 @@ export class Scope3Client { this.persona = config.persona; this.adapter = new RestAdapter({ ...config, apiKey: trimmedKey }); - switch (this.persona) { - case 'buyer': - this._advertisers = new AdvertisersResource(this.adapter); - this._campaigns = new CampaignsResource(this.adapter); - this._bundles = new BundlesResource(this.adapter); - this._signals = new SignalsResource(this.adapter); - this._reporting = new ReportingResource(this.adapter); - this._salesAgents = new SalesAgentsResource(this.adapter); - this._tasks = new TasksResource(this.adapter); - this._propertyListChecks = new PropertyListChecksResource(this.adapter); - break; - case 'storefront': - this._storefront = new StorefrontResource(this.adapter); - this._inventorySources = new InventorySourcesResource(this.adapter); - this._agents = new AgentsResource(this.adapter); - this._readiness = new ReadinessResource(this.adapter); - this._billing = new BillingResource(this.adapter); - this._notifications = new NotificationsResource(this.adapter); - break; - default: { - const _exhaustive: never = this.persona; - throw new Error(`Unknown persona: ${_exhaustive}`); - } - } + this._advertisers = new AdvertisersResource(this.adapter); + this._campaigns = new CampaignsResource(this.adapter); + this._reporting = new ReportingResource(this.adapter); + this._tasks = new TasksResource(this.adapter); + this._propertyListChecks = new PropertyListChecksResource(this.adapter); + this._discovery = new DiscoveryResource(this.adapter); + this._accounts = new AccountsResource(this.adapter); + this._notificationPreferences = new NotificationPreferencesResource(this.adapter); + this._moderation = new ModerationResource(this.adapter); + this._storefronts = new StorefrontsResource(this.adapter); + this._auditLogs = new AuditLogsResource(this.adapter); + this._planningBriefs = new PlanningBriefsResource(this.adapter); + this._billing = new BuyerBillingResource(this.adapter); } - // ── Buyer persona resources ────────────────────────────────────── - get advertisers(): AdvertisersResource { - if (!this._advertisers) { - throw new Error('advertisers is only available with the buyer persona'); - } return this._advertisers; } get campaigns(): CampaignsResource { - if (!this._campaigns) { - throw new Error('campaigns is only available with the buyer persona'); - } return this._campaigns; } - get bundles(): BundlesResource { - if (!this._bundles) { - throw new Error('bundles is only available with the buyer persona'); - } - return this._bundles; - } - - get signals(): SignalsResource { - if (!this._signals) { - throw new Error('signals is only available with the buyer persona'); - } - return this._signals; - } - get reporting(): ReportingResource { - if (!this._reporting) { - throw new Error('reporting is only available with the buyer persona'); - } return this._reporting; } - get salesAgents(): SalesAgentsResource { - if (!this._salesAgents) { - throw new Error('salesAgents is only available with the buyer persona'); - } - return this._salesAgents; - } - get tasks(): TasksResource { - if (!this._tasks) { - throw new Error('tasks is only available with the buyer persona'); - } return this._tasks; } get propertyListChecks(): PropertyListChecksResource { - if (!this._propertyListChecks) { - throw new Error('propertyListChecks is only available with the buyer persona'); - } return this._propertyListChecks; } - // ── Storefront persona resources ───────────────────────────────── + get discovery(): DiscoveryResource { + return this._discovery; + } - get storefront(): StorefrontResource { - if (!this._storefront) { - throw new Error('storefront is only available with the storefront persona'); - } - return this._storefront; + get accounts(): AccountsResource { + return this._accounts; } - get inventorySources(): InventorySourcesResource { - if (!this._inventorySources) { - throw new Error('inventorySources is only available with the storefront persona'); - } - return this._inventorySources; + get notificationPreferences(): NotificationPreferencesResource { + return this._notificationPreferences; } - get agents(): AgentsResource { - if (!this._agents) { - throw new Error('agents is only available with the storefront persona'); - } - return this._agents; + get moderation(): ModerationResource { + return this._moderation; } - get readiness(): ReadinessResource { - if (!this._readiness) { - throw new Error('readiness is only available with the storefront persona'); - } - return this._readiness; + get storefronts(): StorefrontsResource { + return this._storefronts; } - get billing(): BillingResource { - if (!this._billing) { - throw new Error('billing is only available with the storefront persona'); - } - return this._billing; + get auditLogs(): AuditLogsResource { + return this._auditLogs; } - get notifications(): NotificationsResource { - if (!this._notifications) { - throw new Error('notifications is only available with the storefront persona'); - } - return this._notifications; + get planningBriefs(): PlanningBriefsResource { + return this._planningBriefs; } - // ── Shared methods ─────────────────────────────────────────────── + get billing(): BuyerBillingResource { + return this._billing; + } async getSkill(): Promise { if (!this.skillPromise) { diff --git a/src/index.ts b/src/index.ts index cc49556..1e9eb0d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -49,21 +49,18 @@ export type { BaseAdapter } from './adapters/base'; // ── Resources (used by Scope3Client, exported for typing) ────────── export { + AccountsResource, AdvertisersResource, - AgentsResource, - BillingResource, - BundlesResource, - BundleProductsResource, + AuditLogsResource, + BuyerBillingResource, CampaignsResource, - ConversionEventsResource, - CreativeSetsResource, - InventorySourcesResource, - NotificationsResource, - ReadinessResource, + CreativesResource, + DiscoveryResource, + ModerationResource, + NotificationPreferencesResource, + PlanningBriefsResource, ReportingResource, - SalesAgentsResource, - SignalsResource, - StorefrontResource, + StorefrontsResource, TestCohortsResource, EventSourcesResource, MeasurementDataResource, @@ -73,7 +70,6 @@ export { TasksResource, PropertyListsResource, PropertyListChecksResource, - CreativesResource, } from './resources'; // ── skill.md support ─────────────────────────────────────────────── @@ -115,7 +111,7 @@ export type { CreateAdvertiserInput, UpdateAdvertiserInput, ListAdvertisersParams, - // Linked Brand (resolved from advertiser brandDomain) + // Linked Brand LinkedBrand, BrandManifest, BrandLogo, @@ -132,37 +128,9 @@ export type { CampaignConstraints, PerformanceObjective, PerformanceConfig, - CreateDiscoveryCampaignInput, - UpdateDiscoveryCampaignInput, - CreatePerformanceCampaignInput, - UpdatePerformanceCampaignInput, - CreateAudienceCampaignInput, + CreateCampaignInput, + UpdateCampaignInput, ListCampaignsParams, - // Bundle - Bundle, - CreateBundleInput, - DiscoverProductsParams, - DiscoverProductsResponse, - ProductGroup, - Product, - ProductSummary, - BudgetContext, - BundleProductInput, - AddBundleProductsInput, - RemoveBundleProductsInput, - BundleProductsResponse, - SelectedBundleProduct, - BrowseProductsInput, - // Conversion Events - ConversionEvent, - ConversionEventType, - CreateConversionEventInput, - UpdateConversionEventInput, - // Creative Sets - CreativeSet, - CreateCreativeSetInput, - CreativeAsset, - CreateCreativeAssetInput, // Test Cohorts TestCohort, CreateTestCohortInput, @@ -177,51 +145,20 @@ export type { ReportingPackage, ReportingTimeseriesResponse, ReportingTimeseriesEntry, - // Sales Agents - SalesAgent, - SalesAgentAccount, - ListSalesAgentsParams, - RegisterSalesAgentAccountInput, - // Signals - Signal, - DiscoverSignalsInput, - // Storefront - Storefront, - CreateStorefrontInput, - UpdateStorefrontInput, - // Inventory Sources - InventorySource, - InventorySourceExecutionType, - CreateInventorySourceInput, - UpdateInventorySourceInput, - // Storefront Readiness - ReadinessStatus, - ReadinessCheck, - StorefrontReadiness, - // Storefront Billing - BillingFee, - StorefrontBilling, - StorefrontBillingConfig, - StripeConnectResponse, - // Notifications - Notification, - ListNotificationsParams, - // Agent - Agent, - AgentType, - AgentStatus, - AgentAuthenticationType, - AgentProtocol, - UpdateAgentInput, - ListAgentsParams, - OAuthAuthorizeResponse, - OAuthCallbackInput, // Event Sources EventSource, CreateEventSourceInput, UpdateEventSourceInput, // Measurement Data MeasurementDataSync, + MeasurementConfig, + UpdateMeasurementConfigInput, + MeasurementSource, + CreateMeasurementSourceInput, + UpdateMeasurementSourceInput, + UploadMeasurementRecordsInput, + UploadContextRecordsInput, + MeasurementFreshness, // Catalogs Catalog, CatalogSync, @@ -243,9 +180,39 @@ export type { UpdatePropertyListInput, PropertyListCheck, PropertyListReport, - // Billing - BillingStatus, - BillingTransaction, - BillingPayout, - ListBillingParams, + // Discovery + DiscoverProductsInput, + DiscoveryProduct, + AddProductsInput, + RemoveProductsInput, + ApplyProposalInput, + // Accounts + Account, + CreateChildAccountInput, + UpdateDomainInput, + MembershipSettings, + UpdateMembershipInput, + // Notification Preferences + NotificationPreferences, + UpdateNotificationPreferencesInput, + // Moderation + ModerationCheckInput, + ModerationCheckResult, + // Storefronts (buyer browsing) + BuyerStorefront, + StorefrontCredential, + RegisterCredentialsInput, + // Audit Logs + AuditLog, + ListAuditLogsParams, + // Planning Briefs + PlanningBrief, + CreatePlanningBriefInput, + PlanningBriefResponse, + // Buyer Billing + BuyerInvoice, + BuyerPendingInvoiceItem, + // Event Summary + EventSummary, + LogEventInput, } from './types'; diff --git a/src/resources/accounts.ts b/src/resources/accounts.ts new file mode 100644 index 0000000..1f56016 --- /dev/null +++ b/src/resources/accounts.ts @@ -0,0 +1,92 @@ +import { type BaseAdapter, validateResourceId } from '../adapters/base'; +import type { + Account, + CreateChildAccountInput, + UpdateDomainInput, + MembershipSettings, + UpdateMembershipInput, + ApiResponse, +} from '../types'; + +/** + * Resource for managing accounts + */ +export class AccountsResource { + constructor(private readonly adapter: BaseAdapter) {} + + /** + * Get the current account + * @returns Current account details + */ + async getCurrent(): Promise> { + return this.adapter.request>('GET', '/accounts/current'); + } + + /** + * List all accounts + * @returns List of accounts + */ + async list(): Promise> { + return this.adapter.request>('GET', '/accounts'); + } + + /** + * Create a child account + * @param data Child account creation data + * @returns Created child account + */ + async createChild(data: CreateChildAccountInput): Promise> { + return this.adapter.request>('POST', '/accounts/create-child', data); + } + + /** + * Update an account's domain + * @param customerId Customer ID + * @param data Domain update data + * @returns Updated account + */ + async updateDomain(customerId: string, data: UpdateDomainInput): Promise> { + return this.adapter.request>( + 'PATCH', + `/accounts/${validateResourceId(customerId)}/domain`, + data + ); + } + + /** + * Delete an account + * @param customerId Customer ID + */ + async delete(customerId: string): Promise { + await this.adapter.request('DELETE', `/accounts/${validateResourceId(customerId)}`); + } + + /** + * Get membership settings for an account + * @param customerId Customer ID + * @returns Membership settings + */ + async getMembership(customerId: string): Promise> { + return this.adapter.request>( + 'GET', + `/accounts/${validateResourceId(customerId)}/membership` + ); + } + + /** + * Update membership settings for an account + * @param customerId Customer ID + * @param data Membership update data + * @returns Updated membership settings + */ + async updateMembership( + customerId: string, + data: UpdateMembershipInput + ): Promise> { + return this.adapter.request>( + 'PATCH', + `/accounts/${validateResourceId(customerId)}/membership`, + data + ); + } +} diff --git a/src/resources/advertisers.ts b/src/resources/advertisers.ts index 68ac42b..3db0483 100644 --- a/src/resources/advertisers.ts +++ b/src/resources/advertisers.ts @@ -11,8 +11,6 @@ import type { PaginatedApiResponse, ApiResponse, } from '../types'; -import { ConversionEventsResource } from './conversion-events'; -import { CreativeSetsResource } from './creative-sets'; import { TestCohortsResource } from './test-cohorts'; import { EventSourcesResource } from './event-sources'; import { MeasurementDataResource } from './measurement-data'; @@ -93,21 +91,52 @@ export class AdvertisersResource { } /** - * Get the conversion events resource for a specific advertiser + * Validate a data delivery credential for an advertiser * @param advertiserId Advertiser ID - * @returns ConversionEventsResource scoped to the advertiser + * @param name Credential name + * @returns Validation result */ - conversionEvents(advertiserId: string): ConversionEventsResource { - return new ConversionEventsResource(this.adapter, validateResourceId(advertiserId)); + async validateDataDeliveryCredential( + advertiserId: string, + name: string + ): Promise>> { + return this.adapter.request>>( + 'POST', + `/advertisers/${validateResourceId(advertiserId)}/data-delivery-credentials/${encodeURIComponent(name)}/validate` + ); } /** - * Get the creative sets resource for a specific advertiser + * List available accounts for an advertiser * @param advertiserId Advertiser ID - * @returns CreativeSetsResource scoped to the advertiser + * @returns Available accounts */ - creativeSets(advertiserId: string): CreativeSetsResource { - return new CreativeSetsResource(this.adapter, validateResourceId(advertiserId)); + async listAvailableAccounts( + advertiserId: string + ): Promise[]>> { + return this.adapter.request[]>>( + 'GET', + `/advertisers/${validateResourceId(advertiserId)}/accounts/available` + ); + } + + /** + * Update the reporting bucket for an advertiser account link + * @param advertiserId Advertiser ID + * @param linkId Account link ID + * @param data Reporting bucket configuration + * @returns Updated account link + */ + async updateAccountReportingBucket( + advertiserId: string, + linkId: string, + data: Record + ): Promise>> { + return this.adapter.request>>( + 'PUT', + `/advertisers/${validateResourceId(advertiserId)}/accounts/${validateResourceId(linkId)}/reporting-bucket`, + data + ); } /** diff --git a/src/resources/agents.ts b/src/resources/agents.ts index 4dadbbb..e69de29 100644 --- a/src/resources/agents.ts +++ b/src/resources/agents.ts @@ -1,104 +0,0 @@ -/** - * Agents resource for discovering and managing agents under storefront - */ - -import { type BaseAdapter, validateResourceId } from '../adapters/base'; -import type { - Agent, - UpdateAgentInput, - ListAgentsParams, - OAuthAuthorizeResponse, - OAuthCallbackInput, - ApiResponse, -} from '../types'; - -/** - * Resource for managing agents (Storefront persona) - */ -export class AgentsResource { - constructor(private readonly adapter: BaseAdapter) {} - - /** - * List all visible agents - * @param params Filter parameters - * @returns List of agents - */ - async list(params?: ListAgentsParams): Promise> { - return this.adapter.request>('GET', '/agents', undefined, { - params: { - type: params?.type, - status: params?.status, - relationship: params?.relationship, - }, - }); - } - - /** - * Get agent details - * @param agentId Agent ID - * @returns Agent details - */ - async get(agentId: string): Promise> { - return this.adapter.request>( - 'GET', - `/agents/${validateResourceId(agentId)}` - ); - } - - /** - * Update an agent's configuration - * @param agentId Agent ID - * @param data Update data - * @returns Updated agent - */ - async update(agentId: string, data: UpdateAgentInput): Promise> { - return this.adapter.request>( - 'PATCH', - `/agents/${validateResourceId(agentId)}`, - data - ); - } - - /** - * Start agent-level OAuth flow - * @param agentId Agent ID - * @returns Authorization URL to present to the user - */ - async authorizeOAuth(agentId: string): Promise { - return this.adapter.request( - 'POST', - `/agents/${validateResourceId(agentId)}/oauth/authorize`, - {} - ); - } - - /** - * Start per-account OAuth flow - * @param agentId Agent ID - * @returns Authorization URL to present to the user - */ - async authorizeAccountOAuth(agentId: string): Promise { - return this.adapter.request( - 'POST', - `/agents/${validateResourceId(agentId)}/accounts/oauth/authorize`, - {} - ); - } - - /** - * Exchange OAuth authorization code for tokens - * @param agentId Agent ID - * @param data Code and state from OAuth callback - * @returns Exchange result - */ - async exchangeOAuthCode( - agentId: string, - data: OAuthCallbackInput - ): Promise>> { - return this.adapter.request>>( - 'POST', - `/agents/${validateResourceId(agentId)}/oauth/callback`, - data - ); - } -} diff --git a/src/resources/audit-logs.ts b/src/resources/audit-logs.ts new file mode 100644 index 0000000..4e6732c --- /dev/null +++ b/src/resources/audit-logs.ts @@ -0,0 +1,25 @@ +import { type BaseAdapter } from '../adapters/base'; +import type { AuditLog, ListAuditLogsParams, PaginatedApiResponse } from '../types'; + +/** + * Resource for accessing audit logs + */ +export class AuditLogsResource { + constructor(private readonly adapter: BaseAdapter) {} + + /** + * List audit log entries + * @param params Filter and pagination parameters + * @returns Paginated list of audit log entries + */ + async list(params?: ListAuditLogsParams): Promise> { + return this.adapter.request>('GET', '/audit-logs', undefined, { + params: { + take: params?.take, + skip: params?.skip, + resourceType: params?.resourceType, + action: params?.action, + }, + }); + } +} diff --git a/src/resources/billing.ts b/src/resources/billing.ts index 9641e5b..68c5493 100644 --- a/src/resources/billing.ts +++ b/src/resources/billing.ts @@ -1,96 +1,28 @@ -/** - * Billing resource for managing storefront Stripe Connect billing - */ - import { type BaseAdapter } from '../adapters/base'; -import type { - StorefrontBillingConfig, - StripeConnectResponse, - BillingStatus, - BillingTransaction, - BillingPayout, - ApiResponse, -} from '../types'; +import type { BuyerInvoice, BuyerPendingInvoiceItem, ApiResponse } from '../types'; /** - * Resource for managing billing (Storefront persona) + * Resource for buyer billing and invoices */ -export class BillingResource { +export class BuyerBillingResource { constructor(private readonly adapter: BaseAdapter) {} /** - * Get billing configuration - * @returns Billing config with Stripe Connect details - */ - async get(): Promise { - return this.adapter.request('GET', '/billing'); - } - - /** - * Connect to Stripe via Stripe Connect - * @returns Stripe Connect response with onboarding URL - */ - async connect(): Promise { - return this.adapter.request('POST', '/billing/connect'); - } - - /** - * Get billing status - * @returns Billing status + * List invoices for the current buyer + * @returns List of invoices */ - async status(): Promise { - return this.adapter.request('GET', '/billing/status'); + async listInvoices(): Promise> { + return this.adapter.request>('GET', '/billing/invoices'); } /** - * List billing transactions - * @param params Pagination parameters - * @returns List of transactions + * List pending invoice items that have not yet been billed + * @returns List of pending invoice items */ - async transactions(params?: { - limit?: number; - starting_after?: string; - }): Promise> { - return this.adapter.request>( + async listPendingItems(): Promise> { + return this.adapter.request>( 'GET', - '/billing/transactions', - undefined, - { - params: { - limit: params?.limit, - starting_after: params?.starting_after, - }, - } + '/billing/pending-invoice-items' ); } - - /** - * List billing payouts - * @param params Pagination parameters - * @returns List of payouts - */ - async payouts(params?: { - limit?: number; - starting_after?: string; - }): Promise> { - return this.adapter.request>( - 'GET', - '/billing/payouts', - undefined, - { - params: { - limit: params?.limit, - starting_after: params?.starting_after, - }, - } - ); - } - - /** - * Get Stripe onboarding URL - * @returns Onboarding URL details - */ - async onboardingUrl(): Promise { - return this.adapter.request('GET', '/billing/onboard'); - } } diff --git a/src/resources/bundles.ts b/src/resources/bundles.ts index d54bfc1..e69de29 100644 --- a/src/resources/bundles.ts +++ b/src/resources/bundles.ts @@ -1,80 +0,0 @@ -/** - * Bundles resource for creating and managing media bundles - */ - -import { type BaseAdapter, validateResourceId } from '../adapters/base'; -import type { - Bundle, - CreateBundleInput, - DiscoverProductsParams, - DiscoverProductsResponse, - BrowseProductsInput, - ApiResponse, -} from '../types'; -import { BundleProductsResource } from './products'; - -/** - * Resource for managing bundles (Buyer persona) - */ -export class BundlesResource { - constructor(private readonly adapter: BaseAdapter) {} - - /** - * Create a new bundle - * @param data Bundle creation data - * @returns Created bundle with bundleId - */ - async create(data: CreateBundleInput): Promise> { - return this.adapter.request>('POST', '/bundles', data); - } - - /** - * Discover products available for a bundle - * @param bundleId Bundle ID - * @param params Query parameters for pagination and filtering - * @returns Discovered product groups with budget context - */ - async discoverProducts( - bundleId: string, - params?: DiscoverProductsParams - ): Promise> { - return this.adapter.request>( - 'GET', - `/bundles/${validateResourceId(bundleId)}/discover-products`, - undefined, - { - params: { - groupLimit: params?.groupLimit, - groupOffset: params?.groupOffset, - productsPerGroup: params?.productsPerGroup, - productOffset: params?.productOffset, - publisherDomain: params?.publisherDomain, - salesAgentIds: params?.salesAgentIds, - salesAgentNames: params?.salesAgentNames, - }, - } - ); - } - - /** - * Browse products without an existing bundle (auto-creates bundle) - * @param data Browse criteria including advertiser, channels, and filters - * @returns Discovered product groups with auto-created bundleId - */ - async browseProducts(data: BrowseProductsInput): Promise> { - return this.adapter.request>( - 'POST', - '/bundles/discover-products', - data - ); - } - - /** - * Get the products resource for a specific bundle - * @param bundleId Bundle ID - * @returns BundleProductsResource scoped to the bundle - */ - products(bundleId: string): BundleProductsResource { - return new BundleProductsResource(this.adapter, validateResourceId(bundleId)); - } -} diff --git a/src/resources/campaigns.ts b/src/resources/campaigns.ts index 889ec9b..1d44020 100644 --- a/src/resources/campaigns.ts +++ b/src/resources/campaigns.ts @@ -5,11 +5,8 @@ import { type BaseAdapter, validateResourceId } from '../adapters/base'; import type { Campaign, - CreateDiscoveryCampaignInput, - UpdateDiscoveryCampaignInput, - CreatePerformanceCampaignInput, - UpdatePerformanceCampaignInput, - CreateAudienceCampaignInput, + CreateCampaignInput, + UpdateCampaignInput, ListCampaignsParams, PaginatedApiResponse, ApiResponse, @@ -58,96 +55,75 @@ export class CampaignsResource { } /** - * Create a discovery campaign - * @param data Discovery campaign creation data + * Create a campaign + * @param data Campaign creation data * @returns Created campaign */ - async createDiscovery(data: CreateDiscoveryCampaignInput): Promise> { - const result = await this.adapter.request>( - 'POST', - '/campaigns/discovery', - data - ); - if (shouldValidateResponse(this.adapter.validate)) { - result.data = validateResponse(campaignSchemas.response, result.data) as unknown as Campaign; - } - return result; + async create(data: CreateCampaignInput): Promise> { + return this.adapter.request>('POST', '/campaigns', data); } /** - * Update an existing discovery campaign + * Update an existing campaign * @param id Campaign ID - * @param data Discovery campaign update data + * @param data Campaign update data * @returns Updated campaign */ - async updateDiscovery( - id: string, - data: UpdateDiscoveryCampaignInput - ): Promise> { - const result = await this.adapter.request>( + async update(id: string, data: UpdateCampaignInput): Promise> { + return this.adapter.request>( 'PUT', - `/campaigns/discovery/${validateResourceId(id)}`, + `/campaigns/${validateResourceId(id)}`, data ); - if (shouldValidateResponse(this.adapter.validate)) { - result.data = validateResponse(campaignSchemas.response, result.data) as unknown as Campaign; - } - return result; } /** - * Create a performance campaign - * @param data Performance campaign creation data - * @returns Created campaign + * Delete a campaign + * @param id Campaign ID */ - async createPerformance(data: CreatePerformanceCampaignInput): Promise> { - const result = await this.adapter.request>( + async delete(id: string): Promise { + await this.adapter.request('DELETE', `/campaigns/${validateResourceId(id)}`); + } + + /** + * Auto-select products for a campaign + * @param id Campaign ID + * @param data Optional configuration for product selection + * @returns Auto-selection result + */ + async autoSelectProducts( + id: string, + data?: Record + ): Promise>> { + return this.adapter.request>>( 'POST', - '/campaigns/performance', + `/campaigns/${validateResourceId(id)}/auto-select-products`, data ); - if (shouldValidateResponse(this.adapter.validate)) { - result.data = validateResponse(campaignSchemas.response, result.data) as unknown as Campaign; - } - return result; } /** - * Update an existing performance campaign + * Get media buy status for a campaign * @param id Campaign ID - * @param data Performance campaign update data - * @returns Updated campaign + * @returns Media buy status */ - async updatePerformance( - id: string, - data: UpdatePerformanceCampaignInput - ): Promise> { - const result = await this.adapter.request>( - 'PUT', - `/campaigns/performance/${validateResourceId(id)}`, - data + async getMediaBuyStatus(id: string): Promise>> { + return this.adapter.request>>( + 'GET', + `/campaigns/${validateResourceId(id)}/media-buy-status` ); - if (shouldValidateResponse(this.adapter.validate)) { - result.data = validateResponse(campaignSchemas.response, result.data) as unknown as Campaign; - } - return result; } /** - * Create an audience campaign - * @param data Audience campaign creation data - * @returns Created campaign + * Get products associated with a campaign + * @param id Campaign ID + * @returns Campaign products */ - async createAudience(data: CreateAudienceCampaignInput): Promise> { - const result = await this.adapter.request>( - 'POST', - '/campaigns/audience', - data + async getProducts(id: string): Promise>> { + return this.adapter.request>>( + 'GET', + `/campaigns/${validateResourceId(id)}/products` ); - if (shouldValidateResponse(this.adapter.validate)) { - result.data = validateResponse(campaignSchemas.response, result.data) as unknown as Campaign; - } - return result; } /** diff --git a/src/resources/conversion-events.ts b/src/resources/conversion-events.ts index d31229b..e69de29 100644 --- a/src/resources/conversion-events.ts +++ b/src/resources/conversion-events.ts @@ -1,75 +0,0 @@ -/** - * Conversion events resource for managing advertiser conversion tracking - * Scoped to a specific advertiser - */ - -import { type BaseAdapter, validateResourceId } from '../adapters/base'; -import type { - ConversionEvent, - CreateConversionEventInput, - UpdateConversionEventInput, - ApiResponse, -} from '../types'; - -/** - * Resource for managing conversion events (scoped to an advertiser) - */ -export class ConversionEventsResource { - constructor( - private readonly adapter: BaseAdapter, - private readonly advertiserId: string - ) {} - - /** - * List all conversion events for this advertiser - * @returns List of conversion events - */ - async list(): Promise> { - return this.adapter.request>( - 'GET', - `/advertisers/${this.advertiserId}/conversion-events` - ); - } - - /** - * Get a conversion event by ID - * @param eventId Conversion event ID - * @returns Conversion event details - */ - async get(eventId: string): Promise> { - return this.adapter.request>( - 'GET', - `/advertisers/${this.advertiserId}/conversion-events/${validateResourceId(eventId)}` - ); - } - - /** - * Create a new conversion event - * @param data Conversion event creation data - * @returns Created conversion event - */ - async create(data: CreateConversionEventInput): Promise> { - return this.adapter.request>( - 'POST', - `/advertisers/${this.advertiserId}/conversion-events`, - data - ); - } - - /** - * Update an existing conversion event - * @param eventId Conversion event ID - * @param data Update data - * @returns Updated conversion event - */ - async update( - eventId: string, - data: UpdateConversionEventInput - ): Promise> { - return this.adapter.request>( - 'PUT', - `/advertisers/${this.advertiserId}/conversion-events/${validateResourceId(eventId)}`, - data - ); - } -} diff --git a/src/resources/creative-sets.ts b/src/resources/creative-sets.ts index 187a9f3..e69de29 100644 --- a/src/resources/creative-sets.ts +++ b/src/resources/creative-sets.ts @@ -1,76 +0,0 @@ -/** - * Creative sets resource for managing advertiser creative assets - * Scoped to a specific advertiser - */ - -import { type BaseAdapter, validateResourceId } from '../adapters/base'; -import type { - CreativeSet, - CreateCreativeSetInput, - CreativeAsset, - CreateCreativeAssetInput, - ApiResponse, -} from '../types'; - -/** - * Resource for managing creative sets (scoped to an advertiser) - */ -export class CreativeSetsResource { - constructor( - private readonly adapter: BaseAdapter, - private readonly advertiserId: string - ) {} - - /** - * List all creative sets for this advertiser - * @returns List of creative sets - */ - async list(): Promise> { - return this.adapter.request>( - 'GET', - `/advertisers/${this.advertiserId}/creative-sets` - ); - } - - /** - * Create a new creative set - * @param data Creative set creation data - * @returns Created creative set - */ - async create(data: CreateCreativeSetInput): Promise> { - return this.adapter.request>( - 'POST', - `/advertisers/${this.advertiserId}/creative-sets`, - data - ); - } - - /** - * Add an asset to a creative set - * @param creativeSetId Creative set ID - * @param data Asset creation data - * @returns Created creative asset - */ - async addAsset( - creativeSetId: string, - data: CreateCreativeAssetInput - ): Promise> { - return this.adapter.request>( - 'POST', - `/advertisers/${this.advertiserId}/creative-sets/${validateResourceId(creativeSetId)}/assets`, - data - ); - } - - /** - * Remove an asset from a creative set - * @param creativeSetId Creative set ID - * @param assetId Asset ID to remove - */ - async removeAsset(creativeSetId: string, assetId: string): Promise { - await this.adapter.request( - 'DELETE', - `/advertisers/${this.advertiserId}/creative-sets/${validateResourceId(creativeSetId)}/assets/${validateResourceId(assetId)}` - ); - } -} diff --git a/src/resources/creatives.ts b/src/resources/creatives.ts index 7c0e58f..d69a948 100644 --- a/src/resources/creatives.ts +++ b/src/resources/creatives.ts @@ -58,6 +58,19 @@ export class CreativesResource { ); } + /** + * Create a new creative for this campaign + * @param data Creative creation data + * @returns Created creative + */ + async create(data: Record): Promise> { + return this.adapter.request>( + 'POST', + `/campaigns/${validateResourceId(this.campaignId)}/creatives/create`, + data + ); + } + /** * Update creative metadata * @param creativeId Creative ID @@ -82,4 +95,15 @@ export class CreativesResource { `/campaigns/${validateResourceId(this.campaignId)}/creatives/${validateResourceId(creativeId)}` ); } + + /** + * List creative manifests for this campaign + * @returns Creative manifests + */ + async listManifests(): Promise[]>> { + return this.adapter.request[]>>( + 'GET', + `/campaigns/${validateResourceId(this.campaignId)}/creativeManifest` + ); + } } diff --git a/src/resources/discovery.ts b/src/resources/discovery.ts new file mode 100644 index 0000000..8f50215 --- /dev/null +++ b/src/resources/discovery.ts @@ -0,0 +1,116 @@ +import { type BaseAdapter, validateResourceId } from '../adapters/base'; +import type { + DiscoverProductsInput, + AddProductsInput, + RemoveProductsInput, + ApplyProposalInput, + ApiResponse, +} from '../types'; + +/** + * Resource for product discovery sessions + */ +export class DiscoveryResource { + constructor(private readonly adapter: BaseAdapter) {} + + /** + * Start a new product discovery session + * @param data Discovery input parameters + * @returns Discovery session with initial results + */ + async discoverProducts( + data: DiscoverProductsInput + ): Promise>> { + return this.adapter.request>>( + 'POST', + '/discovery/discover-products', + data + ); + } + + /** + * Browse discovered products with pagination + * @param discoveryId Discovery session ID + * @param params Pagination parameters for groups and products + * @returns Paginated product groups + */ + async browseProducts( + discoveryId: string, + params?: { + groupLimit?: number; + groupOffset?: number; + productsPerGroup?: number; + productOffset?: number; + } + ): Promise>> { + return this.adapter.request>>( + 'GET', + `/discovery/${validateResourceId(discoveryId)}/discover-products`, + undefined, + { params } + ); + } + + /** + * Get selected products for a discovery session + * @param discoveryId Discovery session ID + * @returns Selected products + */ + async getProducts(discoveryId: string): Promise>> { + return this.adapter.request>>( + 'GET', + `/discovery/${validateResourceId(discoveryId)}/products` + ); + } + + /** + * Add products to a discovery session + * @param discoveryId Discovery session ID + * @param data Products to add + * @returns Updated product selection + */ + async addProducts( + discoveryId: string, + data: AddProductsInput + ): Promise>> { + return this.adapter.request>>( + 'POST', + `/discovery/${validateResourceId(discoveryId)}/products`, + data + ); + } + + /** + * Remove products from a discovery session + * @param discoveryId Discovery session ID + * @param data Products to remove + * @returns Updated product selection + */ + async removeProducts( + discoveryId: string, + data: RemoveProductsInput + ): Promise>> { + return this.adapter.request>>( + 'DELETE', + `/discovery/${validateResourceId(discoveryId)}/products`, + data + ); + } + + /** + * Apply a proposal from a discovery session + * @param discoveryId Discovery session ID + * @param data Proposal application input + * @returns Result of applying the proposal + */ + async applyProposal( + discoveryId: string, + data: ApplyProposalInput + ): Promise>> { + return this.adapter.request>>( + 'POST', + `/discovery/${validateResourceId(discoveryId)}/apply-proposal`, + data + ); + } +} diff --git a/src/resources/event-sources.ts b/src/resources/event-sources.ts index faed663..68f2932 100644 --- a/src/resources/event-sources.ts +++ b/src/resources/event-sources.ts @@ -4,12 +4,7 @@ */ import { type BaseAdapter, validateResourceId } from '../adapters/base'; -import type { - ApiResponse, - EventSource, - CreateEventSourceInput, - UpdateEventSourceInput, -} from '../types'; +import type { ApiResponse, EventSource, LogEventInput } from '../types'; /** * Resource for managing event sources (scoped to an advertiser) @@ -20,19 +15,6 @@ export class EventSourcesResource { private readonly advertiserId: string ) {} - /** - * Sync (bulk upsert) event sources for this advertiser - * @param data Event sources sync payload - * @returns Sync result - */ - async sync(data: Record): Promise> { - return this.adapter.request>( - 'POST', - `/advertisers/${validateResourceId(this.advertiserId)}/event-sources/sync`, - data - ); - } - /** * List all event sources for this advertiser * @returns List of event sources @@ -45,52 +27,39 @@ export class EventSourcesResource { } /** - * Create a new event source - * @param data Event source creation data - * @returns Created event source + * Sync (bulk upsert) event sources for this advertiser + * @param data Event sources sync payload + * @returns Sync result */ - async create(data: CreateEventSourceInput): Promise> { - return this.adapter.request>( + async sync(data: Record): Promise> { + return this.adapter.request>( 'POST', - `/advertisers/${validateResourceId(this.advertiserId)}/event-sources`, + `/advertisers/${validateResourceId(this.advertiserId)}/event-sources/sync`, data ); } /** - * Get an event source by ID - * @param id Event source ID - * @returns Event source details + * Get event summary for this advertiser + * @returns Event summary */ - async get(id: string): Promise> { - return this.adapter.request>( + async getEventSummary(): Promise>> { + return this.adapter.request>>( 'GET', - `/advertisers/${validateResourceId(this.advertiserId)}/event-sources/${validateResourceId(id)}` + `/advertisers/${validateResourceId(this.advertiserId)}/events/summary` ); } /** - * Update an existing event source - * @param id Event source ID - * @param data Update data - * @returns Updated event source + * Log an event for this advertiser + * @param data Event data to log + * @returns Log result */ - async update(id: string, data: UpdateEventSourceInput): Promise> { - return this.adapter.request>( - 'PUT', - `/advertisers/${validateResourceId(this.advertiserId)}/event-sources/${validateResourceId(id)}`, + async logEvent(data: LogEventInput): Promise>> { + return this.adapter.request>>( + 'POST', + `/advertisers/${validateResourceId(this.advertiserId)}/log-event`, data ); } - - /** - * Delete an event source - * @param id Event source ID - */ - async delete(id: string): Promise { - await this.adapter.request( - 'DELETE', - `/advertisers/${validateResourceId(this.advertiserId)}/event-sources/${validateResourceId(id)}` - ); - } } diff --git a/src/resources/index.ts b/src/resources/index.ts index 9bc8f8e..cadd80e 100644 --- a/src/resources/index.ts +++ b/src/resources/index.ts @@ -2,21 +2,18 @@ * Resource exports */ +export { AccountsResource } from './accounts'; export { AdvertisersResource } from './advertisers'; -export { AgentsResource } from './agents'; -export { BillingResource } from './billing'; -export { BundlesResource } from './bundles'; -export { BundleProductsResource } from './products'; +export { AuditLogsResource } from './audit-logs'; +export { BuyerBillingResource } from './billing'; export { CampaignsResource } from './campaigns'; -export { ConversionEventsResource } from './conversion-events'; -export { CreativeSetsResource } from './creative-sets'; -export { InventorySourcesResource } from './inventory-sources'; -export { NotificationsResource } from './notifications'; -export { ReadinessResource } from './readiness'; +export { CreativesResource } from './creatives'; +export { DiscoveryResource } from './discovery'; +export { ModerationResource } from './moderation'; +export { NotificationPreferencesResource } from './notification-preferences'; +export { PlanningBriefsResource } from './planning-briefs'; export { ReportingResource } from './reporting'; -export { SalesAgentsResource } from './sales-agents'; -export { SignalsResource } from './signals'; -export { StorefrontResource } from './storefront'; +export { StorefrontsResource } from './storefronts'; export { TestCohortsResource } from './test-cohorts'; export { EventSourcesResource } from './event-sources'; export { MeasurementDataResource } from './measurement-data'; @@ -25,4 +22,3 @@ export { AudiencesResource } from './audiences'; export { SyndicationResource } from './syndication'; export { TasksResource } from './tasks'; export { PropertyListsResource, PropertyListChecksResource } from './property-lists'; -export { CreativesResource } from './creatives'; diff --git a/src/resources/inventory-sources.ts b/src/resources/inventory-sources.ts index 26d3030..e69de29 100644 --- a/src/resources/inventory-sources.ts +++ b/src/resources/inventory-sources.ts @@ -1,75 +0,0 @@ -/** - * Inventory Sources resource for managing storefront inventory sources - */ - -import { type BaseAdapter, validateResourceId } from '../adapters/base'; -import type { - InventorySource, - CreateInventorySourceInput, - UpdateInventorySourceInput, - ApiResponse, -} from '../types'; - -/** - * Resource for managing inventory sources (Storefront persona) - */ -export class InventorySourcesResource { - constructor(private readonly adapter: BaseAdapter) {} - - /** - * List all inventory sources - * @returns List of inventory sources - */ - async list(): Promise> { - return this.adapter.request>('GET', '/inventory-sources'); - } - - /** - * Get an inventory source by ID - * @param sourceId Inventory source ID - * @returns Inventory source details - */ - async get(sourceId: string): Promise> { - return this.adapter.request>( - 'GET', - `/inventory-sources/${validateResourceId(sourceId)}` - ); - } - - /** - * Create a new inventory source - * @param data Inventory source creation data - * @returns Created inventory source - */ - async create(data: CreateInventorySourceInput): Promise> { - return this.adapter.request>('POST', '/inventory-sources', data); - } - - /** - * Update an inventory source - * @param sourceId Inventory source ID - * @param data Update data - * @returns Updated inventory source - */ - async update( - sourceId: string, - data: UpdateInventorySourceInput - ): Promise> { - return this.adapter.request>( - 'PUT', - `/inventory-sources/${validateResourceId(sourceId)}`, - data - ); - } - - /** - * Delete an inventory source - * @param sourceId Inventory source ID - */ - async delete(sourceId: string): Promise { - await this.adapter.request( - 'DELETE', - `/inventory-sources/${validateResourceId(sourceId)}` - ); - } -} diff --git a/src/resources/measurement-data.ts b/src/resources/measurement-data.ts index 4ed9ec0..6193ade 100644 --- a/src/resources/measurement-data.ts +++ b/src/resources/measurement-data.ts @@ -1,10 +1,21 @@ /** - * Measurement data resource for syncing advertiser measurement data + * Measurement data resource for managing advertiser measurement configuration and data * Scoped to a specific advertiser */ import { type BaseAdapter, validateResourceId } from '../adapters/base'; -import type { ApiResponse, MeasurementDataSync } from '../types'; +import type { + ApiResponse, + MeasurementConfig, + UpdateMeasurementConfigInput, + MeasurementSource, + CreateMeasurementSourceInput, + UpdateMeasurementSourceInput, + UploadMeasurementRecordsInput, + UploadContextRecordsInput, + MeasurementFreshness, + MeasurementDataSync, +} from '../types'; /** * Resource for managing measurement data (scoped to an advertiser) @@ -15,6 +26,135 @@ export class MeasurementDataResource { private readonly advertiserId: string ) {} + /** + * Get measurement configuration for this advertiser + * @returns Measurement configuration + */ + async getConfig(): Promise> { + return this.adapter.request>( + 'GET', + `/advertisers/${validateResourceId(this.advertiserId)}/measurement-config` + ); + } + + /** + * Update measurement configuration for this advertiser + * @param data Configuration update data + * @returns Updated measurement configuration + */ + async updateConfig(data: UpdateMeasurementConfigInput): Promise> { + return this.adapter.request>( + 'PUT', + `/advertisers/${validateResourceId(this.advertiserId)}/measurement-config`, + data + ); + } + + /** + * List measurement sources for this advertiser + * @returns List of measurement sources + */ + async listSources(): Promise> { + return this.adapter.request>( + 'GET', + `/advertisers/${validateResourceId(this.advertiserId)}/measurement-sources` + ); + } + + /** + * Create a measurement source for this advertiser + * @param data Source creation data + * @returns Created measurement source + */ + async createSource(data: CreateMeasurementSourceInput): Promise> { + return this.adapter.request>( + 'POST', + `/advertisers/${validateResourceId(this.advertiserId)}/measurement-sources`, + data + ); + } + + /** + * Get a measurement source by ID + * @param sourceId Source ID + * @returns Measurement source details + */ + async getSource(sourceId: string): Promise> { + return this.adapter.request>( + 'GET', + `/advertisers/${validateResourceId(this.advertiserId)}/measurement-sources/${validateResourceId(sourceId)}` + ); + } + + /** + * Update a measurement source + * @param sourceId Source ID + * @param data Source update data + * @returns Updated measurement source + */ + async updateSource( + sourceId: string, + data: UpdateMeasurementSourceInput + ): Promise> { + return this.adapter.request>( + 'PATCH', + `/advertisers/${validateResourceId(this.advertiserId)}/measurement-sources/${validateResourceId(sourceId)}`, + data + ); + } + + /** + * Upload measurement records for this advertiser + * @param data Records to upload + * @returns Upload result + */ + async uploadRecords( + data: UploadMeasurementRecordsInput + ): Promise>> { + return this.adapter.request>>( + 'POST', + `/advertisers/${validateResourceId(this.advertiserId)}/measurement-records`, + data + ); + } + + /** + * List measurement records for this advertiser + * @returns Measurement records + */ + async listRecords(): Promise[]>> { + return this.adapter.request[]>>( + 'GET', + `/advertisers/${validateResourceId(this.advertiserId)}/measurement-records` + ); + } + + /** + * Upload context records for this advertiser + * @param data Context records to upload + * @returns Upload result + */ + async uploadContextRecords( + data: UploadContextRecordsInput + ): Promise>> { + return this.adapter.request>>( + 'POST', + `/advertisers/${validateResourceId(this.advertiserId)}/context-records`, + data + ); + } + + /** + * Get measurement data freshness for this advertiser + * @returns Freshness information + */ + async getFreshness(): Promise> { + return this.adapter.request>( + 'GET', + `/advertisers/${validateResourceId(this.advertiserId)}/measurement-freshness` + ); + } + /** * Sync measurement data for this advertiser * @param data Measurement data sync payload diff --git a/src/resources/moderation.ts b/src/resources/moderation.ts new file mode 100644 index 0000000..4dcc701 --- /dev/null +++ b/src/resources/moderation.ts @@ -0,0 +1,22 @@ +import { type BaseAdapter } from '../adapters/base'; +import type { ModerationCheckInput, ModerationCheckResult, ApiResponse } from '../types'; + +/** + * Resource for content moderation + */ +export class ModerationResource { + constructor(private readonly adapter: BaseAdapter) {} + + /** + * Check content against moderation rules + * @param data Content to check + * @returns Moderation check result + */ + async check(data: ModerationCheckInput): Promise> { + return this.adapter.request>( + 'POST', + '/moderation/check', + data + ); + } +} diff --git a/src/resources/notification-preferences.ts b/src/resources/notification-preferences.ts new file mode 100644 index 0000000..bab894a --- /dev/null +++ b/src/resources/notification-preferences.ts @@ -0,0 +1,39 @@ +import { type BaseAdapter } from '../adapters/base'; +import type { + NotificationPreferences, + UpdateNotificationPreferencesInput, + ApiResponse, +} from '../types'; + +/** + * Resource for managing notification preferences + */ +export class NotificationPreferencesResource { + constructor(private readonly adapter: BaseAdapter) {} + + /** + * Get current notification preferences + * @returns Notification preferences + */ + async get(): Promise> { + return this.adapter.request>( + 'GET', + '/notification-preferences' + ); + } + + /** + * Update notification preferences + * @param data Updated preferences + * @returns Updated notification preferences + */ + async update( + data: UpdateNotificationPreferencesInput + ): Promise> { + return this.adapter.request>( + 'PUT', + '/notification-preferences', + data + ); + } +} diff --git a/src/resources/notifications.ts b/src/resources/notifications.ts index 53b5e0f..e69de29 100644 --- a/src/resources/notifications.ts +++ b/src/resources/notifications.ts @@ -1,64 +0,0 @@ -/** - * Notifications resource for managing storefront notifications - */ - -import { type BaseAdapter, validateResourceId } from '../adapters/base'; -import type { ApiResponse, Notification, ListNotificationsParams } from '../types'; - -/** - * Resource for managing notifications (Storefront persona) - */ -export class NotificationsResource { - constructor(private readonly adapter: BaseAdapter) {} - - /** - * List notifications - * @param params Filter and pagination parameters - * @returns List of notifications - */ - async list(params?: ListNotificationsParams): Promise> { - return this.adapter.request>('GET', '/notifications', undefined, { - params: { - unreadOnly: params?.unreadOnly, - brandAgentId: params?.brandAgentId, - types: params?.types, - limit: params?.limit, - offset: params?.offset, - }, - }); - } - - /** - * Mark a notification as read - * @param notificationId Notification ID - */ - async markAsRead(notificationId: string): Promise { - await this.adapter.request( - 'POST', - `/notifications/${validateResourceId(notificationId)}/read` - ); - } - - /** - * Acknowledge a notification - * @param notificationId Notification ID - */ - async acknowledge(notificationId: string): Promise { - await this.adapter.request( - 'POST', - `/notifications/${validateResourceId(notificationId)}/acknowledge` - ); - } - - /** - * Mark all notifications as read - * @param brandAgentId Optional brand agent ID to scope the operation - */ - async markAllAsRead(brandAgentId?: number): Promise { - await this.adapter.request( - 'POST', - '/notifications/read-all', - brandAgentId ? { brandAgentId } : undefined - ); - } -} diff --git a/src/resources/planning-briefs.ts b/src/resources/planning-briefs.ts new file mode 100644 index 0000000..852d0a7 --- /dev/null +++ b/src/resources/planning-briefs.ts @@ -0,0 +1,56 @@ +import { type BaseAdapter, validateResourceId } from '../adapters/base'; +import type { + PlanningBrief, + CreatePlanningBriefInput, + PlanningBriefResponse, + ApiResponse, + PaginatedApiResponse, +} from '../types'; + +/** + * Resource for managing planning briefs + */ +export class PlanningBriefsResource { + constructor(private readonly adapter: BaseAdapter) {} + + /** + * List all planning briefs + * @returns Paginated list of planning briefs + */ + async list(): Promise> { + return this.adapter.request>('GET', '/planning-briefs'); + } + + /** + * Create a new planning brief + * @param data Planning brief creation data + * @returns Created planning brief + */ + async create(data: CreatePlanningBriefInput): Promise> { + return this.adapter.request>('POST', '/planning-briefs', data); + } + + /** + * Get a planning brief by ID + * @param briefId Planning brief ID + * @returns Planning brief details + */ + async get(briefId: string): Promise> { + return this.adapter.request>( + 'GET', + `/planning-briefs/${validateResourceId(briefId)}` + ); + } + + /** + * List responses for a planning brief + * @param briefId Planning brief ID + * @returns Paginated list of responses + */ + async listResponses(briefId: string): Promise> { + return this.adapter.request>( + 'GET', + `/planning-briefs/${validateResourceId(briefId)}/responses` + ); + } +} diff --git a/src/resources/products.ts b/src/resources/products.ts index a9eed41..e69de29 100644 --- a/src/resources/products.ts +++ b/src/resources/products.ts @@ -1,87 +0,0 @@ -/** - * Bundle Products resource for managing products within a bundle - * Scoped to a specific bundle - */ - -import type { BaseAdapter } from '../adapters/base'; -import type { - BundleProductsResponse, - AddBundleProductsInput, - RemoveBundleProductsInput, - ApiResponse, -} from '../types'; -import { discoverySchemas } from '../schemas/registry'; -import { - shouldValidateInput, - shouldValidateResponse, - validateInput, - validateResponse, -} from '../validation'; - -/** - * Resource for managing products within a bundle - */ -export class BundleProductsResource { - constructor( - private readonly adapter: BaseAdapter, - private readonly bundleId: string - ) {} - - /** - * List all products in this bundle - * @returns Bundle products response with product list and budget context - */ - async list(): Promise> { - const result = await this.adapter.request>( - 'GET', - `/bundles/${this.bundleId}/products` - ); - if (shouldValidateResponse(this.adapter.validate)) { - result.data = validateResponse( - discoverySchemas.sessionProductsResponse, - result.data - ) as unknown as BundleProductsResponse; - } - return result; - } - - /** - * Add products to this bundle - * @param data Products to add with selection details - * @returns Updated bundle products response - */ - async add(data: AddBundleProductsInput): Promise> { - if (shouldValidateInput(this.adapter.validate)) { - data = validateInput( - discoverySchemas.addProductsInput, - data - ) as unknown as AddBundleProductsInput; - } - const result = await this.adapter.request>( - 'POST', - `/bundles/${this.bundleId}/products`, - data - ); - if (shouldValidateResponse(this.adapter.validate)) { - result.data = validateResponse( - discoverySchemas.sessionProductsResponse, - result.data - ) as unknown as BundleProductsResponse; - } - return result; - } - - /** - * Remove products from this bundle - * @param data Product IDs to remove - */ - async remove(data: RemoveBundleProductsInput): Promise { - if (shouldValidateInput(this.adapter.validate)) { - data = validateInput( - discoverySchemas.removeProductsInput, - data - ) as unknown as RemoveBundleProductsInput; - } - await this.adapter.request('DELETE', `/bundles/${this.bundleId}/products`, data); - } -} diff --git a/src/resources/readiness.ts b/src/resources/readiness.ts index 43ebba3..e69de29 100644 --- a/src/resources/readiness.ts +++ b/src/resources/readiness.ts @@ -1,21 +0,0 @@ -/** - * Readiness resource for checking storefront readiness status - */ - -import { type BaseAdapter } from '../adapters/base'; -import type { StorefrontReadiness } from '../types'; - -/** - * Resource for checking storefront readiness (Storefront persona) - */ -export class ReadinessResource { - constructor(private readonly adapter: BaseAdapter) {} - - /** - * Check storefront readiness - * @returns Readiness status with individual checks - */ - async check(): Promise { - return this.adapter.request('GET', '/readiness'); - } -} diff --git a/src/resources/sales-agents.ts b/src/resources/sales-agents.ts index 28d515a..e69de29 100644 --- a/src/resources/sales-agents.ts +++ b/src/resources/sales-agents.ts @@ -1,85 +0,0 @@ -/** - * Sales agents resource for browsing and connecting with sales agents - */ - -import { type BaseAdapter, validateResourceId } from '../adapters/base'; -import type { - SalesAgent, - SalesAgentAccount, - ListSalesAgentsParams, - RegisterSalesAgentAccountInput, - PaginatedApiResponse, -} from '../types'; -import { salesAgentSchemas } from '../schemas/registry'; -import { - shouldValidateInput, - shouldValidateResponse, - validateInput, - validateResponse, -} from '../validation'; - -/** - * Resource for managing sales agents (Buyer persona) - */ -export class SalesAgentsResource { - constructor(private readonly adapter: BaseAdapter) {} - - /** - * List all visible sales agents - * @param params Filter and pagination parameters - * @returns Sales agents with account info - */ - async list(params?: ListSalesAgentsParams): Promise> { - let result = await this.adapter.request>( - 'GET', - '/sales-agents', - undefined, - { - params: { - status: params?.status, - relationship: params?.relationship, - name: params?.name, - limit: params?.limit, - offset: params?.offset, - }, - } - ); - if (shouldValidateResponse(this.adapter.validate)) { - result = validateResponse( - salesAgentSchemas.listResponse, - result - ) as unknown as PaginatedApiResponse; - } - return result; - } - - /** - * Register an account for a sales agent - * @param agentId Agent ID - * @param data Account registration data - * @returns Registered account details - */ - async registerAccount( - agentId: string, - data: RegisterSalesAgentAccountInput - ): Promise { - if (shouldValidateInput(this.adapter.validate)) { - data = validateInput( - salesAgentSchemas.registerAccountInput, - data - ) as unknown as RegisterSalesAgentAccountInput; - } - let result = await this.adapter.request( - 'POST', - `/sales-agents/${validateResourceId(agentId)}/accounts`, - data - ); - if (shouldValidateResponse(this.adapter.validate)) { - result = validateResponse( - salesAgentSchemas.accountResponse, - result - ) as unknown as SalesAgentAccount; - } - return result; - } -} diff --git a/src/resources/signals.ts b/src/resources/signals.ts index c0c9ddc..e69de29 100644 --- a/src/resources/signals.ts +++ b/src/resources/signals.ts @@ -1,31 +0,0 @@ -/** - * Signals resource for discovering and listing targeting signals - * Not scoped to a specific advertiser - */ - -import type { BaseAdapter } from '../adapters/base'; -import type { Signal, DiscoverSignalsInput, ApiResponse } from '../types'; - -/** - * Resource for managing signals - */ -export class SignalsResource { - constructor(private readonly adapter: BaseAdapter) {} - - /** - * Discover signals for campaign targeting - * @param data Optional filters for signal discovery - * @returns Discovered signals - */ - async discover(data?: DiscoverSignalsInput): Promise> { - return this.adapter.request>('POST', '/campaign/signals/discover', data); - } - - /** - * List all available signals - * @returns List of signals - */ - async list(): Promise> { - return this.adapter.request>('GET', '/signals'); - } -} diff --git a/src/resources/storefront.ts b/src/resources/storefront.ts index 9f235da..e69de29 100644 --- a/src/resources/storefront.ts +++ b/src/resources/storefront.ts @@ -1,51 +0,0 @@ -/** - * Storefront resource for managing the storefront profile - */ - -import { type BaseAdapter } from '../adapters/base'; -import type { - Storefront, - CreateStorefrontInput, - UpdateStorefrontInput, - ApiResponse, -} from '../types'; - -/** - * Resource for managing the storefront (Storefront persona) - */ -export class StorefrontResource { - constructor(private readonly adapter: BaseAdapter) {} - - /** - * Get the current storefront profile - * @returns Storefront details - */ - async get(): Promise> { - return this.adapter.request>('GET', ''); - } - - /** - * Create a new storefront - * @param data Storefront creation data - * @returns Created storefront - */ - async create(data: CreateStorefrontInput): Promise> { - return this.adapter.request>('POST', '', data); - } - - /** - * Update the storefront profile - * @param data Update data - * @returns Updated storefront - */ - async update(data: UpdateStorefrontInput): Promise> { - return this.adapter.request>('PUT', '', data); - } - - /** - * Delete the storefront - */ - async delete(): Promise { - await this.adapter.request('DELETE', ''); - } -} diff --git a/src/resources/storefronts.ts b/src/resources/storefronts.ts new file mode 100644 index 0000000..56b7220 --- /dev/null +++ b/src/resources/storefronts.ts @@ -0,0 +1,64 @@ +import { type BaseAdapter, validateResourceId } from '../adapters/base'; +import type { + BuyerStorefront, + StorefrontCredential, + RegisterCredentialsInput, + ApiResponse, +} from '../types'; + +/** + * Resource for browsing storefronts as a buyer + */ +export class StorefrontsResource { + constructor(private readonly adapter: BaseAdapter) {} + + /** + * List available storefronts + * @returns List of storefronts + */ + async list(): Promise> { + return this.adapter.request>('GET', '/storefronts'); + } + + /** + * Get a specific storefront + * @param storefrontId Storefront ID + * @returns Storefront details + */ + async get(storefrontId: string): Promise> { + return this.adapter.request>( + 'GET', + `/storefronts/${validateResourceId(storefrontId)}` + ); + } + + /** + * List credentials for all storefronts + * @returns List of storefront credentials + */ + async listCredentials(): Promise> { + return this.adapter.request>( + 'GET', + '/storefronts/credentials' + ); + } + + /** + * Register credentials for a storefront source + * @param storefrontId Storefront ID + * @param sourceId Source ID + * @param data Credentials registration data + * @returns Registered credential + */ + async registerCredentials( + storefrontId: string, + sourceId: string, + data: RegisterCredentialsInput + ): Promise> { + return this.adapter.request>( + 'POST', + `/storefronts/${validateResourceId(storefrontId)}/sources/${validateResourceId(sourceId)}/credentials`, + data + ); + } +} diff --git a/src/resources/test-cohorts.ts b/src/resources/test-cohorts.ts index 8758a73..8f78725 100644 --- a/src/resources/test-cohorts.ts +++ b/src/resources/test-cohorts.ts @@ -3,8 +3,13 @@ * Scoped to a specific advertiser */ -import type { BaseAdapter } from '../adapters/base'; -import type { TestCohort, CreateTestCohortInput, ApiResponse } from '../types'; +import { type BaseAdapter, validateResourceId } from '../adapters/base'; +import type { + TestCohort, + CreateTestCohortInput, + UpdateTestCohortInput, + ApiResponse, +} from '../types'; /** * Resource for managing test cohorts (scoped to an advertiser) @@ -22,7 +27,7 @@ export class TestCohortsResource { async list(): Promise> { return this.adapter.request>( 'GET', - `/advertisers/${this.advertiserId}/test-cohorts` + `/advertisers/${validateResourceId(this.advertiserId)}/test-cohorts` ); } @@ -34,8 +39,45 @@ export class TestCohortsResource { async create(data: CreateTestCohortInput): Promise> { return this.adapter.request>( 'POST', - `/advertisers/${this.advertiserId}/test-cohorts`, + `/advertisers/${validateResourceId(this.advertiserId)}/test-cohorts`, data ); } + + /** + * Get a test cohort by ID + * @param cohortId Test cohort ID + * @returns Test cohort details + */ + async get(cohortId: string): Promise> { + return this.adapter.request>( + 'GET', + `/advertisers/${validateResourceId(this.advertiserId)}/test-cohorts/${validateResourceId(cohortId)}` + ); + } + + /** + * Update a test cohort + * @param cohortId Test cohort ID + * @param data Update data + * @returns Updated test cohort + */ + async update(cohortId: string, data: UpdateTestCohortInput): Promise> { + return this.adapter.request>( + 'PUT', + `/advertisers/${validateResourceId(this.advertiserId)}/test-cohorts/${validateResourceId(cohortId)}`, + data + ); + } + + /** + * Delete a test cohort + * @param cohortId Test cohort ID + */ + async delete(cohortId: string): Promise { + await this.adapter.request( + 'DELETE', + `/advertisers/${validateResourceId(this.advertiserId)}/test-cohorts/${validateResourceId(cohortId)}` + ); + } } diff --git a/src/schemas/buyer.ts b/src/schemas/buyer.ts index 333059d..764b8e2 100644 --- a/src/schemas/buyer.ts +++ b/src/schemas/buyer.ts @@ -12,12 +12,132 @@ const ApiError = z.object({ const ErrorResponse = z.object({ data: z.literal(null).nullable(), error: ApiError }); const LinkedAccountInput = z .object({ - partnerId: z.string().min(1), + storefrontId: z.number().int().lte(9007199254740991), + sourceId: z.string().min(1), accountId: z.string().min(1), billingType: z.string().optional(), }) .passthrough(); const OptimizationApplyMode = z.enum(['AUTO', 'MANUAL']); +const CampaignBudgetType = z.literal('total_budget'); +const GcsCredentialConfig = z + .object({ + type: z.literal('GCS'), + bucket: z + .string() + .min(1) + .max(222) + .regex(/^[a-z0-9][a-z0-9._-]*[a-z0-9]$/), + }) + .passthrough(); +const S3AssumeRoleAuth = z + .object({ + mode: z.literal('ASSUME_ROLE'), + roleArn: z.string().regex(/^arn:aws[a-zA-Z-]*:iam::\d{12}:role\/.+$/), + externalId: z.string().min(2).max(1224).optional(), + }) + .passthrough(); +const S3Auth = S3AssumeRoleAuth; +const S3CredentialConfig = z + .object({ + type: z.literal('S3'), + bucket: z + .string() + .min(3) + .max(63) + .regex(/^[a-z0-9][a-z0-9.-]*[a-z0-9]$/), + region: z.string().regex(/^[a-z]{2}(-[a-z]+)+-\d+$/), + auth: S3Auth, + }) + .passthrough(); +const AzureBlobSasAuth = z + .object({ mode: z.literal('SAS_TOKEN'), sasToken: z.string().min(1).max(4096) }) + .passthrough(); +const AzureBlobAuth = AzureBlobSasAuth; +const AzureBlobCredentialConfig = z + .object({ + type: z.literal('AZURE_BLOB'), + storageAccountName: z + .string() + .min(3) + .max(24) + .regex(/^[a-z0-9]+$/), + containerName: z + .string() + .min(3) + .max(63) + .regex(/^(?!.*--)[a-z0-9][a-z0-9-]*[a-z0-9]$/), + auth: AzureBlobAuth, + }) + .passthrough(); +const CredentialConfig = z.discriminatedUnion('type', [ + GcsCredentialConfig, + S3CredentialConfig, + AzureBlobCredentialConfig, +]); +const DataDeliveryCredentialInput = z + .object({ + name: z + .string() + .min(1) + .max(64) + .regex(/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/), + config: CredentialConfig, + }) + .passthrough(); +const DataDeliveryCredentialArrayInput = z.array(DataDeliveryCredentialInput); +const GcsDeliveryConfig = z + .object({ + type: z.literal('GCS'), + pathPrefix: z.string().max(1024).optional().default(''), + format: z.enum(['JSONL', 'PARQUET', 'CSV']).optional().default('JSONL'), + }) + .passthrough(); +const S3DeliveryConfig = z + .object({ + type: z.literal('S3'), + pathPrefix: z.string().max(1024).optional().default(''), + format: z.enum(['JSONL', 'PARQUET', 'CSV']).optional().default('JSONL'), + }) + .passthrough(); +const AzureBlobDeliveryConfig = z + .object({ + type: z.literal('AZURE_BLOB'), + pathPrefix: z.string().max(1024).optional().default(''), + format: z.enum(['JSONL', 'PARQUET', 'CSV']).optional().default('JSONL'), + }) + .passthrough(); +const DeliveryConfig = z.discriminatedUnion('type', [ + GcsDeliveryConfig, + S3DeliveryConfig, + AzureBlobDeliveryConfig, +]); +const DataDeliveryOutputInput = z + .object({ + dataDeliveryType: z.enum([ + 'MB_DELIVERY', + 'IMPRESSIONS', + 'CLICKS', + 'VAST_EVENTS', + 'CAPI_ATTRIBUTION', + 'MMP_POSTBACKS', + ]), + cadence: z.enum(['HOURLY', 'DAILY', 'WEEKLY']), + syncWeeklyDay: z.number().int().gte(0).lte(6).optional(), + enabled: z.boolean().optional().default(true), + credentialName: z + .string() + .min(1) + .max(64) + .regex(/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/), + deliveryConfig: DeliveryConfig, + }) + .passthrough(); +const DataDeliveryOutputArrayInput = z.array(DataDeliveryOutputInput); +const AdvertiserDataDeliveryInput = z + .object({ credentials: DataDeliveryCredentialArrayInput, outputs: DataDeliveryOutputArrayInput }) + .partial() + .passthrough(); const CreateAdvertiserBody = z .object({ name: z.string().min(1).max(255), @@ -26,6 +146,7 @@ const CreateAdvertiserBody = z saveBrand: z.boolean().optional().default(false), linkedAccounts: z.array(LinkedAccountInput).optional(), optimizationApplyMode: OptimizationApplyMode.optional(), + campaignBudgetType: CampaignBudgetType.optional(), sandbox: z.boolean().optional().default(false), utmConfig: z .array( @@ -38,6 +159,28 @@ const CreateAdvertiserBody = z ) .max(20) .optional(), + dataDelivery: AdvertiserDataDeliveryInput.optional(), + frequencyCaps: z + .array( + z + .object({ + max_impressions: z.number().int().lte(9007199254740991), + window: z + .object({ + interval: z.number().gte(1), + unit: z.union([ + z.literal('seconds'), + z.literal('minutes'), + z.literal('hours'), + z.literal('days'), + z.literal('campaign'), + ]), + }) + .passthrough(), + }) + .passthrough() + ) + .optional(), }) .passthrough(); const UpdateAdvertiserBody = z @@ -47,6 +190,7 @@ const UpdateAdvertiserBody = z brand: z.string().min(1), linkedAccounts: z.array(LinkedAccountInput), optimizationApplyMode: OptimizationApplyMode, + campaignBudgetType: CampaignBudgetType, utmConfig: z .array( z @@ -57,9 +201,85 @@ const UpdateAdvertiserBody = z .passthrough() ) .max(20), + dataDelivery: z + .object({ + credentials: DataDeliveryCredentialArrayInput, + outputs: DataDeliveryOutputArrayInput, + }) + .partial() + .passthrough(), + frequencyCaps: z.array( + z + .object({ + max_impressions: z.number().int().lte(9007199254740991), + window: z + .object({ + interval: z.number().gte(1), + unit: z.union([ + z.literal('seconds'), + z.literal('minutes'), + z.literal('hours'), + z.literal('days'), + z.literal('campaign'), + ]), + }) + .passthrough(), + }) + .passthrough() + ), }) .partial() .passthrough(); +const GcsCredentialConfigOutput = z.object({ + type: z.literal('GCS'), + bucket: z + .string() + .min(1) + .max(222) + .regex(/^[a-z0-9][a-z0-9._-]*[a-z0-9]$/), +}); +const S3AssumeRoleAuthOutput = z.object({ + mode: z.literal('ASSUME_ROLE'), + roleArn: z.string().regex(/^arn:aws[a-zA-Z-]*:iam::\d{12}:role\/.+$/), + externalId: z.string().min(2).max(1224).optional(), +}); +const S3AuthOutput = S3AssumeRoleAuthOutput; +const S3CredentialConfigOutput = z.object({ + type: z.literal('S3'), + bucket: z + .string() + .min(3) + .max(63) + .regex(/^[a-z0-9][a-z0-9.-]*[a-z0-9]$/), + region: z.string().regex(/^[a-z]{2}(-[a-z]+)+-\d+$/), + auth: S3AuthOutput, +}); +const AzureBlobSasAuthResponse = z.object({ mode: z.literal('SAS_TOKEN') }); +const AzureBlobAuthResponse = AzureBlobSasAuthResponse; +const AzureBlobCredentialConfigResponse = z.object({ + type: z.literal('AZURE_BLOB'), + storageAccountName: z.string(), + containerName: z.string(), + auth: AzureBlobAuthResponse, +}); +const CredentialConfigResponse = z.discriminatedUnion('type', [ + GcsCredentialConfigOutput, + S3CredentialConfigOutput, + AzureBlobCredentialConfigResponse, +]); +const DataDeliveryCredential = z.object({ + credentialId: z.string(), + name: z.string(), + destinationType: z.enum(['GCS', 'S3', 'AZURE_BLOB', 'SNOWFLAKE', 'DATABRICKS']), + config: CredentialConfigResponse, + Status: z.enum(['PENDING', 'VALIDATED', 'FAILED']), + statusError: z.string().optional(), + validatedAt: z.string().optional(), + expiresAt: z.string().optional(), + createdAt: z.string(), + updatedAt: z.string(), +}); +const RevalidateDataDeliveryCredentialResponse = z.object({ credential: DataDeliveryCredential }); const DiscoveryRefinementItem = z.union([ z.object({ scope: z.literal('request'), ask: z.string().min(1).max(2000) }).passthrough(), z @@ -84,6 +304,7 @@ const DiscoverProductsBody = z advertiserId: z.number().int().lte(9007199254740991), discoveryId: z.string().optional(), campaignId: z.string().optional(), + proposalCode: z.string().optional(), channels: z.array(z.enum(['display', 'olv', 'ctv', 'social', 'video'])).optional(), countries: z .array(z.string().regex(/^[A-Z]{2}$/)) @@ -100,8 +321,8 @@ const DiscoverProductsBody = z .optional(), publisherDomain: z.string().min(1).optional(), pricingModel: z.enum(['cpm', 'vcpm', 'cpc', 'cpcv', 'cpv', 'cpp', 'flat_rate']).optional(), - SalesAgentIds: z.array(z.string().max(255)).max(50).optional(), - salesAgentNames: z.array(z.string().max(255)).max(50).optional(), + StorefrontIds: z.array(z.number().int().lte(9007199254740991)).max(50).optional(), + StorefrontNames: z.array(z.string().max(255)).max(50).optional(), groupLimit: z.number().int().lte(10).optional().default(10), groupOffset: z.number().int().gte(0).lte(9007199254740991).optional().default(0), productsPerGroup: z.number().int().lte(15).optional().default(10), @@ -133,6 +354,8 @@ const Product = z.object({ cpm: z.number().optional(), salesAgentId: z.string().optional(), salesAgentName: z.string().optional(), + storefrontId: z.string().optional(), + storefrontName: z.string().optional(), description: z.string().optional(), deliveryType: z.enum(['guaranteed', 'non_guaranteed']).optional(), briefRelevance: z.string().optional(), @@ -193,6 +416,8 @@ const Proposal = z.object({ briefAlignment: z.string().optional(), salesAgentId: z.string().optional(), salesAgentName: z.string().optional(), + storefrontId: z.string().optional(), + storefrontName: z.string().optional(), allocations: z.array(ProductAllocation).min(1), expiresAt: z.string().optional(), totalBudgetGuidance: z @@ -215,6 +440,7 @@ const AgentDiscoveryResult = z.object({ success: z.boolean(), productCount: z.number().int().gte(0).lte(9007199254740991), error: z.string().optional(), + skipReason: z.string().optional(), rawResponseData: z.unknown().optional(), debugLogs: z.array(AgentDebugLog).optional(), }); @@ -235,7 +461,10 @@ const DiscoverProductsResponse = z.object({ agentResults: z.array(AgentDiscoveryResult).optional(), refinementApplied: z.array(RefinementApplied).optional(), }); -const SalesAgentIds = z.union([z.array(z.string().max(255)), z.string()]).optional(); +const StorefrontIds = z + .union([z.array(z.number().int().lte(9007199254740991)), z.string()]) + .optional(); +const StorefrontNames = z.union([z.array(z.string().max(255)), z.string()]).optional(); const Debug = z.union([z.boolean(), z.string()]).optional(); const SelectedProduct = z.object({ productId: z.string(), @@ -325,183 +554,28 @@ const MediaBuyStatus = z ]), ]) .optional(); -const DurationOutput = z.object({ - interval: z.number().int().lte(9007199254740991), - unit: z.enum(['minutes', 'hours', 'days', 'campaign']), -}); -const OptimizationAttributionWindowOutput = z.object({ - postClick: DurationOutput, - postView: DurationOutput.optional(), -}); -const EventGoalOutput = z.object({ - kind: z.literal('event'), - eventSources: z - .array( - z.object({ - eventSourceId: z.string().min(1), - EventType: z.enum([ - 'page_view', - 'view_content', - 'select_content', - 'select_item', - 'search', - 'share', - 'add_to_cart', - 'remove_from_cart', - 'viewed_cart', - 'add_to_wishlist', - 'initiate_checkout', - 'add_payment_info', - 'purchase', - 'refund', - 'lead', - 'qualify_lead', - 'close_convert_lead', - 'disqualify_lead', - 'complete_registration', - 'subscribe', - 'start_trial', - 'app_install', - 'app_launch', - 'contact', - 'schedule', - 'donate', - 'submit_application', - 'custom', - ]), - customEventName: z.string().optional(), - valueField: z.string().optional(), - valueFactor: z.number().optional(), - }) - ) - .min(1), - target: z - .union([ - z.object({ kind: z.literal('cost_per'), value: z.number().gt(0) }), - z.object({ kind: z.literal('per_ad_spend'), value: z.number().gt(0) }), - z.object({ kind: z.literal('maximize_value') }), - ]) - .optional(), - attributionWindow: OptimizationAttributionWindowOutput.optional(), - priority: z.number().int().gte(1).lte(9007199254740991).optional(), -}); -const MetricGoalOutput = z.object({ - kind: z.literal('metric'), - metric: z.enum([ - 'clicks', - 'views', - 'completed_views', - 'viewed_seconds', - 'attention_seconds', - 'attention_score', - 'engagements', - 'follows', - 'saves', - 'profile_visits', - ]), - viewDurationSeconds: z.number().gt(0).optional(), - target: z - .union([ - z.object({ kind: z.literal('cost_per'), value: z.number().gt(0) }), - z.object({ kind: z.literal('threshold_rate'), value: z.number().gt(0) }), - ]) - .optional(), - priority: z.number().int().gte(1).lte(9007199254740991).optional(), -}); -const OptimizationGoalOutput = z.discriminatedUnion('kind', [EventGoalOutput, MetricGoalOutput]); -const PerformanceConfigOutput = z.object({ - optimizationGoals: z.array(OptimizationGoalOutput).min(1), -}); -const Campaign = z.object({ +const CampaignType = z.enum(['DECISIONED', 'ROUTED']); +const CampaignSummary = z.object({ campaignId: z.string(), advertiserId: z.string(), name: z.string(), Status: z.enum(['DRAFT', 'ACTIVE', 'PAUSED', 'COMPLETED', 'ARCHIVED']), - brief: z.string().optional(), + campaignType: CampaignType.optional(), flightDates: z .object({ startDate: z.string().datetime({ offset: true }), endDate: z.string().datetime({ offset: true }), }) .optional(), - budget: z - .object({ - total: z.number().gt(0), - currency: z.string().min(3).max(3).default('USD'), - dailyCap: z.number().gt(0).optional(), - pacing: z.enum(['EVEN', 'ASAP', 'FRONTLOADED']).optional(), - }) - .optional(), - constraints: z - .object({ - channels: z.array(z.string()), - countries: z.array(z.string().regex(/^[A-Z]{2}$/)).max(250), - }) - .partial() - .optional(), - performanceConfig: PerformanceConfigOutput.optional(), - optimizationApplyMode: OptimizationApplyMode, - catalogId: z.number().int().lte(9007199254740991).optional(), - discoveryId: z.string().optional(), productCount: z.number().int().gte(0).lte(9007199254740991).optional(), - products: z.array(z.object({ productId: z.string() })).optional(), - audiences: z - .array( - z.object({ - audienceId: z.string(), - name: z.string().nullable(), - Status: z.enum(['PROCESSING', 'ERROR', 'READY', 'TOO_SMALL']), - type: z.enum(['TARGET', 'SUPPRESS']), - enabledAt: z.string().datetime({ offset: true }), - }) - ) - .optional(), - mediaBuys: z - .array( - z.object({ - mediaBuyId: z.string(), - name: z.string(), - Status: z.string(), - products: z - .array( - z.object({ - productId: z.string(), - salesAgentName: z.string().optional(), - budget: z.number().optional(), - budgetCurrency: z.string().optional(), - }) - ) - .optional(), - packages: z - .array( - z.object({ - packageId: z.string(), - Status: z.string(), - budget: z.number().optional(), - budgetCurrency: z.string().optional(), - pacing: z.string().optional(), - bidPrice: z.number().optional(), - productIds: z.array(z.string()), - delivery: z - .object({ - impressions: z.number(), - spend: z.number(), - clicks: z.number().nullable(), - }) - .optional(), - }) - ) - .optional(), - createdAt: z.string().datetime({ offset: true }), - updatedAt: z.string().datetime({ offset: true }), - }) - ) - .optional(), createdAt: z.string().datetime({ offset: true }), updatedAt: z.string().datetime({ offset: true }), + budget: z + .object({ total: z.number().gt(0), currency: z.string().min(3).max(3).default('USD') }) + .optional(), }); const CampaignListResponse = z.object({ - campaigns: z.array(Campaign), + campaigns: z.array(CampaignSummary), total: z.number().int().gte(0).lte(9007199254740991), }); const Duration = z @@ -598,6 +672,25 @@ const OptimizationGoal = z.discriminatedUnion('kind', [EventGoal, MetricGoal]); const PerformanceConfig = z .object({ optimizationGoals: z.array(OptimizationGoal).min(1) }) .passthrough(); +const PacingPeriods = z + .object({ + mode: z.enum(['weight', 'budget']), + periods: z + .array( + z + .object({ + label: z.string().min(1).max(100), + start: z.string().regex(/^\d{4}-\d{2}-\d{2}$/), + end: z.string().regex(/^\d{4}-\d{2}-\d{2}$/), + weight: z.number().gte(0).lte(10).optional(), + budget: z.number().gte(0).optional(), + }) + .passthrough() + ) + .min(1) + .max(52), + }) + .passthrough(); const CampaignUtmConfig = z .object({ params: z @@ -613,6 +706,10 @@ const CampaignUtmConfig = z deleteMissing: z.boolean().optional(), }) .passthrough(); +const CampaignDataDeliveryInput = z + .object({ outputs: DataDeliveryOutputArrayInput }) + .partial() + .passthrough(); const CreateCampaignBody = z .object({ advertiserId: z.number().int().lte(9007199254740991), @@ -631,19 +728,130 @@ const CreateCampaignBody = z pacing: z.enum(['EVEN', 'ASAP', 'FRONTLOADED']).optional(), }) .passthrough(), + campaignType: CampaignType, brief: z.string().max(5000).optional(), constraints: z .object({ - channels: z.array(z.string()), - countries: z.array(z.string().regex(/^[A-Z]{2}$/)).max(250), - }) - .partial() - .passthrough() - .optional(), - discoveryId: z.string().min(1).optional(), - productIds: z.array(z.string()).optional(), - audienceConfig: z - .object({ + geo_countries: z.array(z.string()), + geo_countries_exclude: z.array(z.string()), + geo_regions: z.array(z.string()), + geo_regions_exclude: z.array(z.string()), + geo_metros: z.array( + z + .object({ + system: z.union([ + z.literal('nielsen_dma'), + z.literal('uk_itl1'), + z.literal('uk_itl2'), + z.literal('eurostat_nuts2'), + z.literal('custom'), + ]), + values: z.array(z.string()), + }) + .passthrough() + ), + geo_metros_exclude: z.array( + z + .object({ + system: z.union([ + z.literal('nielsen_dma'), + z.literal('uk_itl1'), + z.literal('uk_itl2'), + z.literal('eurostat_nuts2'), + z.literal('custom'), + ]), + values: z.array(z.string()), + }) + .passthrough() + ), + geo_postal_areas: z.array( + z + .object({ + system: z.union([ + z.literal('us_zip'), + z.literal('us_zip_plus_four'), + z.literal('gb_outward'), + z.literal('gb_full'), + z.literal('ca_fsa'), + z.literal('ca_full'), + z.literal('de_plz'), + z.literal('fr_code_postal'), + z.literal('au_postcode'), + z.literal('ch_plz'), + z.literal('at_plz'), + ]), + values: z.array(z.string()), + }) + .passthrough() + ), + geo_postal_areas_exclude: z.array( + z + .object({ + system: z.union([ + z.literal('us_zip'), + z.literal('us_zip_plus_four'), + z.literal('gb_outward'), + z.literal('gb_full'), + z.literal('ca_fsa'), + z.literal('ca_full'), + z.literal('de_plz'), + z.literal('fr_code_postal'), + z.literal('au_postcode'), + z.literal('ch_plz'), + z.literal('at_plz'), + ]), + values: z.array(z.string()), + }) + .passthrough() + ), + language: z.array(z.string()), + device_platform: z.array( + z.union([ + z.literal('ios'), + z.literal('android'), + z.literal('windows'), + z.literal('macos'), + z.literal('linux'), + z.literal('chromeos'), + z.literal('tvos'), + z.literal('tizen'), + z.literal('webos'), + z.literal('fire_os'), + z.literal('roku_os'), + z.literal('unknown'), + ]) + ), + device_type: z.array( + z.union([ + z.literal('desktop'), + z.literal('mobile'), + z.literal('tablet'), + z.literal('ctv'), + z.literal('dooh'), + z.literal('unknown'), + ]) + ), + device_type_exclude: z.array( + z.union([ + z.literal('desktop'), + z.literal('mobile'), + z.literal('tablet'), + z.literal('ctv'), + z.literal('dooh'), + z.literal('unknown'), + ]) + ), + channels: z.array(z.string()), + countries: z.array(z.string().regex(/^[A-Z]{2}$/)).max(250), + }) + .partial() + .passthrough() + .optional(), + StorefrontIds: z.array(z.number().int().lte(9007199254740991)).max(50).optional(), + discoveryId: z.string().min(1).optional(), + productIds: z.array(z.string()).optional(), + audienceConfig: z + .object({ targetAudienceIds: z.array(z.string().min(1)).max(100), suppressAudienceIds: z.array(z.string().min(1)).max(100), }) @@ -653,9 +861,855 @@ const CreateCampaignBody = z performanceConfig: PerformanceConfig.optional(), optimizationApplyMode: OptimizationApplyMode.optional(), catalogId: z.number().int().lte(9007199254740991).optional(), + pacingPeriods: PacingPeriods.optional(), utmConfig: CampaignUtmConfig.optional(), + dataDelivery: CampaignDataDeliveryInput.optional(), + frequencyCaps: z + .array( + z + .object({ + max_impressions: z.number().int().lte(9007199254740991), + window: z + .object({ + interval: z.number().gte(1), + unit: z.union([ + z.literal('seconds'), + z.literal('minutes'), + z.literal('hours'), + z.literal('days'), + z.literal('campaign'), + ]), + }) + .passthrough(), + }) + .passthrough() + ) + .optional(), }) .passthrough(); +const MediaBuyRef = z.object({ MediaBuyId: z.string(), Status: z.string() }); +const MediaBudget = z.object({ total: z.number().gt(0), currency: z.string().min(3).max(3) }); +const CampaignFeePricingType = z.enum(['MARGIN', 'UNIT']); +const CampaignFeeUnit = z.enum(['CPM', 'RECORD', 'FLAT_RATE']); +const CampaignFee = z.object({ + label: z.string(), + amount: z.number().gte(0), + currency: z.string().min(3).max(3), + pricingType: CampaignFeePricingType.optional(), + marginPercent: z.number().gte(0).optional(), + unit: CampaignFeeUnit.optional(), + unitPrice: z.number().gte(0).optional(), +}); +const CampaignFees = z.array(CampaignFee); +const PacingPeriodsOutput = z.object({ + mode: z.enum(['weight', 'budget']), + periods: z + .array( + z.object({ + label: z.string().min(1).max(100), + start: z.string().regex(/^\d{4}-\d{2}-\d{2}$/), + end: z.string().regex(/^\d{4}-\d{2}-\d{2}$/), + weight: z.number().gte(0).lte(10).optional(), + budget: z.number().gte(0).optional(), + }) + ) + .min(1) + .max(52), +}); +const CampaignStorefrontRef = z.object({ + id: z.number().int().lte(9007199254740991), + platformId: z.string(), + name: z.string(), +}); +const DurationOutput = z.object({ + interval: z.number().int().lte(9007199254740991), + unit: z.enum(['minutes', 'hours', 'days', 'campaign']), +}); +const OptimizationAttributionWindowOutput = z.object({ + postClick: DurationOutput, + postView: DurationOutput.optional(), +}); +const EventGoalOutput = z.object({ + kind: z.literal('event'), + eventSources: z + .array( + z.object({ + eventSourceId: z.string().min(1), + EventType: z.enum([ + 'page_view', + 'view_content', + 'select_content', + 'select_item', + 'search', + 'share', + 'add_to_cart', + 'remove_from_cart', + 'viewed_cart', + 'add_to_wishlist', + 'initiate_checkout', + 'add_payment_info', + 'purchase', + 'refund', + 'lead', + 'qualify_lead', + 'close_convert_lead', + 'disqualify_lead', + 'complete_registration', + 'subscribe', + 'start_trial', + 'app_install', + 'app_launch', + 'contact', + 'schedule', + 'donate', + 'submit_application', + 'custom', + ]), + customEventName: z.string().optional(), + valueField: z.string().optional(), + valueFactor: z.number().optional(), + }) + ) + .min(1), + target: z + .union([ + z.object({ kind: z.literal('cost_per'), value: z.number().gt(0) }), + z.object({ kind: z.literal('per_ad_spend'), value: z.number().gt(0) }), + z.object({ kind: z.literal('maximize_value') }), + ]) + .optional(), + attributionWindow: OptimizationAttributionWindowOutput.optional(), + priority: z.number().int().gte(1).lte(9007199254740991).optional(), +}); +const MetricGoalOutput = z.object({ + kind: z.literal('metric'), + metric: z.enum([ + 'clicks', + 'views', + 'completed_views', + 'viewed_seconds', + 'attention_seconds', + 'attention_score', + 'engagements', + 'follows', + 'saves', + 'profile_visits', + ]), + viewDurationSeconds: z.number().gt(0).optional(), + target: z + .union([ + z.object({ kind: z.literal('cost_per'), value: z.number().gt(0) }), + z.object({ kind: z.literal('threshold_rate'), value: z.number().gt(0) }), + ]) + .optional(), + priority: z.number().int().gte(1).lte(9007199254740991).optional(), +}); +const OptimizationGoalOutput = z.discriminatedUnion('kind', [EventGoalOutput, MetricGoalOutput]); +const PerformanceConfigOutput = z.object({ + optimizationGoals: z.array(OptimizationGoalOutput).min(1), +}); +const GcsDeliveryConfigOutput = z.object({ + type: z.literal('GCS'), + pathPrefix: z.string().max(1024).default(''), + format: z.enum(['JSONL', 'PARQUET', 'CSV']).default('JSONL'), +}); +const S3DeliveryConfigOutput = z.object({ + type: z.literal('S3'), + pathPrefix: z.string().max(1024).default(''), + format: z.enum(['JSONL', 'PARQUET', 'CSV']).default('JSONL'), +}); +const AzureBlobDeliveryConfigOutput = z.object({ + type: z.literal('AZURE_BLOB'), + pathPrefix: z.string().max(1024).default(''), + format: z.enum(['JSONL', 'PARQUET', 'CSV']).default('JSONL'), +}); +const DeliveryConfigOutput = z.discriminatedUnion('type', [ + GcsDeliveryConfigOutput, + S3DeliveryConfigOutput, + AzureBlobDeliveryConfigOutput, +]); +const DataDeliveryOutput = z.object({ + outputConfigId: z.string(), + dataDeliveryType: z.enum([ + 'MB_DELIVERY', + 'IMPRESSIONS', + 'CLICKS', + 'VAST_EVENTS', + 'CAPI_ATTRIBUTION', + 'MMP_POSTBACKS', + ]), + cadence: z.enum(['HOURLY', 'DAILY', 'WEEKLY']), + syncWeeklyDay: z.number().int().gte(0).lte(6).optional(), + enabled: z.boolean(), + credentialId: z.string(), + credentialName: z.string(), + deliveryConfig: DeliveryConfigOutput, + source: z.enum(['advertiser', 'campaign']), + createdAt: z.string(), + updatedAt: z.string(), +}); +const CampaignDataDelivery = z.object({ outputs: z.array(DataDeliveryOutput) }).partial(); +const FrequencyCapTargetLevel = z.enum(['ADVERTISER', 'CAMPAIGN', 'CREATIVE']); +const Campaign = z.object({ + campaignId: z.string(), + advertiserId: z.string(), + name: z.string(), + Status: z.enum(['DRAFT', 'ACTIVE', 'PAUSED', 'COMPLETED', 'ARCHIVED']), + mediaBuyRefs: z.array(MediaBuyRef).optional(), + campaignType: CampaignType.optional(), + brief: z.string().optional(), + flightDates: z + .object({ + startDate: z.string().datetime({ offset: true }), + endDate: z.string().datetime({ offset: true }), + }) + .optional(), + budget: z + .object({ + total: z.number().gt(0), + currency: z.string().min(3).max(3).default('USD'), + dailyCap: z.number().gt(0).optional(), + pacing: z.enum(['EVEN', 'ASAP', 'FRONTLOADED']).optional(), + }) + .optional(), + mediaBudget: MediaBudget.optional(), + fees: CampaignFees.optional(), + allocatedBudget: z.number().gte(0).optional(), + unallocatedBudget: z.number().optional(), + pacingPeriods: PacingPeriodsOutput.optional(), + constraints: z + .object({ + geo_countries: z.array(z.string()), + geo_countries_exclude: z.array(z.string()), + geo_regions: z.array(z.string()), + geo_regions_exclude: z.array(z.string()), + geo_metros: z.array( + z + .object({ + system: z.union([ + z.literal('nielsen_dma'), + z.literal('uk_itl1'), + z.literal('uk_itl2'), + z.literal('eurostat_nuts2'), + z.literal('custom'), + ]), + values: z.array(z.string()), + }) + .passthrough() + ), + geo_metros_exclude: z.array( + z + .object({ + system: z.union([ + z.literal('nielsen_dma'), + z.literal('uk_itl1'), + z.literal('uk_itl2'), + z.literal('eurostat_nuts2'), + z.literal('custom'), + ]), + values: z.array(z.string()), + }) + .passthrough() + ), + geo_postal_areas: z.array( + z + .object({ + system: z.union([ + z.literal('us_zip'), + z.literal('us_zip_plus_four'), + z.literal('gb_outward'), + z.literal('gb_full'), + z.literal('ca_fsa'), + z.literal('ca_full'), + z.literal('de_plz'), + z.literal('fr_code_postal'), + z.literal('au_postcode'), + z.literal('ch_plz'), + z.literal('at_plz'), + ]), + values: z.array(z.string()), + }) + .passthrough() + ), + geo_postal_areas_exclude: z.array( + z + .object({ + system: z.union([ + z.literal('us_zip'), + z.literal('us_zip_plus_four'), + z.literal('gb_outward'), + z.literal('gb_full'), + z.literal('ca_fsa'), + z.literal('ca_full'), + z.literal('de_plz'), + z.literal('fr_code_postal'), + z.literal('au_postcode'), + z.literal('ch_plz'), + z.literal('at_plz'), + ]), + values: z.array(z.string()), + }) + .passthrough() + ), + language: z.array(z.string()), + device_platform: z.array( + z.union([ + z.literal('ios'), + z.literal('android'), + z.literal('windows'), + z.literal('macos'), + z.literal('linux'), + z.literal('chromeos'), + z.literal('tvos'), + z.literal('tizen'), + z.literal('webos'), + z.literal('fire_os'), + z.literal('roku_os'), + z.literal('unknown'), + ]) + ), + device_type: z.array( + z.union([ + z.literal('desktop'), + z.literal('mobile'), + z.literal('tablet'), + z.literal('ctv'), + z.literal('dooh'), + z.literal('unknown'), + ]) + ), + device_type_exclude: z.array( + z.union([ + z.literal('desktop'), + z.literal('mobile'), + z.literal('tablet'), + z.literal('ctv'), + z.literal('dooh'), + z.literal('unknown'), + ]) + ), + channels: z.array(z.string()), + countries: z.array(z.string().regex(/^[A-Z]{2}$/)).max(250), + }) + .partial() + .passthrough() + .optional(), + storefronts: z.array(CampaignStorefrontRef).optional(), + performanceConfig: PerformanceConfigOutput.optional(), + optimizationApplyMode: OptimizationApplyMode, + catalogId: z.number().int().lte(9007199254740991).optional(), + discoveryId: z.string().optional(), + productCount: z.number().int().gte(0).lte(9007199254740991).optional(), + products: z.array(z.object({ productId: z.string() })).optional(), + audiences: z + .array( + z.object({ + audienceId: z.string(), + name: z.string().nullable(), + Status: z.enum(['PROCESSING', 'ERROR', 'READY', 'TOO_SMALL']), + type: z.enum(['TARGET', 'SUPPRESS']), + enabledAt: z.string().datetime({ offset: true }), + }) + ) + .optional(), + creativeFormats: z + .object({ + required: z.array(z.object({ agent_url: z.string(), id: z.string() })), + covered: z.array(z.object({ agent_url: z.string(), id: z.string() })), + missing: z.array(z.object({ agent_url: z.string(), id: z.string() })), + }) + .optional(), + propertyLists: z + .object({ + propertyLists: z.array( + z.object({ + listId: z.string(), + name: z.string(), + purpose: z.enum(['include', 'exclude']), + propertyCount: z.number().int().gte(0).lte(9007199254740991), + createdAt: z.string(), + updatedAt: z.string(), + viaMediaBuys: z.array( + z.object({ MediaBuyId: z.string(), packageIds: z.array(z.string()) }) + ), + }) + ), + summary: z.object({ + totalLists: z.number().int().gte(0).lte(9007199254740991), + includeCount: z.number().int().gte(0).lte(9007199254740991), + excludeCount: z.number().int().gte(0).lte(9007199254740991), + }), + }) + .optional(), + mediaBuys: z + .array( + z.object({ + MediaBuyId: z.string(), + name: z.string(), + Status: z.string(), + startTime: z.string().optional(), + endTime: z.string().optional(), + products: z + .array( + z.object({ + productId: z.string(), + productName: z.string().optional(), + publisherName: z.string().optional(), + salesAgentName: z.string().optional(), + budget: z.number().optional(), + budgetCurrency: z.string().optional(), + }) + ) + .optional(), + pacingPeriods: PacingPeriodsOutput.optional(), + packages: z + .array( + z.object({ + packageId: z.string(), + Status: z.string(), + budget: z.number().optional(), + budgetCurrency: z.string().optional(), + pacing: z.string().optional(), + bidPrice: z.number().optional(), + productIds: z.array(z.string()), + delivery: z + .object({ + impressions: z.number(), + spend: z.number(), + clicks: z.number().nullable(), + }) + .optional(), + }) + ) + .optional(), + optimizationGoals: z + .array( + z.union([ + z + .object({ + kind: z.literal('metric'), + metric: z.union([ + z.literal('clicks'), + z.literal('views'), + z.literal('completed_views'), + z.literal('viewed_seconds'), + z.literal('attention_seconds'), + z.literal('attention_score'), + z.literal('engagements'), + z.literal('follows'), + z.literal('saves'), + z.literal('profile_visits'), + z.literal('reach'), + ]), + reach_unit: z + .union([ + z.literal('individuals'), + z.literal('households'), + z.literal('devices'), + z.literal('accounts'), + z.literal('cookies'), + z.literal('custom'), + ]) + .optional(), + target_frequency: z + .object({ + min: z.number().gte(1).optional(), + max: z.number().gte(1).optional(), + window: z + .object({ + interval: z.number().gte(1), + unit: z.union([ + z.literal('seconds'), + z.literal('minutes'), + z.literal('hours'), + z.literal('days'), + z.literal('campaign'), + ]), + }) + .passthrough(), + }) + .passthrough() + .optional(), + view_duration_seconds: z.number().optional(), + target: z + .union([ + z.object({ kind: z.literal('cost_per'), value: z.number() }).passthrough(), + z + .object({ kind: z.literal('threshold_rate'), value: z.number() }) + .passthrough(), + ]) + .optional(), + priority: z.number().gte(1).optional(), + }) + .passthrough(), + z + .object({ + kind: z.literal('event'), + event_sources: z.array( + z + .object({ + event_source_id: z.string().min(1), + event_type: z.union([ + z.literal('page_view'), + z.literal('view_content'), + z.literal('select_content'), + z.literal('select_item'), + z.literal('search'), + z.literal('share'), + z.literal('add_to_cart'), + z.literal('remove_from_cart'), + z.literal('viewed_cart'), + z.literal('add_to_wishlist'), + z.literal('initiate_checkout'), + z.literal('add_payment_info'), + z.literal('purchase'), + z.literal('refund'), + z.literal('lead'), + z.literal('qualify_lead'), + z.literal('close_convert_lead'), + z.literal('disqualify_lead'), + z.literal('complete_registration'), + z.literal('subscribe'), + z.literal('start_trial'), + z.literal('app_install'), + z.literal('app_launch'), + z.literal('contact'), + z.literal('schedule'), + z.literal('donate'), + z.literal('submit_application'), + z.literal('custom'), + ]), + custom_event_name: z.string().optional(), + value_field: z.string().optional(), + value_factor: z.number().optional(), + }) + .passthrough() + ), + target: z + .union([ + z.object({ kind: z.literal('cost_per'), value: z.number() }).passthrough(), + z + .object({ kind: z.literal('per_ad_spend'), value: z.number() }) + .passthrough(), + z.object({ kind: z.literal('maximize_value') }).passthrough(), + ]) + .optional(), + attribution_window: z + .object({ + post_click: z + .object({ + interval: z.number().gte(1), + unit: z.union([ + z.literal('seconds'), + z.literal('minutes'), + z.literal('hours'), + z.literal('days'), + z.literal('campaign'), + ]), + }) + .passthrough(), + post_view: z + .object({ + interval: z.number().gte(1), + unit: z.union([ + z.literal('seconds'), + z.literal('minutes'), + z.literal('hours'), + z.literal('days'), + z.literal('campaign'), + ]), + }) + .passthrough(), + model: z.union([ + z.literal('last_touch'), + z.literal('first_touch'), + z.literal('linear'), + z.literal('time_decay'), + z.literal('data_driven'), + ]), + }) + .partial() + .passthrough() + .optional(), + priority: z.number().gte(1).optional(), + }) + .passthrough(), + z + .object({ + kind: z.literal('vendor_metric'), + vendor: z + .object({ + domain: z + .string() + .regex(/^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/), + brand_id: z.string().optional(), + industries: z.array(z.string()).optional(), + data_subject_contestation: z + .object({ + url: z.string().regex(/^https:\/\//), + email: z + .string() + .regex( + /^(?!\.)(?!.*\.\.)([A-Za-z0-9_'+\-\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\-]*\.)+[A-Za-z]{2,}$/ + ) + .email(), + languages: z.array(z.string()), + }) + .partial() + .passthrough() + .optional(), + brand_kit_override: z + .object({ + logo: z + .object({ + asset_type: z.literal('image'), + url: z.string(), + width: z.number().gte(1), + height: z.number().gte(1), + format: z.string().optional(), + alt_text: z.string().optional(), + provenance: z + .object({ + digital_source_type: z.union([ + z.literal('digital_capture'), + z.literal('digital_creation'), + z.literal('trained_algorithmic_media'), + z.literal('composite_with_trained_algorithmic_media'), + z.literal('algorithmic_media'), + z.literal('composite_capture'), + z.literal('composite_synthetic'), + z.literal('human_edits'), + z.literal('data_driven_media'), + ]), + ai_tool: z + .object({ + name: z.string(), + version: z.string().optional(), + provider: z.string().optional(), + }) + .passthrough(), + human_oversight: z.union([ + z.literal('none'), + z.literal('prompt_only'), + z.literal('selected'), + z.literal('edited'), + z.literal('directed'), + ]), + declared_by: z + .object({ + agent_url: z.string().optional(), + role: z.union([ + z.literal('creator'), + z.literal('advertiser'), + z.literal('agency'), + z.literal('platform'), + z.literal('tool'), + ]), + }) + .passthrough(), + declared_at: z.string().datetime({ offset: true }), + created_time: z.string().datetime({ offset: true }), + c2pa: z.object({ manifest_url: z.string() }).passthrough(), + embedded_provenance: z.array( + z + .object({ + method: z.union([ + z.literal('manifest_wrapper'), + z.literal('provenance_markers'), + ]), + standard: z.string().optional(), + provider: z.string(), + verify_agent: z + .object({ + agent_url: z.string().regex(/^https:\/\//), + feature_id: z.string().optional(), + }) + .passthrough() + .optional(), + embedded_at: z + .string() + .datetime({ offset: true }) + .optional(), + }) + .passthrough() + ), + watermarks: z.array( + z + .object({ + media_type: z.union([ + z.literal('audio'), + z.literal('image'), + z.literal('video'), + z.literal('text'), + ]), + provider: z.string(), + verify_agent: z + .object({ + agent_url: z.string().regex(/^https:\/\//), + feature_id: z.string().optional(), + }) + .passthrough() + .optional(), + c2pa_action: z + .union([ + z.literal('c2pa.watermarked.bound'), + z.literal('c2pa.watermarked.unbound'), + ]) + .optional(), + embedded_at: z + .string() + .datetime({ offset: true }) + .optional(), + }) + .passthrough() + ), + disclosure: z + .object({ + required: z.boolean(), + jurisdictions: z + .array( + z + .object({ + country: z.string(), + region: z.string().optional(), + regulation: z.string(), + label_text: z.string().optional(), + render_guidance: z + .object({ + persistence: z.union([ + z.literal('continuous'), + z.literal('initial'), + z.literal('flexible'), + ]), + min_duration_ms: z.number().gte(1), + positions: z.array( + z.union([ + z.literal('prominent'), + z.literal('footer'), + z.literal('audio'), + z.literal('subtitle'), + z.literal('overlay'), + z.literal('end_card'), + z.literal('pre_roll'), + z.literal('companion'), + ]) + ), + ext: z.object({}).partial().passthrough(), + }) + .partial() + .passthrough() + .optional(), + }) + .passthrough() + ) + .optional(), + }) + .passthrough(), + verification: z.array( + z + .object({ + verified_by: z.string(), + verified_time: z + .string() + .datetime({ offset: true }) + .optional(), + result: z.union([ + z.literal('authentic'), + z.literal('ai_generated'), + z.literal('ai_modified'), + z.literal('inconclusive'), + ]), + confidence: z.number().gte(0).lte(1).optional(), + details_url: z.string().optional(), + }) + .passthrough() + ), + ext: z.object({}).partial().passthrough(), + }) + .partial() + .passthrough() + .optional(), + }) + .passthrough(), + colors: z + .object({ + primary: z.string().regex(/^#[0-9a-fA-F]{6}$/), + secondary: z.string().regex(/^#[0-9a-fA-F]{6}$/), + accent: z.string().regex(/^#[0-9a-fA-F]{6}$/), + }) + .partial() + .passthrough(), + voice: z.string(), + tagline: z.string(), + }) + .partial() + .passthrough() + .optional(), + }) + .passthrough(), + metric_id: z.string(), + target: z + .union([ + z.object({ kind: z.literal('cost_per'), value: z.number() }).passthrough(), + z + .object({ kind: z.literal('threshold_rate'), value: z.number() }) + .passthrough(), + ]) + .optional(), + priority: z.number().gte(1).optional(), + }) + .passthrough(), + ]) + ) + .optional(), + performance: z + .object({ + impressions: z.number(), + spend: z.number(), + clicks: z.number(), + views: z.number(), + completedViews: z.number(), + conversions: z.number(), + leads: z.number(), + lastUpdated: z.string().datetime({ offset: true }).optional(), + }) + .optional(), + createdAt: z.string().datetime({ offset: true }), + updatedAt: z.string().datetime({ offset: true }), + }) + ) + .optional(), + createdAt: z.string().datetime({ offset: true }), + updatedAt: z.string().datetime({ offset: true }), + dataDelivery: CampaignDataDelivery.optional(), + frequencyCaps: z + .array( + z + .object({ + max_impressions: z.number().int().lte(9007199254740991), + window: z + .object({ + interval: z.number().gte(1), + unit: z.union([ + z.literal('seconds'), + z.literal('minutes'), + z.literal('hours'), + z.literal('days'), + z.literal('campaign'), + ]), + }) + .passthrough(), + id: z.string(), + targetLevel: FrequencyCapTargetLevel, + targetId: z.string(), + createdAt: z.string(), + updatedAt: z.string(), + archivedAt: z.string().nullish(), + }) + .passthrough() + ) + .optional(), +}); const CampaignResponse = z.object({ campaign: Campaign }); const UpdateCampaignBody = z .object({ @@ -678,11 +1732,121 @@ const UpdateCampaignBody = z brief: z.string().max(5000), constraints: z .object({ + geo_countries: z.array(z.string()), + geo_countries_exclude: z.array(z.string()), + geo_regions: z.array(z.string()), + geo_regions_exclude: z.array(z.string()), + geo_metros: z.array( + z + .object({ + system: z.union([ + z.literal('nielsen_dma'), + z.literal('uk_itl1'), + z.literal('uk_itl2'), + z.literal('eurostat_nuts2'), + z.literal('custom'), + ]), + values: z.array(z.string()), + }) + .passthrough() + ), + geo_metros_exclude: z.array( + z + .object({ + system: z.union([ + z.literal('nielsen_dma'), + z.literal('uk_itl1'), + z.literal('uk_itl2'), + z.literal('eurostat_nuts2'), + z.literal('custom'), + ]), + values: z.array(z.string()), + }) + .passthrough() + ), + geo_postal_areas: z.array( + z + .object({ + system: z.union([ + z.literal('us_zip'), + z.literal('us_zip_plus_four'), + z.literal('gb_outward'), + z.literal('gb_full'), + z.literal('ca_fsa'), + z.literal('ca_full'), + z.literal('de_plz'), + z.literal('fr_code_postal'), + z.literal('au_postcode'), + z.literal('ch_plz'), + z.literal('at_plz'), + ]), + values: z.array(z.string()), + }) + .passthrough() + ), + geo_postal_areas_exclude: z.array( + z + .object({ + system: z.union([ + z.literal('us_zip'), + z.literal('us_zip_plus_four'), + z.literal('gb_outward'), + z.literal('gb_full'), + z.literal('ca_fsa'), + z.literal('ca_full'), + z.literal('de_plz'), + z.literal('fr_code_postal'), + z.literal('au_postcode'), + z.literal('ch_plz'), + z.literal('at_plz'), + ]), + values: z.array(z.string()), + }) + .passthrough() + ), + language: z.array(z.string()), + device_platform: z.array( + z.union([ + z.literal('ios'), + z.literal('android'), + z.literal('windows'), + z.literal('macos'), + z.literal('linux'), + z.literal('chromeos'), + z.literal('tvos'), + z.literal('tizen'), + z.literal('webos'), + z.literal('fire_os'), + z.literal('roku_os'), + z.literal('unknown'), + ]) + ), + device_type: z.array( + z.union([ + z.literal('desktop'), + z.literal('mobile'), + z.literal('tablet'), + z.literal('ctv'), + z.literal('dooh'), + z.literal('unknown'), + ]) + ), + device_type_exclude: z.array( + z.union([ + z.literal('desktop'), + z.literal('mobile'), + z.literal('tablet'), + z.literal('ctv'), + z.literal('dooh'), + z.literal('unknown'), + ]) + ), channels: z.array(z.string()), countries: z.array(z.string().regex(/^[A-Z]{2}$/)).max(250), }) .partial() .passthrough(), + StorefrontIds: z.array(z.number().int().lte(9007199254740991)).max(50), discoveryId: z.string().min(1), audienceConfig: z .object({ @@ -696,49 +1860,449 @@ const UpdateCampaignBody = z optimizationApplyMode: OptimizationApplyMode, catalogId: z.number().int().lte(9007199254740991).nullable(), mediaBuys: z.array( - z - .object({ - action: z.enum(['update', 'cancel', 'delete']).optional(), - mediaBuyId: z.string().min(1), - reason: z.string().max(1000).optional(), - packageIds: z.array(z.string().min(1)).optional(), - name: z.string().min(1).max(255).optional(), - packages: z - .array( + z.object({ + action: z.enum(['update', 'cancel', 'delete']).optional(), + MediaBuyId: z.string().min(1), + reason: z.string().max(1000).optional(), + packageIds: z.array(z.string().min(1)).optional(), + name: z.string().min(1).max(255).optional(), + packages: z + .array( + z.object({ + packageId: z.string().min(1), + budget: z.number().gt(0).optional(), + pacing: z.enum(['even', 'asap']).optional(), + bid_price: z.number().nullish(), + }) + ) + .optional(), + pacingPeriods: PacingPeriods.nullish(), + products: z + .array( + z.object({ + product_id: z.string().min(1), + pricing_option_id: z.string().optional(), + budget: z.number().gt(0).optional(), + pacing: z.enum(['asap', 'even', 'front_loaded']).optional(), + bid_price: z.number().nullish(), + remove: z.boolean().optional(), + }) + ) + .optional(), + start_time: z.string().optional(), + end_time: z.string().optional(), + updated_reason: z.string().optional(), + suggestion_id: z.string().optional(), + optimization_goals: z + .array( + z.union([ z .object({ - packageId: z.string().min(1), - budget: z.number().gt(0).optional(), - pacing: z.enum(['even', 'asap']).optional(), - bid_price: z.number().nullish(), + kind: z.literal('metric'), + metric: z.union([ + z.literal('clicks'), + z.literal('views'), + z.literal('completed_views'), + z.literal('viewed_seconds'), + z.literal('attention_seconds'), + z.literal('attention_score'), + z.literal('engagements'), + z.literal('follows'), + z.literal('saves'), + z.literal('profile_visits'), + z.literal('reach'), + ]), + reach_unit: z + .union([ + z.literal('individuals'), + z.literal('households'), + z.literal('devices'), + z.literal('accounts'), + z.literal('cookies'), + z.literal('custom'), + ]) + .optional(), + target_frequency: z + .object({ + min: z.number().gte(1).optional(), + max: z.number().gte(1).optional(), + window: z + .object({ + interval: z.number().gte(1), + unit: z.union([ + z.literal('seconds'), + z.literal('minutes'), + z.literal('hours'), + z.literal('days'), + z.literal('campaign'), + ]), + }) + .passthrough(), + }) + .passthrough() + .optional(), + view_duration_seconds: z.number().optional(), + target: z + .union([ + z.object({ kind: z.literal('cost_per'), value: z.number() }).passthrough(), + z + .object({ kind: z.literal('threshold_rate'), value: z.number() }) + .passthrough(), + ]) + .optional(), + priority: z.number().gte(1).optional(), }) - .passthrough() - ) - .optional(), - products: z - .array( + .passthrough(), z .object({ - product_id: z.string().min(1), - pricing_option_id: z.string().optional(), - budget: z.number().gt(0).optional(), - pacing: z.enum(['asap', 'even', 'front_loaded']).optional(), - bid_price: z.number().nullish(), + kind: z.literal('event'), + event_sources: z.array( + z + .object({ + event_source_id: z.string().min(1), + event_type: z.union([ + z.literal('page_view'), + z.literal('view_content'), + z.literal('select_content'), + z.literal('select_item'), + z.literal('search'), + z.literal('share'), + z.literal('add_to_cart'), + z.literal('remove_from_cart'), + z.literal('viewed_cart'), + z.literal('add_to_wishlist'), + z.literal('initiate_checkout'), + z.literal('add_payment_info'), + z.literal('purchase'), + z.literal('refund'), + z.literal('lead'), + z.literal('qualify_lead'), + z.literal('close_convert_lead'), + z.literal('disqualify_lead'), + z.literal('complete_registration'), + z.literal('subscribe'), + z.literal('start_trial'), + z.literal('app_install'), + z.literal('app_launch'), + z.literal('contact'), + z.literal('schedule'), + z.literal('donate'), + z.literal('submit_application'), + z.literal('custom'), + ]), + custom_event_name: z.string().optional(), + value_field: z.string().optional(), + value_factor: z.number().optional(), + }) + .passthrough() + ), + target: z + .union([ + z.object({ kind: z.literal('cost_per'), value: z.number() }).passthrough(), + z + .object({ kind: z.literal('per_ad_spend'), value: z.number() }) + .passthrough(), + z.object({ kind: z.literal('maximize_value') }).passthrough(), + ]) + .optional(), + attribution_window: z + .object({ + post_click: z + .object({ + interval: z.number().gte(1), + unit: z.union([ + z.literal('seconds'), + z.literal('minutes'), + z.literal('hours'), + z.literal('days'), + z.literal('campaign'), + ]), + }) + .passthrough(), + post_view: z + .object({ + interval: z.number().gte(1), + unit: z.union([ + z.literal('seconds'), + z.literal('minutes'), + z.literal('hours'), + z.literal('days'), + z.literal('campaign'), + ]), + }) + .passthrough(), + model: z.union([ + z.literal('last_touch'), + z.literal('first_touch'), + z.literal('linear'), + z.literal('time_decay'), + z.literal('data_driven'), + ]), + }) + .partial() + .passthrough() + .optional(), + priority: z.number().gte(1).optional(), }) - .passthrough() - ) - .optional(), - start_time: z.string().optional(), - end_time: z.string().optional(), - updated_reason: z.string().optional(), - suggestion_id: z.string().optional(), + .passthrough(), + z + .object({ + kind: z.literal('vendor_metric'), + vendor: z + .object({ + domain: z + .string() + .regex(/^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/), + brand_id: z.string().optional(), + industries: z.array(z.string()).optional(), + data_subject_contestation: z + .object({ + url: z.string().regex(/^https:\/\//), + email: z + .string() + .regex( + /^(?!\.)(?!.*\.\.)([A-Za-z0-9_'+\-\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\-]*\.)+[A-Za-z]{2,}$/ + ) + .email(), + languages: z.array(z.string()), + }) + .partial() + .passthrough() + .optional(), + brand_kit_override: z + .object({ + logo: z + .object({ + asset_type: z.literal('image'), + url: z.string(), + width: z.number().gte(1), + height: z.number().gte(1), + format: z.string().optional(), + alt_text: z.string().optional(), + provenance: z + .object({ + digital_source_type: z.union([ + z.literal('digital_capture'), + z.literal('digital_creation'), + z.literal('trained_algorithmic_media'), + z.literal('composite_with_trained_algorithmic_media'), + z.literal('algorithmic_media'), + z.literal('composite_capture'), + z.literal('composite_synthetic'), + z.literal('human_edits'), + z.literal('data_driven_media'), + ]), + ai_tool: z + .object({ + name: z.string(), + version: z.string().optional(), + provider: z.string().optional(), + }) + .passthrough(), + human_oversight: z.union([ + z.literal('none'), + z.literal('prompt_only'), + z.literal('selected'), + z.literal('edited'), + z.literal('directed'), + ]), + declared_by: z + .object({ + agent_url: z.string().optional(), + role: z.union([ + z.literal('creator'), + z.literal('advertiser'), + z.literal('agency'), + z.literal('platform'), + z.literal('tool'), + ]), + }) + .passthrough(), + declared_at: z.string().datetime({ offset: true }), + created_time: z.string().datetime({ offset: true }), + c2pa: z.object({ manifest_url: z.string() }).passthrough(), + embedded_provenance: z.array( + z + .object({ + method: z.union([ + z.literal('manifest_wrapper'), + z.literal('provenance_markers'), + ]), + standard: z.string().optional(), + provider: z.string(), + verify_agent: z + .object({ + agent_url: z.string().regex(/^https:\/\//), + feature_id: z.string().optional(), + }) + .passthrough() + .optional(), + embedded_at: z + .string() + .datetime({ offset: true }) + .optional(), + }) + .passthrough() + ), + watermarks: z.array( + z + .object({ + media_type: z.union([ + z.literal('audio'), + z.literal('image'), + z.literal('video'), + z.literal('text'), + ]), + provider: z.string(), + verify_agent: z + .object({ + agent_url: z.string().regex(/^https:\/\//), + feature_id: z.string().optional(), + }) + .passthrough() + .optional(), + c2pa_action: z + .union([ + z.literal('c2pa.watermarked.bound'), + z.literal('c2pa.watermarked.unbound'), + ]) + .optional(), + embedded_at: z + .string() + .datetime({ offset: true }) + .optional(), + }) + .passthrough() + ), + disclosure: z + .object({ + required: z.boolean(), + jurisdictions: z + .array( + z + .object({ + country: z.string(), + region: z.string().optional(), + regulation: z.string(), + label_text: z.string().optional(), + render_guidance: z + .object({ + persistence: z.union([ + z.literal('continuous'), + z.literal('initial'), + z.literal('flexible'), + ]), + min_duration_ms: z.number().gte(1), + positions: z.array( + z.union([ + z.literal('prominent'), + z.literal('footer'), + z.literal('audio'), + z.literal('subtitle'), + z.literal('overlay'), + z.literal('end_card'), + z.literal('pre_roll'), + z.literal('companion'), + ]) + ), + ext: z.object({}).partial().passthrough(), + }) + .partial() + .passthrough() + .optional(), + }) + .passthrough() + ) + .optional(), + }) + .passthrough(), + verification: z.array( + z + .object({ + verified_by: z.string(), + verified_time: z + .string() + .datetime({ offset: true }) + .optional(), + result: z.union([ + z.literal('authentic'), + z.literal('ai_generated'), + z.literal('ai_modified'), + z.literal('inconclusive'), + ]), + confidence: z.number().gte(0).lte(1).optional(), + details_url: z.string().optional(), + }) + .passthrough() + ), + ext: z.object({}).partial().passthrough(), + }) + .partial() + .passthrough() + .optional(), + }) + .passthrough(), + colors: z + .object({ + primary: z.string().regex(/^#[0-9a-fA-F]{6}$/), + secondary: z.string().regex(/^#[0-9a-fA-F]{6}$/), + accent: z.string().regex(/^#[0-9a-fA-F]{6}$/), + }) + .partial() + .passthrough(), + voice: z.string(), + tagline: z.string(), + }) + .partial() + .passthrough() + .optional(), + }) + .passthrough(), + metric_id: z.string(), + target: z + .union([ + z.object({ kind: z.literal('cost_per'), value: z.number() }).passthrough(), + z + .object({ kind: z.literal('threshold_rate'), value: z.number() }) + .passthrough(), + ]) + .optional(), + priority: z.number().gte(1).optional(), + }) + .passthrough(), + ]) + ) + .optional(), + creative_ids: z.array(z.string().min(1)).optional(), + }) + ), + pacingPeriods: PacingPeriods.nullable(), + utmConfig: CampaignUtmConfig, + dataDelivery: z.object({ outputs: DataDeliveryOutputArrayInput }).partial().passthrough(), + frequencyCaps: z.array( + z + .object({ + max_impressions: z.number().int().lte(9007199254740991), + window: z + .object({ + interval: z.number().gte(1), + unit: z.union([ + z.literal('seconds'), + z.literal('minutes'), + z.literal('hours'), + z.literal('days'), + z.literal('campaign'), + ]), + }) + .passthrough(), }) .passthrough() ), - utmConfig: CampaignUtmConfig, }) .partial() .passthrough(); +const MediaBuyId = z.union([z.array(z.string()), z.string()]).optional(); +const IncludePropertyLists = z.union([z.boolean(), z.enum(['true', 'false'])]).optional(); const ExecuteCampaignBody = z.object({ Debug: z.boolean() }).partial().passthrough(); const ExecuteMediaBuyDebugInfo = z .object({ @@ -749,7 +2313,7 @@ const ExecuteMediaBuyDebugInfo = z }) .partial(); const ExecutionError = z.object({ - mediaBuyId: z.string(), + MediaBuyId: z.string(), salesAgentId: z.string(), message: z.string(), Debug: ExecuteMediaBuyDebugInfo.optional(), @@ -774,36 +2338,134 @@ const RefinementItem = z.union([ ]); const AutoSelectProductsRequest = z .object({ - refine: z.array(RefinementItem).min(1), - maxProducts: z.number().int().lte(9007199254740991), - minBudgetPerProduct: z.number().gt(0), + refine: z.array(RefinementItem).min(1), + maxProducts: z.number().int().lte(9007199254740991), + minBudgetPerProduct: z.number().gt(0), + }) + .partial() + .passthrough(); +const AutoSelectProductsResponse = z.object({ + campaignId: z.string(), + discoveryId: z.string(), + selectedProducts: z.array( + z.object({ + productId: z.string(), + name: z.string(), + salesAgentId: z.string(), + groupId: z.string(), + groupName: z.string(), + cpm: z.number().optional(), + budget: z.number(), + pricingOptionId: z.string().optional(), + }) + ), + budgetContext: z.object({ + campaignBudget: z.number(), + totalAllocated: z.number(), + remainingBudget: z.number(), + currency: z.string(), + }), + productCount: z.number().int().gte(0).lte(9007199254740991), + previouslySelectedCount: z.number().int().gte(0).lte(9007199254740991).optional(), +}); +const GetAdcpStatusOutput = z.object({ + campaign_id: z.string(), + media_buys: z.array( + z.object({ + media_buy_id: z.string(), + adcp_media_buy_id: z.string(), + internal_status: z.string(), + adcp_status: z.string().nullable(), + previous_internal_status: z.string(), + previous_adcp_status: z.string().nullable(), + updated: z.boolean(), + }) + ), + agents_queried: z.number().int().gte(0).lte(9007199254740991), + errors: z.array(z.object({ media_buy_id: z.string(), error: z.string() })), +}); +const CampaignProductEntry = z.object({ + productId: z.string(), + productName: z.string().optional(), + salesAgentId: z.string(), + salesAgentName: z.string().optional(), + publisherDomain: z.string().optional(), + publisherName: z.string().optional(), + bidPrice: z.number().optional(), + budget: z.number().optional(), + pricingOptionId: z.string().optional(), + pricingModel: z.string().optional(), + selectedAt: z.string(), + searchContext: z.object({ id: z.string(), brief: z.string() }).optional(), + mediaBuys: z.array(z.object({ MediaBuyId: z.string(), Status: z.string(), name: z.string() })), +}); +const CampaignSearchContextSummary = z.object({ + id: z.string(), + brief: z.string(), + channels: z.array(z.string()), + countries: z.array(z.string()), + createdAt: z.string(), + productCount: z.number().int().gte(0).lte(9007199254740991), +}); +const CampaignProductsResponse = z.object({ + campaignId: z.string(), + discoveryId: z.string().nullable(), + products: z.array(CampaignProductEntry), + searchContexts: z.array(CampaignSearchContextSummary), + summary: z.object({ + totalProducts: z.number().int().gte(0).lte(9007199254740991), + productsOnMediaBuys: z.number().int().gte(0).lte(9007199254740991), + productsPending: z.number().int().gte(0).lte(9007199254740991), + }), +}); +const ResourceTypes = z + .union([z.array(z.enum(['CAMPAIGN', 'CREATIVE', 'MEDIA_BUY', 'PRODUCT', 'PACKAGE'])), z.string()]) + .optional(); +const BuyerAuditLog = z.object({ + id: z.number().int().gte(-9007199254740991).lte(9007199254740991), + timestamp: z.string(), + createdAt: z.string(), + action: z.string(), + resourceType: z.string(), + resourceId: z.string().nullable(), + resourceName: z.string().nullable(), + parentType: z.string().nullable(), + advertiserId: z.number().int().gte(-9007199254740991).lte(9007199254740991).nullable(), + userId: z.number().int().gte(-9007199254740991).lte(9007199254740991).nullable(), + userEmail: z.string().nullable(), + userName: z.string().nullable(), + serviceTokenId: z.number().int().gte(-9007199254740991).lte(9007199254740991).nullable(), + serviceTokenName: z.string().nullable(), + parameters: z.object({}).partial().passthrough().nullable(), + changes: z.object({}).partial().passthrough().nullable(), + description: z.string(), +}); +const ListBuyerActivityResponse = z.object({ + logs: z.array(BuyerAuditLog), + total: z.number().int().gte(0).lte(9007199254740991), +}); +const UrlAssetSlotInput = z + .object({ + asset_id: z.string().min(1).max(255), + url: z.string().url(), + url_type: z.enum(['CLICKTHROUGH', 'TRACKER_PIXEL', 'TRACKER_SCRIPT', 'VAST']).optional(), + }) + .passthrough(); +const TextAssetSlotInput = z + .object({ asset_id: z.string().min(1).max(255), content: z.string().min(1).max(5000) }) + .passthrough(); +const CardInput = z + .object({ + filename: z.string().min(1), + url: z.string().url(), + headline: z.string().max(255), + description: z.string().max(2000), + cta: z.string().max(50), + landing_page_url: z.string().url(), + platform_extensions: z.array(z.object({}).partial().passthrough()), }) .partial() .passthrough(); -const AutoSelectProductsResponse = z.object({ - campaignId: z.string(), - discoveryId: z.string(), - selectedProducts: z.array( - z.object({ - productId: z.string(), - name: z.string(), - salesAgentId: z.string(), - groupId: z.string(), - groupName: z.string(), - cpm: z.number().optional(), - budget: z.number(), - pricingOptionId: z.string().optional(), - }) - ), - budgetContext: z.object({ - campaignBudget: z.number(), - totalAllocated: z.number(), - remainingBudget: z.number(), - currency: z.string(), - }), - productCount: z.number().int().gte(0).lte(9007199254740991), - previouslySelectedCount: z.number().int().gte(0).lte(9007199254740991).optional(), -}); const CreateCreativeManifestMetadata = z .object({ name: z.string().min(1).max(255), @@ -811,9 +2473,11 @@ const CreateCreativeManifestMetadata = z url_asset: z .object({ url: z.string().url(), - url_type: z.enum(['CLICKTHROUGH', 'TRACKER_PIXEL', 'TRACKER_SCRIPT', 'VIDEO_VAST']), + url_type: z.enum(['CLICKTHROUGH', 'TRACKER_PIXEL', 'TRACKER_SCRIPT', 'VAST']), }) .passthrough(), + url_assets: z.array(UrlAssetSlotInput).max(50), + text_assets: z.array(TextAssetSlotInput).max(50), webhook_asset: z .object({ url: z.string().url(), @@ -832,10 +2496,10 @@ const CreateCreativeManifestMetadata = z format_id: z .object({ agent_url: z.string(), - id: z.string(), - width: z.number().nullish(), - height: z.number().nullish(), - duration_ms: z.number().nullish(), + id: z.string().regex(/^[a-zA-Z0-9_-]+$/), + width: z.number().gte(1).optional(), + height: z.number().gte(1).optional(), + duration_ms: z.number().gte(1).optional(), }) .passthrough(), template_id: z.string(), @@ -860,9 +2524,30 @@ const CreateCreativeManifestMetadata = z ]) .optional(), label: z.string().optional(), + slot_asset_id: z.string().min(1).max(255).optional(), }) .passthrough() ), + frequencyCaps: z.array( + z + .object({ + max_impressions: z.number().int().lte(9007199254740991), + window: z + .object({ + interval: z.number().gte(1), + unit: z.union([ + z.literal('seconds'), + z.literal('minutes'), + z.literal('hours'), + z.literal('days'), + z.literal('campaign'), + ]), + }) + .passthrough(), + }) + .passthrough() + ), + cards: z.array(CardInput).min(2).max(10), }) .partial() .passthrough(); @@ -888,8 +2573,14 @@ const ManifestAssetResponse = z.object({ file_size: z.number().int().gte(-9007199254740991).lte(9007199254740991), public_url: z.string().url(), asset_source: z.enum(['CREATIVE_SOURCE', 'USER_UPLOADED', 'SYSTEM_PROCESSED']), + slot_asset_id: z.string().optional(), created_at: z.string().datetime({ offset: true }), }); +const CreativeManifestSyncStatus = z.object({ + synced: z.boolean(), + agent_count: z.number().int().gte(-9007199254740991).lte(9007199254740991), + last_synced_at: z.string().datetime({ offset: true }).optional(), +}); const CreativeManifestResponse = z.object({ creative_id: z.string(), name: z.string(), @@ -938,13 +2629,7 @@ const CreativeManifestResponse = z.object({ }) .optional(), creative_manifest: z.unknown().optional(), - sync_status: z - .object({ - synced: z.boolean(), - agent_count: z.number().int().gte(-9007199254740991).lte(9007199254740991), - last_synced_at: z.string().datetime({ offset: true }).optional(), - }) - .optional(), + sync_status: CreativeManifestSyncStatus.optional(), tracking: z .object({ impression_tracker_url: z.string().optional(), @@ -953,11 +2638,80 @@ const CreativeManifestResponse = z.object({ }) .optional(), campaign_id: z.string(), + frequencyCaps: z + .array( + z + .object({ + max_impressions: z.number().int().lte(9007199254740991), + window: z + .object({ + interval: z.number().gte(1), + unit: z.union([ + z.literal('seconds'), + z.literal('minutes'), + z.literal('hours'), + z.literal('days'), + z.literal('campaign'), + ]), + }) + .passthrough(), + id: z.string(), + targetLevel: FrequencyCapTargetLevel, + targetId: z.string(), + createdAt: z.string(), + updatedAt: z.string(), + archivedAt: z.string().nullish(), + }) + .passthrough() + ) + .optional(), + already_exists: z.boolean().optional(), + ignored_files: z.number().int().gte(0).lte(9007199254740991).optional(), + created_at: z.string().datetime({ offset: true }), + updated_at: z.string().datetime({ offset: true }), +}); +const CreativeManifestSummary = z.object({ + creative_id: z.string(), + campaign_id: z.string(), + name: z.string(), + format_id: z + .object({ + id: z.string(), + agent_url: z.string(), + width: z.number().optional(), + height: z.number().optional(), + duration_ms: z.number().optional(), + }) + .passthrough() + .optional(), + template_id: z.string().optional(), + brand_domain: z.string().optional(), + preview_url: z.string().url().optional(), created_at: z.string().datetime({ offset: true }), updated_at: z.string().datetime({ offset: true }), + asset_count: z.number().int().gte(0).lte(9007199254740991), + sync_status: z + .object({ + synced: z.boolean(), + agent_count: z.number().int().gte(-9007199254740991).lte(9007199254740991), + }) + .optional(), + target_format_ids: z + .array( + z + .object({ + id: z.string(), + agent_url: z.string(), + width: z.number().optional(), + height: z.number().optional(), + duration_ms: z.number().optional(), + }) + .passthrough() + ) + .optional(), }); const CreativeManifestListResponse = z.object({ - manifests: z.array(CreativeManifestResponse), + manifests: z.array(CreativeManifestSummary), total: z.number().int().gte(0).lte(9007199254740991), }); const UpdateCreativeManifestMetadata = z @@ -967,19 +2721,32 @@ const UpdateCreativeManifestMetadata = z format_id: z .object({ agent_url: z.string(), - id: z.string(), - width: z.number().nullish(), - height: z.number().nullish(), - duration_ms: z.number().nullish(), + id: z.string().regex(/^[a-zA-Z0-9_-]+$/), + width: z.number().gte(1).optional(), + height: z.number().gte(1).optional(), + duration_ms: z.number().gte(1).optional(), }) .passthrough(), + target_format_ids: z.array( + z + .object({ + agent_url: z.string(), + id: z.string().regex(/^[a-zA-Z0-9_-]+$/), + width: z.number().gte(1).optional(), + height: z.number().gte(1).optional(), + duration_ms: z.number().gte(1).optional(), + }) + .passthrough() + ), template_id: z.string(), url_asset: z .object({ url: z.string().url(), - url_type: z.enum(['CLICKTHROUGH', 'TRACKER_PIXEL', 'TRACKER_SCRIPT', 'VIDEO_VAST']), + url_type: z.enum(['CLICKTHROUGH', 'TRACKER_PIXEL', 'TRACKER_SCRIPT', 'VAST']), }) .passthrough(), + url_assets: z.array(UrlAssetSlotInput).max(50), + text_assets: z.array(TextAssetSlotInput).max(50), webhook_asset: z .object({ url: z.string().url(), @@ -996,6 +2763,7 @@ const UpdateCreativeManifestMetadata = z }) .passthrough(), delete_asset_ids: z.array(z.string().min(1)).max(100), + primary_asset_id: z.string().min(1), reclassify_assets: z .array( z @@ -1040,6 +2808,26 @@ const UpdateCreativeManifestMetadata = z ]) .optional(), label: z.string().optional(), + slot_asset_id: z.string().min(1).max(255).optional(), + }) + .passthrough() + ), + frequencyCaps: z.array( + z + .object({ + max_impressions: z.number().int().lte(9007199254740991), + window: z + .object({ + interval: z.number().gte(1), + unit: z.union([ + z.literal('seconds'), + z.literal('minutes'), + z.literal('hours'), + z.literal('days'), + z.literal('campaign'), + ]), + }) + .passthrough(), }) .passthrough() ), @@ -1159,6 +2947,91 @@ const EventSummaryResponse = z.object({ entries: z.array(EventSummaryEntry), totalEventCount: z.number().int().gte(0).lte(9007199254740991), }); +const UserMatch = z + .object({ + uids: z.array( + z.object({ type: z.string().min(1).max(64), value: z.string().min(1).max(512) }).passthrough() + ), + hashed_email: z.string().regex(/^[a-f0-9]{64}$/), + hashed_phone: z.string().regex(/^[a-f0-9]{64}$/), + click_id: z.string().max(512), + click_id_type: z.string().max(64), + client_ip: z.string().max(45), + client_user_agent: z.string().max(512), + }) + .partial() + .passthrough(); +const ContentItem = z + .object({ + id: z.string().max(256), + quantity: z.number().int().lte(9007199254740991), + price: z.number(), + brand: z.string().max(256), + }) + .partial() + .passthrough(); +const CustomData = z + .object({ + value: z.number(), + currency: z.string().regex(/^[A-Z]{3}$/), + order_id: z.string().max(256), + content_ids: z.array(z.string().max(256)), + content_type: z.string().max(128), + num_items: z.number().int().gte(0).lte(9007199254740991), + contents: z.array(ContentItem), + }) + .partial() + .passthrough(); +const LogEventObject = z + .object({ + event_id: z.string().min(1).max(256), + event_type: z.enum([ + 'purchase', + 'lead', + 'add_to_cart', + 'initiate_checkout', + 'view_content', + 'complete_registration', + 'page_view', + 'app_install', + 'deposit', + 'subscription', + 'custom', + ]), + event_time: z + .string() + .regex( + /^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$/ + ) + .datetime({ offset: true }), + user_match: UserMatch.optional(), + custom_data: CustomData.optional(), + action_source: z + .enum(['website', 'app', 'in_store', 'phone_call', 'system_generated', 'other']) + .optional(), + event_source_url: z.string().url().optional(), + custom_event_name: z.string().max(256).optional(), + }) + .passthrough(); +const LogEventRequest = z + .object({ + event_source_id: z.string().min(1).max(256), + events: z.array(LogEventObject).min(1).max(10000), + test_event_code: z.string().max(256).optional(), + }) + .passthrough(); +const LogEventPartialFailure = z.object({ + event_id: z.string(), + code: z.string(), + message: z.string(), +}); +const LogEventResponse = z.object({ + events_received: z.number().int().gte(0).lte(9007199254740991), + events_processed: z.number().int().gte(0).lte(9007199254740991), + partial_failures: z.array(LogEventPartialFailure).optional(), + warnings: z.array(z.string()).optional(), + match_quality: z.number().gte(0).lte(1).optional(), +}); const SyncMeasurementObject = z .object({ start_time: z @@ -1210,6 +3083,183 @@ const MeasurementDataSyncResult = z.object({ error: z.string().optional(), }); const SyncMeasurementDataResponse = z.object({ measurements: z.array(MeasurementDataSyncResult) }); +const TestCohortSummary = z.object({ + id: z.string(), + advertiserId: z.string(), + name: z.string(), + cohortType: z.string(), + role: z.enum(['TREATMENT', 'CONTROL', 'OBSERVATION']), + estimatedSize: z.number().int().gte(0).lte(9007199254740991).optional(), + isActive: z.boolean(), + createdAt: z.string().datetime({ offset: true }), + updatedAt: z.string().datetime({ offset: true }), +}); +const TestCohortListResponse = z.object({ + cohorts: z.array(TestCohortSummary), + total: z.number().int().gte(0).lte(9007199254740991), +}); +const CohortDefinition = z.object({ type: z.string().min(1) }).passthrough(); +const CreateTestCohortInput = z + .object({ + name: z.string().min(1).max(255), + description: z.string().max(1000).optional(), + cohortType: z.string().min(1), + role: z.enum(['TREATMENT', 'CONTROL', 'OBSERVATION']).optional().default('TREATMENT'), + definition: CohortDefinition, + estimatedSize: z.number().int().gte(0).lte(9007199254740991).optional(), + }) + .passthrough(); +const TestCohortOutput = z.object({ + id: z.string(), + advertiserId: z.string(), + name: z.string(), + description: z.string().optional(), + cohortType: z.string(), + role: z.enum(['TREATMENT', 'CONTROL', 'OBSERVATION']), + definition: CohortDefinition, + estimatedSize: z.number().int().gte(0).lte(9007199254740991).optional(), + isActive: z.boolean(), + createdAt: z.string().datetime({ offset: true }), + updatedAt: z.string().datetime({ offset: true }), +}); +const TestCohortResponse = z.object({ cohort: TestCohortOutput }); +const UpdateTestCohortInput = z + .object({ + name: z.string().min(1).max(255), + description: z.string().max(1000), + cohortType: z.string().min(1), + role: z.enum(['TREATMENT', 'CONTROL', 'OBSERVATION']), + definition: CohortDefinition, + estimatedSize: z.number().int().gte(0).lte(9007199254740991), + }) + .partial() + .passthrough(); +const MmmConfig = z + .object({ + provider: z.string(), + dataSourceIds: z.array(z.string()), + reportingFrequency: z.enum(['weekly', 'monthly', 'quarterly']), + }) + .partial() + .passthrough(); +const MeasurementConfigOutput = z.object({ + advertiserId: z.string(), + mmmEnabled: z.boolean(), + mmmConfig: MmmConfig.optional(), + incrementalityTestingEnabled: z.boolean(), + brandLiftEnabled: z.boolean(), + settings: z.object({}).partial().passthrough().optional(), + createdAt: z.string().datetime({ offset: true }), + updatedAt: z.string().datetime({ offset: true }), +}); +const MeasurementConfigResponse = z.object({ measurementConfig: MeasurementConfigOutput }); +const UpdateMeasurementConfigInput = z + .object({ + mmmEnabled: z.boolean(), + mmmConfig: MmmConfig, + incrementalityTestingEnabled: z.boolean(), + brandLiftEnabled: z.boolean(), + settings: z.object({}).partial().passthrough(), + }) + .partial() + .passthrough(); +const CreateMeasurementSourceBody = z + .object({ + sourceKey: z.string().min(1), + name: z.string().min(1), + outcomeType: z.string().min(1), + outcomeTypes: z.array(z.string()).optional(), + granularity: z.string().min(1), + lagWeeks: z.number().int().gte(0).lte(9007199254740991).optional().default(1), + cadence: z.enum(['continuous', 'daily', 'weekly', 'biweekly', 'monthly', 'quarterly']), + provider: z.string().min(1), + ingestionMethod: z.string().optional(), + attributionConfig: z.object({}).partial().passthrough().optional(), + signalWeight: z.number().gte(0).lte(1).optional().default(1), + Status: z.enum(['pending', 'active', 'paused']).optional().default('pending'), + notes: z.string().optional(), + }) + .passthrough(); +const UpdateMeasurementSourceBody = z + .object({ + name: z.string().min(1), + outcomeType: z.string().min(1), + outcomeTypes: z.array(z.string()), + granularity: z.string().min(1), + lagWeeks: z.number().int().gte(0).lte(9007199254740991).default(1), + cadence: z.enum(['continuous', 'daily', 'weekly', 'biweekly', 'monthly', 'quarterly']), + provider: z.string().min(1), + ingestionMethod: z.string(), + attributionConfig: z.object({}).partial().passthrough(), + signalWeight: z.number().gte(0).lte(1).default(1), + Status: z.enum(['pending', 'active', 'paused']).default('pending'), + notes: z.string(), + }) + .partial() + .passthrough(); +const UploadMeasurementRecordsBody = z + .object({ + records: z + .array( + z + .object({ + outcomeType: z.string().min(1), + geo: z.string().min(1), + timeWindowStart: z + .string() + .regex( + /^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))$/ + ), + timeWindowEnd: z + .string() + .regex( + /^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))$/ + ), + value: z.number(), + baselineValue: z.number().optional(), + confidenceInterval: z.number().optional(), + source: z.string().min(1), + lagDays: z.number().int().gte(-9007199254740991).lte(9007199254740991).optional(), + }) + .passthrough() + ) + .min(1) + .max(5000), + }) + .passthrough(); +const UploadContextRecordsBody = z + .object({ + records: z + .array( + z + .object({ + geo: z.string().min(1), + timeWindowStart: z + .string() + .regex( + /^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))$/ + ), + timeWindowEnd: z + .string() + .regex( + /^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))$/ + ), + promoActive: z.boolean().optional().default(false), + promoType: z.string().optional(), + temperatureAvg: z.number().optional(), + competitorActivity: z.object({}).partial().passthrough().optional(), + seasonalityIndex: z.number().optional(), + flightStatus: z + .enum(['active', 'dark', 'pre_flight', 'post_flight']) + .optional() + .default('active'), + }) + .passthrough() + ) + .min(1) + .max(5000), + }) + .passthrough(); const ReportingMetrics = z.object({ impressions: z.number().int().gte(0).lte(9007199254740991), spend: z.number().gte(0), @@ -1227,10 +3277,11 @@ const ReportingMetrics = z.object({ const PackageReporting = z.object({ packageId: z.string(), productId: z.string().nullable(), + productName: z.string().nullable(), metrics: ReportingMetrics, }); const MediaBuyReporting = z.object({ - mediaBuyId: z.string(), + MediaBuyId: z.string(), name: z.string(), Status: z.string(), budget: z.number().nullable(), @@ -1255,6 +3306,147 @@ const ReportingMetricsResponse = z.object({ periodStart: z.string().regex(/^\d{4}-\d{2}-\d{2}$/), periodEnd: z.string().regex(/^\d{4}-\d{2}-\d{2}$/), }); +const CurrentAccountResponse = z.object({ + id: z.number().int().gte(-9007199254740991).lte(9007199254740991), + company: z.string(), + name: z.string(), + role: z.string().optional(), + customerDomain: z.string().nullable(), +}); +const OrgAccountSummary = z.object({ + id: z.number().int().gte(-9007199254740991).lte(9007199254740991), + company: z.string(), + name: z.string(), + role: z.string().optional(), +}); +const ListCustomerAccountsResponse = z.object({ accounts: z.array(OrgAccountSummary) }); +const CreateChildAccountBody = z + .object({ + name: z.string().min(1).max(255), + customerRole: z.enum(['BUYER', 'SELLER']), + parentName: z.string().min(1).max(255).optional(), + customerDomain: z + .string() + .max(255) + .regex(/^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/) + .optional(), + }) + .passthrough(); +const CreateChildAccountResponse = z.object({ + user: z.object({}).partial().passthrough(), + customer: z.object({}).partial().passthrough(), + customers: z.array(z.object({}).partial().passthrough()), + showTosBox: z.boolean(), + organizationContractMissing: z.boolean().optional(), + hasContract: z.boolean(), + latestTosVersion: z.string(), + convertedFromStandalone: z.boolean(), +}); +const UpdateCustomerDomainBody = z + .object({ + customerDomain: z + .string() + .max(255) + .regex(/^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/), + }) + .passthrough(); +const UpdateCustomerDomainResponse = z.object({ + id: z.number().int().gte(-9007199254740991).lte(9007199254740991), + customerDomain: z.string().nullable(), +}); +const MembershipSettingsResponse = z.object({ + customerId: z.number().int().gte(-9007199254740991).lte(9007199254740991), + allowDomainAutoJoin: z.boolean(), +}); +const UpdateMembershipSettingsBody = z.object({ allowDomainAutoJoin: z.boolean() }).passthrough(); +const UpdateNotificationPreferencesBody = z + .object({ + optIns: z + .array( + z + .object({ + notificationType: z.enum([ + 'brand_agent.created', + 'brand_agent.updated', + 'brand_agent.deleted', + 'campaign.healthy', + 'campaign.unhealthy', + 'campaign.created', + 'campaign.updated', + 'campaign.deleted', + 'campaign.completed', + 'creative.approved', + 'creative.rejected', + 'creative.changes_requested', + 'creative.sync_started', + 'creative.sync_completed', + 'creative.sync_failed', + 'creative.created', + 'creative.updated', + 'creative.deleted', + 'strategy.created', + 'strategy.updated', + 'strategy.deleted', + 'media_buy.created', + 'media_buy.updated', + 'media_buy.deleted', + 'salesagent.available', + 'salesagent.unavailable', + 'salesagent.registered', + 'salesagent.unregistered', + 'salesagent.updated', + 'signalsagent.registered', + 'signalsagent.unregistered', + 'signalsagent.updated', + 'signalsagent.signal_activated', + 'signalsagent.signals_fetched', + 'outcomesagent.registered', + 'outcomesagent.unregistered', + 'outcomesagent.updated', + 'syndication.completed', + 'syndication.failed', + 'audience.synced', + 'audience.sync_failed', + 'optimization.suggestion_received', + 'optimization.suggestion_approved', + 'optimization.suggestion_rejected', + 'optimization.suggestion_applied', + 'optimization.suggestion_failed', + 'system.warning', + 'system.error', + 'learning_cycle.completed', + 'learning_cycle.failed', + 'hypothesis.status_changed', + 'hypothesis.review_requested', + 'hypothesis.proven', + 'hypothesis.disproven', + 'measurement.received', + 'measurement.stale', + 'opportunity.evaluated', + 'opportunity.recommended', + 'opportunity.flagged', + 'opportunity.explore', + ]), + channel: z.enum(['email', 'in_app']), + }) + .passthrough() + ) + .max(200), + }) + .passthrough(); +const CheckModerationBody = z + .object({ + text: z.string().min(1).max(10000), + direction: z.enum(['input', 'output']).optional().default('input'), + surface: z.string().min(1).max(100).optional().default('moderation.check'), + }) + .passthrough(); +const BuyerCredentialSourceRef = z.object({ + storefrontId: z.number().int().gte(-9007199254740991).lte(9007199254740991), + storefrontName: z.string(), + sourceId: z.string(), + sourceName: z.string(), +}); const AvailableAccountOutput = z.object({ accountId: z.string(), name: z.string().nullish(), @@ -1262,8 +3454,7 @@ const AvailableAccountOutput = z.object({ billingProxy: z.string().nullish(), house: z.string().nullish(), billing: z.string().nullish(), - partnerId: z.string(), - partnerName: z.string(), + sources: z.array(BuyerCredentialSourceRef), Status: z.enum(['active', 'pending_approval', 'payment_required', 'suspended', 'closed']), }); const AvailableAccountListResponse = z.object({ @@ -1274,6 +3465,48 @@ const AvailableAccountListResponse = z.object({ .object({ default: z.string().nullable(), supported: z.array(z.string()) }) .optional(), }); +const ReportingBucket = z + .object({ + protocol: z.enum(['s3', 'gcs', 'azure_blob']), + bucket: z + .string() + .min(3) + .max(63) + .regex(/^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$/), + prefix: z + .string() + .max(512) + .regex(/^[a-zA-Z0-9\/_.-]+$/) + .optional(), + region: z + .string() + .max(64) + .regex(/^[a-z0-9-]+$/) + .optional(), + format: z.enum(['jsonl', 'csv', 'parquet', 'avro', 'orc']).optional().default('jsonl'), + compression: z.enum(['gzip', 'none']).optional().default('gzip'), + file_retention_days: z.number().int().gte(1).lte(9007199254740991), + setup_instructions: z.string().url().optional(), + }) + .passthrough(); +const UpdateReportingBucketBody = z + .object({ reporting_bucket: ReportingBucket.nullable() }) + .passthrough(); +const AccountOutput = z.object({ + linkId: z.string(), + accountId: z.string(), + name: z.string().nullish(), + advertiser: z.string().nullish(), + billingProxy: z.string().nullish(), + house: z.string().nullish(), + billing: z.string().nullish(), + sources: z.array(BuyerCredentialSourceRef), + advertiserId: z.string(), + Status: z.enum(['active', 'pending_approval', 'payment_required', 'suspended', 'closed']), + createdAt: z.string().datetime({ offset: true }), + updatedAt: z.string().datetime({ offset: true }), +}); +const AccountResponse = z.object({ account: AccountOutput }); const SyncCatalogsBody = z .object({ account: z.object({ account_id: z.string().min(1) }).passthrough(), @@ -1321,10 +3554,25 @@ const SyncCatalogsBody = z validation_mode: z.enum(['strict', 'lenient']).optional().default('strict'), }) .passthrough(); +const BuyerStorefrontSummary = z.object({ + id: z.number().int().gte(-9007199254740991).lte(9007199254740991), + platformId: z.string(), + name: z.string(), + publisherDomain: z.string().nullable(), + displayStatus: z.enum(['configuring', 'transacting', 'archived']), + channels: z.array(z.string()), + sourceCount: z.number().int().gte(0).lte(9007199254740991), + connectedSourceCount: z.number().int().gte(0).lte(9007199254740991), +}); +const BuyerStorefrontList = z.object({ + items: z.array(BuyerStorefrontSummary), + total: z.number().int().gte(0).lte(9007199254740991), + hasMore: z.boolean(), + nextOffset: z.number().int().gte(0).lte(9007199254740991).nullable(), +}); const BuyerStorefrontSource = z.object({ sourceId: z.string(), name: z.string(), - protocol: z.enum(['MCP', 'A2A']), requiresCredentials: z.boolean(), connected: z.boolean(), customerAccounts: z.array(z.object({ accountIdentifier: z.string(), Status: z.string() })), @@ -1334,26 +3582,19 @@ const BuyerStorefront = z.object({ platformId: z.string(), name: z.string(), publisherDomain: z.string().nullable(), + displayStatus: z.enum(['configuring', 'transacting', 'archived']), + channels: z.array(z.string()), sources: z.array(BuyerStorefrontSource), }); -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 OAuthInfo = z.object({ - authorizationUrl: z.string().url(), - agentId: z.string(), - agentName: z.string(), -}); -const AgentAccount = z.object({ +const BuyerCredential = z.object({ id: z.string(), accountIdentifier: z.string(), + accountType: z.string(), Status: z.string(), - registeredBy: z.string(), - createdAt: z.string().datetime({ offset: true }), - oauth: OAuthInfo.optional(), + registeredBy: z.string().nullable(), + createdAt: z.string(), + updatedAt: z.string(), + sources: z.array(BuyerCredentialSourceRef), }); const RegisterSourceCredentialsBody = z .object({ @@ -1383,6 +3624,20 @@ const RegisterSourceCredentialsBody = z marketplaceAccount: z.boolean().optional(), }) .passthrough(); +const BuyerCredentialOAuthInfo = z.object({ + authorizationUrl: z.string().url(), + storefrontId: z.number().int().gte(-9007199254740991).lte(9007199254740991), + sourceId: z.string(), + sourceName: z.string(), +}); +const BuyerCredentialRegistrationResponse = z.object({ + id: z.string(), + accountIdentifier: z.string(), + Status: z.string(), + registeredBy: z.string().nullable(), + createdAt: z.string(), + oauth: BuyerCredentialOAuthInfo.optional(), +}); const SyndicateBody = z .object({ resourceType: z.enum(['AUDIENCE', 'EVENT_SOURCE', 'CATALOG']), @@ -1456,9 +3711,9 @@ const AudienceItem = z }) .passthrough() ) - .max(10000) + .max(100000) .optional(), - remove: z.array(RemoveMember).max(10000).optional(), + remove: z.array(RemoveMember).max(100000).optional(), delete: z.boolean().optional(), consentBasis: z .enum(['consent', 'legitimate_interest', 'contract', 'legal_obligation']) @@ -1472,13 +3727,20 @@ const SyncAudiencesBody = z pushNotificationConfig: z .object({ url: z.string(), - token: z.string().nullish(), + operation_id: z + .string() + .min(1) + .max(255) + .regex(/^[A-Za-z0-9_.:-]{1,255}$/) + .optional(), + token: z.string().min(16).max(4096).optional(), authentication: z .object({ schemes: z.array(z.union([z.literal('Bearer'), z.literal('HMAC-SHA256')])), - credentials: z.string(), + credentials: z.string().min(32), }) - .passthrough(), + .passthrough() + .optional(), }) .passthrough() .optional(), @@ -1500,23 +3762,19 @@ const SyncAudiencesResponse = z.object({ ) .uuid(), }); -const Audience = z.object({ +const AudienceSummary = z.object({ audienceId: z.string(), name: z.string().nullable(), accountId: z.string(), - consentBasis: z - .enum(['consent', 'legitimate_interest', 'contract', 'legal_obligation']) - .nullable(), Status: z.enum(['PROCESSING', 'ERROR', 'READY', 'TOO_SMALL']), deleted: z.boolean(), uploadedCount: z.number().nullable(), matchedCount: z.number().nullable(), - lastOperationStatus: z.enum(['PROCESSING', 'COMPLETED', 'ERROR']).nullable(), createdAt: z.string(), updatedAt: z.string(), }); const AudienceListResponse = z.object({ - audiences: z.array(Audience), + audiences: z.array(AudienceSummary), total: z.number(), take: z.number(), skip: z.number(), @@ -1549,7 +3807,19 @@ const TaskOutput = z.object({ updatedAt: z.string().datetime({ offset: true }), }); const TaskResponse = z.object({ task: TaskOutput }); -const PropertyListFiltersOutput = z +const PropertyListSummary = z.object({ + listId: z.string(), + name: z.string(), + purpose: z.enum(['include', 'exclude']), + propertyCount: z.number().int().gte(0).lte(9007199254740991), + createdAt: z.string().datetime({ offset: true }), + updatedAt: z.string().datetime({ offset: true }), +}); +const PropertyListListResponse = z.object({ + propertyLists: z.array(PropertyListSummary), + total: z.number().int().gte(0).lte(9007199254740991), +}); +const PropertyListFilters = z .object({ channels_any: z .array( @@ -1593,34 +3863,74 @@ const PropertyListFiltersOutput = z .nullable(), feature_requirements: z .array( - z.object({ - feature_id: z.string(), - min_value: z.number().nullish(), - max_value: z.number().nullish(), - allowed_values: z.array(z.unknown()).nullish(), - if_not_covered: z.enum(['exclude', 'include']).nullish(), - }) + z + .object({ + feature_id: z.string(), + min_value: z.number().nullish(), + max_value: z.number().nullish(), + allowed_values: z.array(z.unknown()).nullish(), + if_not_covered: z.enum(['exclude', 'include']).nullish(), + }) + .passthrough() ) .nullable(), }) - .partial(); -const PropertyListOutput = z.object({ - listId: z.string(), - name: z.string(), - purpose: z.enum(['include', 'exclude']), - domains: z.array(z.string()), - unresolvedDomains: z.array(z.string()), - registeredDomains: z.array(z.string()), - propertyCount: z.number().int().gte(0).lte(9007199254740991), - filters: PropertyListFiltersOutput.nullish(), - createdAt: z.string().datetime({ offset: true }), - updatedAt: z.string().datetime({ offset: true }), + .partial() + .passthrough(); +const CreatePropertyListInput = z + .object({ + name: z.string().min(1).max(255), + purpose: z.enum(['include', 'exclude']), + domains: z.array(z.string().min(1)).max(100000).optional(), + identifiers: z + .array( + z + .object({ + type: z.union([ + z.literal('domain'), + z.literal('subdomain'), + z.literal('network_id'), + z.literal('ios_bundle'), + z.literal('android_package'), + z.literal('apple_app_store_id'), + z.literal('google_play_id'), + z.literal('roku_store_id'), + z.literal('fire_tv_asin'), + z.literal('samsung_app_id'), + z.literal('apple_tv_bundle'), + z.literal('bundle_id'), + z.literal('venue_id'), + z.literal('screen_id'), + z.literal('openooh_venue_type'), + z.literal('rss_url'), + z.literal('apple_podcast_id'), + z.literal('spotify_collection_id'), + z.literal('podcast_guid'), + z.literal('station_id'), + z.literal('facility_id'), + ]), + value: z.string(), + }) + .passthrough() + ) + .max(100000) + .optional(), + filters: PropertyListFilters.nullish(), + }) + .passthrough(); +const PropertyListResolutionSummary = z.object({ + totalRequested: z.number().int().gte(0).lte(9007199254740991), + resolvedCount: z.number().int().gte(0).lte(9007199254740991), + registeredCount: z.number().int().gte(0).lte(9007199254740991), + unresolvedCount: z.number().int().gte(0).lte(9007199254740991), + resolutionRate: z.number().gte(0).lte(1), }); -const PropertyListListResponse = z.object({ - propertyLists: z.array(PropertyListOutput), - total: z.number().int().gte(0).lte(9007199254740991), +const PropertyListCascadeSummary = z.object({ + totalMediaBuys: z.number().int().gte(0).lte(9007199254740991), + updatedCount: z.number().int().gte(0).lte(9007199254740991), + failedCount: z.number().int().gte(0).lte(9007199254740991), }); -const PropertyListFilters = z +const PropertyListFiltersOutput = z .object({ channels_any: z .array( @@ -1663,41 +3973,467 @@ const PropertyListFilters = z ) .nullable(), feature_requirements: z + .array( + z.object({ + feature_id: z.string(), + min_value: z.number().nullish(), + max_value: z.number().nullish(), + allowed_values: z.array(z.unknown()).nullish(), + if_not_covered: z.enum(['exclude', 'include']).nullish(), + }) + ) + .nullable(), + }) + .partial(); +const PropertyListOutput = z.object({ + listId: z.string(), + name: z.string(), + purpose: z.enum(['include', 'exclude']), + identifiers: z.array( + z + .object({ + type: z.union([ + z.literal('domain'), + z.literal('subdomain'), + z.literal('network_id'), + z.literal('ios_bundle'), + z.literal('android_package'), + z.literal('apple_app_store_id'), + z.literal('google_play_id'), + z.literal('roku_store_id'), + z.literal('fire_tv_asin'), + z.literal('samsung_app_id'), + z.literal('apple_tv_bundle'), + z.literal('bundle_id'), + z.literal('venue_id'), + z.literal('screen_id'), + z.literal('openooh_venue_type'), + z.literal('rss_url'), + z.literal('apple_podcast_id'), + z.literal('spotify_collection_id'), + z.literal('podcast_guid'), + z.literal('station_id'), + z.literal('facility_id'), + ]), + value: z.string(), + }) + .passthrough() + ), + unresolvedIdentifiers: z.array( + z + .object({ + type: z.union([ + z.literal('domain'), + z.literal('subdomain'), + z.literal('network_id'), + z.literal('ios_bundle'), + z.literal('android_package'), + z.literal('apple_app_store_id'), + z.literal('google_play_id'), + z.literal('roku_store_id'), + z.literal('fire_tv_asin'), + z.literal('samsung_app_id'), + z.literal('apple_tv_bundle'), + z.literal('bundle_id'), + z.literal('venue_id'), + z.literal('screen_id'), + z.literal('openooh_venue_type'), + z.literal('rss_url'), + z.literal('apple_podcast_id'), + z.literal('spotify_collection_id'), + z.literal('podcast_guid'), + z.literal('station_id'), + z.literal('facility_id'), + ]), + value: z.string(), + }) + .passthrough() + ), + registeredIdentifiers: z.array( + z + .object({ + type: z.union([ + z.literal('domain'), + z.literal('subdomain'), + z.literal('network_id'), + z.literal('ios_bundle'), + z.literal('android_package'), + z.literal('apple_app_store_id'), + z.literal('google_play_id'), + z.literal('roku_store_id'), + z.literal('fire_tv_asin'), + z.literal('samsung_app_id'), + z.literal('apple_tv_bundle'), + z.literal('bundle_id'), + z.literal('venue_id'), + z.literal('screen_id'), + z.literal('openooh_venue_type'), + z.literal('rss_url'), + z.literal('apple_podcast_id'), + z.literal('spotify_collection_id'), + z.literal('podcast_guid'), + z.literal('station_id'), + z.literal('facility_id'), + ]), + value: z.string(), + }) + .passthrough() + ), + domains: z.array(z.string()), + unresolvedDomains: z.array(z.string()), + registeredDomains: z.array(z.string()), + propertyCount: z.number().int().gte(0).lte(9007199254740991), + resolutionSummary: PropertyListResolutionSummary.optional(), + cascadeSummary: PropertyListCascadeSummary.optional(), + filters: PropertyListFiltersOutput.nullish(), + createdAt: z.string().datetime({ offset: true }), + updatedAt: z.string().datetime({ offset: true }), + Status: z.enum(['processing', 'ready', 'failed']).optional(), + errorMessage: z.string().optional(), +}); +const PropertyListResponse = z.object({ propertyList: PropertyListOutput }); +const UpdatePropertyListInput = z + .object({ + name: z.string().min(1).max(255), + domains: z.array(z.string().min(1)).max(100000), + identifiers: z .array( z .object({ - feature_id: z.string(), - min_value: z.number().nullish(), - max_value: z.number().nullish(), - allowed_values: z.array(z.unknown()).nullish(), - if_not_covered: z.enum(['exclude', 'include']).nullish(), + type: z.union([ + z.literal('domain'), + z.literal('subdomain'), + z.literal('network_id'), + z.literal('ios_bundle'), + z.literal('android_package'), + z.literal('apple_app_store_id'), + z.literal('google_play_id'), + z.literal('roku_store_id'), + z.literal('fire_tv_asin'), + z.literal('samsung_app_id'), + z.literal('apple_tv_bundle'), + z.literal('bundle_id'), + z.literal('venue_id'), + z.literal('screen_id'), + z.literal('openooh_venue_type'), + z.literal('rss_url'), + z.literal('apple_podcast_id'), + z.literal('spotify_collection_id'), + z.literal('podcast_guid'), + z.literal('station_id'), + z.literal('facility_id'), + ]), + value: z.string(), }) .passthrough() ) - .nullable(), + .max(100000), }) .partial() .passthrough(); -const CreatePropertyListInput = z +const EmptyResponse = z.object({}).partial(); +const CheckPropertyListBody = z .object({ - name: z.string().min(1).max(255), - purpose: z.enum(['include', 'exclude']), - domains: z.array(z.string().min(1)).min(1).max(10000), - filters: PropertyListFilters.nullish(), + domains: z.array(z.string().min(1)).max(100000), + identifiers: z + .array( + z + .object({ + type: z.union([ + z.literal('domain'), + z.literal('subdomain'), + z.literal('network_id'), + z.literal('ios_bundle'), + z.literal('android_package'), + z.literal('apple_app_store_id'), + z.literal('google_play_id'), + z.literal('roku_store_id'), + z.literal('fire_tv_asin'), + z.literal('samsung_app_id'), + z.literal('apple_tv_bundle'), + z.literal('bundle_id'), + z.literal('venue_id'), + z.literal('screen_id'), + z.literal('openooh_venue_type'), + z.literal('rss_url'), + z.literal('apple_podcast_id'), + z.literal('spotify_collection_id'), + z.literal('podcast_guid'), + z.literal('station_id'), + z.literal('facility_id'), + ]), + value: z.string(), + }) + .passthrough() + ) + .max(100000), }) + .partial() .passthrough(); -const PropertyListResponse = z.object({ propertyList: PropertyListOutput }); -const UpdatePropertyListInput = z +const BuyerInvoiceItem = z.object({ + id: z.string(), + number: z.string().nullable(), + Status: z.string().nullable(), + currency: z.string(), + amountDue: z.number().int().gte(-9007199254740991).lte(9007199254740991), + amountPaid: z.number().int().gte(-9007199254740991).lte(9007199254740991), + amountRemaining: z.number().int().gte(-9007199254740991).lte(9007199254740991), + total: z.number().int().gte(-9007199254740991).lte(9007199254740991), + created: z.number().int().gte(-9007199254740991).lte(9007199254740991), + dueDate: z.number().int().gte(-9007199254740991).lte(9007199254740991).nullable(), + periodStart: z.number().int().gte(-9007199254740991).lte(9007199254740991).nullable(), + periodEnd: z.number().int().gte(-9007199254740991).lte(9007199254740991).nullable(), + hostedInvoiceUrl: z.string().nullable(), + invoicePdf: z.string().nullable(), + description: z.string().nullable(), + customerEmail: z.string().nullish(), + customerName: z.string().nullish(), +}); +const BuyerInvoicesResponse = z.object({ + invoices: z.array(BuyerInvoiceItem), + hasMore: z.boolean(), + cursor: z.string().nullable(), +}); +const BuyerPendingInvoiceItem = z.object({ + id: z.string(), + description: z.string().nullable(), + currency: z.string(), + amount: z.number().int().gte(-9007199254740991).lte(9007199254740991), + quantity: z.number().int().gte(-9007199254740991).lte(9007199254740991), + unitAmount: z.number().int().gte(-9007199254740991).lte(9007199254740991).nullable(), + date: z.number().int().gte(-9007199254740991).lte(9007199254740991), + period: z.object({ + start: z.number().int().gte(-9007199254740991).lte(9007199254740991), + end: z.number().int().gte(-9007199254740991).lte(9007199254740991), + }), + proration: z.boolean(), + customerId: z.string().nullish(), +}); +const BuyerPendingInvoiceItemsResponse = z.object({ + items: z.array(BuyerPendingInvoiceItem), + hasMore: z.boolean(), + cursor: z.string().nullable(), +}); +const DemandSignalExclusionOutput = z.object({ + type: z.enum(['category', 'adjacency', 'competitor', 'domain', 'keyword', 'raw']), + values: z.array(z.string().min(1)).default([]), + raw: z.string().optional(), +}); +const DemandSignalFlexibilityOutput = z.object({ + mode: z.enum(['firm', 'flexible', 'open']), + days: z.number().int().gte(0).lte(9007199254740991).optional(), +}); +const DemandSignalKpiOutput = z.object({ + raw: z.string(), + parsed: z + .object({ + kpi: z.enum([ + 'attention_seconds', + 'viewability_pct', + 'ctr', + 'vcr', + 'vtr', + 'reach', + 'frequency_cap', + 'sov', + 'brand_lift', + 'cpa', + ]), + op: z.enum(['gt', 'gte', 'eq', 'lte', 'lt']), + value: z.number(), + }) + .optional(), +}); +const DemandSignalRecommendedProduct = z.object({ + productId: z.string(), + name: z.string(), + estimatedCpm: z.number().optional(), + estimatedImpressions: z.number().int().gte(0).lte(9007199254740991).optional(), + estimatedReach: z.number().int().gte(0).lte(9007199254740991).optional(), + matchPct: z.number().gte(0).lte(100).optional(), + rationale: z.string().optional(), +}); +const DemandSignalResponseSummary = z.object({ + quotes: z.number().int().gte(0).lte(9007199254740991), + clarifies: z.number().int().gte(0).lte(9007199254740991), + declines: z.number().int().gte(0).lte(9007199254740991), + books: z.number().int().gte(0).lte(9007199254740991), +}); +const DemandSignal = z.object({ + id: z.string(), + customerId: z.number().int().gte(-9007199254740991).lte(9007199254740991), + advertiserId: z.number().int().gte(-9007199254740991).lte(9007199254740991).nullable(), + buyerName: z.string(), + advertiserName: z.string(), + campaignName: z.string().nullable(), + audience: z.string(), + geo: z.string(), + channels: z.array( + z.enum(['display', 'native', 'video', 'audio', 'ctv', 'dooh', 'newsletter', 'podcast']) + ), + exclusions: z.array(DemandSignalExclusionOutput), + startDate: z.string(), + endDate: z.string(), + flexibility: DemandSignalFlexibilityOutput.nullable(), + statedBudget: z.number(), + currency: z.string(), + primaryKpi: DemandSignalKpiOutput.nullable(), + priceExpect: z.number().nullable(), + creativeReady: z.boolean().nullable(), + rawPrompt: z.string().nullable(), + Status: z.enum(['SEARCHING', 'QUOTED', 'BOOKED', 'ABANDONED', 'DECLINED']), + targetingMode: z.enum(['DIRECT', 'FILTERED', 'BROAD']), + qualifiedBudget: z.number().nullable(), + recommendedProducts: z.array(DemandSignalRecommendedProduct), + createdAt: z.string(), + updatedAt: z.string(), + targetCount: z.number().int().gte(0).lte(9007199254740991).optional(), + responseSummary: DemandSignalResponseSummary.optional(), +}); +const DemandSignalList = z.object({ + items: z.array(DemandSignal), + total: z.number().int().gte(0).lte(9007199254740991), + hasMore: z.boolean(), + nextOffset: z.number().int().gte(0).lte(9007199254740991).nullable(), +}); +const DemandSignalExclusion = z .object({ - name: z.string().min(1).max(255), - domains: z.array(z.string().min(1)).min(1).max(10000), + type: z.enum(['category', 'adjacency', 'competitor', 'domain', 'keyword', 'raw']), + values: z.array(z.string().min(1)).optional().default([]), + raw: z.string().optional(), }) - .partial() .passthrough(); -const EmptyResponse = z.object({}).partial(); -const CheckPropertyListBody = z - .object({ domains: z.array(z.string().min(1)).min(1).max(1000) }) +const DemandSignalFlexibility = z + .object({ + mode: z.enum(['firm', 'flexible', 'open']), + days: z.number().int().gte(0).lte(9007199254740991).optional(), + }) + .passthrough(); +const DemandSignalKpi = z + .object({ + raw: z.string(), + parsed: z + .object({ + kpi: z.enum([ + 'attention_seconds', + 'viewability_pct', + 'ctr', + 'vcr', + 'vtr', + 'reach', + 'frequency_cap', + 'sov', + 'brand_lift', + 'cpa', + ]), + op: z.enum(['gt', 'gte', 'eq', 'lte', 'lt']), + value: z.number(), + }) + .passthrough() + .optional(), + }) + .passthrough(); +const CreateDemandSignalTargetInput = z + .object({ + storefrontId: z.number().int().lte(9007199254740991).optional(), + externalPublisherName: z.string().min(1).optional(), + externalPublisherDomain: z.string().min(1).optional(), + bucket: z.enum(['LIVE', 'COMING_SOON', 'NOT_ON_INTERCHANGE']), + matchPct: z.number().int().gte(0).lte(100).optional(), + }) + .passthrough(); +const CreateDemandSignalBody = z + .object({ + advertiserId: z.number().int().lte(9007199254740991).optional(), + buyerName: z.string().min(1).max(200), + advertiserName: z.string().min(1).max(200), + campaignName: z.string().min(1).max(200).optional(), + audience: z.string().min(1).max(1000), + geo: z.string().min(1).max(200), + channels: z + .array( + z.enum(['display', 'native', 'video', 'audio', 'ctv', 'dooh', 'newsletter', 'podcast']) + ) + .min(1), + exclusions: z.array(DemandSignalExclusion).optional().default([]), + startDate: z.string(), + endDate: z.string(), + flexibility: DemandSignalFlexibility.optional(), + statedBudget: z.number().gt(0), + currency: z.string().min(3).max(3), + primaryKpi: DemandSignalKpi.optional(), + priceExpect: z.number().gt(0).optional(), + creativeReady: z.boolean().optional(), + rawPrompt: z.string().max(4000).optional(), + targetingMode: z.enum(['DIRECT', 'FILTERED', 'BROAD']).optional(), + targets: z.array(CreateDemandSignalTargetInput).max(200).optional().default([]), + }) .passthrough(); +const DemandSignalTarget = z.object({ + id: z.string(), + demandSignalId: z.string(), + storefrontId: z.string().nullable(), + externalPublisherName: z.string().nullable(), + externalPublisherDomain: z.string().nullable(), + bucket: z.enum(['LIVE', 'COMING_SOON', 'NOT_ON_INTERCHANGE']), + dispatchStatus: z.enum(['QUEUED', 'DISPATCHED', 'ACKNOWLEDGED', 'ON_HOLD', 'FAILED', 'DECLINED']), + matchPct: z.number().int().gte(0).lte(100).nullable(), + dispatchAttemptedAt: z.string().nullable(), + dispatchedAt: z.string().nullable(), + acknowledgedAt: z.string().nullable(), + errorMessage: z.string().nullable(), + createdAt: z.string(), + updatedAt: z.string(), +}); +const DemandSignalWithTargets = z.object({ + id: z.string(), + customerId: z.number().int().gte(-9007199254740991).lte(9007199254740991), + advertiserId: z.number().int().gte(-9007199254740991).lte(9007199254740991).nullable(), + buyerName: z.string(), + advertiserName: z.string(), + campaignName: z.string().nullable(), + audience: z.string(), + geo: z.string(), + channels: z.array( + z.enum(['display', 'native', 'video', 'audio', 'ctv', 'dooh', 'newsletter', 'podcast']) + ), + exclusions: z.array(DemandSignalExclusionOutput), + startDate: z.string(), + endDate: z.string(), + flexibility: DemandSignalFlexibilityOutput.nullable(), + statedBudget: z.number(), + currency: z.string(), + primaryKpi: DemandSignalKpiOutput.nullable(), + priceExpect: z.number().nullable(), + creativeReady: z.boolean().nullable(), + rawPrompt: z.string().nullable(), + Status: z.enum(['SEARCHING', 'QUOTED', 'BOOKED', 'ABANDONED', 'DECLINED']), + targetingMode: z.enum(['DIRECT', 'FILTERED', 'BROAD']), + qualifiedBudget: z.number().nullable(), + recommendedProducts: z.array(DemandSignalRecommendedProduct), + createdAt: z.string(), + updatedAt: z.string(), + targetCount: z.number().int().gte(0).lte(9007199254740991).optional(), + responseSummary: DemandSignalResponseSummary.optional(), + targets: z.array(DemandSignalTarget), +}); +const DemandSignalResponse = z.object({ + id: z.string(), + demandSignalId: z.string(), + demandSignalTargetId: z.string(), + sellerCustomerId: z.number().int().gte(-9007199254740991).lte(9007199254740991), + storefrontId: z.string().nullable(), + kind: z.enum(['QUOTE', 'CLARIFY', 'DECLINE', 'BOOK']), + payload: z.object({}).partial().passthrough().nullable(), + matchScore: z.number().int().gte(0).lte(100).nullable(), + actorUserId: z.number().int().gte(-9007199254740991).lte(9007199254740991).nullable(), + createdAt: z.string(), + proposalCode: z.string().nullish(), +}); +const DemandSignalResponseList = z.object({ + items: z.array(DemandSignalResponse), + total: z.number().int().gte(0).lte(9007199254740991), +}); const McpInitializeRequest = z .object({ jsonrpc: z.literal('2.0'), @@ -1784,8 +4520,36 @@ export const schemas: Record = { ErrorResponse, LinkedAccountInput, OptimizationApplyMode, + CampaignBudgetType, + GcsCredentialConfig, + S3AssumeRoleAuth, + S3Auth, + S3CredentialConfig, + AzureBlobSasAuth, + AzureBlobAuth, + AzureBlobCredentialConfig, + CredentialConfig, + DataDeliveryCredentialInput, + DataDeliveryCredentialArrayInput, + GcsDeliveryConfig, + S3DeliveryConfig, + AzureBlobDeliveryConfig, + DeliveryConfig, + DataDeliveryOutputInput, + DataDeliveryOutputArrayInput, + AdvertiserDataDeliveryInput, CreateAdvertiserBody, UpdateAdvertiserBody, + GcsCredentialConfigOutput, + S3AssumeRoleAuthOutput, + S3AuthOutput, + S3CredentialConfigOutput, + AzureBlobSasAuthResponse, + AzureBlobAuthResponse, + AzureBlobCredentialConfigResponse, + CredentialConfigResponse, + DataDeliveryCredential, + RevalidateDataDeliveryCredentialResponse, DiscoveryRefinementItem, DiscoverProductsBody, @@ -1801,7 +4565,8 @@ export const schemas: Record = { AgentDiscoveryResult, RefinementApplied, DiscoverProductsResponse, - SalesAgentIds, + StorefrontIds, + StorefrontNames, Debug, SelectedProduct, SessionProductsResponse, @@ -1813,13 +4578,8 @@ export const schemas: Record = { ApplyProposalResponse, Status, MediaBuyStatus, - DurationOutput, - OptimizationAttributionWindowOutput, - EventGoalOutput, - MetricGoalOutput, - OptimizationGoalOutput, - PerformanceConfigOutput, - Campaign, + CampaignType, + CampaignSummary, CampaignListResponse, Duration, OptimizationAttributionWindow, @@ -1827,10 +4587,36 @@ export const schemas: Record = { MetricGoal, OptimizationGoal, PerformanceConfig, + PacingPeriods, CampaignUtmConfig, + CampaignDataDeliveryInput, CreateCampaignBody, + MediaBuyRef, + MediaBudget, + CampaignFeePricingType, + CampaignFeeUnit, + CampaignFee, + CampaignFees, + PacingPeriodsOutput, + CampaignStorefrontRef, + DurationOutput, + OptimizationAttributionWindowOutput, + EventGoalOutput, + MetricGoalOutput, + OptimizationGoalOutput, + PerformanceConfigOutput, + GcsDeliveryConfigOutput, + S3DeliveryConfigOutput, + AzureBlobDeliveryConfigOutput, + DeliveryConfigOutput, + DataDeliveryOutput, + CampaignDataDelivery, + FrequencyCapTargetLevel, + Campaign, CampaignResponse, UpdateCampaignBody, + MediaBuyId, + IncludePropertyLists, ExecuteCampaignBody, ExecuteMediaBuyDebugInfo, ExecutionError, @@ -1838,9 +4624,21 @@ export const schemas: Record = { RefinementItem, AutoSelectProductsRequest, AutoSelectProductsResponse, + GetAdcpStatusOutput, + CampaignProductEntry, + CampaignSearchContextSummary, + CampaignProductsResponse, + ResourceTypes, + BuyerAuditLog, + ListBuyerActivityResponse, + UrlAssetSlotInput, + TextAssetSlotInput, + CardInput, CreateCreativeManifestMetadata, ManifestAssetResponse, + CreativeManifestSyncStatus, CreativeManifestResponse, + CreativeManifestSummary, CreativeManifestListResponse, UpdateCreativeManifestMetadata, EventSourceOutput, @@ -1853,25 +4651,65 @@ export const schemas: Record = { EventType, EventSummaryEntry, EventSummaryResponse, + UserMatch, + ContentItem, + CustomData, + LogEventObject, + LogEventRequest, + LogEventPartialFailure, + LogEventResponse, SyncMeasurementObject, SyncMeasurementDataRequest, MeasurementDataSyncResult, SyncMeasurementDataResponse, + TestCohortSummary, + TestCohortListResponse, + CohortDefinition, + CreateTestCohortInput, + TestCohortOutput, + TestCohortResponse, + UpdateTestCohortInput, + MmmConfig, + MeasurementConfigOutput, + MeasurementConfigResponse, + UpdateMeasurementConfigInput, + CreateMeasurementSourceBody, + UpdateMeasurementSourceBody, + UploadMeasurementRecordsBody, + UploadContextRecordsBody, ReportingMetrics, PackageReporting, MediaBuyReporting, CampaignReporting, AdvertiserReporting, ReportingMetricsResponse, + CurrentAccountResponse, + OrgAccountSummary, + ListCustomerAccountsResponse, + CreateChildAccountBody, + CreateChildAccountResponse, + UpdateCustomerDomainBody, + UpdateCustomerDomainResponse, + MembershipSettingsResponse, + UpdateMembershipSettingsBody, + UpdateNotificationPreferencesBody, + CheckModerationBody, + BuyerCredentialSourceRef, AvailableAccountOutput, AvailableAccountListResponse, + ReportingBucket, + UpdateReportingBucketBody, + AccountOutput, + AccountResponse, SyncCatalogsBody, + BuyerStorefrontSummary, + BuyerStorefrontList, BuyerStorefrontSource, BuyerStorefront, - BuyerStorefrontList, - OAuthInfo, - AgentAccount, + BuyerCredential, RegisterSourceCredentialsBody, + BuyerCredentialOAuthInfo, + BuyerCredentialRegistrationResponse, SyndicateBody, SyndicationStatusOutput, SyndicateResponse, @@ -1880,20 +4718,43 @@ export const schemas: Record = { AudienceItem, SyncAudiencesBody, SyncAudiencesResponse, - Audience, + AudienceSummary, AudienceListResponse, TaskError, TaskOutput, TaskResponse, - PropertyListFiltersOutput, - PropertyListOutput, + PropertyListSummary, PropertyListListResponse, PropertyListFilters, CreatePropertyListInput, + PropertyListResolutionSummary, + PropertyListCascadeSummary, + PropertyListFiltersOutput, + PropertyListOutput, PropertyListResponse, UpdatePropertyListInput, EmptyResponse, CheckPropertyListBody, + BuyerInvoiceItem, + BuyerInvoicesResponse, + BuyerPendingInvoiceItem, + BuyerPendingInvoiceItemsResponse, + DemandSignalExclusionOutput, + DemandSignalFlexibilityOutput, + DemandSignalKpiOutput, + DemandSignalRecommendedProduct, + DemandSignalResponseSummary, + DemandSignal, + DemandSignalList, + DemandSignalExclusion, + DemandSignalFlexibility, + DemandSignalKpi, + CreateDemandSignalTargetInput, + CreateDemandSignalBody, + DemandSignalTarget, + DemandSignalWithTargets, + DemandSignalResponse, + DemandSignalResponseList, McpInitializeRequest, McpInitializeResponse, McpApiCallRequest, diff --git a/src/schemas/registry.ts b/src/schemas/registry.ts index ad4f88b..af95c64 100644 --- a/src/schemas/registry.ts +++ b/src/schemas/registry.ts @@ -25,10 +25,3 @@ export const discoverySchemas = { export const reportingSchemas = { response: schemas.ReportingMetricsResponse, }; - -export const salesAgentSchemas = { - registerAccountInput: schemas.RegisterSalesAgentAccountBody, - response: schemas.Agent, - listResponse: schemas.AgentList, - accountResponse: schemas.AgentAccount, -}; diff --git a/src/types/index.ts b/src/types/index.ts index ad65dc4..b8024f1 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -238,55 +238,27 @@ export interface Campaign { updatedAt: string; } -/** Input for creating a discovery campaign */ -export interface CreateDiscoveryCampaignInput { +export interface CreateCampaignInput { advertiserId: string; name: string; - bundleId: string; + type: CampaignType; flightDates: FlightDates; budget: Budget; - productIds?: string[]; - constraints?: CampaignConstraints; brief?: string; + constraints?: CampaignConstraints; + performanceConfig?: PerformanceConfig; + [key: string]: unknown; } -/** Input for updating a discovery campaign */ -export interface UpdateDiscoveryCampaignInput { +export interface UpdateCampaignInput { name?: string; + type?: CampaignType; flightDates?: FlightDates; budget?: Budget; - productIds?: string[]; - constraints?: CampaignConstraints; brief?: string; -} - -/** Input for creating a performance campaign */ -export interface CreatePerformanceCampaignInput { - advertiserId: string; - name: string; - flightDates: FlightDates; - budget: Budget; - performanceConfig: PerformanceConfig; constraints?: CampaignConstraints; -} - -/** Input for updating a performance campaign */ -export interface UpdatePerformanceCampaignInput { - name?: string; - flightDates?: FlightDates; - budget?: Budget; performanceConfig?: PerformanceConfig; - constraints?: CampaignConstraints; -} - -/** Input for creating an audience campaign (not yet implemented) */ -export interface CreateAudienceCampaignInput { - advertiserId: string; - name: string; - flightDates: FlightDates; - budget: Budget; - signals?: string[]; - constraints?: CampaignConstraints; + [key: string]: unknown; } export interface ListCampaignsParams extends PaginationParams { @@ -295,204 +267,6 @@ export interface ListCampaignsParams extends PaginationParams { status?: CampaignStatus; } -// ============================================================================ -// Bundle Types (Buyer Persona) -// ============================================================================ - -export interface Bundle { - bundleId: string; -} - -export interface CreateBundleInput { - advertiserId: string; - channels?: string[]; - countries?: string[]; - brief?: string; - budget?: number; - flightDates?: FlightDates; - salesAgentIds?: string[]; - salesAgentNames?: string[]; -} - -/** Parameters for discovering products in a bundle */ -export interface DiscoverProductsParams { - /** Max groups to return (default: 10, max: 50) */ - groupLimit?: number; - /** Groups to skip for pagination */ - groupOffset?: number; - /** Products per group (default: 5, max: 50) */ - productsPerGroup?: number; - /** Products to skip within each group */ - productOffset?: number; - /** Filter by publisher domain */ - publisherDomain?: string; - /** Filter by sales agent IDs (comma-separated) */ - salesAgentIds?: string; - /** Filter by sales agent names (comma-separated) */ - salesAgentNames?: string; -} - -/** Response from discover-products endpoint */ -export interface DiscoverProductsResponse { - bundleId: string; - productGroups: ProductGroup[]; - totalGroups: number; - hasMoreGroups: boolean; - summary: ProductSummary; - budgetContext?: BudgetContext; -} - -export interface ProductGroup { - groupId: string; - groupName: string; - products: Product[]; - productCount: number; - totalProducts: number; - hasMoreProducts: boolean; -} - -export interface Product { - productId: string; - name: string; - publisher: string; - channel: string; - cpm: number; - salesAgentId: string; - briefRelevance?: string; -} - -export interface ProductSummary { - totalProducts: number; - publishersCount: number; - priceRange?: { - min: number; - max: number; - avg: number; - }; -} - -export interface BudgetContext { - sessionBudget: number; - allocatedBudget: number; - remainingBudget: number; -} - -/** Product selection for adding to a bundle */ -export interface BundleProductInput { - productId: string; - salesAgentId: string; - groupId: string; - groupName: string; - cpm?: number; - budget?: number; -} - -export interface AddBundleProductsInput { - products: BundleProductInput[]; -} - -export interface RemoveBundleProductsInput { - productIds: string[]; -} - -export interface BundleProductsResponse { - bundleId: string; - products: SelectedBundleProduct[]; - totalProducts: number; - budgetContext?: BudgetContext; -} - -export interface SelectedBundleProduct { - productId: string; - salesAgentId: string; - cpm?: number; - budget?: number; - selectedAt: string; - groupId: string; - groupName: string; -} - -/** Input for browse products without a campaign */ -export interface BrowseProductsInput { - advertiserId: string; - channels?: string[]; - countries?: string[]; - brief?: string; - publisherDomain?: string; - salesAgentIds?: string[]; - salesAgentNames?: string[]; -} - -// ============================================================================ -// Conversion Event Types (Buyer Persona) -// ============================================================================ - -export type ConversionEventType = - | 'PURCHASE' - | 'SIGNUP' - | 'LEAD' - | 'PAGE_VIEW' - | 'ADD_TO_CART' - | 'CUSTOM'; - -export interface ConversionEvent { - id: string; - name: string; - type: ConversionEventType; - description?: string; - value?: number; - currency?: string; - createdAt: string; - updatedAt: string; -} - -export interface CreateConversionEventInput { - name: string; - type: ConversionEventType; - description?: string; - value?: number; - currency?: string; -} - -export interface UpdateConversionEventInput { - name?: string; - value?: number; - currency?: string; - description?: string; -} - -// ============================================================================ -// Creative Set Types (Buyer Persona) -// ============================================================================ - -export interface CreativeSet { - id: string; - name: string; - type: string; - createdAt: string; - updatedAt: string; -} - -export interface CreateCreativeSetInput { - name: string; - type: string; -} - -export interface CreativeAsset { - id: string; - assetUrl: string; - name: string; - type: string; - duration?: number; -} - -export interface CreateCreativeAssetInput { - assetUrl: string; - name: string; - type: string; - duration?: number; -} - // ============================================================================ // Test Cohort Types (Buyer Persona) // ============================================================================ @@ -512,6 +286,12 @@ export interface CreateTestCohortInput { splitPercentage: number; } +export interface UpdateTestCohortInput { + name?: string; + description?: string; + splitPercentage?: number; +} + // ============================================================================ // Reporting Types (Buyer Persona) // ============================================================================ @@ -592,299 +372,6 @@ export interface ReportingTimeseriesEntry { metrics: Partial; } -// ============================================================================ -// Sales Agent Types (Buyer Persona) -// ============================================================================ - -export interface SalesAgent { - agentId: string; - type: string; - name: string; - description?: string; - endpointUrl?: string; - protocol?: string; - authenticationType?: string; - accountPolicy?: string[]; - status: string; - relationship?: string; - customerAccounts?: SalesAgentAccount[]; - requiresAccount?: boolean; - authConfigured?: boolean; - createdAt: string; -} - -export interface SalesAgentAccount { - id?: string; - accountIdentifier: string; - status: string; - registeredBy?: string; - createdAt?: string; - oauth?: { - authorizationUrl: string; - agentId: string; - agentName: string; - }; -} - -export interface ListSalesAgentsParams { - status?: string; - relationship?: string; - name?: string; - limit?: number; - offset?: number; -} - -export interface RegisterSalesAgentAccountInput { - advertiserId: string; - accountIdentifier: string; - auth?: { - type: string; - token?: string; - }; - marketplaceAccount?: boolean; -} - -// ============================================================================ -// Signal Types (Buyer Persona) -// ============================================================================ - -export interface Signal { - id: string; - name: string; - type: string; - catalogType?: string; -} - -export interface DiscoverSignalsInput { - filters?: { - catalogTypes?: string[]; - }; -} - -// ============================================================================ -// Storefront Types (Storefront Persona) -// ============================================================================ - -export interface Storefront { - platformId: string; - name: string; - publisherDomain?: string; - plan?: string; - enabled: boolean; - createdAt: string; - updatedAt: string; -} - -export interface CreateStorefrontInput { - platformId: string; - name: string; - publisherDomain?: string; - plan?: string; -} - -export interface UpdateStorefrontInput { - name?: string; - publisherDomain?: string; - platformId?: string; - plan?: string; - enabled?: boolean; -} - -// ============================================================================ -// Inventory Source Types (Storefront Persona) -// ============================================================================ - -export type InventorySourceExecutionType = 'agent'; - -export interface InventorySource { - sourceId: string; - name: string; - executionType?: InventorySourceExecutionType; - status: string; - agentId?: string; - type?: AgentType; - endpointUrl?: string; - protocol?: AgentProtocol; - authenticationType?: AgentAuthenticationType; - authConfigured?: boolean; - executionConfig?: Record; - description?: string; - oauth?: { - authorizationUrl: string; - agentId: string; - agentName: string; - }; - createdAt: string; - updatedAt: string; -} - -export interface CreateInventorySourceInput { - sourceId: string; - name: string; - executionType?: InventorySourceExecutionType; - type?: AgentType; - endpointUrl?: string; - protocol?: AgentProtocol; - authenticationType?: AgentAuthenticationType; - auth?: { - type: string; - token?: string; - privateKey?: string; - }; - executionConfig?: Record; - description?: string; -} - -export interface UpdateInventorySourceInput { - name?: string; - executionType?: InventorySourceExecutionType; - executionConfig?: Record; - status?: string; -} - -// ============================================================================ -// Storefront Readiness Types -// ============================================================================ - -export type ReadinessStatus = 'ready' | 'blocked'; - -export interface ReadinessCheck { - id: string; - name: string; - description: string; - category: string; - status: ReadinessStatus; - isBlocker: boolean; - details?: string; -} - -export interface StorefrontReadiness { - platformId: string; - status: ReadinessStatus; - checks: ReadinessCheck[]; -} - -// ============================================================================ -// Storefront Billing Types (Stripe Connect) -// ============================================================================ - -export interface BillingFee { - name: string; - feePercent: number; -} - -export interface StorefrontBilling { - stripeConnectedAccountId: string; - onboardingStatus: string; - platformFeePercent?: number; - fees?: BillingFee[]; - currency?: string; - defaultNetDays?: number; - createdAt: string; - updatedAt: string; -} - -export interface StorefrontBillingConfig { - platformId: string; - billing: StorefrontBilling | null; -} - -export interface StripeConnectResponse { - platformId: string; - stripeConnectedAccountId: string; - onboardingUrl: string; -} - -// ============================================================================ -// Notification Types (Storefront Persona) -// ============================================================================ - -export interface Notification { - id: string; - type: string; - message: string; - read: boolean; - acknowledged: boolean; - brandAgentId?: number; - createdAt: string; -} - -export interface ListNotificationsParams { - unreadOnly?: boolean; - brandAgentId?: number; - types?: string; - limit?: number; - offset?: number; -} - -// ============================================================================ -// Agent Types (Storefront Persona) -// ============================================================================ - -export type AgentType = 'SALES' | 'SIGNAL' | 'CREATIVE' | 'OUTCOME'; -export type AgentStatus = 'PENDING' | 'ACTIVE' | 'DISABLED' | 'COMING_SOON'; -export type AgentAuthenticationType = 'API_KEY' | 'NO_AUTH' | 'JWT' | 'OAUTH'; -export type AgentProtocol = 'MCP' | 'A2A'; - -export interface Agent { - agentId: string; - type: AgentType; - name: string; - description?: string; - endpointUrl?: string; - protocol?: AgentProtocol; - authenticationType?: AgentAuthenticationType; - accountPolicy?: string[]; - status: AgentStatus; - relationship?: string; - customerAccounts?: SalesAgentAccount[]; - requiresAccount?: boolean; - authConfigured?: boolean; - customerId?: number; - reportingType?: string; - reportingPollingCadence?: string; - oauth?: { - authorizationUrl: string; - agentId: string; - agentName: string; - }; - createdAt: string; - updatedAt?: string; -} - -export interface UpdateAgentInput { - name?: string; - description?: string; - endpointUrl?: string; - protocol?: AgentProtocol; - accountPolicy?: string[]; - authenticationType?: AgentAuthenticationType; - auth?: { - type: string; - token?: string; - }; - reportingType?: string; - reportingPollingCadence?: string; - status?: string; -} - -export interface ListAgentsParams { - type?: AgentType; - status?: string; - relationship?: string; -} - -export interface OAuthAuthorizeResponse { - authorizationUrl: string; - agentId: string; - agentName: string; -} - -export interface OAuthCallbackInput { - code: string; - state: string; -} - // ============================================================================ // Event Source Types (Buyer Persona) // ============================================================================ @@ -1067,34 +554,221 @@ export interface PropertyListReport { } // ============================================================================ -// Billing Types (Storefront Persona) +// Discovery Types (Buyer Persona) // ============================================================================ -export interface BillingStatus { - status: string; - connected: boolean; - accountId?: string; +export interface DiscoverProductsInput { + advertiserId: string; + channels?: string[]; + brief?: string; + budget?: number; + [key: string]: unknown; +} + +export interface DiscoveryProduct { + productId: string; + name: string; + publisher: string; + channel: string; + cpm: number; + [key: string]: unknown; +} + +export interface AddProductsInput { + products: Array<{ productId: string; [key: string]: unknown }>; } -export interface BillingTransaction { +export interface RemoveProductsInput { + productIds: string[]; +} + +export interface ApplyProposalInput { + proposalId: string; + [key: string]: unknown; +} + +// ============================================================================ +// Account Types (Buyer Persona) +// ============================================================================ + +export interface Account { id: string; - amount: number; - currency: string; - description: string; + name: string; + domain?: string; + parentId?: string; createdAt: string; + [key: string]: unknown; +} + +export interface CreateChildAccountInput { + name: string; + domain?: string; + [key: string]: unknown; +} + +export interface UpdateDomainInput { + domain: string; +} + +export interface MembershipSettings { + [key: string]: unknown; +} + +export interface UpdateMembershipInput { + [key: string]: unknown; +} + +// ============================================================================ +// Notification Preferences Types (Buyer Persona) +// ============================================================================ + +export interface NotificationPreferences { + [key: string]: unknown; +} + +export interface UpdateNotificationPreferencesInput { + [key: string]: unknown; } -export interface BillingPayout { +// ============================================================================ +// Moderation Types (Buyer Persona) +// ============================================================================ + +export interface ModerationCheckInput { + content: string; + [key: string]: unknown; +} + +export interface ModerationCheckResult { + passed: boolean; + [key: string]: unknown; +} + +// ============================================================================ +// Buyer Storefront Types (Buyer Persona) +// ============================================================================ + +export interface BuyerStorefront { + id: string; + name: string; + [key: string]: unknown; +} + +export interface StorefrontCredential { + [key: string]: unknown; +} + +export interface RegisterCredentialsInput { + [key: string]: unknown; +} + +// ============================================================================ +// Audit Log Types (Buyer Persona) +// ============================================================================ + +export interface AuditLog { + id: string; + action: string; + resourceType: string; + resourceId?: string; + createdAt: string; + [key: string]: unknown; +} + +export interface ListAuditLogsParams extends PaginationParams { + resourceType?: string; + action?: string; +} + +// ============================================================================ +// Planning Brief Types (Buyer Persona) +// ============================================================================ + +export interface PlanningBrief { + id: string; + status: string; + brief: string; + createdAt: string; + [key: string]: unknown; +} + +export interface CreatePlanningBriefInput { + brief: string; + [key: string]: unknown; +} + +export interface PlanningBriefResponse { + id: string; + briefId: string; + [key: string]: unknown; +} + +// ============================================================================ +// Buyer Billing Types (Buyer Persona) +// ============================================================================ + +export interface BuyerInvoice { id: string; amount: number; currency: string; status: string; createdAt: string; + [key: string]: unknown; } -export interface ListBillingParams { - limit?: number; - offset?: number; - startDate?: string; - endDate?: string; +export interface BuyerPendingInvoiceItem { + id: string; + amount: number; + description: string; + [key: string]: unknown; +} + +// ============================================================================ +// Measurement Types (Buyer Persona) +// ============================================================================ + +export interface MeasurementConfig { + [key: string]: unknown; +} + +export interface UpdateMeasurementConfigInput { + [key: string]: unknown; +} + +export interface MeasurementSource { + id: string; + [key: string]: unknown; +} + +export interface CreateMeasurementSourceInput { + [key: string]: unknown; +} + +export interface UpdateMeasurementSourceInput { + [key: string]: unknown; +} + +export interface UploadMeasurementRecordsInput { + [key: string]: unknown; +} + +export interface UploadContextRecordsInput { + [key: string]: unknown; +} + +export interface MeasurementFreshness { + [key: string]: unknown; +} + +// ============================================================================ +// Event Summary Types (Buyer Persona) +// ============================================================================ + +export interface EventSummary { + [key: string]: unknown; +} + +export interface LogEventInput { + events: Array>; + [key: string]: unknown; }