A TypeScript SDK for implementing the Conversational Transfer Protocol (CTP) on LLM platforms.
CTP enables platforms to securely transfer conversation context to external destinations when users follow links, enabling personalized experiences while preserving privacy.
- Discovery: Fetch and cache destination CTP discovery documents
- Token Generation: Create nested JWTs (JWE + JWS) per the CTP specification
- Signal Extraction: LLM-powered extraction of shopping signals from conversations
- Schema Validation: Validate signals against JSON Schema (draft 2020-12)
- Security: SSRF protection, URL validation, secure key management
- React Hooks: Client-side hooks for CTP-aware components
The SDK is currently bundled with the reference platform. To use it in your own
project, copy the /lib/ctp directory or extract it as an npm package.
npm install jose ajv ajv-formatsimport { encodeJwkToBase64, generateSigningKeyPair } from "./lib/ctp";
const keyId = "platform-key-2026";
const result = await generateSigningKeyPair("ES256", keyId);
if (result.success) {
console.log(
"Private Key (base64):",
encodeJwkToBase64(result.data.privateKey),
);
console.log("Key ID:", keyId);
}Or use the CLI script:
npx tsx scripts/generate-keys.tsCTP_PLATFORM_ISSUER=https://your-platform.example.com
CTP_SIGNING_KEY_ID=platform-key-2026
CTP_SIGNING_KEY_PRIVATE=<base64-encoded-private-jwk>import { createCtpClient } from "./lib/ctp";
const client = createCtpClient({
platformIssuer: "https://platform.example.com",
signingKey: privateKeyJwk,
signingKeyId: "platform-key-2026",
signingAlgorithm: "ES256",
});Or create from environment variables:
import { createCtpClientFromEnv } from "./lib/ctp";
const result = createCtpClientFromEnv();
if (result.success) {
const client = result.data;
}const support = await client.checkDestination(
"https://shop.example.com/products",
);
if (support.supported) {
console.log("CTP supported!");
console.log("Discovery:", support.discovery);
}import { openai } from "@ai-sdk/openai";
import { generateText } from "ai";
import { createOpenAIExtractor } from "./lib/ctp";
const extractFn = createOpenAIExtractor(openai, generateText, "gpt-5.2");
const signals = await client.extractSignals(
[
{ role: "user", content: "I'm looking for running shoes under $150" },
{
role: "assistant",
content: "I'd recommend checking out Nike or Brooks...",
},
],
extractFn,
);
// Result:
// {
// itemOffered: { category: 'Shoes > Athletic > Running', brand: 'Nike' },
// priceSpecification: { maxPrice: 150, priceCurrency: 'USD' }
// }const result = await client.prepareLink({
url: "https://shop.example.com/running-shoes",
messages: conversationMessages,
extractFn: extractFn,
});
if (result.success) {
console.log("Original URL:", result.data.originalUrl);
console.log("Augmented URL:", result.data.augmentedUrl);
console.log("Signals:", result.data.signals);
}Creates a CTP client instance.
interface CtpClientConfig {
platformIssuer: string; // Platform's issuer URL
signingKey: JWK; // Private signing key (JWK)
signingKeyId: string; // Key ID for the signing key
signingAlgorithm: JwsAlgorithm; // 'ES256' | 'ES384' | 'RS256' | etc.
discoveryCacheTtl?: number; // Cache TTL in ms (default: 1 hour)
jwksCacheTtl?: number; // JWKS cache TTL in ms (default: 1 hour)
}Check if a destination supports CTP.
const result = await checkCtpSupport("https://shop.example.com");
// { supported: boolean, discovery?: DestinationDiscovery, error?: string }Prepare a CTP-augmented link with full flow.
const result = await prepareCtpLink({
url: "https://shop.example.com/product",
messages: conversationMessages,
config: ctpClientConfig,
extractFn: llmExtractFunction,
signals: preExtractedSignals, // optional
});Fetch a destination's CTP discovery document.
const result = await fetchDestinationDiscovery("https://shop.example.com");
if (result.success) {
const discovery: DestinationDiscovery = result.data;
}Fetch a JWKS from a URL.
const result = await fetchJwks(
"https://shop.example.com/.well-known/jwks.json",
);Create a CTP token (nested JWE + JWS).
const result = await createToken({
platformIssuer: "https://platform.example.com",
destinationOrigin: "https://shop.example.com",
signals: { itemOffered: { category: "Shoes" } },
platformPrivateKey: privateJwk,
platformKeyId: "platform-key-2026",
destinationPublicKey: destPublicJwk,
destinationKeyId: "dest-key-1",
encAlg: "RSA-OAEP-256",
encEnc: "A256GCM",
sigAlg: "ES256",
});Extract structured signals from conversation using an LLM.
const result = await extractSignals(messages, {
extractFn: async (systemPrompt, userPrompt) => {
// Call your LLM here
return llmResponse;
},
schemaProperties: ["itemOffered.category", "priceSpecification.maxPrice"],
});Create an extraction function for OpenAI models.
import { openai } from "@ai-sdk/openai";
import { generateText } from "ai";
const extractFn = createOpenAIExtractor(openai, generateText, "gpt-5.2");Validate signals against a JSON Schema.
const result = validateSignals(signals, schemaObject);Filter signals to only include requested properties.
const filtered = filterByProperties(signals, [
"itemOffered.category",
"priceSpecification.maxPrice",
]);Generate an ES256/RS256 key pair for signing.
const result = await generateSigningKeyPair("ES256", "my-key-id");
if (result.success) {
const { publicKey, privateKey } = result.data;
}Build a JWKS document from public keys.
const jwks = buildJwks([publicKey1, publicKey2]);Fetch with SSRF protection, timeout, and size limits.
const result = await safeFetch("https://example.com/api", {
timeout: 5000,
maxSize: 1024 * 1024,
});Generate a cryptographically secure JWT ID.
const jti = generateJti(); // 'ctp_xK9...' (128+ bits of entropy)Check if a URL's destination supports CTP.
const { supported, discovery, isChecking, error } = useCtpSupport(url);Batch check multiple URLs for CTP support.
const { results, isChecking } = useCtpSupportBatch(urls);
// results is Map<string, { supported: boolean, discovery?: DestinationDiscovery }>Extract signals from conversation messages.
const { signals, isExtracting, error } = useCtpSignals(messages);Hook to prepare CTP-augmented links.
const { prepareLink, isLoading } = useCtpPrepareLink();
const result = await prepareLink(url, signals);interface Signals {
itemOffered?: {
category?: string; // e.g., 'Shoes > Athletic > Running'
name?: string; // Product name
brand?: string; // Brand name
};
priceSpecification?: {
minPrice?: number;
maxPrice?: number;
priceCurrency?: string; // ISO 4217 (e.g., 'USD')
};
eligibleRegion?: string;
location?: {
latitude?: number;
longitude?: number;
addressLocality?: string;
};
aggregateRating?: { ratingValue?: number; reviewCount?: number };
}interface DestinationDiscovery {
version: string;
issuer: string;
jwks_uri: string;
supported_algs: JweAlgorithm[];
supported_enc: JweEncryption[];
signal_schema?: string;
signal_properties?: string[];
}
interface PlatformDiscovery {
version: string;
issuer: string;
jwks_uri: string;
supported_signing_algs: JwsAlgorithm[];
}All async functions return a CtpResult<T> type:
type CtpResult<T> =
| { success: true; data: T }
| { success: false; error: CtpError };
interface CtpError {
code: CtpErrorCode;
message: string;
cause?: unknown;
}Error codes include:
invalid_token- Token format invalidtoken_expired- Token has expiredsignature_invalid- JWS signature failedissuer_mismatch- Issuer doesn't matchschema_mismatch- Signals don't match schemaunauthorized_schema- Schema URI is not the standard CTP schemadiscovery_failed- Failed to fetch discoveryssrf_blocked- URL blocked for securitytoken_too_large- Token exceeds 4096 chars
import {
CTP_QUERY_PARAM, // 'ctp'
CTP_SCHEMAS, // { SIGNALS, ... }
CTP_VERSION, // '0.1.0'
DEFAULT_ALGORITHMS, // { JWE_ALG: 'RSA-OAEP-256', ... }
FETCH_LIMITS, // { TIMEOUT_MS: 5000, MAX_RESPONSE_SIZE: 1MB }
TOKEN_LIMITS, // { MAX_TOKEN_LENGTH: 4096, ... }
} from "./lib/ctp";- SSRF Protection: All URL fetching validates against private IP ranges
- Token Expiry: Tokens expire within 10 minutes
- Consent Required: Per-click consent must be obtained before generating tokens
- Key Rotation: Support multiple keys in JWKS during rotation periods
Apache License 2.0