Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/server-handler-payload-types.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@adcp/sdk': patch
---

Type server/platform handler returns as domain payloads rather than requiring protocol task-envelope fields from generated wire response types. The SDK continues to stamp envelope fields such as `status: "completed"` at dispatch time, and exports `ServerPayload<T>` for adopters that want explicit payload return annotations.
15 changes: 7 additions & 8 deletions examples/hello_seller_adapter_guaranteed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ import {
type DecisioningPlatform,
type SalesCorePlatform,
type SalesIngestionPlatform,
type GetProductsPayload,
type GetMediaBuyDeliveryPayload,
type GetMediaBuysPayload,
type AccountStore,
type Account,
type SyncCreativesRow,
Expand All @@ -86,9 +89,7 @@ import type {
UpdateMediaBuyRequest,
UpdateMediaBuySuccess,
GetMediaBuysRequest,
GetMediaBuysResponse,
GetMediaBuyDeliveryRequest,
GetMediaBuyDeliveryResponse,
} from '@adcp/sdk/types';

// `Product` isn't re-exported from `@adcp/sdk/types` (#1254 in the rollup);
Expand Down Expand Up @@ -730,7 +731,7 @@ class SalesGuaranteedAdapter implements DecisioningPlatform<Record<string, never
// level. See `decisioning.type-checks.ts` for the regression-locked
// patterns.
sales: SalesCorePlatform<NetworkMeta> & SalesIngestionPlatform<NetworkMeta> = {
getProducts: async (req: GetProductsRequest, ctx): Promise<GetProductsResponse> => {
getProducts: async (req: GetProductsRequest, ctx): Promise<GetProductsPayload> => {
const networkCode = ctx.account.ctx_metadata.network_code;
const publisherDomain = ctx.account.ctx_metadata.publisher_domain;
// Storyboard sends buying_mode: 'brief' with a free-text brief —
Expand All @@ -753,7 +754,6 @@ class SalesGuaranteedAdapter implements DecisioningPlatform<Record<string, never
...(briefBudget !== undefined && { budget: briefBudget }),
});
return {
status: 'completed',
products: guaranteed.map(p => projectProduct(p, publisherDomain)),
cache_scope: 'account',
};
Expand Down Expand Up @@ -1019,7 +1019,7 @@ class SalesGuaranteedAdapter implements DecisioningPlatform<Record<string, never
};
},

getMediaBuys: async (req: GetMediaBuysRequest, ctx): Promise<GetMediaBuysResponse> => {
getMediaBuys: async (req: GetMediaBuysRequest, ctx): Promise<GetMediaBuysPayload> => {
const networkCode = ctx.account.ctx_metadata.network_code;
const orders = await upstream.listOrders(networkCode);
const filtered = req.media_buy_ids ? orders.filter(o => req.media_buy_ids!.includes(o.order_id)) : orders;
Expand Down Expand Up @@ -1049,7 +1049,7 @@ class SalesGuaranteedAdapter implements DecisioningPlatform<Record<string, never
};
})
);
return { status: 'completed', media_buys: buys };
return { media_buys: buys };
},

syncCreatives: async (creatives, ctx): Promise<SyncCreativesRow[]> => {
Expand Down Expand Up @@ -1090,7 +1090,7 @@ class SalesGuaranteedAdapter implements DecisioningPlatform<Record<string, never
return rows;
},

getMediaBuyDelivery: async (req: GetMediaBuyDeliveryRequest, ctx): Promise<GetMediaBuyDeliveryResponse> => {
getMediaBuyDelivery: async (req: GetMediaBuyDeliveryRequest, ctx): Promise<GetMediaBuyDeliveryPayload> => {
const networkCode = ctx.account.ctx_metadata.network_code;
const ids = req.media_buy_ids ?? [];
const deliveries = await Promise.all(ids.map(id => upstream.getDelivery(networkCode, id)));
Expand All @@ -1111,7 +1111,6 @@ class SalesGuaranteedAdapter implements DecisioningPlatform<Record<string, never
const aggregateClicks = present.reduce((s, d) => s + d.totals.clicks, 0);

return {
status: 'completed',
reporting_period: { start: earliest, end: latest },
currency,
aggregated_totals: {
Expand Down
22 changes: 10 additions & 12 deletions examples/hello_seller_adapter_non_guaranteed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ import {
type DecisioningPlatform,
type SalesCorePlatform,
type SalesIngestionPlatform,
type GetProductsPayload,
type GetMediaBuyDeliveryPayload,
type GetMediaBuysPayload,
type ListCreativeFormatsPayload,
type AccountStore,
type Account,
type AdcpMediaBuyStatus,
Expand All @@ -81,10 +85,7 @@ import type {
UpdateMediaBuyRequest,
UpdateMediaBuySuccess,
GetMediaBuysRequest,
GetMediaBuysResponse,
GetMediaBuyDeliveryRequest,
GetMediaBuyDeliveryResponse,
ListCreativeFormatsResponse,
} from '@adcp/sdk/types';

// `Product` isn't re-exported from `@adcp/sdk/types`; derive from response.
Expand Down Expand Up @@ -580,7 +581,7 @@ class SalesNonGuaranteedAdapter implements DecisioningPlatform<Record<string, ne
// all-optional and `RequiredPlatformsFor<'sales-non-guaranteed'>` requires
// the closed shape on the way out.
sales: SalesCorePlatform<NetworkMeta> & SalesIngestionPlatform<NetworkMeta> = {
getProducts: async (req: GetProductsRequest, ctx): Promise<GetProductsResponse> => {
getProducts: async (req: GetProductsRequest, ctx): Promise<GetProductsPayload> => {
const networkCode = ctx.account.ctx_metadata.network_code;
const publisherDomain = ctx.account.ctx_metadata.publisher_domain;
// When the buyer provides structured filters (flight dates, budget),
Expand All @@ -594,7 +595,6 @@ class SalesNonGuaranteedAdapter implements DecisioningPlatform<Record<string, ne
...(briefBudget !== undefined && { budget: briefBudget }),
});
return {
status: 'completed',
products: products.map(p => projectProduct(p, publisherDomain)),
cache_scope: 'account',
};
Expand Down Expand Up @@ -790,7 +790,7 @@ class SalesNonGuaranteedAdapter implements DecisioningPlatform<Record<string, ne
};
},

getMediaBuyDelivery: async (req: GetMediaBuyDeliveryRequest, ctx): Promise<GetMediaBuyDeliveryResponse> => {
getMediaBuyDelivery: async (req: GetMediaBuyDeliveryRequest, ctx): Promise<GetMediaBuyDeliveryPayload> => {
const networkCode = ctx.account.ctx_metadata.network_code;
const requestedIds = req.media_buy_ids ?? [];
// Multi-id pass-through per #1342 contract — fan out per id; framework
Expand Down Expand Up @@ -828,8 +828,7 @@ class SalesNonGuaranteedAdapter implements DecisioningPlatform<Record<string, ne
`[sales-non-guaranteed] get_media_buy_delivery: ${missing.length} unknown media_buy_id(s) returned no delivery rows: ${missing.join(', ')}`
);
}
const response: GetMediaBuyDeliveryResponse = {
status: 'completed',
const response: GetMediaBuyDeliveryPayload = {
currency: filtered[0]?.currency ?? 'USD',
reporting_period: {
start: filtered[0]?.reporting_period.start ?? new Date().toISOString(),
Expand Down Expand Up @@ -858,7 +857,7 @@ class SalesNonGuaranteedAdapter implements DecisioningPlatform<Record<string, ne
return response;
},

getMediaBuys: async (req: GetMediaBuysRequest, ctx): Promise<GetMediaBuysResponse> => {
getMediaBuys: async (req: GetMediaBuysRequest, ctx): Promise<GetMediaBuysPayload> => {
const networkCode = ctx.account.ctx_metadata.network_code;
const requestedIds = req.media_buy_ids ?? [];
let orders: UpstreamOrder[];
Expand Down Expand Up @@ -896,7 +895,7 @@ class SalesNonGuaranteedAdapter implements DecisioningPlatform<Record<string, ne
};
})
);
const response: GetMediaBuysResponse = { status: 'completed', media_buys };
const response: GetMediaBuysPayload = { media_buys };
return response;
},

Expand Down Expand Up @@ -943,7 +942,7 @@ class SalesNonGuaranteedAdapter implements DecisioningPlatform<Record<string, ne
return out;
},

listCreativeFormats: async (_req, _ctx): Promise<ListCreativeFormatsResponse> => {
listCreativeFormats: async (_req, _ctx): Promise<ListCreativeFormatsPayload> => {
// Publisher-owned format catalog. The mock doesn't have a discrete
// formats endpoint (formats live inline on Product); production sellers
// typically expose `/v1/formats` separately. SWAP: replace with your
Expand All @@ -964,7 +963,6 @@ class SalesNonGuaranteedAdapter implements DecisioningPlatform<Record<string, ne
FormatAsset.url({ asset_id: 'click_url', required: true }),
];
return {
status: 'completed',
formats: [
{
format_id: { agent_url: FORMAT_AGENT_URL, id: 'display_300x250' },
Expand Down
103 changes: 94 additions & 9 deletions scripts/check-adopter-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ const REPO_ROOT = join(__dirname, '..');
const ADOPTER_TSCONFIG = {
compilerOptions: {
target: 'ES2022',
module: 'commonjs',
moduleResolution: 'node',
module: 'ESNext',
moduleResolution: 'bundler',
esModuleInterop: true,
strict: true,
skipLibCheck: false,
Expand All @@ -45,14 +45,24 @@ const ADOPTER_TSCONFIG = {
};

const ADOPTER_SOURCE = `
// Mirrors the repro from issue #1236 — minimal adopter import via the
// subpaths that produced TS2304 leaks (\`stripInternal\` deleted the
// declaration but consumers kept referencing it). Narrow on purpose:
// pulling \`@adcp/sdk/server\`'s full transitive surface drags in
// peerDependency type errors (express, @opentelemetry/api) that are
// orthogonal to this guard's scope. Widen when those are addressed.
import type { AdcpServer } from '@adcp/sdk/server';
// Mirrors the repro from issue #1236 and locks the server-side handler
// payload typing surface that adopters consume from a packed SDK tarball.
import type {
AdcpServer,
CheckGovernancePayload,
CreatePropertyListPayload,
ListContentStandardsPayload,
OperationalContext,
OperationalPlatform,
SalesCorePlatform,
SalesIngestionPlatform,
ServerPayload,
SIGetOfferingPayload,
} from '@adcp/sdk/server';
import { createAdcpServerFromPlatform, defineOperationalPlatform } from '@adcp/sdk/server';
import { createAdcpServer as createLegacyAdcpServer } from '@adcp/sdk/server/legacy/v5';
import { createSingleAgentClient, extractAdcpErrorFromMcp, extractAdcpErrorFromTransport } from '@adcp/sdk';
import type { CreateMediaBuySuccess, ServerPayload as ServerPayloadFromTypes } from '@adcp/sdk/types';
import type { AccountReference } from '@adcp/sdk';
import { customToolFor, TOOL_INPUT_SHAPES, TOOL_REQUEST_SCHEMAS } from '@adcp/sdk/schemas';

Expand All @@ -61,6 +71,81 @@ void _server;
void createSingleAgentClient;
void extractAdcpErrorFromMcp;
void extractAdcpErrorFromTransport;
void createAdcpServerFromPlatform;

const _legacyServer = createLegacyAdcpServer({
name: 'packed-adopter',
version: '1.0.0',
mediaBuy: {
getProducts: async () => ({ products: [], cache_scope: 'account' }),
createMediaBuy: async () => ({ media_buy_id: 'mb_1', packages: [] }),
getMediaBuys: async () => ({ media_buys: [] }),
getMediaBuyDelivery: async () => ({
reporting_period: { start: '2026-01-01', end: '2026-01-31' },
media_buy_deliveries: [],
}),
},
});
void _legacyServer;

const _sales: SalesCorePlatform & SalesIngestionPlatform = {
getProducts: async () => ({ products: [], cache_scope: 'account' }),
createMediaBuy: async () => ({ media_buy_id: 'mb_1', packages: [] }),
updateMediaBuy: async () => ({ media_buy_id: 'mb_1' }),
getMediaBuys: async () => ({ media_buys: [] }),
getMediaBuyDelivery: async () => ({
reporting_period: { start: '2026-01-01', end: '2026-01-31' },
media_buy_deliveries: [],
}),
syncCreatives: async () => [],
};
void _sales;

interface OpsCtx extends OperationalContext {
advertiserId: string;
}

const _ops: OperationalPlatform<OpsCtx> = defineOperationalPlatform<OpsCtx>({
platformId: 'packed-adopter',
extractContext: async () => ({ accessToken: undefined, advertiserId: 'adv_1' }),
updateMediaBuy: async () => ({ media_buy_id: 'mb_1' }),
getMediaBuyDelivery: async () => ({
reporting_period: { start: '2026-01-01', end: '2026-01-31' },
media_buy_deliveries: [],
}),
getProducts: async () => ({ products: [], cache_scope: 'account' }),
});
void _ops;

const _checkGovernancePayload: CheckGovernancePayload = {
check_id: 'check_1',
verdict: 'approved',
plan_id: 'plan_1',
explanation: 'Approved',
governance_context: 'gc_123',
};
const _propertyListPayload: CreatePropertyListPayload = {
list: { list_id: 'list_1', name: 'Test list' },
auth_token: 'token_1',
};
const _contentStandardsPayload: ListContentStandardsPayload = { standards: [] };
const _siPayload: SIGetOfferingPayload = { available: true };
void _checkGovernancePayload;
void _propertyListPayload;
void _contentStandardsPayload;
void _siPayload;

const _serverPayload: ServerPayload<CreateMediaBuySuccess> = {
media_buy_id: 'mb_1',
packages: [],
status: 'active',
};
const _typesPayload: ServerPayloadFromTypes<CreateMediaBuySuccess> = _serverPayload;
void _typesPayload;

// @ts-expect-error ServerPayload must preserve required domain fields.
const _missingRequiredDomainField: ServerPayload<CreateMediaBuySuccess> = { packages: [] };
void _missingRequiredDomainField;

void TOOL_REQUEST_SCHEMAS.get_products.shape.brief;
void TOOL_REQUEST_SCHEMAS.create_media_buy.shape.account;
Expand Down
13 changes: 13 additions & 0 deletions src/lib/server/adcp-server.type-checks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
// Run with `npm run typecheck`.

import type { AdcpServer } from './adcp-server';
import type { MediaBuyHandlers } from './create-adcp-server';

// ── Plain object with same structural shape isn't an AdcpServer ──────────

Expand Down Expand Up @@ -44,8 +45,20 @@ async function _adcpServerCallSitesStillWork(s: AdcpServer): Promise<void> {
});
}

function _legacy_media_buy_handlers_accept_payload_returns(): MediaBuyHandlers {
return {
getProducts: async () => ({ products: [], cache_scope: 'account' }),
getMediaBuys: async () => ({ media_buys: [] }),
getMediaBuyDelivery: async () => ({
reporting_period: { start: '2026-01-01', end: '2026-01-31' },
media_buy_deliveries: [],
}),
};
}

export const _references = [
_imitationCannotBeAdcpServer,
_registerToolNotOnAdcpServer,
_adcpServerCallSitesStillWork,
_legacy_media_buy_handlers_accept_payload_returns,
] as const;
Loading
Loading