@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.
- package/CLAUDE.md +109 -0
- package/CONTRIBUTING.md +113 -0
- package/LICENSE +21 -0
- package/README.md +203 -0
- package/api/analyze-feed.js +140 -0
- package/api/badge.js +185 -0
- package/api/benchmark.js +177 -0
- package/api/directory-stats.ts +29 -0
- package/api/directory.ts +73 -0
- package/api/generate-compliance.js +143 -0
- package/api/generate-schema.js +457 -0
- package/api/generate.js +132 -0
- package/api/security-scan.js +133 -0
- package/api/simulate.js +187 -0
- package/api/tsconfig.json +10 -0
- package/api/validate.js +1351 -0
- package/apify-actor/.actor/actor.json +68 -0
- package/apify-actor/.actor/input_schema.json +32 -0
- package/apify-actor/APIFY-STORE-LISTING.md +412 -0
- package/apify-actor/Dockerfile +8 -0
- package/apify-actor/README.md +166 -0
- package/apify-actor/main.ts +111 -0
- package/apify-actor/package.json +17 -0
- package/apify-actor/src/main.js +199 -0
- package/docs/BRAND-IDENTITY.md +238 -0
- package/docs/BRAND-STYLE-GUIDE.md +356 -0
- package/drizzle/0000_black_king_cobra.sql +39 -0
- package/drizzle/meta/0000_snapshot.json +309 -0
- package/drizzle/meta/_journal.json +13 -0
- package/drizzle.config.ts +10 -0
- package/examples/full-profile.json +70 -0
- package/examples/minimal-profile.json +23 -0
- package/package.json +69 -0
- package/public/.well-known/ucp +25 -0
- 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 +321 -0
- package/public/directory.html +701 -0
- 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 +743 -0
- package/public/guides/fastucp.html +838 -0
- package/public/guides/magento.html +779 -0
- package/public/guides/shopify.html +726 -0
- package/public/guides/squarespace.html +749 -0
- package/public/guides/wix.html +747 -0
- package/public/guides/woocommerce.html +733 -0
- package/public/index.html +3835 -0
- package/public/learn.html +396 -0
- 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 +6 -0
- package/public/site.webmanifest +31 -0
- package/public/sitemap.xml +69 -0
- 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 +410 -0
- package/scripts/generate-favicons.js +44 -0
- package/scripts/generate-ico.js +23 -0
- package/scripts/generate-og-image.js +45 -0
- package/scripts/reset-db.ts +77 -0
- package/scripts/seed-db.ts +71 -0
- package/scripts/setup-benchmark-db.js +70 -0
- package/src/api/server.ts +266 -0
- package/src/cli/index.ts +302 -0
- package/src/compliance/compliance-generator.ts +452 -0
- package/src/compliance/index.ts +28 -0
- package/src/compliance/templates.ts +338 -0
- package/src/compliance/types.ts +170 -0
- package/src/db/index.ts +28 -0
- package/src/db/schema.ts +84 -0
- package/src/feed-analyzer/feed-analyzer.ts +726 -0
- package/src/feed-analyzer/index.ts +34 -0
- package/src/feed-analyzer/types.ts +354 -0
- package/src/generator/index.ts +7 -0
- package/src/generator/key-generator.ts +124 -0
- package/src/generator/profile-builder.ts +402 -0
- package/src/hosting/artifacts-generator.ts +679 -0
- package/src/hosting/index.ts +6 -0
- package/src/index.ts +105 -0
- package/src/security/index.ts +15 -0
- package/src/security/security-scanner.ts +604 -0
- package/src/security/types.ts +55 -0
- package/src/services/directory.ts +434 -0
- package/src/simulator/agent-simulator.ts +941 -0
- package/src/simulator/index.ts +7 -0
- package/src/simulator/types.ts +170 -0
- package/src/types/generator.ts +140 -0
- package/src/types/index.ts +7 -0
- package/src/types/ucp-profile.ts +140 -0
- package/src/types/validation.ts +89 -0
- package/src/validator/index.ts +194 -0
- package/src/validator/network-validator.ts +417 -0
- package/src/validator/rules-validator.ts +297 -0
- package/src/validator/sdk-validator.ts +330 -0
- package/src/validator/structural-validator.ts +476 -0
- package/tests/fixtures/non-compliant-profile.json +25 -0
- package/tests/fixtures/official-sample-profile.json +75 -0
- package/tests/integration/benchmark.test.ts +207 -0
- package/tests/integration/database.test.ts +163 -0
- package/tests/integration/directory-api.test.ts +268 -0
- package/tests/integration/simulate-api.test.ts +230 -0
- package/tests/integration/validate-api.test.ts +269 -0
- package/tests/setup.ts +15 -0
- package/tests/unit/agent-simulator.test.ts +575 -0
- package/tests/unit/compliance-generator.test.ts +374 -0
- package/tests/unit/directory-service.test.ts +272 -0
- package/tests/unit/feed-analyzer.test.ts +517 -0
- package/tests/unit/lint-suggestions.test.ts +423 -0
- package/tests/unit/official-samples.test.ts +211 -0
- package/tests/unit/pdf-report.test.ts +390 -0
- package/tests/unit/sdk-validator.test.ts +531 -0
- package/tests/unit/security-scanner.test.ts +410 -0
- package/tests/unit/validation.test.ts +390 -0
- package/tsconfig.json +20 -0
- package/vercel.json +34 -0
- package/vitest.config.ts +22 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Product Feed Quality Analyzer Module
|
|
3
|
+
* Deep analysis of product data quality for AI agent visibility
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export {
|
|
7
|
+
analyzeProductFeed,
|
|
8
|
+
analyzeProductFeedFromHtml,
|
|
9
|
+
analyzeProduct,
|
|
10
|
+
extractProductsFromHtml,
|
|
11
|
+
validateGtin,
|
|
12
|
+
} from './feed-analyzer.js';
|
|
13
|
+
|
|
14
|
+
export type {
|
|
15
|
+
ProductData,
|
|
16
|
+
ProductOffer,
|
|
17
|
+
ProductAnalysis,
|
|
18
|
+
QualityCheck,
|
|
19
|
+
FeedAnalysisResult,
|
|
20
|
+
FeedAnalysisInput,
|
|
21
|
+
CategoryScores,
|
|
22
|
+
Recommendation,
|
|
23
|
+
FeedSummary,
|
|
24
|
+
GtinValidation,
|
|
25
|
+
IssueSeverity,
|
|
26
|
+
CheckCategory,
|
|
27
|
+
} from './types.js';
|
|
28
|
+
|
|
29
|
+
export {
|
|
30
|
+
QUALITY_CHECKS,
|
|
31
|
+
VALID_AVAILABILITY_VALUES,
|
|
32
|
+
CATEGORY_WEIGHTS,
|
|
33
|
+
GRADE_THRESHOLDS,
|
|
34
|
+
} from './types.js';
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Product Feed Quality Analyzer Types
|
|
3
|
+
* Deep analysis of product data quality for AI agent visibility
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Severity levels for feed issues
|
|
8
|
+
*/
|
|
9
|
+
export type IssueSeverity = 'critical' | 'warning' | 'info';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Categories of feed quality checks
|
|
13
|
+
*/
|
|
14
|
+
export type CheckCategory =
|
|
15
|
+
| 'completeness'
|
|
16
|
+
| 'identifiers'
|
|
17
|
+
| 'images'
|
|
18
|
+
| 'pricing'
|
|
19
|
+
| 'descriptions'
|
|
20
|
+
| 'categories'
|
|
21
|
+
| 'availability';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Individual product data from Schema.org JSON-LD
|
|
25
|
+
*/
|
|
26
|
+
export interface ProductData {
|
|
27
|
+
name?: string;
|
|
28
|
+
description?: string;
|
|
29
|
+
sku?: string;
|
|
30
|
+
gtin?: string;
|
|
31
|
+
gtin8?: string;
|
|
32
|
+
gtin12?: string;
|
|
33
|
+
gtin13?: string;
|
|
34
|
+
gtin14?: string;
|
|
35
|
+
mpn?: string;
|
|
36
|
+
brand?: string | { name?: string };
|
|
37
|
+
image?: string | string[] | { url?: string }[];
|
|
38
|
+
offers?: ProductOffer | ProductOffer[];
|
|
39
|
+
category?: string;
|
|
40
|
+
color?: string;
|
|
41
|
+
size?: string;
|
|
42
|
+
material?: string;
|
|
43
|
+
weight?: string | { value?: number; unitCode?: string };
|
|
44
|
+
aggregateRating?: {
|
|
45
|
+
ratingValue?: number;
|
|
46
|
+
reviewCount?: number;
|
|
47
|
+
ratingCount?: number;
|
|
48
|
+
};
|
|
49
|
+
review?: unknown[];
|
|
50
|
+
url?: string;
|
|
51
|
+
'@type'?: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Product offer/pricing data
|
|
56
|
+
*/
|
|
57
|
+
export interface ProductOffer {
|
|
58
|
+
'@type'?: string;
|
|
59
|
+
price?: number | string;
|
|
60
|
+
priceCurrency?: string;
|
|
61
|
+
availability?: string;
|
|
62
|
+
url?: string;
|
|
63
|
+
seller?: { name?: string };
|
|
64
|
+
priceValidUntil?: string;
|
|
65
|
+
itemCondition?: string;
|
|
66
|
+
shippingDetails?: unknown;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Individual quality check result
|
|
71
|
+
*/
|
|
72
|
+
export interface QualityCheck {
|
|
73
|
+
id: string;
|
|
74
|
+
name: string;
|
|
75
|
+
category: CheckCategory;
|
|
76
|
+
passed: boolean;
|
|
77
|
+
severity: IssueSeverity;
|
|
78
|
+
message: string;
|
|
79
|
+
details?: string;
|
|
80
|
+
recommendation?: string;
|
|
81
|
+
affectedProducts?: string[];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Product-level analysis result
|
|
86
|
+
*/
|
|
87
|
+
export interface ProductAnalysis {
|
|
88
|
+
name: string;
|
|
89
|
+
url?: string;
|
|
90
|
+
sku?: string;
|
|
91
|
+
score: number;
|
|
92
|
+
issues: QualityCheck[];
|
|
93
|
+
attributes: {
|
|
94
|
+
hasName: boolean;
|
|
95
|
+
hasDescription: boolean;
|
|
96
|
+
hasSku: boolean;
|
|
97
|
+
hasGtin: boolean;
|
|
98
|
+
hasBrand: boolean;
|
|
99
|
+
hasImage: boolean;
|
|
100
|
+
hasPrice: boolean;
|
|
101
|
+
hasAvailability: boolean;
|
|
102
|
+
hasCategory: boolean;
|
|
103
|
+
descriptionLength: number;
|
|
104
|
+
imageCount: number;
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Category scores breakdown
|
|
110
|
+
*/
|
|
111
|
+
export interface CategoryScores {
|
|
112
|
+
completeness: number;
|
|
113
|
+
identifiers: number;
|
|
114
|
+
images: number;
|
|
115
|
+
pricing: number;
|
|
116
|
+
descriptions: number;
|
|
117
|
+
categories: number;
|
|
118
|
+
availability: number;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Overall feed analysis result
|
|
123
|
+
*/
|
|
124
|
+
export interface FeedAnalysisResult {
|
|
125
|
+
url: string;
|
|
126
|
+
analyzedAt: string;
|
|
127
|
+
productsFound: number;
|
|
128
|
+
productsAnalyzed: number;
|
|
129
|
+
overallScore: number;
|
|
130
|
+
agentVisibilityScore: number;
|
|
131
|
+
grade: 'A' | 'B' | 'C' | 'D' | 'F';
|
|
132
|
+
categoryScores: CategoryScores;
|
|
133
|
+
issues: QualityCheck[];
|
|
134
|
+
topIssues: QualityCheck[];
|
|
135
|
+
products: ProductAnalysis[];
|
|
136
|
+
recommendations: Recommendation[];
|
|
137
|
+
summary: FeedSummary;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Recommendation for improving feed quality
|
|
142
|
+
*/
|
|
143
|
+
export interface Recommendation {
|
|
144
|
+
priority: 'high' | 'medium' | 'low';
|
|
145
|
+
category: CheckCategory;
|
|
146
|
+
title: string;
|
|
147
|
+
description: string;
|
|
148
|
+
impact: string;
|
|
149
|
+
affectedCount: number;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Summary statistics for the feed
|
|
154
|
+
*/
|
|
155
|
+
export interface FeedSummary {
|
|
156
|
+
totalProducts: number;
|
|
157
|
+
withName: number;
|
|
158
|
+
withDescription: number;
|
|
159
|
+
withSku: number;
|
|
160
|
+
withGtin: number;
|
|
161
|
+
withBrand: number;
|
|
162
|
+
withImages: number;
|
|
163
|
+
withPrice: number;
|
|
164
|
+
withAvailability: number;
|
|
165
|
+
withCategory: number;
|
|
166
|
+
averageDescriptionLength: number;
|
|
167
|
+
averageImageCount: number;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Input options for feed analysis
|
|
172
|
+
*/
|
|
173
|
+
export interface FeedAnalysisInput {
|
|
174
|
+
url: string;
|
|
175
|
+
maxProducts?: number;
|
|
176
|
+
includeProductDetails?: boolean;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* GTIN validation result
|
|
181
|
+
*/
|
|
182
|
+
export interface GtinValidation {
|
|
183
|
+
isValid: boolean;
|
|
184
|
+
type?: 'GTIN-8' | 'GTIN-12' | 'GTIN-13' | 'GTIN-14' | 'UPC' | 'EAN';
|
|
185
|
+
error?: string;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Quality check definitions
|
|
190
|
+
*/
|
|
191
|
+
export const QUALITY_CHECKS: Record<string, {
|
|
192
|
+
name: string;
|
|
193
|
+
category: CheckCategory;
|
|
194
|
+
severity: IssueSeverity;
|
|
195
|
+
description: string;
|
|
196
|
+
}> = {
|
|
197
|
+
// Completeness checks
|
|
198
|
+
'missing-name': {
|
|
199
|
+
name: 'Missing Product Name',
|
|
200
|
+
category: 'completeness',
|
|
201
|
+
severity: 'critical',
|
|
202
|
+
description: 'Product name is required for AI agents to identify products',
|
|
203
|
+
},
|
|
204
|
+
'missing-description': {
|
|
205
|
+
name: 'Missing Description',
|
|
206
|
+
category: 'completeness',
|
|
207
|
+
severity: 'warning',
|
|
208
|
+
description: 'Product description helps AI agents understand and recommend products',
|
|
209
|
+
},
|
|
210
|
+
'short-description': {
|
|
211
|
+
name: 'Short Description',
|
|
212
|
+
category: 'descriptions',
|
|
213
|
+
severity: 'info',
|
|
214
|
+
description: 'Longer descriptions provide better context for AI agents',
|
|
215
|
+
},
|
|
216
|
+
'missing-brand': {
|
|
217
|
+
name: 'Missing Brand',
|
|
218
|
+
category: 'completeness',
|
|
219
|
+
severity: 'warning',
|
|
220
|
+
description: 'Brand information helps with product identification and search',
|
|
221
|
+
},
|
|
222
|
+
|
|
223
|
+
// Identifier checks
|
|
224
|
+
'missing-sku': {
|
|
225
|
+
name: 'Missing SKU',
|
|
226
|
+
category: 'identifiers',
|
|
227
|
+
severity: 'warning',
|
|
228
|
+
description: 'SKU helps uniquely identify products in your catalog',
|
|
229
|
+
},
|
|
230
|
+
'missing-gtin': {
|
|
231
|
+
name: 'Missing GTIN/UPC/EAN',
|
|
232
|
+
category: 'identifiers',
|
|
233
|
+
severity: 'warning',
|
|
234
|
+
description: 'Global identifiers enable cross-platform product matching',
|
|
235
|
+
},
|
|
236
|
+
'invalid-gtin': {
|
|
237
|
+
name: 'Invalid GTIN Format',
|
|
238
|
+
category: 'identifiers',
|
|
239
|
+
severity: 'critical',
|
|
240
|
+
description: 'GTIN has invalid format or check digit',
|
|
241
|
+
},
|
|
242
|
+
|
|
243
|
+
// Image checks
|
|
244
|
+
'missing-image': {
|
|
245
|
+
name: 'Missing Product Image',
|
|
246
|
+
category: 'images',
|
|
247
|
+
severity: 'critical',
|
|
248
|
+
description: 'Product images are essential for AI shopping experiences',
|
|
249
|
+
},
|
|
250
|
+
'single-image': {
|
|
251
|
+
name: 'Single Image Only',
|
|
252
|
+
category: 'images',
|
|
253
|
+
severity: 'info',
|
|
254
|
+
description: 'Multiple images improve product presentation',
|
|
255
|
+
},
|
|
256
|
+
'missing-image-alt': {
|
|
257
|
+
name: 'Missing Image Alt Text',
|
|
258
|
+
category: 'images',
|
|
259
|
+
severity: 'info',
|
|
260
|
+
description: 'Alt text helps with accessibility and SEO',
|
|
261
|
+
},
|
|
262
|
+
|
|
263
|
+
// Pricing checks
|
|
264
|
+
'missing-price': {
|
|
265
|
+
name: 'Missing Price',
|
|
266
|
+
category: 'pricing',
|
|
267
|
+
severity: 'critical',
|
|
268
|
+
description: 'Price is required for AI agents to complete purchases',
|
|
269
|
+
},
|
|
270
|
+
'missing-currency': {
|
|
271
|
+
name: 'Missing Currency',
|
|
272
|
+
category: 'pricing',
|
|
273
|
+
severity: 'warning',
|
|
274
|
+
description: 'Currency must be specified for international commerce',
|
|
275
|
+
},
|
|
276
|
+
'invalid-price': {
|
|
277
|
+
name: 'Invalid Price Format',
|
|
278
|
+
category: 'pricing',
|
|
279
|
+
severity: 'critical',
|
|
280
|
+
description: 'Price must be a valid number',
|
|
281
|
+
},
|
|
282
|
+
|
|
283
|
+
// Availability checks
|
|
284
|
+
'missing-availability': {
|
|
285
|
+
name: 'Missing Availability',
|
|
286
|
+
category: 'availability',
|
|
287
|
+
severity: 'warning',
|
|
288
|
+
description: 'Availability status helps AI agents make purchase decisions',
|
|
289
|
+
},
|
|
290
|
+
'invalid-availability': {
|
|
291
|
+
name: 'Invalid Availability Value',
|
|
292
|
+
category: 'availability',
|
|
293
|
+
severity: 'warning',
|
|
294
|
+
description: 'Availability should use Schema.org ItemAvailability values',
|
|
295
|
+
},
|
|
296
|
+
|
|
297
|
+
// Category checks
|
|
298
|
+
'missing-category': {
|
|
299
|
+
name: 'Missing Category',
|
|
300
|
+
category: 'categories',
|
|
301
|
+
severity: 'info',
|
|
302
|
+
description: 'Product categorization improves discoverability',
|
|
303
|
+
},
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Valid Schema.org availability values
|
|
308
|
+
*/
|
|
309
|
+
export const VALID_AVAILABILITY_VALUES = [
|
|
310
|
+
'https://schema.org/InStock',
|
|
311
|
+
'https://schema.org/OutOfStock',
|
|
312
|
+
'https://schema.org/PreOrder',
|
|
313
|
+
'https://schema.org/PreSale',
|
|
314
|
+
'https://schema.org/SoldOut',
|
|
315
|
+
'https://schema.org/InStoreOnly',
|
|
316
|
+
'https://schema.org/OnlineOnly',
|
|
317
|
+
'https://schema.org/LimitedAvailability',
|
|
318
|
+
'https://schema.org/Discontinued',
|
|
319
|
+
'https://schema.org/BackOrder',
|
|
320
|
+
'InStock',
|
|
321
|
+
'OutOfStock',
|
|
322
|
+
'PreOrder',
|
|
323
|
+
'PreSale',
|
|
324
|
+
'SoldOut',
|
|
325
|
+
'InStoreOnly',
|
|
326
|
+
'OnlineOnly',
|
|
327
|
+
'LimitedAvailability',
|
|
328
|
+
'Discontinued',
|
|
329
|
+
'BackOrder',
|
|
330
|
+
];
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Scoring weights for different categories
|
|
334
|
+
*/
|
|
335
|
+
export const CATEGORY_WEIGHTS: Record<CheckCategory, number> = {
|
|
336
|
+
completeness: 25,
|
|
337
|
+
identifiers: 15,
|
|
338
|
+
images: 20,
|
|
339
|
+
pricing: 20,
|
|
340
|
+
descriptions: 10,
|
|
341
|
+
categories: 5,
|
|
342
|
+
availability: 5,
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Grade thresholds (aligned with all other scoring models)
|
|
347
|
+
*/
|
|
348
|
+
export const GRADE_THRESHOLDS = {
|
|
349
|
+
A: 90,
|
|
350
|
+
B: 80,
|
|
351
|
+
C: 70,
|
|
352
|
+
D: 60,
|
|
353
|
+
F: 0,
|
|
354
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UCP Profile Generator Module
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export { buildProfile, generateMinimalProfile } from './profile-builder.js';
|
|
6
|
+
export { generateSigningKeyPair, validatePublicKey } from './key-generator.js';
|
|
7
|
+
export type { KeyAlgorithm, KeyPairResult } from './key-generator.js';
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Signing Key Generator for UCP Webhook Signing
|
|
3
|
+
* Generates EC or RSA key pairs in JWK format
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as jose from 'jose';
|
|
7
|
+
import { nanoid } from 'nanoid';
|
|
8
|
+
import type { JwkKey } from '../types/ucp-profile.js';
|
|
9
|
+
|
|
10
|
+
export type KeyAlgorithm = 'ES256' | 'RS256';
|
|
11
|
+
|
|
12
|
+
export interface KeyPairResult {
|
|
13
|
+
publicKey: JwkKey;
|
|
14
|
+
privateKey: string; // PEM format for merchant storage
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Generate a new signing key pair
|
|
19
|
+
*/
|
|
20
|
+
export async function generateSigningKeyPair(
|
|
21
|
+
algorithm: KeyAlgorithm = 'ES256'
|
|
22
|
+
): Promise<KeyPairResult> {
|
|
23
|
+
const keyId = `ucp-${nanoid(12)}`;
|
|
24
|
+
|
|
25
|
+
if (algorithm === 'ES256') {
|
|
26
|
+
return generateECKeyPair(keyId);
|
|
27
|
+
} else {
|
|
28
|
+
return generateRSAKeyPair(keyId);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Generate EC (P-256) key pair
|
|
34
|
+
*/
|
|
35
|
+
async function generateECKeyPair(keyId: string): Promise<KeyPairResult> {
|
|
36
|
+
const { publicKey, privateKey } = await jose.generateKeyPair('ES256', {
|
|
37
|
+
extractable: true,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Export public key as JWK
|
|
41
|
+
const publicJwk = await jose.exportJWK(publicKey);
|
|
42
|
+
|
|
43
|
+
// Export private key as PEM
|
|
44
|
+
const privatePem = await jose.exportPKCS8(privateKey);
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
publicKey: {
|
|
48
|
+
kty: 'EC',
|
|
49
|
+
kid: keyId,
|
|
50
|
+
use: 'sig',
|
|
51
|
+
alg: 'ES256',
|
|
52
|
+
crv: publicJwk.crv as string,
|
|
53
|
+
x: publicJwk.x as string,
|
|
54
|
+
y: publicJwk.y as string,
|
|
55
|
+
},
|
|
56
|
+
privateKey: privatePem,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Generate RSA (2048-bit) key pair
|
|
62
|
+
*/
|
|
63
|
+
async function generateRSAKeyPair(keyId: string): Promise<KeyPairResult> {
|
|
64
|
+
const { publicKey, privateKey } = await jose.generateKeyPair('RS256', {
|
|
65
|
+
extractable: true,
|
|
66
|
+
modulusLength: 2048,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Export public key as JWK
|
|
70
|
+
const publicJwk = await jose.exportJWK(publicKey);
|
|
71
|
+
|
|
72
|
+
// Export private key as PEM
|
|
73
|
+
const privatePem = await jose.exportPKCS8(privateKey);
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
publicKey: {
|
|
77
|
+
kty: 'RSA',
|
|
78
|
+
kid: keyId,
|
|
79
|
+
use: 'sig',
|
|
80
|
+
alg: 'RS256',
|
|
81
|
+
n: publicJwk.n as string,
|
|
82
|
+
e: publicJwk.e as string,
|
|
83
|
+
},
|
|
84
|
+
privateKey: privatePem,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Validate a JWK public key structure
|
|
90
|
+
*/
|
|
91
|
+
export function validatePublicKey(key: JwkKey): string[] {
|
|
92
|
+
const errors: string[] = [];
|
|
93
|
+
|
|
94
|
+
if (!key.kty) {
|
|
95
|
+
errors.push('Missing required field: kty');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (!key.kid) {
|
|
99
|
+
errors.push('Missing required field: kid');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (key.kty === 'EC') {
|
|
103
|
+
if (!key.crv) errors.push('EC key missing curve (crv)');
|
|
104
|
+
if (!key.x) errors.push('EC key missing x coordinate');
|
|
105
|
+
if (!key.y) errors.push('EC key missing y coordinate');
|
|
106
|
+
if (key.crv && !['P-256', 'P-384', 'P-521'].includes(key.crv)) {
|
|
107
|
+
errors.push(`Unsupported EC curve: ${key.crv}`);
|
|
108
|
+
}
|
|
109
|
+
} else if (key.kty === 'RSA') {
|
|
110
|
+
if (!key.n) errors.push('RSA key missing modulus (n)');
|
|
111
|
+
if (!key.e) errors.push('RSA key missing exponent (e)');
|
|
112
|
+
} else if (key.kty) {
|
|
113
|
+
errors.push(`Unsupported key type: ${key.kty}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return errors;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Import and validate a public key from JWK
|
|
121
|
+
*/
|
|
122
|
+
export async function importPublicKey(jwk: JwkKey): Promise<jose.KeyLike | Uint8Array> {
|
|
123
|
+
return jose.importJWK(jwk as jose.JWK, jwk.alg);
|
|
124
|
+
}
|