@ucptools/validator 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/CLAUDE.md +109 -0
  2. package/CONTRIBUTING.md +113 -0
  3. package/LICENSE +21 -0
  4. package/README.md +203 -0
  5. package/api/analyze-feed.js +140 -0
  6. package/api/badge.js +185 -0
  7. package/api/benchmark.js +177 -0
  8. package/api/directory-stats.ts +29 -0
  9. package/api/directory.ts +73 -0
  10. package/api/generate-compliance.js +143 -0
  11. package/api/generate-schema.js +457 -0
  12. package/api/generate.js +132 -0
  13. package/api/security-scan.js +133 -0
  14. package/api/simulate.js +187 -0
  15. package/api/tsconfig.json +10 -0
  16. package/api/validate.js +1351 -0
  17. package/apify-actor/.actor/actor.json +68 -0
  18. package/apify-actor/.actor/input_schema.json +32 -0
  19. package/apify-actor/APIFY-STORE-LISTING.md +412 -0
  20. package/apify-actor/Dockerfile +8 -0
  21. package/apify-actor/README.md +166 -0
  22. package/apify-actor/main.ts +111 -0
  23. package/apify-actor/package.json +17 -0
  24. package/apify-actor/src/main.js +199 -0
  25. package/docs/BRAND-IDENTITY.md +238 -0
  26. package/docs/BRAND-STYLE-GUIDE.md +356 -0
  27. package/drizzle/0000_black_king_cobra.sql +39 -0
  28. package/drizzle/meta/0000_snapshot.json +309 -0
  29. package/drizzle/meta/_journal.json +13 -0
  30. package/drizzle.config.ts +10 -0
  31. package/examples/full-profile.json +70 -0
  32. package/examples/minimal-profile.json +23 -0
  33. package/package.json +69 -0
  34. package/public/.well-known/ucp +25 -0
  35. package/public/android-chrome-192x192.png +0 -0
  36. package/public/android-chrome-512x512.png +0 -0
  37. package/public/apple-touch-icon.png +0 -0
  38. package/public/brand.css +321 -0
  39. package/public/directory.html +701 -0
  40. package/public/favicon-16x16.png +0 -0
  41. package/public/favicon-32x32.png +0 -0
  42. package/public/favicon.ico +0 -0
  43. package/public/guides/bigcommerce.html +743 -0
  44. package/public/guides/fastucp.html +838 -0
  45. package/public/guides/magento.html +779 -0
  46. package/public/guides/shopify.html +726 -0
  47. package/public/guides/squarespace.html +749 -0
  48. package/public/guides/wix.html +747 -0
  49. package/public/guides/woocommerce.html +733 -0
  50. package/public/index.html +3835 -0
  51. package/public/learn.html +396 -0
  52. package/public/logo.jpeg +0 -0
  53. package/public/og-image-icon.png +0 -0
  54. package/public/og-image.png +0 -0
  55. package/public/robots.txt +6 -0
  56. package/public/site.webmanifest +31 -0
  57. package/public/sitemap.xml +69 -0
  58. package/public/social/linkedin-banner-1128x191.png +0 -0
  59. package/public/social/temp.PNG +0 -0
  60. package/public/social/x-header-1500x500.png +0 -0
  61. package/public/verify.html +410 -0
  62. package/scripts/generate-favicons.js +44 -0
  63. package/scripts/generate-ico.js +23 -0
  64. package/scripts/generate-og-image.js +45 -0
  65. package/scripts/reset-db.ts +77 -0
  66. package/scripts/seed-db.ts +71 -0
  67. package/scripts/setup-benchmark-db.js +70 -0
  68. package/src/api/server.ts +266 -0
  69. package/src/cli/index.ts +302 -0
  70. package/src/compliance/compliance-generator.ts +452 -0
  71. package/src/compliance/index.ts +28 -0
  72. package/src/compliance/templates.ts +338 -0
  73. package/src/compliance/types.ts +170 -0
  74. package/src/db/index.ts +28 -0
  75. package/src/db/schema.ts +84 -0
  76. package/src/feed-analyzer/feed-analyzer.ts +726 -0
  77. package/src/feed-analyzer/index.ts +34 -0
  78. package/src/feed-analyzer/types.ts +354 -0
  79. package/src/generator/index.ts +7 -0
  80. package/src/generator/key-generator.ts +124 -0
  81. package/src/generator/profile-builder.ts +402 -0
  82. package/src/hosting/artifacts-generator.ts +679 -0
  83. package/src/hosting/index.ts +6 -0
  84. package/src/index.ts +105 -0
  85. package/src/security/index.ts +15 -0
  86. package/src/security/security-scanner.ts +604 -0
  87. package/src/security/types.ts +55 -0
  88. package/src/services/directory.ts +434 -0
  89. package/src/simulator/agent-simulator.ts +941 -0
  90. package/src/simulator/index.ts +7 -0
  91. package/src/simulator/types.ts +170 -0
  92. package/src/types/generator.ts +140 -0
  93. package/src/types/index.ts +7 -0
  94. package/src/types/ucp-profile.ts +140 -0
  95. package/src/types/validation.ts +89 -0
  96. package/src/validator/index.ts +194 -0
  97. package/src/validator/network-validator.ts +417 -0
  98. package/src/validator/rules-validator.ts +297 -0
  99. package/src/validator/sdk-validator.ts +330 -0
  100. package/src/validator/structural-validator.ts +476 -0
  101. package/tests/fixtures/non-compliant-profile.json +25 -0
  102. package/tests/fixtures/official-sample-profile.json +75 -0
  103. package/tests/integration/benchmark.test.ts +207 -0
  104. package/tests/integration/database.test.ts +163 -0
  105. package/tests/integration/directory-api.test.ts +268 -0
  106. package/tests/integration/simulate-api.test.ts +230 -0
  107. package/tests/integration/validate-api.test.ts +269 -0
  108. package/tests/setup.ts +15 -0
  109. package/tests/unit/agent-simulator.test.ts +575 -0
  110. package/tests/unit/compliance-generator.test.ts +374 -0
  111. package/tests/unit/directory-service.test.ts +272 -0
  112. package/tests/unit/feed-analyzer.test.ts +517 -0
  113. package/tests/unit/lint-suggestions.test.ts +423 -0
  114. package/tests/unit/official-samples.test.ts +211 -0
  115. package/tests/unit/pdf-report.test.ts +390 -0
  116. package/tests/unit/sdk-validator.test.ts +531 -0
  117. package/tests/unit/security-scanner.test.ts +410 -0
  118. package/tests/unit/validation.test.ts +390 -0
  119. package/tsconfig.json +20 -0
  120. package/vercel.json +34 -0
  121. package/vitest.config.ts +22 -0
@@ -0,0 +1,941 @@
1
+ /**
2
+ * AI Agent Simulator
3
+ * Simulates how an AI agent discovers and interacts with UCP-enabled merchants
4
+ *
5
+ * The goal is to test real-world functionality, not just spec compliance.
6
+ * This proves that a UCP implementation actually works for AI agent commerce.
7
+ */
8
+
9
+ import type { UcpProfile, UcpCapability, UcpService } from '../types/ucp-profile.js';
10
+ import type {
11
+ AgentSimulationResult,
12
+ SimulationOptions,
13
+ SimulationStepResult,
14
+ SimulationStepStatus,
15
+ DiscoveryFlowResult,
16
+ CapabilityInspectionResult,
17
+ ServiceInspectionResult,
18
+ RestApiSimulationResult,
19
+ CheckoutSimulationResult,
20
+ PaymentReadinessResult,
21
+ OperationTestResult,
22
+ DEFAULT_SIMULATION_OPTIONS,
23
+ } from './types.js';
24
+
25
+ const DEFAULT_TIMEOUT_MS = 30000;
26
+ const DEFAULT_FETCH_TIMEOUT_MS = 10000;
27
+
28
+ /**
29
+ * Normalize capability to extract the name string
30
+ * Handles both string format and object format capabilities
31
+ */
32
+ function getCapabilityName(cap: unknown): string {
33
+ if (typeof cap === 'string') {
34
+ return cap;
35
+ }
36
+ if (cap && typeof cap === 'object' && 'name' in cap) {
37
+ return String((cap as { name: unknown }).name);
38
+ }
39
+ return '';
40
+ }
41
+
42
+ /**
43
+ * Get capability object (for accessing schema, version, etc.)
44
+ * Returns null for string-only capabilities
45
+ */
46
+ function getCapabilityObject(cap: unknown): UcpCapability | null {
47
+ if (cap && typeof cap === 'object' && 'name' in cap) {
48
+ return cap as UcpCapability;
49
+ }
50
+ return null;
51
+ }
52
+
53
+ /**
54
+ * Step builder helper
55
+ */
56
+ function createStep(
57
+ step: string,
58
+ status: SimulationStepStatus,
59
+ message: string,
60
+ details?: string,
61
+ durationMs?: number,
62
+ data?: Record<string, unknown>
63
+ ): SimulationStepResult {
64
+ return { step, status, message, details, durationMs, data };
65
+ }
66
+
67
+ /**
68
+ * Fetch with timeout
69
+ */
70
+ async function fetchWithTimeout(
71
+ url: string,
72
+ timeoutMs: number = DEFAULT_FETCH_TIMEOUT_MS
73
+ ): Promise<{ ok: boolean; status?: number; data?: unknown; error?: string }> {
74
+ const controller = new AbortController();
75
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
76
+
77
+ try {
78
+ const response = await fetch(url, {
79
+ signal: controller.signal,
80
+ headers: {
81
+ 'User-Agent': 'UCP-Agent-Simulator/1.0',
82
+ 'Accept': 'application/json',
83
+ },
84
+ });
85
+
86
+ clearTimeout(timeoutId);
87
+
88
+ if (!response.ok) {
89
+ return { ok: false, status: response.status, error: `HTTP ${response.status}` };
90
+ }
91
+
92
+ const contentType = response.headers.get('content-type') || '';
93
+ if (contentType.includes('application/json')) {
94
+ const data = await response.json();
95
+ return { ok: true, status: response.status, data };
96
+ }
97
+
98
+ return { ok: true, status: response.status };
99
+ } catch (error) {
100
+ clearTimeout(timeoutId);
101
+ const message = error instanceof Error ? error.message : 'Unknown error';
102
+ return { ok: false, error: message };
103
+ }
104
+ }
105
+
106
+ /**
107
+ * HEAD request to check endpoint responsiveness
108
+ */
109
+ async function checkEndpointResponsive(
110
+ url: string,
111
+ timeoutMs: number = DEFAULT_FETCH_TIMEOUT_MS
112
+ ): Promise<{ ok: boolean; status?: number; error?: string }> {
113
+ const controller = new AbortController();
114
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
115
+
116
+ try {
117
+ const response = await fetch(url, {
118
+ method: 'HEAD',
119
+ signal: controller.signal,
120
+ headers: {
121
+ 'User-Agent': 'UCP-Agent-Simulator/1.0',
122
+ },
123
+ });
124
+
125
+ clearTimeout(timeoutId);
126
+ // Accept various success codes - we just want to know endpoint exists
127
+ return { ok: response.status < 500, status: response.status };
128
+ } catch (error) {
129
+ clearTimeout(timeoutId);
130
+ const message = error instanceof Error ? error.message : 'Unknown error';
131
+ return { ok: false, error: message };
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Simulate discovery flow - how an AI agent discovers a UCP merchant
137
+ */
138
+ export async function simulateDiscoveryFlow(
139
+ domain: string,
140
+ timeoutMs: number = DEFAULT_FETCH_TIMEOUT_MS
141
+ ): Promise<DiscoveryFlowResult> {
142
+ const steps: SimulationStepResult[] = [];
143
+ let profileUrl: string | undefined;
144
+ let profile: UcpProfile | null = null;
145
+ const capabilities: string[] = [];
146
+ const services: string[] = [];
147
+ const transports: string[] = [];
148
+
149
+ // Step 1: Try to fetch /.well-known/ucp
150
+ const startTime = Date.now();
151
+ const urls = [
152
+ `https://${domain}/.well-known/ucp`,
153
+ `https://${domain}/.well-known/ucp.json`,
154
+ ];
155
+
156
+ let foundProfile = false;
157
+ for (const url of urls) {
158
+ const fetchStart = Date.now();
159
+ const result = await fetchWithTimeout(url, timeoutMs);
160
+ const fetchDuration = Date.now() - fetchStart;
161
+
162
+ if (result.ok && result.data && typeof result.data === 'object') {
163
+ const data = result.data as Record<string, unknown>;
164
+ if (data.ucp) {
165
+ profile = data as UcpProfile;
166
+ profileUrl = url;
167
+ foundProfile = true;
168
+ steps.push(createStep(
169
+ 'discover_profile',
170
+ 'passed',
171
+ `Found UCP profile at ${url}`,
172
+ `Response time: ${fetchDuration}ms`,
173
+ fetchDuration,
174
+ { url, responseTime: fetchDuration }
175
+ ));
176
+ break;
177
+ }
178
+ }
179
+ }
180
+
181
+ if (!foundProfile) {
182
+ steps.push(createStep(
183
+ 'discover_profile',
184
+ 'failed',
185
+ 'Could not find UCP profile',
186
+ `Tried: ${urls.join(', ')}`
187
+ ));
188
+ return { success: false, steps, capabilities, services, transports };
189
+ }
190
+
191
+ // Step 2: Parse UCP version
192
+ if (profile?.ucp?.version) {
193
+ steps.push(createStep(
194
+ 'parse_version',
195
+ 'passed',
196
+ `UCP version: ${profile.ucp.version}`,
197
+ undefined,
198
+ undefined,
199
+ { version: profile.ucp.version }
200
+ ));
201
+ } else {
202
+ steps.push(createStep(
203
+ 'parse_version',
204
+ 'failed',
205
+ 'Missing UCP version',
206
+ 'AI agent cannot determine protocol version'
207
+ ));
208
+ }
209
+
210
+ // Step 3: Enumerate services
211
+ const serviceEntries = Object.entries(profile?.ucp?.services || {});
212
+ if (serviceEntries.length > 0) {
213
+ for (const [serviceName, service] of serviceEntries) {
214
+ services.push(serviceName);
215
+
216
+ // Track transports
217
+ if ((service as UcpService).rest) transports.push('rest');
218
+ if ((service as UcpService).mcp) transports.push('mcp');
219
+ if ((service as UcpService).a2a) transports.push('a2a');
220
+ if ((service as UcpService).embedded) transports.push('embedded');
221
+ }
222
+
223
+ steps.push(createStep(
224
+ 'enumerate_services',
225
+ 'passed',
226
+ `Found ${serviceEntries.length} service(s): ${services.join(', ')}`,
227
+ `Available transports: ${[...new Set(transports)].join(', ')}`,
228
+ undefined,
229
+ { services, transports: [...new Set(transports)] }
230
+ ));
231
+ } else {
232
+ steps.push(createStep(
233
+ 'enumerate_services',
234
+ 'failed',
235
+ 'No services found',
236
+ 'AI agent has no entry point for commerce operations'
237
+ ));
238
+ }
239
+
240
+ // Step 4: Enumerate capabilities
241
+ const capList = profile?.ucp?.capabilities || [];
242
+ if (capList.length > 0) {
243
+ for (const cap of capList) {
244
+ const capName = getCapabilityName(cap);
245
+ if (capName) capabilities.push(capName);
246
+ }
247
+
248
+ // Check for required checkout capability
249
+ const hasCheckout = capabilities.some(c => c && c.includes('checkout'));
250
+ const hasOrder = capabilities.some(c => c && c.includes('order'));
251
+
252
+ steps.push(createStep(
253
+ 'enumerate_capabilities',
254
+ 'passed',
255
+ `Found ${capList.length} capability/ies: ${capabilities.join(', ')}`,
256
+ hasCheckout ? 'Checkout capability present - commerce ready' : 'No checkout capability - limited commerce support',
257
+ undefined,
258
+ { capabilities, hasCheckout, hasOrder }
259
+ ));
260
+ } else {
261
+ steps.push(createStep(
262
+ 'enumerate_capabilities',
263
+ 'warning',
264
+ 'No capabilities declared',
265
+ 'AI agent cannot determine supported operations'
266
+ ));
267
+ }
268
+
269
+ const totalDuration = Date.now() - startTime;
270
+
271
+ return {
272
+ success: foundProfile && serviceEntries.length > 0,
273
+ steps,
274
+ profileUrl,
275
+ capabilities,
276
+ services,
277
+ transports: [...new Set(transports)],
278
+ };
279
+ }
280
+
281
+ /**
282
+ * Inspect capabilities in detail
283
+ */
284
+ export async function inspectCapabilities(
285
+ profile: UcpProfile,
286
+ timeoutMs: number = DEFAULT_FETCH_TIMEOUT_MS
287
+ ): Promise<CapabilityInspectionResult[]> {
288
+ const results: CapabilityInspectionResult[] = [];
289
+ const capList = profile.ucp?.capabilities || [];
290
+
291
+ for (const cap of capList) {
292
+ const capName = getCapabilityName(cap);
293
+ const capObj = getCapabilityObject(cap);
294
+
295
+ let schemaAccessible = false;
296
+ let specAccessible = false;
297
+
298
+ // Check schema URL (only for object-format capabilities)
299
+ if (capObj?.schema) {
300
+ const schemaResult = await fetchWithTimeout(capObj.schema, timeoutMs);
301
+ schemaAccessible = schemaResult.ok;
302
+ }
303
+
304
+ // Check spec URL (only for object-format capabilities)
305
+ if (capObj?.spec) {
306
+ const specResult = await checkEndpointResponsive(capObj.spec, timeoutMs);
307
+ specAccessible = specResult.ok;
308
+ }
309
+
310
+ results.push({
311
+ name: capName,
312
+ version: capObj?.version || 'unknown',
313
+ schemaAccessible,
314
+ specAccessible,
315
+ isExtension: !!capObj?.extends,
316
+ parentCapability: capObj?.extends,
317
+ });
318
+ }
319
+
320
+ return results;
321
+ }
322
+
323
+ /**
324
+ * Inspect services and their transports
325
+ */
326
+ export async function inspectServices(
327
+ profile: UcpProfile,
328
+ timeoutMs: number = DEFAULT_FETCH_TIMEOUT_MS
329
+ ): Promise<ServiceInspectionResult[]> {
330
+ const results: ServiceInspectionResult[] = [];
331
+ const serviceEntries = Object.entries(profile.ucp?.services || {});
332
+
333
+ for (const [name, service] of serviceEntries) {
334
+ const svc = service as UcpService;
335
+ const result: ServiceInspectionResult = {
336
+ name,
337
+ version: svc.version,
338
+ transports: {},
339
+ };
340
+
341
+ // Check REST transport
342
+ if (svc.rest) {
343
+ const schemaCheck = svc.rest.schema
344
+ ? await fetchWithTimeout(svc.rest.schema, timeoutMs)
345
+ : { ok: false };
346
+ const endpointCheck = await checkEndpointResponsive(svc.rest.endpoint, timeoutMs);
347
+
348
+ result.transports.rest = {
349
+ endpoint: svc.rest.endpoint,
350
+ schemaAccessible: schemaCheck.ok,
351
+ endpointResponsive: endpointCheck.ok,
352
+ };
353
+ }
354
+
355
+ // Check MCP transport
356
+ if (svc.mcp) {
357
+ const schemaCheck = svc.mcp.schema
358
+ ? await fetchWithTimeout(svc.mcp.schema, timeoutMs)
359
+ : { ok: false };
360
+
361
+ result.transports.mcp = {
362
+ endpoint: svc.mcp.endpoint,
363
+ schemaAccessible: schemaCheck.ok,
364
+ };
365
+ }
366
+
367
+ // Check A2A transport
368
+ if (svc.a2a) {
369
+ const agentCardCheck = await fetchWithTimeout(svc.a2a.agentCard, timeoutMs);
370
+
371
+ result.transports.a2a = {
372
+ agentCard: svc.a2a.agentCard,
373
+ agentCardAccessible: agentCardCheck.ok,
374
+ };
375
+ }
376
+
377
+ results.push(result);
378
+ }
379
+
380
+ return results;
381
+ }
382
+
383
+ /**
384
+ * Simulate REST API interaction
385
+ */
386
+ export async function simulateRestApi(
387
+ profile: UcpProfile,
388
+ timeoutMs: number = DEFAULT_FETCH_TIMEOUT_MS
389
+ ): Promise<RestApiSimulationResult> {
390
+ const steps: SimulationStepResult[] = [];
391
+ const sampleOperations: OperationTestResult[] = [];
392
+ let schemaLoaded = false;
393
+ let endpointAccessible = false;
394
+
395
+ // Find REST service
396
+ const shoppingService = profile.ucp?.services?.['dev.ucp.shopping'] as UcpService | undefined;
397
+ if (!shoppingService?.rest) {
398
+ steps.push(createStep(
399
+ 'find_rest_service',
400
+ 'skipped',
401
+ 'No REST service configured',
402
+ 'Merchant may use MCP or A2A transport instead'
403
+ ));
404
+ return { success: false, steps, schemaLoaded, endpointAccessible, sampleOperations };
405
+ }
406
+
407
+ // Step 1: Load OpenAPI schema
408
+ if (shoppingService.rest.schema) {
409
+ const schemaStart = Date.now();
410
+ const schemaResult = await fetchWithTimeout(shoppingService.rest.schema, timeoutMs);
411
+ const schemaDuration = Date.now() - schemaStart;
412
+
413
+ if (schemaResult.ok && schemaResult.data) {
414
+ schemaLoaded = true;
415
+ const schema = schemaResult.data as Record<string, unknown>;
416
+
417
+ steps.push(createStep(
418
+ 'load_openapi_schema',
419
+ 'passed',
420
+ `Loaded OpenAPI schema from ${shoppingService.rest.schema}`,
421
+ `Response time: ${schemaDuration}ms`,
422
+ schemaDuration,
423
+ { schemaUrl: shoppingService.rest.schema }
424
+ ));
425
+
426
+ // Check for expected paths in schema
427
+ const paths = schema.paths as Record<string, unknown> | undefined;
428
+ if (paths) {
429
+ const pathCount = Object.keys(paths).length;
430
+ steps.push(createStep(
431
+ 'analyze_schema_paths',
432
+ pathCount > 0 ? 'passed' : 'warning',
433
+ `Schema defines ${pathCount} operation path(s)`,
434
+ pathCount > 0 ? `Paths: ${Object.keys(paths).slice(0, 5).join(', ')}${pathCount > 5 ? '...' : ''}` : undefined
435
+ ));
436
+ }
437
+ } else {
438
+ steps.push(createStep(
439
+ 'load_openapi_schema',
440
+ 'failed',
441
+ 'Could not load OpenAPI schema',
442
+ schemaResult.error || 'Unknown error'
443
+ ));
444
+ }
445
+ } else {
446
+ steps.push(createStep(
447
+ 'load_openapi_schema',
448
+ 'warning',
449
+ 'No schema URL provided',
450
+ 'AI agent cannot discover available operations'
451
+ ));
452
+ }
453
+
454
+ // Step 2: Check endpoint responsiveness
455
+ const endpointStart = Date.now();
456
+ const endpointResult = await checkEndpointResponsive(shoppingService.rest.endpoint, timeoutMs);
457
+ const endpointDuration = Date.now() - endpointStart;
458
+
459
+ if (endpointResult.ok) {
460
+ endpointAccessible = true;
461
+ steps.push(createStep(
462
+ 'check_endpoint',
463
+ 'passed',
464
+ `REST endpoint responsive: ${shoppingService.rest.endpoint}`,
465
+ `Status: ${endpointResult.status}, Response time: ${endpointDuration}ms`,
466
+ endpointDuration
467
+ ));
468
+ } else {
469
+ steps.push(createStep(
470
+ 'check_endpoint',
471
+ 'failed',
472
+ `REST endpoint not accessible: ${shoppingService.rest.endpoint}`,
473
+ endpointResult.error || `Status: ${endpointResult.status}`
474
+ ));
475
+ }
476
+
477
+ return {
478
+ success: schemaLoaded && endpointAccessible,
479
+ steps,
480
+ schemaLoaded,
481
+ endpointAccessible,
482
+ sampleOperations,
483
+ };
484
+ }
485
+
486
+ /**
487
+ * Simulate checkout flow capability
488
+ */
489
+ export async function simulateCheckoutFlow(
490
+ profile: UcpProfile,
491
+ timeoutMs: number = DEFAULT_FETCH_TIMEOUT_MS
492
+ ): Promise<CheckoutSimulationResult> {
493
+ const steps: SimulationStepResult[] = [];
494
+ let canCreateCheckout = false;
495
+ let checkoutSchemaValid = false;
496
+ let orderFlowSupported = false;
497
+ let fulfillmentSupported = false;
498
+
499
+ const capabilities = profile.ucp?.capabilities || [];
500
+
501
+ // Check for checkout capability (handle both string and object formats)
502
+ const checkoutCapRaw = capabilities.find(c => getCapabilityName(c).includes('checkout'));
503
+ const checkoutCapName = checkoutCapRaw ? getCapabilityName(checkoutCapRaw) : null;
504
+ const checkoutCapObj = checkoutCapRaw ? getCapabilityObject(checkoutCapRaw) : null;
505
+
506
+ if (checkoutCapName) {
507
+ canCreateCheckout = true;
508
+ steps.push(createStep(
509
+ 'find_checkout_capability',
510
+ 'passed',
511
+ `Found checkout capability: ${checkoutCapName}`,
512
+ checkoutCapObj?.version ? `Version: ${checkoutCapObj.version}` : 'String-format capability'
513
+ ));
514
+
515
+ // Validate checkout schema (only for object-format capabilities)
516
+ if (checkoutCapObj?.schema) {
517
+ const schemaResult = await fetchWithTimeout(checkoutCapObj.schema, timeoutMs);
518
+ if (schemaResult.ok && schemaResult.data) {
519
+ checkoutSchemaValid = true;
520
+ const schema = schemaResult.data as Record<string, unknown>;
521
+
522
+ // Check for required checkout properties
523
+ const properties = (schema.properties || schema.$defs) as Record<string, unknown> | undefined;
524
+ const hasCheckoutProps = properties && (
525
+ properties.checkout_id ||
526
+ properties.items ||
527
+ properties.CheckoutSession
528
+ );
529
+
530
+ steps.push(createStep(
531
+ 'validate_checkout_schema',
532
+ hasCheckoutProps ? 'passed' : 'warning',
533
+ `Checkout schema ${hasCheckoutProps ? 'has expected structure' : 'loaded but structure unclear'}`,
534
+ hasCheckoutProps ? 'AI agent can create checkout sessions' : 'Schema may need review'
535
+ ));
536
+ } else {
537
+ steps.push(createStep(
538
+ 'validate_checkout_schema',
539
+ 'failed',
540
+ 'Could not load checkout schema',
541
+ schemaResult.error || 'Unknown error'
542
+ ));
543
+ }
544
+ } else {
545
+ // String-format capability - no schema to validate, mark as valid for basic functionality
546
+ checkoutSchemaValid = true;
547
+ steps.push(createStep(
548
+ 'validate_checkout_schema',
549
+ 'info' as SimulationStepStatus,
550
+ 'No schema URL in capability (string format)',
551
+ 'Capability declared but schema validation skipped'
552
+ ));
553
+ }
554
+ } else {
555
+ steps.push(createStep(
556
+ 'find_checkout_capability',
557
+ 'failed',
558
+ 'No checkout capability found',
559
+ 'AI agent cannot create checkout sessions'
560
+ ));
561
+ }
562
+
563
+ // Check for order capability
564
+ const orderCapRaw = capabilities.find(c => getCapabilityName(c).includes('order'));
565
+ const orderCapName = orderCapRaw ? getCapabilityName(orderCapRaw) : null;
566
+ if (orderCapName) {
567
+ orderFlowSupported = true;
568
+ steps.push(createStep(
569
+ 'find_order_capability',
570
+ 'passed',
571
+ `Found order capability: ${orderCapName}`,
572
+ 'AI agent can track order status'
573
+ ));
574
+ } else {
575
+ steps.push(createStep(
576
+ 'find_order_capability',
577
+ 'warning',
578
+ 'No order capability found',
579
+ 'Order tracking may not be available'
580
+ ));
581
+ }
582
+
583
+ // Check for fulfillment capability
584
+ const fulfillmentCapRaw = capabilities.find(c => getCapabilityName(c).includes('fulfillment'));
585
+ const fulfillmentCapName = fulfillmentCapRaw ? getCapabilityName(fulfillmentCapRaw) : null;
586
+ if (fulfillmentCapName) {
587
+ fulfillmentSupported = true;
588
+ steps.push(createStep(
589
+ 'find_fulfillment_capability',
590
+ 'passed',
591
+ `Found fulfillment capability: ${fulfillmentCapName}`,
592
+ 'AI agent can track shipping and delivery'
593
+ ));
594
+ } else {
595
+ steps.push(createStep(
596
+ 'find_fulfillment_capability',
597
+ 'info' as SimulationStepStatus,
598
+ 'No fulfillment capability found',
599
+ 'Fulfillment tracking not available via UCP'
600
+ ));
601
+ }
602
+
603
+ return {
604
+ success: canCreateCheckout && checkoutSchemaValid,
605
+ steps,
606
+ canCreateCheckout,
607
+ checkoutSchemaValid,
608
+ orderFlowSupported,
609
+ fulfillmentSupported,
610
+ };
611
+ }
612
+
613
+ /**
614
+ * Check payment readiness
615
+ */
616
+ export async function simulatePaymentReadiness(
617
+ profile: UcpProfile,
618
+ timeoutMs: number = DEFAULT_FETCH_TIMEOUT_MS
619
+ ): Promise<PaymentReadinessResult> {
620
+ const steps: SimulationStepResult[] = [];
621
+ let handlersFound = 0;
622
+ let webhookVerifiable = false;
623
+ let signingKeyValid = false;
624
+
625
+ // Check payment handlers
626
+ const handlers = profile.payment?.handlers || [];
627
+ handlersFound = handlers.length;
628
+
629
+ if (handlersFound > 0) {
630
+ const handlerNames = handlers.map(h => h.name).join(', ');
631
+ steps.push(createStep(
632
+ 'find_payment_handlers',
633
+ 'passed',
634
+ `Found ${handlersFound} payment handler(s): ${handlerNames}`,
635
+ 'AI agent can initiate payments'
636
+ ));
637
+
638
+ // Check handler configs
639
+ for (const handler of handlers) {
640
+ if (handler.config_schema) {
641
+ const schemaResult = await checkEndpointResponsive(handler.config_schema, timeoutMs);
642
+ steps.push(createStep(
643
+ `check_handler_${handler.id}`,
644
+ schemaResult.ok ? 'passed' : 'warning',
645
+ `Payment handler "${handler.name}" config schema ${schemaResult.ok ? 'accessible' : 'not accessible'}`,
646
+ handler.config_schema
647
+ ));
648
+ }
649
+ }
650
+ } else {
651
+ steps.push(createStep(
652
+ 'find_payment_handlers',
653
+ 'warning',
654
+ 'No payment handlers configured',
655
+ 'Payment processing may use external flow'
656
+ ));
657
+ }
658
+
659
+ // Check signing keys for webhook verification
660
+ const signingKeys = profile.signing_keys;
661
+ if (signingKeys && Array.isArray(signingKeys) && signingKeys.length > 0) {
662
+ webhookVerifiable = true;
663
+
664
+ // Validate key structure
665
+ const validKeys = signingKeys.filter(key =>
666
+ key.kty && key.kid && (
667
+ (key.kty === 'EC' && key.crv && key.x && key.y) ||
668
+ (key.kty === 'RSA' && key.n && key.e)
669
+ )
670
+ );
671
+
672
+ signingKeyValid = validKeys.length > 0;
673
+
674
+ steps.push(createStep(
675
+ 'check_signing_keys',
676
+ signingKeyValid ? 'passed' : 'warning',
677
+ `Found ${signingKeys.length} signing key(s), ${validKeys.length} valid`,
678
+ signingKeyValid
679
+ ? 'AI agent can verify webhook signatures'
680
+ : 'Signing keys present but may be incomplete'
681
+ ));
682
+ } else {
683
+ steps.push(createStep(
684
+ 'check_signing_keys',
685
+ 'warning',
686
+ 'No signing keys found',
687
+ 'Webhook verification not available'
688
+ ));
689
+ }
690
+
691
+ return {
692
+ success: handlersFound > 0 || webhookVerifiable,
693
+ steps,
694
+ handlersFound,
695
+ webhookVerifiable,
696
+ signingKeyValid,
697
+ };
698
+ }
699
+
700
+ /**
701
+ * Generate recommendations based on simulation results
702
+ */
703
+ function generateRecommendations(
704
+ discovery: DiscoveryFlowResult,
705
+ capabilities: CapabilityInspectionResult[],
706
+ services: ServiceInspectionResult[],
707
+ restApi?: RestApiSimulationResult,
708
+ checkout?: CheckoutSimulationResult,
709
+ payment?: PaymentReadinessResult
710
+ ): string[] {
711
+ const recommendations: string[] = [];
712
+
713
+ // Discovery issues
714
+ if (!discovery.success) {
715
+ recommendations.push('Ensure UCP profile is accessible at /.well-known/ucp');
716
+ }
717
+
718
+ // Service issues
719
+ if (discovery.services.length === 0) {
720
+ recommendations.push('Add at least one service (e.g., dev.ucp.shopping) to enable commerce');
721
+ }
722
+
723
+ // Transport issues
724
+ if (discovery.transports.length === 0) {
725
+ recommendations.push('Configure at least one transport binding (REST, MCP, or A2A)');
726
+ }
727
+
728
+ // Capability issues
729
+ const inaccessibleSchemas = capabilities.filter(c => !c.schemaAccessible);
730
+ if (inaccessibleSchemas.length > 0) {
731
+ recommendations.push(`Fix inaccessible capability schemas: ${inaccessibleSchemas.map(c => c.name).join(', ')}`);
732
+ }
733
+
734
+ // REST API issues
735
+ if (restApi && !restApi.schemaLoaded) {
736
+ recommendations.push('Provide accessible OpenAPI schema for REST service');
737
+ }
738
+ if (restApi && !restApi.endpointAccessible) {
739
+ recommendations.push('Ensure REST endpoint is publicly accessible');
740
+ }
741
+
742
+ // Checkout issues
743
+ if (checkout && !checkout.canCreateCheckout) {
744
+ recommendations.push('Add checkout capability (dev.ucp.shopping.checkout) to enable purchases');
745
+ }
746
+ if (checkout && checkout.canCreateCheckout && !checkout.checkoutSchemaValid) {
747
+ recommendations.push('Ensure checkout schema is accessible and valid');
748
+ }
749
+
750
+ // Payment issues
751
+ if (payment && payment.handlersFound === 0) {
752
+ recommendations.push('Configure payment handlers in the profile');
753
+ }
754
+ if (payment && !payment.signingKeyValid) {
755
+ recommendations.push('Add valid signing keys (EC or RSA JWK) for webhook verification');
756
+ }
757
+
758
+ // Add positive note if everything looks good
759
+ if (recommendations.length === 0) {
760
+ recommendations.push('Profile is well-configured for AI agent commerce!');
761
+ }
762
+
763
+ return recommendations;
764
+ }
765
+
766
+ /**
767
+ * Calculate overall AI readiness score
768
+ */
769
+ function calculateScore(
770
+ discovery: DiscoveryFlowResult,
771
+ capabilities: CapabilityInspectionResult[],
772
+ services: ServiceInspectionResult[],
773
+ restApi?: RestApiSimulationResult,
774
+ checkout?: CheckoutSimulationResult,
775
+ payment?: PaymentReadinessResult
776
+ ): number {
777
+ let score = 0;
778
+ const maxScore = 100;
779
+
780
+ // Discovery (25 points)
781
+ if (discovery.success) score += 15;
782
+ if (discovery.services.length > 0) score += 5;
783
+ if (discovery.capabilities.length > 0) score += 5;
784
+
785
+ // Capabilities (25 points)
786
+ const accessibleCapabilities = capabilities.filter(c => c.schemaAccessible);
787
+ if (capabilities.length > 0) {
788
+ score += Math.round((accessibleCapabilities.length / capabilities.length) * 15);
789
+ }
790
+ // Bonus for having checkout
791
+ if (capabilities.some(c => c.name.includes('checkout'))) score += 10;
792
+
793
+ // Services & Transport (25 points)
794
+ for (const service of services) {
795
+ if (service.transports.rest?.endpointResponsive) score += 10;
796
+ if (service.transports.rest?.schemaAccessible) score += 5;
797
+ if (service.transports.mcp) score += 5;
798
+ if (service.transports.a2a?.agentCardAccessible) score += 5;
799
+ }
800
+ // Cap at 25
801
+ score = Math.min(score, 75);
802
+
803
+ // Checkout (15 points)
804
+ if (checkout?.canCreateCheckout) score += 8;
805
+ if (checkout?.checkoutSchemaValid) score += 4;
806
+ if (checkout?.orderFlowSupported) score += 3;
807
+
808
+ // Payment (10 points)
809
+ if (payment?.handlersFound || 0 > 0) score += 5;
810
+ if (payment?.signingKeyValid) score += 5;
811
+
812
+ return Math.min(score, maxScore);
813
+ }
814
+
815
+ /**
816
+ * Get grade from score (aligned with all other scoring models)
817
+ */
818
+ function getGrade(score: number): 'A' | 'B' | 'C' | 'D' | 'F' {
819
+ if (score >= 90) return 'A';
820
+ if (score >= 80) return 'B';
821
+ if (score >= 70) return 'C';
822
+ if (score >= 60) return 'D';
823
+ return 'F';
824
+ }
825
+
826
+ /**
827
+ * Main simulation entry point
828
+ * Simulates a complete AI agent interaction with a UCP-enabled merchant
829
+ */
830
+ export async function simulateAgentInteraction(
831
+ domain: string,
832
+ options: SimulationOptions = {}
833
+ ): Promise<AgentSimulationResult> {
834
+ const startTime = Date.now();
835
+ const timeoutMs = options.timeoutMs || DEFAULT_TIMEOUT_MS;
836
+ const fetchTimeout = Math.min(timeoutMs / 3, DEFAULT_FETCH_TIMEOUT_MS);
837
+
838
+ // Step 1: Discovery flow
839
+ const discovery = await simulateDiscoveryFlow(domain, fetchTimeout);
840
+
841
+ // Early exit if discovery failed
842
+ if (!discovery.success || !discovery.profileUrl) {
843
+ const durationMs = Date.now() - startTime;
844
+ return {
845
+ ok: false,
846
+ domain,
847
+ simulatedAt: new Date().toISOString(),
848
+ durationMs,
849
+ overallScore: 0,
850
+ grade: 'F' as const,
851
+ discovery,
852
+ capabilities: [],
853
+ services: [],
854
+ summary: {
855
+ totalSteps: discovery.steps.length,
856
+ passedSteps: discovery.steps.filter(s => s.status === 'passed').length,
857
+ failedSteps: discovery.steps.filter(s => s.status === 'failed').length,
858
+ warningSteps: discovery.steps.filter(s => s.status === 'warning').length,
859
+ skippedSteps: discovery.steps.filter(s => s.status === 'skipped').length,
860
+ },
861
+ recommendations: ['Ensure UCP profile is accessible at /.well-known/ucp or /.well-known/ucp.json'],
862
+ };
863
+ }
864
+
865
+ // Fetch full profile for detailed inspection
866
+ const profileResult = await fetchWithTimeout(discovery.profileUrl, fetchTimeout);
867
+ const profile = profileResult.data as UcpProfile;
868
+
869
+ // Step 2: Inspect capabilities
870
+ const capabilities = await inspectCapabilities(profile, fetchTimeout);
871
+
872
+ // Step 3: Inspect services
873
+ const services = await inspectServices(profile, fetchTimeout);
874
+
875
+ // Step 4: REST API simulation (if applicable)
876
+ let restApi: RestApiSimulationResult | undefined;
877
+ if (!options.skipRestApiTest) {
878
+ restApi = await simulateRestApi(profile, fetchTimeout);
879
+ }
880
+
881
+ // Step 5: Checkout flow simulation
882
+ let checkout: CheckoutSimulationResult | undefined;
883
+ if (options.testCheckoutFlow !== false) {
884
+ checkout = await simulateCheckoutFlow(profile, fetchTimeout);
885
+ }
886
+
887
+ // Step 6: Payment readiness check
888
+ const payment = await simulatePaymentReadiness(profile, fetchTimeout);
889
+
890
+ // Collect all steps
891
+ const allSteps = [
892
+ ...discovery.steps,
893
+ ...(restApi?.steps || []),
894
+ ...(checkout?.steps || []),
895
+ ...payment.steps,
896
+ ];
897
+
898
+ const durationMs = Date.now() - startTime;
899
+
900
+ // Generate recommendations
901
+ const recommendations = generateRecommendations(
902
+ discovery, capabilities, services, restApi, checkout, payment
903
+ );
904
+
905
+ // Calculate score
906
+ const overallScore = calculateScore(
907
+ discovery, capabilities, services, restApi, checkout, payment
908
+ );
909
+
910
+ return {
911
+ ok: discovery.success && (checkout?.success ?? true),
912
+ domain,
913
+ simulatedAt: new Date().toISOString(),
914
+ durationMs,
915
+ overallScore,
916
+ grade: getGrade(overallScore),
917
+ discovery,
918
+ capabilities,
919
+ services,
920
+ restApi,
921
+ checkout,
922
+ payment,
923
+ summary: {
924
+ totalSteps: allSteps.length,
925
+ passedSteps: allSteps.filter(s => s.status === 'passed').length,
926
+ failedSteps: allSteps.filter(s => s.status === 'failed').length,
927
+ warningSteps: allSteps.filter(s => s.status === 'warning').length,
928
+ skippedSteps: allSteps.filter(s => s.status === 'skipped').length,
929
+ },
930
+ recommendations,
931
+ };
932
+ }
933
+
934
+ // Export helper functions for testing
935
+ export {
936
+ fetchWithTimeout,
937
+ checkEndpointResponsive,
938
+ generateRecommendations,
939
+ calculateScore,
940
+ getGrade,
941
+ };