@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,297 @@
1
+ /**
2
+ * UCP Rules Validator
3
+ * Validates UCP-specific business rules (no network calls)
4
+ */
5
+
6
+ import type { UcpProfile, UcpCapability } from '../types/ucp-profile.js';
7
+ import type { ValidationIssue } from '../types/validation.js';
8
+ import { ValidationErrorCodes } from '../types/validation.js';
9
+ import { CAPABILITY_NAMESPACES, KNOWN_CAPABILITIES } from '../types/ucp-profile.js';
10
+
11
+ /**
12
+ * Validate UCP business rules
13
+ */
14
+ export function validateRules(profile: UcpProfile): ValidationIssue[] {
15
+ const issues: ValidationIssue[] = [];
16
+
17
+ // Validate namespace/origin binding for capabilities
18
+ issues.push(...validateNamespaceOrigins(profile));
19
+
20
+ // Validate extension chains (no orphaned extends)
21
+ issues.push(...validateExtensions(profile));
22
+
23
+ // Validate endpoint rules
24
+ issues.push(...validateEndpoints(profile));
25
+
26
+ // Validate signing keys if Order capability is present
27
+ issues.push(...validateSigningKeysRequirement(profile));
28
+
29
+ return issues;
30
+ }
31
+
32
+ /**
33
+ * Validate namespace and URL origin binding
34
+ * - dev.ucp.* capabilities must have spec/schema from ucp.dev
35
+ * - com.vendor.* capabilities must have spec/schema from vendor's domain
36
+ */
37
+ function validateNamespaceOrigins(profile: UcpProfile): ValidationIssue[] {
38
+ const issues: ValidationIssue[] = [];
39
+ const capabilities = profile.ucp.capabilities || [];
40
+
41
+ for (let i = 0; i < capabilities.length; i++) {
42
+ const cap = capabilities[i];
43
+ const path = `$.ucp.capabilities[${i}]`;
44
+
45
+ // Check dev.ucp.* namespace
46
+ if (cap.name.startsWith(CAPABILITY_NAMESPACES.UCP_OFFICIAL)) {
47
+ // Spec must be from ucp.dev
48
+ if (cap.spec && !isUcpDevOrigin(cap.spec)) {
49
+ issues.push({
50
+ severity: 'error',
51
+ code: ValidationErrorCodes.NS_ORIGIN_MISMATCH,
52
+ path: `${path}.spec`,
53
+ message: `dev.ucp.* capability spec must be hosted on ucp.dev`,
54
+ hint: `Use https://ucp.dev/specification/... instead of "${cap.spec}"`,
55
+ });
56
+ }
57
+
58
+ // Schema must be from ucp.dev
59
+ if (cap.schema && !isUcpDevOrigin(cap.schema)) {
60
+ issues.push({
61
+ severity: 'error',
62
+ code: ValidationErrorCodes.NS_ORIGIN_MISMATCH,
63
+ path: `${path}.schema`,
64
+ message: `dev.ucp.* capability schema must be hosted on ucp.dev`,
65
+ hint: `Use https://ucp.dev/schemas/... instead of "${cap.schema}"`,
66
+ });
67
+ }
68
+ }
69
+
70
+ // Check vendor namespace (com.vendor.*)
71
+ if (cap.name.startsWith(CAPABILITY_NAMESPACES.VENDOR_PREFIX)) {
72
+ const vendorDomain = extractVendorDomain(cap.name);
73
+ if (vendorDomain) {
74
+ // Spec origin should match vendor domain
75
+ if (cap.spec && !isOriginFromDomain(cap.spec, vendorDomain)) {
76
+ issues.push({
77
+ severity: 'warn',
78
+ code: ValidationErrorCodes.NS_ORIGIN_MISMATCH,
79
+ path: `${path}.spec`,
80
+ message: `Vendor capability spec should be hosted on vendor's domain (${vendorDomain})`,
81
+ hint: `Consider hosting spec at https://${vendorDomain}/...`,
82
+ });
83
+ }
84
+
85
+ // Schema origin should match vendor domain
86
+ if (cap.schema && !isOriginFromDomain(cap.schema, vendorDomain)) {
87
+ issues.push({
88
+ severity: 'warn',
89
+ code: ValidationErrorCodes.NS_ORIGIN_MISMATCH,
90
+ path: `${path}.schema`,
91
+ message: `Vendor capability schema should be hosted on vendor's domain (${vendorDomain})`,
92
+ hint: `Consider hosting schema at https://${vendorDomain}/...`,
93
+ });
94
+ }
95
+ }
96
+ }
97
+ }
98
+
99
+ return issues;
100
+ }
101
+
102
+ /**
103
+ * Validate extension chains - ensure parent capabilities exist
104
+ */
105
+ function validateExtensions(profile: UcpProfile): ValidationIssue[] {
106
+ const issues: ValidationIssue[] = [];
107
+ const capabilities = profile.ucp.capabilities || [];
108
+
109
+ // Build set of capability names
110
+ const capabilityNames = new Set(capabilities.map(c => c.name));
111
+
112
+ for (let i = 0; i < capabilities.length; i++) {
113
+ const cap = capabilities[i];
114
+
115
+ if (cap.extends) {
116
+ // Check if parent capability exists in this profile
117
+ if (!capabilityNames.has(cap.extends)) {
118
+ issues.push({
119
+ severity: 'error',
120
+ code: ValidationErrorCodes.ORPHANED_EXTENSION,
121
+ path: `$.ucp.capabilities[${i}].extends`,
122
+ message: `Extension "${cap.name}" references non-existent parent capability "${cap.extends}"`,
123
+ hint: `Add "${cap.extends}" to capabilities or remove the extends field`,
124
+ });
125
+ }
126
+ }
127
+ }
128
+
129
+ return issues;
130
+ }
131
+
132
+ /**
133
+ * Validate endpoint rules (https, no trailing slash)
134
+ */
135
+ function validateEndpoints(profile: UcpProfile): ValidationIssue[] {
136
+ const issues: ValidationIssue[] = [];
137
+ const services = profile.ucp.services || {};
138
+
139
+ for (const [serviceName, service] of Object.entries(services)) {
140
+ const basePath = `$.ucp.services["${serviceName}"]`;
141
+
142
+ // Validate REST endpoint
143
+ if (service.rest?.endpoint) {
144
+ issues.push(...validateEndpoint(service.rest.endpoint, `${basePath}.rest.endpoint`));
145
+ }
146
+
147
+ // Validate MCP endpoint
148
+ if (service.mcp?.endpoint) {
149
+ issues.push(...validateEndpoint(service.mcp.endpoint, `${basePath}.mcp.endpoint`));
150
+ }
151
+
152
+ // Validate A2A agent card URL
153
+ if (service.a2a?.agentCard) {
154
+ issues.push(...validateEndpoint(service.a2a.agentCard, `${basePath}.a2a.agentCard`));
155
+ }
156
+ }
157
+
158
+ return issues;
159
+ }
160
+
161
+ /**
162
+ * Validate a single endpoint URL
163
+ */
164
+ function validateEndpoint(endpoint: string, path: string): ValidationIssue[] {
165
+ const issues: ValidationIssue[] = [];
166
+
167
+ // Must be HTTPS
168
+ if (!endpoint.startsWith('https://')) {
169
+ issues.push({
170
+ severity: 'error',
171
+ code: ValidationErrorCodes.ENDPOINT_NOT_HTTPS,
172
+ path,
173
+ message: `Endpoint must use HTTPS`,
174
+ hint: `Change "${endpoint}" to use https://`,
175
+ });
176
+ }
177
+
178
+ // Should not have trailing slash
179
+ if (endpoint.endsWith('/')) {
180
+ issues.push({
181
+ severity: 'warn',
182
+ code: ValidationErrorCodes.ENDPOINT_TRAILING_SLASH,
183
+ path,
184
+ message: `Endpoint should not have a trailing slash`,
185
+ hint: `Remove trailing slash from "${endpoint}"`,
186
+ });
187
+ }
188
+
189
+ // Check for private IP ranges (basic check)
190
+ if (isPrivateIpEndpoint(endpoint)) {
191
+ issues.push({
192
+ severity: 'warn',
193
+ code: ValidationErrorCodes.PRIVATE_IP_ENDPOINT,
194
+ path,
195
+ message: `Endpoint appears to use a private IP address`,
196
+ hint: `Use a public domain name for production profiles`,
197
+ });
198
+ }
199
+
200
+ return issues;
201
+ }
202
+
203
+ /**
204
+ * Validate signing keys requirement for Order capability
205
+ */
206
+ function validateSigningKeysRequirement(profile: UcpProfile): ValidationIssue[] {
207
+ const issues: ValidationIssue[] = [];
208
+ const capabilities = profile.ucp.capabilities || [];
209
+
210
+ // Check if Order capability is present
211
+ const hasOrderCapability = capabilities.some(
212
+ c => c.name === KNOWN_CAPABILITIES.ORDER
213
+ );
214
+
215
+ if (hasOrderCapability) {
216
+ // Signing keys should be present for webhook signing
217
+ if (!profile.signing_keys || profile.signing_keys.length === 0) {
218
+ issues.push({
219
+ severity: 'error',
220
+ code: ValidationErrorCodes.MISSING_SIGNING_KEYS,
221
+ path: '$.signing_keys',
222
+ message: `Order capability requires signing_keys for webhook verification`,
223
+ hint: `Add signing_keys array with at least one JWK public key`,
224
+ });
225
+ }
226
+ }
227
+
228
+ return issues;
229
+ }
230
+
231
+ /**
232
+ * Check if URL is from ucp.dev origin
233
+ */
234
+ function isUcpDevOrigin(url: string): boolean {
235
+ try {
236
+ const parsed = new URL(url);
237
+ return parsed.hostname === 'ucp.dev' || parsed.hostname.endsWith('.ucp.dev');
238
+ } catch {
239
+ return false;
240
+ }
241
+ }
242
+
243
+ /**
244
+ * Extract vendor domain from capability name
245
+ * e.g., "com.example.feature" -> "example.com"
246
+ */
247
+ function extractVendorDomain(name: string): string | null {
248
+ if (!name.startsWith('com.')) {
249
+ return null;
250
+ }
251
+
252
+ const parts = name.split('.');
253
+ if (parts.length < 3) {
254
+ return null;
255
+ }
256
+
257
+ // "com.example.feature" -> "example.com"
258
+ return `${parts[1]}.com`;
259
+ }
260
+
261
+ /**
262
+ * Check if URL origin matches expected domain
263
+ */
264
+ function isOriginFromDomain(url: string, domain: string): boolean {
265
+ try {
266
+ const parsed = new URL(url);
267
+ return parsed.hostname === domain || parsed.hostname.endsWith(`.${domain}`);
268
+ } catch {
269
+ return false;
270
+ }
271
+ }
272
+
273
+ /**
274
+ * Check if endpoint uses private IP address
275
+ */
276
+ function isPrivateIpEndpoint(endpoint: string): boolean {
277
+ try {
278
+ const parsed = new URL(endpoint);
279
+ const hostname = parsed.hostname;
280
+
281
+ // Check for localhost
282
+ if (hostname === 'localhost' || hostname === '127.0.0.1') {
283
+ return true;
284
+ }
285
+
286
+ // Check for private IP ranges (simplified)
287
+ if (hostname.startsWith('10.') ||
288
+ hostname.startsWith('192.168.') ||
289
+ hostname.match(/^172\.(1[6-9]|2[0-9]|3[0-1])\./)) {
290
+ return true;
291
+ }
292
+
293
+ return false;
294
+ } catch {
295
+ return false;
296
+ }
297
+ }
@@ -0,0 +1,330 @@
1
+ /**
2
+ * SDK-based Validator
3
+ * Validates UCP profiles using the official @ucp-js/sdk Zod schemas
4
+ *
5
+ * This provides spec-compliant validation using the official UCP SDK,
6
+ * ensuring alignment with the latest UCP specification.
7
+ */
8
+
9
+ import {
10
+ UcpDiscoveryProfileSchema,
11
+ UcpClassSchema,
12
+ UcpServiceSchema,
13
+ CapabilityDiscoverySchema,
14
+ SigningKeySchema,
15
+ type UcpDiscoveryProfile,
16
+ type UcpClass,
17
+ type UcpService,
18
+ type CapabilityDiscovery,
19
+ type SigningKey,
20
+ } from '@ucp-js/sdk';
21
+ import { z, ZodError, ZodIssue } from 'zod';
22
+ import type { ValidationIssue } from '../types/validation.js';
23
+ import { ValidationErrorCodes } from '../types/validation.js';
24
+
25
+ /**
26
+ * SDK validation result
27
+ */
28
+ export interface SdkValidationResult {
29
+ valid: boolean;
30
+ issues: ValidationIssue[];
31
+ sdkVersion: string;
32
+ parsedProfile?: UcpDiscoveryProfile;
33
+ }
34
+
35
+ /**
36
+ * Get the current SDK version
37
+ */
38
+ export function getSdkVersion(): string {
39
+ // Note: In production, this would be read from package.json
40
+ return '0.1.0';
41
+ }
42
+
43
+ /**
44
+ * Map Zod error path to JSON path string
45
+ */
46
+ function zodPathToJsonPath(path: (string | number)[]): string {
47
+ if (path.length === 0) return '$';
48
+
49
+ return '$.' + path.map((segment) => {
50
+ if (typeof segment === 'number') {
51
+ return `[${segment}]`;
52
+ }
53
+ // Handle keys with special characters
54
+ if (/[.\[\]"]/.test(segment)) {
55
+ return `["${segment}"]`;
56
+ }
57
+ return segment;
58
+ }).join('.').replace(/\.\[/g, '[');
59
+ }
60
+
61
+ /**
62
+ * Convert Zod issue to ValidationIssue
63
+ */
64
+ function zodIssueToValidationIssue(issue: ZodIssue): ValidationIssue {
65
+ const path = zodPathToJsonPath(issue.path);
66
+
67
+ // Map Zod error codes to our validation codes
68
+ let code: string = ValidationErrorCodes.INVALID_SERVICE_STRUCTURE;
69
+ let hint: string | undefined;
70
+
71
+ switch (issue.code) {
72
+ case 'invalid_type':
73
+ if (issue.path.includes('version')) {
74
+ code = ValidationErrorCodes.INVALID_VERSION_FORMAT;
75
+ hint = 'Use YYYY-MM-DD format (e.g., "2026-01-11")';
76
+ } else if (issue.path.includes('services')) {
77
+ code = ValidationErrorCodes.INVALID_SERVICE_STRUCTURE;
78
+ } else if (issue.path.includes('capabilities')) {
79
+ code = ValidationErrorCodes.INVALID_CAPABILITY_STRUCTURE;
80
+ } else if (issue.path.includes('signing_keys')) {
81
+ code = ValidationErrorCodes.INVALID_SIGNING_KEY;
82
+ }
83
+ break;
84
+ case 'invalid_string':
85
+ case 'invalid_enum_value':
86
+ if (issue.path.includes('version')) {
87
+ code = ValidationErrorCodes.INVALID_VERSION_FORMAT;
88
+ }
89
+ break;
90
+ case 'unrecognized_keys':
91
+ // SDK allows extra keys, just warn
92
+ return {
93
+ severity: 'warn',
94
+ code: code as any,
95
+ path,
96
+ message: `Unrecognized field(s): ${(issue as any).keys?.join(', ')}`,
97
+ hint: 'Extra fields are allowed but may not be used by UCP clients',
98
+ };
99
+ default:
100
+ break;
101
+ }
102
+
103
+ return {
104
+ severity: 'error',
105
+ code: code as any,
106
+ path,
107
+ message: issue.message,
108
+ hint,
109
+ };
110
+ }
111
+
112
+ /**
113
+ * Validate a UCP profile using the official SDK schema
114
+ *
115
+ * This uses the UcpDiscoveryProfileSchema from @ucp-js/sdk to validate
116
+ * the entire profile structure against the official UCP specification.
117
+ */
118
+ export function validateWithSdk(profile: unknown): SdkValidationResult {
119
+ const issues: ValidationIssue[] = [];
120
+
121
+ try {
122
+ // Parse with the official SDK schema (strict mode)
123
+ const parsed = UcpDiscoveryProfileSchema.parse(profile);
124
+
125
+ return {
126
+ valid: true,
127
+ issues: [],
128
+ sdkVersion: getSdkVersion(),
129
+ parsedProfile: parsed,
130
+ };
131
+ } catch (error) {
132
+ if (error instanceof ZodError) {
133
+ // Convert Zod errors to our validation issues
134
+ for (const issue of error.issues) {
135
+ issues.push(zodIssueToValidationIssue(issue));
136
+ }
137
+ } else {
138
+ // Unexpected error
139
+ issues.push({
140
+ severity: 'error',
141
+ code: ValidationErrorCodes.MISSING_UCP_OBJECT,
142
+ path: '$',
143
+ message: error instanceof Error ? error.message : 'Unknown validation error',
144
+ });
145
+ }
146
+
147
+ return {
148
+ valid: false,
149
+ issues,
150
+ sdkVersion: getSdkVersion(),
151
+ };
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Safe parse - doesn't throw, returns result with errors
157
+ */
158
+ export function safeValidateWithSdk(profile: unknown): SdkValidationResult {
159
+ const result = UcpDiscoveryProfileSchema.safeParse(profile);
160
+
161
+ if (result.success) {
162
+ return {
163
+ valid: true,
164
+ issues: [],
165
+ sdkVersion: getSdkVersion(),
166
+ parsedProfile: result.data,
167
+ };
168
+ }
169
+
170
+ const issues = result.error.issues.map(zodIssueToValidationIssue);
171
+
172
+ return {
173
+ valid: false,
174
+ issues,
175
+ sdkVersion: getSdkVersion(),
176
+ };
177
+ }
178
+
179
+ /**
180
+ * Validate only the UCP object (version, services, capabilities)
181
+ */
182
+ export function validateUcpObject(ucp: unknown): SdkValidationResult {
183
+ const issues: ValidationIssue[] = [];
184
+
185
+ const result = UcpClassSchema.safeParse(ucp);
186
+
187
+ if (result.success) {
188
+ return {
189
+ valid: true,
190
+ issues: [],
191
+ sdkVersion: getSdkVersion(),
192
+ };
193
+ }
194
+
195
+ for (const issue of result.error.issues) {
196
+ const validationIssue = zodIssueToValidationIssue(issue);
197
+ // Prefix path with $.ucp
198
+ validationIssue.path = validationIssue.path === '$'
199
+ ? '$.ucp'
200
+ : validationIssue.path.replace('$', '$.ucp');
201
+ issues.push(validationIssue);
202
+ }
203
+
204
+ return {
205
+ valid: false,
206
+ issues,
207
+ sdkVersion: getSdkVersion(),
208
+ };
209
+ }
210
+
211
+ /**
212
+ * Validate a single service definition
213
+ */
214
+ export function validateServiceWithSdk(
215
+ serviceName: string,
216
+ service: unknown
217
+ ): SdkValidationResult {
218
+ const result = UcpServiceSchema.safeParse(service);
219
+
220
+ if (result.success) {
221
+ return {
222
+ valid: true,
223
+ issues: [],
224
+ sdkVersion: getSdkVersion(),
225
+ };
226
+ }
227
+
228
+ const issues = result.error.issues.map(issue => {
229
+ const validationIssue = zodIssueToValidationIssue(issue);
230
+ validationIssue.path = validationIssue.path === '$'
231
+ ? `$.ucp.services["${serviceName}"]`
232
+ : validationIssue.path.replace('$', `$.ucp.services["${serviceName}"]`);
233
+ return validationIssue;
234
+ });
235
+
236
+ return {
237
+ valid: false,
238
+ issues,
239
+ sdkVersion: getSdkVersion(),
240
+ };
241
+ }
242
+
243
+ /**
244
+ * Validate a single capability definition
245
+ */
246
+ export function validateCapabilityWithSdk(
247
+ index: number,
248
+ capability: unknown
249
+ ): SdkValidationResult {
250
+ const result = CapabilityDiscoverySchema.safeParse(capability);
251
+
252
+ if (result.success) {
253
+ return {
254
+ valid: true,
255
+ issues: [],
256
+ sdkVersion: getSdkVersion(),
257
+ };
258
+ }
259
+
260
+ const issues = result.error.issues.map(issue => {
261
+ const validationIssue = zodIssueToValidationIssue(issue);
262
+ validationIssue.path = validationIssue.path === '$'
263
+ ? `$.ucp.capabilities[${index}]`
264
+ : validationIssue.path.replace('$', `$.ucp.capabilities[${index}]`);
265
+ return validationIssue;
266
+ });
267
+
268
+ return {
269
+ valid: false,
270
+ issues,
271
+ sdkVersion: getSdkVersion(),
272
+ };
273
+ }
274
+
275
+ /**
276
+ * Validate signing keys array
277
+ */
278
+ export function validateSigningKeysWithSdk(
279
+ signingKeys: unknown
280
+ ): SdkValidationResult {
281
+ // SDK expects signing_keys as an array of SigningKey objects
282
+ const SigningKeysArraySchema = z.array(SigningKeySchema);
283
+ const result = SigningKeysArraySchema.safeParse(signingKeys);
284
+
285
+ if (result.success) {
286
+ return {
287
+ valid: true,
288
+ issues: [],
289
+ sdkVersion: getSdkVersion(),
290
+ };
291
+ }
292
+
293
+ const issues = result.error.issues.map(issue => {
294
+ const validationIssue = zodIssueToValidationIssue(issue);
295
+ validationIssue.path = validationIssue.path === '$'
296
+ ? '$.signing_keys'
297
+ : validationIssue.path.replace('$', '$.signing_keys');
298
+ return validationIssue;
299
+ });
300
+
301
+ return {
302
+ valid: false,
303
+ issues,
304
+ sdkVersion: getSdkVersion(),
305
+ };
306
+ }
307
+
308
+ /**
309
+ * Check if a profile passes SDK validation (quick boolean check)
310
+ */
311
+ export function isSdkCompliant(profile: unknown): boolean {
312
+ const result = UcpDiscoveryProfileSchema.safeParse(profile);
313
+ return result.success;
314
+ }
315
+
316
+ /**
317
+ * Export SDK schemas for direct use
318
+ */
319
+ export {
320
+ UcpDiscoveryProfileSchema,
321
+ UcpClassSchema,
322
+ UcpServiceSchema,
323
+ CapabilityDiscoverySchema,
324
+ SigningKeySchema,
325
+ type UcpDiscoveryProfile,
326
+ type UcpClass,
327
+ type UcpService,
328
+ type CapabilityDiscovery,
329
+ type SigningKey,
330
+ };