@ucptools/validator 1.0.0 → 1.1.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.
- package/dist/auth/config.d.ts +20 -0
- package/dist/auth/config.d.ts.map +1 -0
- package/dist/auth/config.js +114 -0
- package/dist/auth/config.js.map +1 -0
- package/dist/auth/index.d.ts +5 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +17 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/middleware.d.ts +45 -0
- package/dist/auth/middleware.d.ts.map +1 -0
- package/dist/auth/middleware.js +170 -0
- package/dist/auth/middleware.js.map +1 -0
- package/dist/auth/service.d.ts +80 -0
- package/dist/auth/service.d.ts.map +1 -0
- package/dist/auth/service.js +298 -0
- package/dist/auth/service.js.map +1 -0
- package/dist/cli/index.d.ts +6 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +375 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/mock-server.d.ts +20 -0
- package/dist/cli/mock-server.d.ts.map +1 -0
- package/dist/cli/mock-server.js +261 -0
- package/dist/cli/mock-server.js.map +1 -0
- package/dist/compliance/compliance-generator.d.ts +34 -0
- package/dist/compliance/compliance-generator.d.ts.map +1 -0
- package/dist/compliance/compliance-generator.js +320 -0
- package/dist/compliance/compliance-generator.js.map +1 -0
- package/dist/compliance/index.d.ts +8 -0
- package/dist/compliance/index.d.ts.map +1 -0
- package/dist/compliance/index.js +17 -0
- package/dist/compliance/index.js.map +1 -0
- package/dist/compliance/templates.d.ts +34 -0
- package/dist/compliance/templates.d.ts.map +1 -0
- package/{src/compliance/templates.ts → dist/compliance/templates.js} +117 -155
- package/dist/compliance/templates.js.map +1 -0
- package/dist/compliance/types.d.ts +64 -0
- package/dist/compliance/types.d.ts.map +1 -0
- package/dist/compliance/types.js +64 -0
- package/dist/compliance/types.js.map +1 -0
- package/dist/db/index.d.ts +17 -0
- package/dist/db/index.d.ts.map +1 -0
- package/dist/db/index.js +80 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/schema.d.ts +3886 -0
- package/dist/db/schema.d.ts.map +1 -0
- package/dist/db/schema.js +425 -0
- package/dist/db/schema.js.map +1 -0
- package/dist/db/utils.d.ts +252 -0
- package/dist/db/utils.d.ts.map +1 -0
- package/dist/db/utils.js +295 -0
- package/dist/db/utils.js.map +1 -0
- package/dist/feed-analyzer/feed-analyzer.d.ts +26 -0
- package/dist/feed-analyzer/feed-analyzer.d.ts.map +1 -0
- package/{src/feed-analyzer/feed-analyzer.ts → dist/feed-analyzer/feed-analyzer.js} +856 -726
- package/dist/feed-analyzer/feed-analyzer.js.map +1 -0
- package/dist/feed-analyzer/index.d.ts +8 -0
- package/dist/feed-analyzer/index.d.ts.map +1 -0
- package/dist/feed-analyzer/index.js +19 -0
- package/dist/feed-analyzer/index.js.map +1 -0
- package/dist/feed-analyzer/types.d.ts +285 -0
- package/dist/feed-analyzer/types.d.ts.map +1 -0
- package/dist/feed-analyzer/types.js +175 -0
- package/dist/feed-analyzer/types.js.map +1 -0
- package/{src/generator/index.ts → dist/generator/index.d.ts} +1 -1
- package/dist/generator/index.d.ts.map +1 -0
- package/dist/generator/index.js +13 -0
- package/dist/generator/index.js.map +1 -0
- package/dist/generator/key-generator.d.ts +24 -0
- package/dist/generator/key-generator.d.ts.map +1 -0
- package/dist/generator/key-generator.js +144 -0
- package/dist/generator/key-generator.js.map +1 -0
- package/dist/generator/profile-builder.d.ts +15 -0
- package/dist/generator/profile-builder.d.ts.map +1 -0
- package/dist/generator/profile-builder.js +338 -0
- package/dist/generator/profile-builder.js.map +1 -0
- package/dist/hosting/artifacts-generator.d.ts +10 -0
- package/dist/hosting/artifacts-generator.d.ts.map +1 -0
- package/{src/hosting/artifacts-generator.ts → dist/hosting/artifacts-generator.js} +191 -241
- package/dist/hosting/artifacts-generator.js.map +1 -0
- package/{src/hosting/index.ts → dist/hosting/index.d.ts} +1 -1
- package/dist/hosting/index.d.ts.map +1 -0
- package/dist/hosting/index.js +10 -0
- package/dist/hosting/index.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +78 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/analytics.d.ts +337 -0
- package/dist/lib/analytics.d.ts.map +1 -0
- package/dist/lib/analytics.js +188 -0
- package/dist/lib/analytics.js.map +1 -0
- package/{src/security/index.ts → dist/security/index.d.ts} +8 -15
- package/dist/security/index.d.ts.map +1 -0
- package/dist/security/index.js +12 -0
- package/dist/security/index.js.map +1 -0
- package/dist/security/security-scanner.d.ts +10 -0
- package/dist/security/security-scanner.d.ts.map +1 -0
- package/dist/security/security-scanner.js +669 -0
- package/dist/security/security-scanner.js.map +1 -0
- package/dist/security/types.d.ts +80 -0
- package/dist/security/types.d.ts.map +1 -0
- package/dist/security/types.js +21 -0
- package/dist/security/types.js.map +1 -0
- package/dist/services/analytics.d.ts +114 -0
- package/dist/services/analytics.d.ts.map +1 -0
- package/dist/services/analytics.js +862 -0
- package/dist/services/analytics.js.map +1 -0
- package/dist/services/badge.d.ts +31 -0
- package/dist/services/badge.d.ts.map +1 -0
- package/dist/services/badge.js +152 -0
- package/dist/services/badge.js.map +1 -0
- package/dist/services/cron.d.ts +125 -0
- package/dist/services/cron.d.ts.map +1 -0
- package/dist/services/cron.js +613 -0
- package/dist/services/cron.js.map +1 -0
- package/dist/services/directory.d.ts +106 -0
- package/dist/services/directory.d.ts.map +1 -0
- package/dist/services/directory.js +351 -0
- package/dist/services/directory.js.map +1 -0
- package/dist/services/email.d.ts +112 -0
- package/dist/services/email.d.ts.map +1 -0
- package/dist/services/email.js +772 -0
- package/dist/services/email.js.map +1 -0
- package/dist/services/hosted-profiles.d.ts +77 -0
- package/dist/services/hosted-profiles.d.ts.map +1 -0
- package/dist/services/hosted-profiles.js +433 -0
- package/dist/services/hosted-profiles.js.map +1 -0
- package/dist/services/latency.d.ts +67 -0
- package/dist/services/latency.d.ts.map +1 -0
- package/dist/services/latency.js +274 -0
- package/dist/services/latency.js.map +1 -0
- package/dist/services/manifest-compliance.d.ts +64 -0
- package/dist/services/manifest-compliance.d.ts.map +1 -0
- package/dist/services/manifest-compliance.js +271 -0
- package/dist/services/manifest-compliance.js.map +1 -0
- package/dist/services/monitoring-diff.d.ts +31 -0
- package/dist/services/monitoring-diff.d.ts.map +1 -0
- package/dist/services/monitoring-diff.js +189 -0
- package/dist/services/monitoring-diff.js.map +1 -0
- package/dist/services/notifications.d.ts +46 -0
- package/dist/services/notifications.d.ts.map +1 -0
- package/dist/services/notifications.js +88 -0
- package/dist/services/notifications.js.map +1 -0
- package/dist/services/stripe.d.ts +93 -0
- package/dist/services/stripe.d.ts.map +1 -0
- package/dist/services/stripe.js +490 -0
- package/dist/services/stripe.js.map +1 -0
- package/dist/services/validation-history.d.ts +99 -0
- package/dist/services/validation-history.d.ts.map +1 -0
- package/dist/services/validation-history.js +344 -0
- package/dist/services/validation-history.js.map +1 -0
- package/dist/services/validation-logging.d.ts +103 -0
- package/dist/services/validation-logging.d.ts.map +1 -0
- package/dist/services/validation-logging.js +210 -0
- package/dist/services/validation-logging.js.map +1 -0
- package/dist/services/validation.d.ts +119 -0
- package/dist/services/validation.d.ts.map +1 -0
- package/dist/services/validation.js +1185 -0
- package/dist/services/validation.js.map +1 -0
- package/dist/simulator/agent-simulator.d.ts +69 -0
- package/dist/simulator/agent-simulator.d.ts.map +1 -0
- package/dist/simulator/agent-simulator.js +870 -0
- package/dist/simulator/agent-simulator.js.map +1 -0
- package/{src/simulator/index.ts → dist/simulator/index.d.ts} +7 -7
- package/dist/simulator/index.d.ts.map +1 -0
- package/dist/simulator/index.js +23 -0
- package/dist/simulator/index.js.map +1 -0
- package/{src/simulator/types.ts → dist/simulator/types.d.ts} +171 -170
- package/dist/simulator/types.d.ts.map +1 -0
- package/dist/simulator/types.js +18 -0
- package/dist/simulator/types.js.map +1 -0
- package/dist/types/acp-validation.d.ts +87 -0
- package/dist/types/acp-validation.d.ts.map +1 -0
- package/dist/types/acp-validation.js +40 -0
- package/dist/types/acp-validation.js.map +1 -0
- package/dist/types/analytics.d.ts +182 -0
- package/dist/types/analytics.d.ts.map +1 -0
- package/dist/types/analytics.js +7 -0
- package/dist/types/analytics.js.map +1 -0
- package/dist/types/generator.d.ts +106 -0
- package/dist/types/generator.d.ts.map +1 -0
- package/dist/types/generator.js +6 -0
- package/dist/types/generator.js.map +1 -0
- package/{src/types/index.ts → dist/types/index.d.ts} +1 -1
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +23 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/ucp-profile.d.ts +111 -0
- package/dist/types/ucp-profile.d.ts.map +1 -0
- package/dist/types/ucp-profile.js +45 -0
- package/dist/types/ucp-profile.js.map +1 -0
- package/dist/types/validation.d.ts +76 -0
- package/dist/types/validation.d.ts.map +1 -0
- package/dist/types/validation.js +42 -0
- package/dist/types/validation.js.map +1 -0
- package/dist/validator/acp/index.d.ts +31 -0
- package/dist/validator/acp/index.d.ts.map +1 -0
- package/dist/validator/acp/index.js +574 -0
- package/dist/validator/acp/index.js.map +1 -0
- package/dist/validator/index.d.ts +26 -0
- package/dist/validator/index.d.ts.map +1 -0
- package/dist/validator/index.js +161 -0
- package/dist/validator/index.js.map +1 -0
- package/dist/validator/network-validator.d.ts +28 -0
- package/dist/validator/network-validator.d.ts.map +1 -0
- package/dist/validator/network-validator.js +319 -0
- package/dist/validator/network-validator.js.map +1 -0
- package/dist/validator/rules-validator.d.ts +19 -0
- package/dist/validator/rules-validator.d.ts.map +1 -0
- package/dist/validator/rules-validator.js +306 -0
- package/dist/validator/rules-validator.js.map +1 -0
- package/dist/validator/sdk-validator.d.ts +58 -0
- package/dist/validator/sdk-validator.d.ts.map +1 -0
- package/{src/validator/sdk-validator.ts → dist/validator/sdk-validator.js} +273 -330
- package/dist/validator/sdk-validator.js.map +1 -0
- package/dist/validator/structural-validator.d.ts +11 -0
- package/dist/validator/structural-validator.d.ts.map +1 -0
- package/dist/validator/structural-validator.js +549 -0
- package/dist/validator/structural-validator.js.map +1 -0
- package/dist/validator/utils.d.ts +51 -0
- package/dist/validator/utils.d.ts.map +1 -0
- package/dist/validator/utils.js +132 -0
- package/dist/validator/utils.js.map +1 -0
- package/package.json +44 -12
- package/CLAUDE.md +0 -109
- package/api/analyze-feed.js +0 -140
- package/api/badge.js +0 -185
- package/api/benchmark.js +0 -177
- package/api/directory-stats.ts +0 -29
- package/api/directory.ts +0 -73
- package/api/generate-compliance.js +0 -143
- package/api/generate-schema.js +0 -457
- package/api/generate.js +0 -132
- package/api/security-scan.js +0 -133
- package/api/simulate.js +0 -187
- package/api/tsconfig.json +0 -10
- package/api/validate.js +0 -1351
- package/apify-actor/.actor/actor.json +0 -68
- package/apify-actor/.actor/input_schema.json +0 -32
- package/apify-actor/APIFY-STORE-LISTING.md +0 -412
- package/apify-actor/Dockerfile +0 -8
- package/apify-actor/README.md +0 -166
- package/apify-actor/main.ts +0 -111
- package/apify-actor/package.json +0 -17
- package/apify-actor/src/main.js +0 -199
- package/docs/BRAND-IDENTITY.md +0 -238
- package/docs/BRAND-STYLE-GUIDE.md +0 -356
- package/drizzle/0000_black_king_cobra.sql +0 -39
- package/drizzle/meta/0000_snapshot.json +0 -309
- package/drizzle/meta/_journal.json +0 -13
- package/drizzle.config.ts +0 -10
- package/public/.well-known/ucp +0 -25
- package/public/android-chrome-192x192.png +0 -0
- package/public/android-chrome-512x512.png +0 -0
- package/public/apple-touch-icon.png +0 -0
- package/public/brand.css +0 -321
- package/public/directory.html +0 -701
- package/public/favicon-16x16.png +0 -0
- package/public/favicon-32x32.png +0 -0
- package/public/favicon.ico +0 -0
- package/public/guides/bigcommerce.html +0 -743
- package/public/guides/fastucp.html +0 -838
- package/public/guides/magento.html +0 -779
- package/public/guides/shopify.html +0 -726
- package/public/guides/squarespace.html +0 -749
- package/public/guides/wix.html +0 -747
- package/public/guides/woocommerce.html +0 -733
- package/public/index.html +0 -3835
- package/public/learn.html +0 -396
- package/public/logo.jpeg +0 -0
- package/public/og-image-icon.png +0 -0
- package/public/og-image.png +0 -0
- package/public/robots.txt +0 -6
- package/public/site.webmanifest +0 -31
- package/public/sitemap.xml +0 -69
- package/public/social/linkedin-banner-1128x191.png +0 -0
- package/public/social/temp.PNG +0 -0
- package/public/social/x-header-1500x500.png +0 -0
- package/public/verify.html +0 -410
- package/scripts/generate-favicons.js +0 -44
- package/scripts/generate-ico.js +0 -23
- package/scripts/generate-og-image.js +0 -45
- package/scripts/reset-db.ts +0 -77
- package/scripts/seed-db.ts +0 -71
- package/scripts/setup-benchmark-db.js +0 -70
- package/src/api/server.ts +0 -266
- package/src/cli/index.ts +0 -302
- package/src/compliance/compliance-generator.ts +0 -452
- package/src/compliance/index.ts +0 -28
- package/src/compliance/types.ts +0 -170
- package/src/db/index.ts +0 -28
- package/src/db/schema.ts +0 -84
- package/src/feed-analyzer/index.ts +0 -34
- package/src/feed-analyzer/types.ts +0 -354
- package/src/generator/key-generator.ts +0 -124
- package/src/generator/profile-builder.ts +0 -402
- package/src/index.ts +0 -105
- package/src/security/security-scanner.ts +0 -604
- package/src/security/types.ts +0 -55
- package/src/services/directory.ts +0 -434
- package/src/simulator/agent-simulator.ts +0 -941
- package/src/types/generator.ts +0 -140
- package/src/types/ucp-profile.ts +0 -140
- package/src/types/validation.ts +0 -89
- package/src/validator/index.ts +0 -194
- package/src/validator/network-validator.ts +0 -417
- package/src/validator/rules-validator.ts +0 -297
- package/src/validator/structural-validator.ts +0 -476
- package/tests/fixtures/non-compliant-profile.json +0 -25
- package/tests/fixtures/official-sample-profile.json +0 -75
- package/tests/integration/benchmark.test.ts +0 -207
- package/tests/integration/database.test.ts +0 -163
- package/tests/integration/directory-api.test.ts +0 -268
- package/tests/integration/simulate-api.test.ts +0 -230
- package/tests/integration/validate-api.test.ts +0 -269
- package/tests/setup.ts +0 -15
- package/tests/unit/agent-simulator.test.ts +0 -575
- package/tests/unit/compliance-generator.test.ts +0 -374
- package/tests/unit/directory-service.test.ts +0 -272
- package/tests/unit/feed-analyzer.test.ts +0 -517
- package/tests/unit/lint-suggestions.test.ts +0 -423
- package/tests/unit/official-samples.test.ts +0 -211
- package/tests/unit/pdf-report.test.ts +0 -390
- package/tests/unit/sdk-validator.test.ts +0 -531
- package/tests/unit/security-scanner.test.ts +0 -410
- package/tests/unit/validation.test.ts +0 -390
- package/tsconfig.json +0 -20
- package/vercel.json +0 -34
- package/vitest.config.ts +0 -22
|
@@ -1,417 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Network Validator
|
|
3
|
-
* Validates UCP profile with network checks (fetches remote schemas)
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { UcpProfile } from '../types/ucp-profile.js';
|
|
7
|
-
import type { ValidationIssue, FetchResult, SchemaCacheEntry } from '../types/validation.js';
|
|
8
|
-
import { ValidationErrorCodes } from '../types/validation.js';
|
|
9
|
-
|
|
10
|
-
// Simple in-memory cache for schema fetches
|
|
11
|
-
const schemaCache = new Map<string, SchemaCacheEntry>();
|
|
12
|
-
const DEFAULT_CACHE_TTL_MS = 15 * 60 * 1000; // 15 minutes
|
|
13
|
-
const DEFAULT_TIMEOUT_MS = 10000; // 10 seconds
|
|
14
|
-
|
|
15
|
-
export interface NetworkValidationOptions {
|
|
16
|
-
timeoutMs?: number;
|
|
17
|
-
cacheTtlMs?: number;
|
|
18
|
-
skipSchemaFetch?: boolean;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Validate UCP profile with network checks
|
|
23
|
-
*/
|
|
24
|
-
export async function validateNetwork(
|
|
25
|
-
profile: UcpProfile,
|
|
26
|
-
options: NetworkValidationOptions = {}
|
|
27
|
-
): Promise<ValidationIssue[]> {
|
|
28
|
-
const issues: ValidationIssue[] = [];
|
|
29
|
-
const timeoutMs = options.timeoutMs || DEFAULT_TIMEOUT_MS;
|
|
30
|
-
const cacheTtlMs = options.cacheTtlMs || DEFAULT_CACHE_TTL_MS;
|
|
31
|
-
|
|
32
|
-
if (options.skipSchemaFetch) {
|
|
33
|
-
return issues;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const capabilities = profile.ucp.capabilities || [];
|
|
37
|
-
|
|
38
|
-
// Validate each capability's schema
|
|
39
|
-
for (let i = 0; i < capabilities.length; i++) {
|
|
40
|
-
const cap = capabilities[i];
|
|
41
|
-
const path = `$.ucp.capabilities[${i}]`;
|
|
42
|
-
|
|
43
|
-
if (cap.schema) {
|
|
44
|
-
const schemaIssues = await validateCapabilitySchema(
|
|
45
|
-
cap.schema,
|
|
46
|
-
cap.name,
|
|
47
|
-
cap.version,
|
|
48
|
-
path,
|
|
49
|
-
timeoutMs,
|
|
50
|
-
cacheTtlMs
|
|
51
|
-
);
|
|
52
|
-
issues.push(...schemaIssues);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
return issues;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Validate remote profile fetch
|
|
61
|
-
*/
|
|
62
|
-
export async function validateRemoteProfile(
|
|
63
|
-
domain: string,
|
|
64
|
-
options: NetworkValidationOptions = {}
|
|
65
|
-
): Promise<{ profile: UcpProfile | null; profileUrl?: string; issues: ValidationIssue[] }> {
|
|
66
|
-
const issues: ValidationIssue[] = [];
|
|
67
|
-
const timeoutMs = options.timeoutMs || DEFAULT_TIMEOUT_MS;
|
|
68
|
-
|
|
69
|
-
// Try both /.well-known/ucp and /.well-known/ucp.json
|
|
70
|
-
const urls = [
|
|
71
|
-
`https://${domain}/.well-known/ucp`,
|
|
72
|
-
`https://${domain}/.well-known/ucp.json`,
|
|
73
|
-
];
|
|
74
|
-
|
|
75
|
-
for (const profileUrl of urls) {
|
|
76
|
-
const result = await fetchProfileWithTimeout(profileUrl, timeoutMs);
|
|
77
|
-
|
|
78
|
-
if (!result.success) {
|
|
79
|
-
// Try next URL
|
|
80
|
-
continue;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// Verify it's an object with ucp field
|
|
84
|
-
if (!result.data || typeof result.data !== 'object') {
|
|
85
|
-
continue;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const profileData = result.data as Record<string, unknown>;
|
|
89
|
-
if (!profileData.ucp) {
|
|
90
|
-
continue;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return { profile: result.data as UcpProfile, profileUrl, issues };
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// All URLs failed
|
|
97
|
-
issues.push({
|
|
98
|
-
severity: 'error',
|
|
99
|
-
code: ValidationErrorCodes.PROFILE_FETCH_FAILED,
|
|
100
|
-
path: '$.well-known/ucp',
|
|
101
|
-
message: 'No UCP profile found at /.well-known/ucp or /.well-known/ucp.json',
|
|
102
|
-
hint: 'Check that the profile is accessible and returns valid JSON',
|
|
103
|
-
});
|
|
104
|
-
return { profile: null, issues };
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Validate a capability's schema (fetch and check self-describing)
|
|
109
|
-
*/
|
|
110
|
-
async function validateCapabilitySchema(
|
|
111
|
-
schemaUrl: string,
|
|
112
|
-
expectedName: string,
|
|
113
|
-
expectedVersion: string,
|
|
114
|
-
basePath: string,
|
|
115
|
-
timeoutMs: number,
|
|
116
|
-
cacheTtlMs: number
|
|
117
|
-
): Promise<ValidationIssue[]> {
|
|
118
|
-
const issues: ValidationIssue[] = [];
|
|
119
|
-
|
|
120
|
-
// Check cache first
|
|
121
|
-
const cached = getCachedSchema(schemaUrl, cacheTtlMs);
|
|
122
|
-
let schemaData: Record<string, unknown>;
|
|
123
|
-
|
|
124
|
-
if (cached) {
|
|
125
|
-
schemaData = cached;
|
|
126
|
-
} else {
|
|
127
|
-
// Fetch schema
|
|
128
|
-
const result = await fetchWithTimeout<Record<string, unknown>>(schemaUrl, timeoutMs);
|
|
129
|
-
|
|
130
|
-
if (!result.success) {
|
|
131
|
-
issues.push({
|
|
132
|
-
severity: 'warn',
|
|
133
|
-
code: ValidationErrorCodes.SCHEMA_FETCH_FAILED,
|
|
134
|
-
path: `${basePath}.schema`,
|
|
135
|
-
message: `Failed to fetch schema from ${schemaUrl}`,
|
|
136
|
-
hint: result.error || 'Schema URL may be incorrect or temporarily unavailable',
|
|
137
|
-
});
|
|
138
|
-
return issues;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
if (!result.data) {
|
|
142
|
-
issues.push({
|
|
143
|
-
severity: 'warn',
|
|
144
|
-
code: ValidationErrorCodes.SCHEMA_FETCH_FAILED,
|
|
145
|
-
path: `${basePath}.schema`,
|
|
146
|
-
message: `Schema response is empty`,
|
|
147
|
-
});
|
|
148
|
-
return issues;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
schemaData = result.data;
|
|
152
|
-
|
|
153
|
-
// Cache the schema
|
|
154
|
-
cacheSchema(schemaUrl, schemaData, result.etag, cacheTtlMs);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// Check if schema is self-describing
|
|
158
|
-
const selfDescribingIssues = validateSelfDescribingSchema(
|
|
159
|
-
schemaData,
|
|
160
|
-
expectedName,
|
|
161
|
-
expectedVersion,
|
|
162
|
-
basePath
|
|
163
|
-
);
|
|
164
|
-
issues.push(...selfDescribingIssues);
|
|
165
|
-
|
|
166
|
-
return issues;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* Validate schema is self-describing (contains name and version matching capability)
|
|
171
|
-
*/
|
|
172
|
-
function validateSelfDescribingSchema(
|
|
173
|
-
schema: Record<string, unknown>,
|
|
174
|
-
expectedName: string,
|
|
175
|
-
expectedVersion: string,
|
|
176
|
-
basePath: string
|
|
177
|
-
): ValidationIssue[] {
|
|
178
|
-
const issues: ValidationIssue[] = [];
|
|
179
|
-
|
|
180
|
-
// Check for $id or name field
|
|
181
|
-
const schemaName = (schema.$id as string) || (schema.name as string);
|
|
182
|
-
const schemaVersion = schema.version as string;
|
|
183
|
-
|
|
184
|
-
if (!schemaName && !schema.$id) {
|
|
185
|
-
issues.push({
|
|
186
|
-
severity: 'info',
|
|
187
|
-
code: ValidationErrorCodes.SCHEMA_NOT_SELF_DESCRIBING,
|
|
188
|
-
path: `${basePath}.schema`,
|
|
189
|
-
message: 'Schema does not contain self-describing $id or name field',
|
|
190
|
-
hint: 'Consider adding $id field to schema for better discoverability',
|
|
191
|
-
});
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// If schema has a name, check it contains the capability name
|
|
195
|
-
if (schemaName) {
|
|
196
|
-
// Extract capability name from schema $id if it's a URL
|
|
197
|
-
const nameFromId = extractNameFromSchemaId(schemaName);
|
|
198
|
-
if (nameFromId && !expectedName.includes(nameFromId) && !nameFromId.includes(expectedName.split('.').pop() || '')) {
|
|
199
|
-
issues.push({
|
|
200
|
-
severity: 'warn',
|
|
201
|
-
code: ValidationErrorCodes.SCHEMA_NAME_MISMATCH,
|
|
202
|
-
path: `${basePath}.schema`,
|
|
203
|
-
message: `Schema name "${nameFromId}" may not match capability "${expectedName}"`,
|
|
204
|
-
});
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// Check version if present
|
|
209
|
-
if (schemaVersion && schemaVersion !== expectedVersion) {
|
|
210
|
-
issues.push({
|
|
211
|
-
severity: 'info',
|
|
212
|
-
code: ValidationErrorCodes.SCHEMA_VERSION_MISMATCH,
|
|
213
|
-
path: `${basePath}.schema`,
|
|
214
|
-
message: `Schema version "${schemaVersion}" differs from capability version "${expectedVersion}"`,
|
|
215
|
-
hint: 'Ensure schema and capability versions are aligned',
|
|
216
|
-
});
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
return issues;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/**
|
|
223
|
-
* Extract capability name from schema $id URL
|
|
224
|
-
*/
|
|
225
|
-
function extractNameFromSchemaId(schemaId: string): string | null {
|
|
226
|
-
try {
|
|
227
|
-
const url = new URL(schemaId);
|
|
228
|
-
// Extract last path segment without .json extension
|
|
229
|
-
const pathParts = url.pathname.split('/').filter(Boolean);
|
|
230
|
-
const lastPart = pathParts[pathParts.length - 1] || '';
|
|
231
|
-
return lastPart.replace('.json', '');
|
|
232
|
-
} catch {
|
|
233
|
-
// Not a URL, return as-is
|
|
234
|
-
return schemaId;
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
/**
|
|
239
|
-
* Fetch profile URL with timeout, checking for HTML responses
|
|
240
|
-
*/
|
|
241
|
-
async function fetchProfileWithTimeout(
|
|
242
|
-
url: string,
|
|
243
|
-
timeoutMs: number
|
|
244
|
-
): Promise<FetchResult<unknown>> {
|
|
245
|
-
const controller = new AbortController();
|
|
246
|
-
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
247
|
-
|
|
248
|
-
try {
|
|
249
|
-
const response = await fetch(url, {
|
|
250
|
-
signal: controller.signal,
|
|
251
|
-
headers: {
|
|
252
|
-
'Accept': 'application/json',
|
|
253
|
-
'User-Agent': 'UCP-Profile-Validator/1.0',
|
|
254
|
-
},
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
clearTimeout(timeoutId);
|
|
258
|
-
|
|
259
|
-
if (!response.ok) {
|
|
260
|
-
return {
|
|
261
|
-
success: false,
|
|
262
|
-
error: `HTTP ${response.status}: ${response.statusText}`,
|
|
263
|
-
statusCode: response.status,
|
|
264
|
-
};
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
const text = await response.text();
|
|
268
|
-
|
|
269
|
-
// Check if response looks like JSON (not HTML)
|
|
270
|
-
if (text.trim().startsWith('<')) {
|
|
271
|
-
return {
|
|
272
|
-
success: false,
|
|
273
|
-
error: 'Response is HTML, not JSON',
|
|
274
|
-
};
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
const data = JSON.parse(text);
|
|
278
|
-
const etag = response.headers.get('etag') || undefined;
|
|
279
|
-
|
|
280
|
-
return {
|
|
281
|
-
success: true,
|
|
282
|
-
data,
|
|
283
|
-
statusCode: response.status,
|
|
284
|
-
etag,
|
|
285
|
-
};
|
|
286
|
-
} catch (error) {
|
|
287
|
-
clearTimeout(timeoutId);
|
|
288
|
-
|
|
289
|
-
if (error instanceof Error) {
|
|
290
|
-
if (error.name === 'AbortError') {
|
|
291
|
-
return {
|
|
292
|
-
success: false,
|
|
293
|
-
error: `Request timed out after ${timeoutMs}ms`,
|
|
294
|
-
};
|
|
295
|
-
}
|
|
296
|
-
return {
|
|
297
|
-
success: false,
|
|
298
|
-
error: error.message,
|
|
299
|
-
};
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
return {
|
|
303
|
-
success: false,
|
|
304
|
-
error: 'Unknown error occurred',
|
|
305
|
-
};
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
/**
|
|
310
|
-
* Fetch URL with timeout
|
|
311
|
-
*/
|
|
312
|
-
async function fetchWithTimeout<T>(
|
|
313
|
-
url: string,
|
|
314
|
-
timeoutMs: number
|
|
315
|
-
): Promise<FetchResult<T>> {
|
|
316
|
-
const controller = new AbortController();
|
|
317
|
-
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
318
|
-
|
|
319
|
-
try {
|
|
320
|
-
const response = await fetch(url, {
|
|
321
|
-
signal: controller.signal,
|
|
322
|
-
headers: {
|
|
323
|
-
'Accept': 'application/json',
|
|
324
|
-
'User-Agent': 'UCP-Profile-Validator/1.0',
|
|
325
|
-
},
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
clearTimeout(timeoutId);
|
|
329
|
-
|
|
330
|
-
if (!response.ok) {
|
|
331
|
-
return {
|
|
332
|
-
success: false,
|
|
333
|
-
error: `HTTP ${response.status}: ${response.statusText}`,
|
|
334
|
-
statusCode: response.status,
|
|
335
|
-
};
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
const data = await response.json() as T;
|
|
339
|
-
const etag = response.headers.get('etag') || undefined;
|
|
340
|
-
|
|
341
|
-
return {
|
|
342
|
-
success: true,
|
|
343
|
-
data,
|
|
344
|
-
statusCode: response.status,
|
|
345
|
-
etag,
|
|
346
|
-
};
|
|
347
|
-
} catch (error) {
|
|
348
|
-
clearTimeout(timeoutId);
|
|
349
|
-
|
|
350
|
-
if (error instanceof Error) {
|
|
351
|
-
if (error.name === 'AbortError') {
|
|
352
|
-
return {
|
|
353
|
-
success: false,
|
|
354
|
-
error: `Request timed out after ${timeoutMs}ms`,
|
|
355
|
-
};
|
|
356
|
-
}
|
|
357
|
-
return {
|
|
358
|
-
success: false,
|
|
359
|
-
error: error.message,
|
|
360
|
-
};
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
return {
|
|
364
|
-
success: false,
|
|
365
|
-
error: 'Unknown error occurred',
|
|
366
|
-
};
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
/**
|
|
371
|
-
* Get cached schema if valid
|
|
372
|
-
*/
|
|
373
|
-
function getCachedSchema(
|
|
374
|
-
url: string,
|
|
375
|
-
cacheTtlMs: number
|
|
376
|
-
): Record<string, unknown> | null {
|
|
377
|
-
const cached = schemaCache.get(url);
|
|
378
|
-
if (!cached) {
|
|
379
|
-
return null;
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
const now = new Date().toISOString();
|
|
383
|
-
if (cached.expiresAt < now) {
|
|
384
|
-
schemaCache.delete(url);
|
|
385
|
-
return null;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
return cached.body;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
/**
|
|
392
|
-
* Cache a schema
|
|
393
|
-
*/
|
|
394
|
-
function cacheSchema(
|
|
395
|
-
url: string,
|
|
396
|
-
body: Record<string, unknown>,
|
|
397
|
-
etag: string | undefined,
|
|
398
|
-
cacheTtlMs: number
|
|
399
|
-
): void {
|
|
400
|
-
const now = new Date();
|
|
401
|
-
const expiresAt = new Date(now.getTime() + cacheTtlMs);
|
|
402
|
-
|
|
403
|
-
schemaCache.set(url, {
|
|
404
|
-
url,
|
|
405
|
-
etag,
|
|
406
|
-
fetchedAt: now.toISOString(),
|
|
407
|
-
body,
|
|
408
|
-
expiresAt: expiresAt.toISOString(),
|
|
409
|
-
});
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
/**
|
|
413
|
-
* Clear the schema cache
|
|
414
|
-
*/
|
|
415
|
-
export function clearSchemaCache(): void {
|
|
416
|
-
schemaCache.clear();
|
|
417
|
-
}
|
|
@@ -1,297 +0,0 @@
|
|
|
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
|
-
}
|