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
7 changes: 7 additions & 0 deletions .changeset/fix-server-handler-payload-aliases.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@adcp/sdk': patch
---

Export named server `*Payload` and `*HandlerResult` aliases for decisioning handlers, and keep those payload types aligned with runtime response projection by stripping write-only webhook credentials and billing bank fields.

Adopters that annotated server helper layers with generated wire `*Response` / `*Success` types should switch those annotations to the exported aliases from `@adcp/sdk/server`, or use `ServerPayload<T>` directly for less common generated response shapes.
135 changes: 134 additions & 1 deletion scripts/check-adopter-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,20 +49,45 @@ const ADOPTER_SOURCE = `
// payload typing surface that adopters consume from a packed SDK tarball.
import type {
AdcpServer,
ActivateSignalPayload,
BuildCreativePayload,
BuildCreativeMultiPayload,
CheckGovernancePayload,
CreativeApprovedPayload,
CreatePropertyListPayload,
CreateMediaBuyPayload,
CreateMediaBuyHandlerResult,
GetMediaBuyDeliveryPayload,
GetMediaBuysPayload,
GetAccountFinancialsHandlerResult,
GetBrandIdentityPayload,
GetProductsPayload,
GetRightsPayload,
ListAccountsHandlerResult,
ListAccountsPayload,
ListCreativeFormatsPayload,
ListContentStandardsPayload,
OperationalContext,
OperationalPlatform,
RightsTerms,
ReportUsageHandlerResult,
SalesCorePlatform,
SalesIngestionPlatform,
ServerPayload,
SIGetOfferingPayload,
SyncAudiencesPayload,
SyncAccountsHandlerResult,
SyncCreativesPayload,
SyncCreativesHandlerResult,
SyncEventSourcesPayload,
SyncGovernanceHandlerResult,
UpdateRightsPayload,
UpdateMediaBuyPayload,
} 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 { CreateMediaBuySuccess, CreativeManifest, ServerPayload as ServerPayloadFromTypes } from '@adcp/sdk/types';
import type { AccountReference } from '@adcp/sdk';
import { customToolFor, customToolForSchema, TOOL_INPUT_SCHEMAS, TOOL_INPUT_SHAPES, TOOL_REQUEST_SCHEMAS } from '@adcp/sdk/schemas';

Expand Down Expand Up @@ -101,6 +126,114 @@ const _sales: SalesCorePlatform & SalesIngestionPlatform = {
};
void _sales;

const _salesWithHandoff: SalesCorePlatform & SalesIngestionPlatform = {
getProducts: async () => ({ products: [], cache_scope: 'account' }),
createMediaBuy: async (_req, ctx) =>
ctx.handoffToTask(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 (_creatives, ctx) => ctx.handoffToTask(async () => []),
};
void _salesWithHandoff;

type Ok<T> = { ok: true; value: T };
type Err<E> = { ok: false; error: E };
type Result<T, E> = Ok<T> | Err<E>;
const ok = <T,>(value: T): Result<T, Error> => ({ ok: true, value });
const creativeManifest = {} as CreativeManifest;
const rightsTerms = {} as RightsTerms;

// Issue #1988: adopter helper layers often wrap handler payloads in their
// own Result<T, E>. They need named payload types that do not require the
// protocol task envelope (status, timestamp, context_id, etc.).
const _payloadResults: [
Result<GetProductsPayload, Error>,
Result<ListCreativeFormatsPayload, Error>,
Result<CreateMediaBuyPayload, Error>,
Result<UpdateMediaBuyPayload, Error>,
Result<SyncCreativesPayload, Error>,
Result<SyncEventSourcesPayload, Error>,
Result<ListAccountsPayload, Error>,
Result<GetMediaBuysPayload, Error>,
Result<GetMediaBuyDeliveryPayload, Error>,
Result<BuildCreativePayload, Error>,
Result<BuildCreativeMultiPayload, Error>,
Result<SyncAudiencesPayload, Error>,
Result<ActivateSignalPayload, Error>,
Result<GetBrandIdentityPayload, Error>,
Result<GetRightsPayload, Error>,
Result<UpdateRightsPayload, Error>,
Result<CreativeApprovedPayload, Error>,
Result<CreateMediaBuyHandlerResult, Error>,
Result<SyncCreativesHandlerResult, Error>,
] = [
ok({ products: [], cache_scope: 'account' }),
ok({ formats: [] }),
ok({ media_buy_id: 'mb_1', packages: [] }),
ok({ media_buy_id: 'mb_1' }),
ok({ creatives: [] }),
ok({ event_sources: [] }),
ok({ accounts: [] }),
ok({ media_buys: [] }),
ok({
reporting_period: { start: '2026-01-01', end: '2026-01-31' },
media_buy_deliveries: [],
}),
ok({ creative_manifest: creativeManifest }),
ok({ creative_manifests: [] }),
ok({ audiences: [] }),
ok({ deployments: [] }),
ok({ brand_id: 'brand_1', house: { domain: 'acme.com', name: 'Acme' }, names: [{ en: 'Acme' }] }),
ok({ rights: [] }),
ok({ rights_id: 'rights_1', terms: rightsTerms }),
ok({ approval_status: 'approved', rights_id: 'rights_1' }),
ok({ media_buy_id: 'mb_1', packages: [] }),
ok([]),
];
void _payloadResults;

const _accountHandlerResults: [
ListAccountsHandlerResult,
SyncAccountsHandlerResult,
SyncGovernanceHandlerResult,
Result<ReportUsageHandlerResult, Error>,
Result<GetAccountFinancialsHandlerResult, Error>,
] = [{ items: [] }, [], [], ok({ accepted: 0 }), ok({} as GetAccountFinancialsHandlerResult)];
void _accountHandlerResults;

const _safeListAccountsPayload: ListAccountsPayload = {
accounts: [
{
account_id: 'acct_1',
name: 'Acme',
status: 'active',
billing_entity: { legal_name: 'Acme Inc.' },
notification_configs: [
{
subscriber_id: 'buyer-primary',
url: 'https://hooks.test/notify',
event_types: [],
authentication: { schemes: ['Bearer'] },
},
],
},
],
};
const _safeNotificationAuth = _safeListAccountsPayload.accounts[0]?.notification_configs?.[0]?.authentication;
if (_safeNotificationAuth) {
// @ts-expect-error response payload aliases must not expose write-only webhook credentials
void _safeNotificationAuth.credentials;
}
const _safeBillingEntity = _safeListAccountsPayload.accounts[0]?.billing_entity;
if (_safeBillingEntity) {
// @ts-expect-error response payload aliases must not expose write-only bank coordinates
void _safeBillingEntity.bank;
}

interface OpsCtx extends OperationalContext {
advertiserId: string;
}
Expand Down
30 changes: 25 additions & 5 deletions src/lib/server/decisioning/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,16 @@ import type {
BusinessEntity,
ExtensionObject,
PaymentTerms,
ListAccountsResponse,
ReportUsageRequest,
ReportUsageResponse,
SyncAccountsResponse,
SyncAccountsSuccess,
SyncGovernanceRequest,
SyncGovernanceResponse,
SyncGovernanceSuccess,
GetAccountFinancialsRequest,
GetAccountFinancialsResponse,
GetAccountFinancialsSuccess,
} from '../../types/tools.generated';
import type { ServerPayload } from '../../types/server-payload';
Expand All @@ -38,6 +42,22 @@ type WireNotificationConfig = Omit<NotificationConfig, 'authentication'> & {
authentication?: Omit<NonNullable<NotificationConfig['authentication']>, 'credentials'>;
};

export type ListAccountsPayload = ServerPayload<ListAccountsResponse>;
export type SyncAccountsPayload = ServerPayload<SyncAccountsResponse>;
export type SyncAccountsSuccessPayload = ServerPayload<SyncAccountsSuccess>;
export type SyncAccountsRow = SyncAccountsSuccess['accounts'][number];
export type SyncGovernancePayload = ServerPayload<SyncGovernanceResponse>;
export type SyncGovernanceSuccessPayload = ServerPayload<SyncGovernanceSuccess>;
export type SyncGovernanceRow = SyncGovernanceSuccess['accounts'][number];
export type ReportUsagePayload = ServerPayload<ReportUsageResponse>;
export type GetAccountFinancialsPayload = ServerPayload<GetAccountFinancialsResponse>;
export type GetAccountFinancialsSuccessPayload = ServerPayload<GetAccountFinancialsSuccess>;
export type ListAccountsHandlerResult<TCtxMeta = Record<string, unknown>> = CursorPage<Account<TCtxMeta>>;
export type SyncAccountsHandlerResult = SyncAccountsResultRow[];
export type SyncGovernanceHandlerResult = SyncGovernanceRow[];
export type ReportUsageHandlerResult = ReportUsagePayload;
export type GetAccountFinancialsHandlerResult = GetAccountFinancialsSuccessPayload;

/**
* Account — framework's rich representation. A strict superset of the wire
* `Account` shape (from `list_accounts` / response envelopes):
Expand Down Expand Up @@ -524,7 +544,7 @@ export interface AccountStore<TCtxMeta = Record<string, unknown>> {
* verified `agent_url` from `credential.kind === 'http_sig'` when
* `agentRegistry` is not configured).
*/
upsert?(refs: AccountReference[], ctx?: ResolveContext): Promise<SyncAccountsResultRow[]>;
upsert?(refs: AccountReference[], ctx?: ResolveContext): Promise<SyncAccountsHandlerResult>;

/**
* sync_governance API surface. Buyers register governance agent endpoints
Expand Down Expand Up @@ -566,7 +586,7 @@ export interface AccountStore<TCtxMeta = Record<string, unknown>> {
syncGovernance?(
entries: SyncGovernanceRequest['accounts'],
ctx?: ResolveContext
): Promise<SyncGovernanceSuccess['accounts']>;
): Promise<SyncGovernanceHandlerResult>;

/**
* list_accounts API surface. Framework wraps with cursor envelope.
Expand All @@ -577,7 +597,7 @@ export interface AccountStore<TCtxMeta = Record<string, unknown>> {
* scope the listing per-principal (e.g., return only accounts visible to
* the calling buyer agent) without re-deriving identity from the request.
*/
list?(filter: AccountFilter & CursorRequest, ctx?: ResolveContext): Promise<CursorPage<Account<TCtxMeta>>>;
list?(filter: AccountFilter & CursorRequest, ctx?: ResolveContext): Promise<ListAccountsHandlerResult<TCtxMeta>>;

/**
* report_usage API surface. Operator-billed platforms accept usage rows
Expand All @@ -597,7 +617,7 @@ export interface AccountStore<TCtxMeta = Record<string, unknown>> {
* pattern as `accounts.resolve`. Prefer `ctx.agent` for principal-keyed
* commercial gates; see `upsert?` for the rationale.
*/
reportUsage?(req: ReportUsageRequest, ctx?: ResolveContext): Promise<ServerPayload<ReportUsageResponse>>;
reportUsage?(req: ReportUsageRequest, ctx?: ResolveContext): Promise<ReportUsageHandlerResult>;

/**
* get_account_financials API surface. Operator-billed platforms expose
Expand All @@ -624,7 +644,7 @@ export interface AccountStore<TCtxMeta = Record<string, unknown>> {
getAccountFinancials?(
req: GetAccountFinancialsRequest,
ctx: AccountToolContext<TCtxMeta>
): Promise<GetAccountFinancialsSuccess>;
): Promise<GetAccountFinancialsHandlerResult>;

/**
* Mid-request token refresh hook. Optional. Called by the framework when
Expand Down
Loading
Loading