Skip to content
Open
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
74 changes: 59 additions & 15 deletions scripts/project-current-ucp-schemas.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -828,6 +828,21 @@ function buildResponseEnvelopeSchema(ucpSchema, extraRequired = []) {
type: "object",
additionalProperties: { type: "array", items: { $ref: compat } },
};
} else if (
typeof schema?.$ref === "string" &&
schema.$ref.startsWith("#/$defs/") &&
ucpSchema.$defs[schema.$ref.slice("#/$defs/".length)]?.type === "object"
) {
// An object valued local def (map_order): keep its map shape instead of
// collapsing the $ref to a string, which rejected the map_order example
// in the overview.
const def = ucpSchema.$defs[schema.$ref.slice("#/$defs/".length)];
properties[name] = {
type: "object",
additionalProperties: def.additionalProperties
? toCompatLeaf(def.additionalProperties)
: true,
};
} else {
properties[name] = toCompatLeaf(schema);
}
Expand All @@ -841,6 +856,49 @@ function buildResponseEnvelopeSchema(ucpSchema, extraRequired = []) {
};
}

// The `ucp` member of the discovery profile, DERIVED from ucp.json#/$defs/business_schema
// (allOf: base + overlay). base contributes every registry (services,
// capabilities, payment_handlers) as an object keyed by reverse domain name
// whose values are entity arrays, plus version/status/map_order, requiring
// only version; the business overlay adds required services and
// payment_handlers, and supported_versions. Registry items map to the
// per-entity RESPONSE compat shape exactly as the response envelope does.
// Replaces the hand written node that carried the withdrawn 2026-01-11 shape
// (capabilities as a flat array and required, services as single objects, no
// payment_handlers), which rejected the business profile example the specification
// publishes. No title: the type keeps the property-derived name (UcpSchema), so
// the existing export survives.
function buildBusinessProfileUcpSchema(ucpSchema) {
const overlay = ucpSchema.$defs.business_schema.allOf[1];
const envelope = buildResponseEnvelopeSchema(
ucpSchema,
overlay.required ?? []
);
const { title: _title, ...derived } = envelope;
const properties = { ...derived.properties };
for (const [name, schema] of Object.entries(overlay.properties ?? {})) {
// Registry properties are already modeled by the envelope (the overlay only
// re-points their item refs); carry the additions of the overlay through.
if (name in properties) continue;
// An open map with a typed value (supported_versions: version -> profile
// URI) keeps that value type; toCompatLeaf would widen it to any.
properties[name] =
schema?.type === "object" &&
schema.additionalProperties &&
typeof schema.additionalProperties === "object"
? {
type: "object",
additionalProperties: toCompatLeaf(schema.additionalProperties),
}
: toCompatLeaf(schema);
}
return {
...derived,
required: [...new Set(derived.required)],
properties,
};
}

function writeCompatibilityDiscoverySchemas() {
const ucpSchema = loadRootSchema("ucp.json");
const paymentHandlerSchema = loadRootSchema("payment_handler.json");
Expand Down Expand Up @@ -1012,21 +1070,7 @@ function writeCompatibilityDiscoverySchemas() {
type: "array",
items: { $ref: "signing_key.json" },
},
ucp: {
type: "object",
required: ["capabilities", "services", "version"],
properties: {
capabilities: {
type: "array",
items: { $ref: "capability.json" },
},
services: {
type: "object",
additionalProperties: { $ref: "ucp_service.json" },
},
version: clone(version),
},
},
ucp: buildBusinessProfileUcpSchema(ucpSchema),
},
};

Expand Down
162 changes: 102 additions & 60 deletions src/spec_generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@ import * as z from "zod";
export const UseSchema = z.enum(["enc", "sig"]);
export type Use = z.infer<typeof UseSchema>;

export const TransportSchema = z.enum(["a2a", "embedded", "mcp", "rest"]);
export type Transport = z.infer<typeof TransportSchema>;

export const UcpCheckoutResponseStatusSchema = z.enum(["error", "success"]);
export type UcpCheckoutResponseStatus = z.infer<
typeof UcpCheckoutResponseStatusSchema
>;

// Content format, default = plain.

export const ContentTypeSchema = z.enum(["markdown", "plain"]);
Expand Down Expand Up @@ -43,14 +51,6 @@ export type CheckoutResponseStatus = z.infer<
typeof CheckoutResponseStatusSchema
>;

export const TransportSchema = z.enum(["a2a", "embedded", "mcp", "rest"]);
export type Transport = z.infer<typeof TransportSchema>;

export const UcpCheckoutResponseStatusSchema = z.enum(["error", "success"]);
export type UcpCheckoutResponseStatus = z.infer<
typeof UcpCheckoutResponseStatusSchema
>;

// Adjustment status.

export const AdjustmentStatusSchema = z.enum([
Expand Down Expand Up @@ -233,6 +233,33 @@ export type NetworkTokenCredentialType = z.infer<
export const PanCredentialTypeSchema = z.enum(["pan"]);
export type PanCredentialType = z.infer<typeof PanCredentialTypeSchema>;

export const CapabilityDiscoverySchema = z.object({
config: z.record(z.string(), z.any()).optional(),
extends: z
.union([
z
.array(
z
.string()
.regex(
/^[a-z](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9_-]*[a-z0-9_])?)+$/
)
)
.min(1),
z
.string()
.regex(
/^[a-z](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9_-]*[a-z0-9_])?)+$/
),
])
.optional(),
name: z.string(),
schema: z.string().url(),
spec: z.string().url(),
version: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
});
export type CapabilityDiscovery = z.infer<typeof CapabilityDiscoverySchema>;

export const SigningKeySchema = z.object({
alg: z.string().optional(),
crv: z.string().optional(),
Expand Down Expand Up @@ -263,7 +290,7 @@ export type ConstraintsProperty = z.infer<typeof ConstraintsPropertySchema>;
export const ConstraintExpressionPropertySchema = ConstraintsPropertySchema;
export type ConstraintExpressionProperty = ConstraintsProperty;

export const CapabilityDiscoverySchema = z.object({
export const CapabilityResponseSchema = z.object({
config: z.record(z.string(), z.any()).optional(),
extends: z
.union([
Expand All @@ -283,12 +310,23 @@ export const CapabilityDiscoverySchema = z.object({
),
])
.optional(),
name: z.string(),
schema: z.string().url(),
spec: z.string().url(),
id: z.string().optional(),
schema: z.string().url().optional(),
spec: z.string().url().optional(),
version: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
});
export type CapabilityDiscovery = z.infer<typeof CapabilityDiscoverySchema>;
export type CapabilityResponse = z.infer<typeof CapabilityResponseSchema>;

export const ServiceResponseSchema = z.object({
config: z.record(z.string(), z.any()).optional(),
endpoint: z.string().url().optional(),
id: z.string().optional(),
schema: z.string().url().optional(),
spec: z.string().url().optional(),
transport: TransportSchema,
version: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
});
export type ServiceResponse = z.infer<typeof ServiceResponseSchema>;

export const A2ASchema = z.object({
endpoint: z
Expand Down Expand Up @@ -712,44 +750,6 @@ export type TotalLine = z.infer<typeof TotalLineSchema>;
export const TotalLineClassSchema = TotalLineSchema;
export type TotalLineClass = TotalLine;

export const CapabilityResponseSchema = z.object({
config: z.record(z.string(), z.any()).optional(),
extends: z
.union([
z
.array(
z
.string()
.regex(
/^[a-z](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9_-]*[a-z0-9_])?)+$/
)
)
.min(1),
z
.string()
.regex(
/^[a-z](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9_-]*[a-z0-9_])?)+$/
),
])
.optional(),
id: z.string().optional(),
schema: z.string().url().optional(),
spec: z.string().url().optional(),
version: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
});
export type CapabilityResponse = z.infer<typeof CapabilityResponseSchema>;

export const ServiceResponseSchema = z.object({
config: z.record(z.string(), z.any()).optional(),
endpoint: z.string().url().optional(),
id: z.string().optional(),
schema: z.string().url().optional(),
spec: z.string().url().optional(),
transport: TransportSchema,
version: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
});
export type ServiceResponse = z.infer<typeof ServiceResponseSchema>;

export const EventLineItemSchema = z.object({
id: z.string(),
quantity: z.number().int().gte(1).lte(9007199254740991),
Expand Down Expand Up @@ -2136,13 +2136,6 @@ export type AvailablePaymentInstrument = z.infer<
typeof AvailablePaymentInstrumentSchema
>;

export const UcpSchema = z.object({
capabilities: z.array(CapabilityDiscoverySchema),
services: z.record(z.string(), UcpServiceSchema),
version: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
});
export type Ucp = z.infer<typeof UcpSchema>;

export const LineItemCreateRequestSchema = z.object({
item: ItemCreateRequestSchema,
quantity: z.number().int().gte(1).lte(9007199254740991),
Expand Down Expand Up @@ -2388,6 +2381,55 @@ export type PaymentHandlerResponse = z.infer<
typeof PaymentHandlerResponseSchema
>;

export const UcpSchema = z.object({
capabilities: z
.record(z.string(), z.array(CapabilityResponseSchema))
.refine(
(value) =>
Object.keys(value).every((key) =>
/^[a-z](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9_-]*[a-z0-9_])?)+$/.test(
key
)
),
{ message: "Record keys must match the required pattern (propertyNames)" }
)
.optional(),
map_order: z.record(z.string(), z.array(z.string())).optional(),
payment_handlers: z
.record(z.string(), z.array(PaymentHandlerResponseSchema))
.refine(
(value) =>
Object.keys(value).every((key) =>
/^[a-z](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9_-]*[a-z0-9_])?)+$/.test(
key
)
),
{ message: "Record keys must match the required pattern (propertyNames)" }
),
services: z
.record(z.string(), z.array(ServiceResponseSchema))
.refine(
(value) =>
Object.keys(value).every((key) =>
/^[a-z](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9_-]*[a-z0-9_])?)+$/.test(
key
)
),
{ message: "Record keys must match the required pattern (propertyNames)" }
),
status: UcpCheckoutResponseStatusSchema.optional(),
supported_versions: z
.record(z.string(), z.string())
.refine(
(value) =>
Object.keys(value).every((key) => /^\d{4}-\d{2}-\d{2}$/.test(key)),
{ message: "Record keys must match the required pattern (propertyNames)" }
)
.optional(),
version: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
});
export type Ucp = z.infer<typeof UcpSchema>;

export const CheckoutCreateRequestSchema = z.object({
attribution: z.record(z.string(), z.string()).optional(),
buyer: BuyerClassSchema.optional(),
Expand Down Expand Up @@ -2420,7 +2462,7 @@ export const UcpCheckoutResponseSchema = z.object({
{ message: "Record keys must match the required pattern (propertyNames)" }
)
.optional(),
map_order: z.string().optional(),
map_order: z.record(z.string(), z.array(z.string())).optional(),
payment_handlers: z
.record(z.string(), z.array(PaymentHandlerResponseSchema))
.refine(
Expand Down Expand Up @@ -2462,7 +2504,7 @@ export const UcpResponseSchema = z.object({
{ message: "Record keys must match the required pattern (propertyNames)" }
)
.optional(),
map_order: z.string().optional(),
map_order: z.record(z.string(), z.array(z.string())).optional(),
payment_handlers: z
.record(z.string(), z.array(PaymentHandlerResponseSchema))
.refine(
Expand Down
Loading
Loading