@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,457 @@
1
+ /**
2
+ * Vercel Serverless Function: Generate Schema.org Snippets
3
+ * POST /api/generate-schema
4
+ *
5
+ * Generates ready-to-use JSON-LD snippets for:
6
+ * - MerchantReturnPolicy
7
+ * - OfferShippingDetails
8
+ * - Complete Product schema
9
+ */
10
+
11
+ // Default templates for common scenarios
12
+ const RETURN_POLICY_TEMPLATES = {
13
+ '30-day-free': {
14
+ '@context': 'https://schema.org',
15
+ '@type': 'MerchantReturnPolicy',
16
+ '@id': '#return-policy',
17
+ name: '30-Day Free Returns',
18
+ applicableCountry: 'US',
19
+ returnPolicyCategory: 'https://schema.org/MerchantReturnFiniteReturnWindow',
20
+ merchantReturnDays: 30,
21
+ returnMethod: 'https://schema.org/ReturnByMail',
22
+ returnFees: 'https://schema.org/FreeReturn',
23
+ returnShippingFeesAmount: {
24
+ '@type': 'MonetaryAmount',
25
+ value: 0,
26
+ currency: 'USD',
27
+ },
28
+ },
29
+ '14-day-free': {
30
+ '@context': 'https://schema.org',
31
+ '@type': 'MerchantReturnPolicy',
32
+ '@id': '#return-policy',
33
+ name: '14-Day Free Returns',
34
+ applicableCountry: 'US',
35
+ returnPolicyCategory: 'https://schema.org/MerchantReturnFiniteReturnWindow',
36
+ merchantReturnDays: 14,
37
+ returnMethod: 'https://schema.org/ReturnByMail',
38
+ returnFees: 'https://schema.org/FreeReturn',
39
+ },
40
+ '30-day-paid': {
41
+ '@context': 'https://schema.org',
42
+ '@type': 'MerchantReturnPolicy',
43
+ '@id': '#return-policy',
44
+ name: '30-Day Returns',
45
+ applicableCountry: 'US',
46
+ returnPolicyCategory: 'https://schema.org/MerchantReturnFiniteReturnWindow',
47
+ merchantReturnDays: 30,
48
+ returnMethod: 'https://schema.org/ReturnByMail',
49
+ returnFees: 'https://schema.org/ReturnShippingFees',
50
+ },
51
+ 'no-returns': {
52
+ '@context': 'https://schema.org',
53
+ '@type': 'MerchantReturnPolicy',
54
+ '@id': '#return-policy',
55
+ name: 'Final Sale - No Returns',
56
+ applicableCountry: 'US',
57
+ returnPolicyCategory: 'https://schema.org/MerchantReturnNotPermitted',
58
+ },
59
+ };
60
+
61
+ const SHIPPING_TEMPLATES = {
62
+ 'us-standard': {
63
+ '@context': 'https://schema.org',
64
+ '@type': 'OfferShippingDetails',
65
+ '@id': '#shipping-us-standard',
66
+ shippingRate: {
67
+ '@type': 'MonetaryAmount',
68
+ value: 5.99,
69
+ currency: 'USD',
70
+ },
71
+ shippingDestination: {
72
+ '@type': 'DefinedRegion',
73
+ addressCountry: 'US',
74
+ },
75
+ deliveryTime: {
76
+ '@type': 'ShippingDeliveryTime',
77
+ handlingTime: {
78
+ '@type': 'QuantitativeValue',
79
+ minValue: 1,
80
+ maxValue: 2,
81
+ unitCode: 'd',
82
+ },
83
+ transitTime: {
84
+ '@type': 'QuantitativeValue',
85
+ minValue: 3,
86
+ maxValue: 7,
87
+ unitCode: 'd',
88
+ },
89
+ },
90
+ },
91
+ 'us-free': {
92
+ '@context': 'https://schema.org',
93
+ '@type': 'OfferShippingDetails',
94
+ '@id': '#shipping-us-free',
95
+ shippingRate: {
96
+ '@type': 'MonetaryAmount',
97
+ value: 0,
98
+ currency: 'USD',
99
+ },
100
+ shippingDestination: {
101
+ '@type': 'DefinedRegion',
102
+ addressCountry: 'US',
103
+ },
104
+ deliveryTime: {
105
+ '@type': 'ShippingDeliveryTime',
106
+ handlingTime: {
107
+ '@type': 'QuantitativeValue',
108
+ minValue: 1,
109
+ maxValue: 2,
110
+ unitCode: 'd',
111
+ },
112
+ transitTime: {
113
+ '@type': 'QuantitativeValue',
114
+ minValue: 5,
115
+ maxValue: 10,
116
+ unitCode: 'd',
117
+ },
118
+ },
119
+ },
120
+ 'us-express': {
121
+ '@context': 'https://schema.org',
122
+ '@type': 'OfferShippingDetails',
123
+ '@id': '#shipping-us-express',
124
+ shippingRate: {
125
+ '@type': 'MonetaryAmount',
126
+ value: 14.99,
127
+ currency: 'USD',
128
+ },
129
+ shippingDestination: {
130
+ '@type': 'DefinedRegion',
131
+ addressCountry: 'US',
132
+ },
133
+ deliveryTime: {
134
+ '@type': 'ShippingDeliveryTime',
135
+ handlingTime: {
136
+ '@type': 'QuantitativeValue',
137
+ minValue: 0,
138
+ maxValue: 1,
139
+ unitCode: 'd',
140
+ },
141
+ transitTime: {
142
+ '@type': 'QuantitativeValue',
143
+ minValue: 1,
144
+ maxValue: 2,
145
+ unitCode: 'd',
146
+ },
147
+ },
148
+ },
149
+ 'international': {
150
+ '@context': 'https://schema.org',
151
+ '@type': 'OfferShippingDetails',
152
+ '@id': '#shipping-international',
153
+ shippingRate: {
154
+ '@type': 'MonetaryAmount',
155
+ value: 19.99,
156
+ currency: 'USD',
157
+ },
158
+ shippingDestination: {
159
+ '@type': 'DefinedRegion',
160
+ addressCountry: ['CA', 'GB', 'AU', 'DE', 'FR'],
161
+ },
162
+ deliveryTime: {
163
+ '@type': 'ShippingDeliveryTime',
164
+ handlingTime: {
165
+ '@type': 'QuantitativeValue',
166
+ minValue: 1,
167
+ maxValue: 3,
168
+ unitCode: 'd',
169
+ },
170
+ transitTime: {
171
+ '@type': 'QuantitativeValue',
172
+ minValue: 7,
173
+ maxValue: 21,
174
+ unitCode: 'd',
175
+ },
176
+ },
177
+ },
178
+ };
179
+
180
+ function generateCustomReturnPolicy(options) {
181
+ const {
182
+ country = 'US',
183
+ returnDays = 30,
184
+ freeReturns = true,
185
+ returnFee = 0,
186
+ currency = 'USD',
187
+ policyName = `${returnDays}-Day Returns`,
188
+ noReturns = false,
189
+ } = options;
190
+
191
+ if (noReturns) {
192
+ return {
193
+ '@context': 'https://schema.org',
194
+ '@type': 'MerchantReturnPolicy',
195
+ '@id': '#return-policy',
196
+ name: policyName || 'Final Sale - No Returns',
197
+ applicableCountry: country,
198
+ returnPolicyCategory: 'https://schema.org/MerchantReturnNotPermitted',
199
+ };
200
+ }
201
+
202
+ const policy = {
203
+ '@context': 'https://schema.org',
204
+ '@type': 'MerchantReturnPolicy',
205
+ '@id': '#return-policy',
206
+ name: policyName,
207
+ applicableCountry: country,
208
+ returnPolicyCategory: 'https://schema.org/MerchantReturnFiniteReturnWindow',
209
+ merchantReturnDays: returnDays,
210
+ returnMethod: 'https://schema.org/ReturnByMail',
211
+ };
212
+
213
+ if (freeReturns) {
214
+ policy.returnFees = 'https://schema.org/FreeReturn';
215
+ } else {
216
+ policy.returnFees = 'https://schema.org/ReturnShippingFees';
217
+ if (returnFee > 0) {
218
+ policy.returnShippingFeesAmount = {
219
+ '@type': 'MonetaryAmount',
220
+ value: returnFee,
221
+ currency: currency,
222
+ };
223
+ }
224
+ }
225
+
226
+ return policy;
227
+ }
228
+
229
+ function generateCustomShipping(options) {
230
+ const {
231
+ country = 'US',
232
+ rate = 5.99,
233
+ currency = 'USD',
234
+ freeShipping = false,
235
+ handlingMin = 1,
236
+ handlingMax = 2,
237
+ transitMin = 3,
238
+ transitMax = 7,
239
+ } = options;
240
+
241
+ return {
242
+ '@context': 'https://schema.org',
243
+ '@type': 'OfferShippingDetails',
244
+ '@id': `#shipping-${country.toLowerCase()}`,
245
+ shippingRate: {
246
+ '@type': 'MonetaryAmount',
247
+ value: freeShipping ? 0 : rate,
248
+ currency: currency,
249
+ },
250
+ shippingDestination: {
251
+ '@type': 'DefinedRegion',
252
+ addressCountry: country,
253
+ },
254
+ deliveryTime: {
255
+ '@type': 'ShippingDeliveryTime',
256
+ handlingTime: {
257
+ '@type': 'QuantitativeValue',
258
+ minValue: handlingMin,
259
+ maxValue: handlingMax,
260
+ unitCode: 'd',
261
+ },
262
+ transitTime: {
263
+ '@type': 'QuantitativeValue',
264
+ minValue: transitMin,
265
+ maxValue: transitMax,
266
+ unitCode: 'd',
267
+ },
268
+ },
269
+ };
270
+ }
271
+
272
+ function generateProductSchema(options) {
273
+ const {
274
+ name,
275
+ description,
276
+ image,
277
+ sku,
278
+ brand,
279
+ price,
280
+ currency = 'USD',
281
+ availability = 'InStock',
282
+ condition = 'NewCondition',
283
+ includeReturnPolicy = true,
284
+ includeShipping = true,
285
+ returnPolicyRef = '#return-policy',
286
+ shippingRef = '#shipping-us-standard',
287
+ } = options;
288
+
289
+ const product = {
290
+ '@context': 'https://schema.org',
291
+ '@type': 'Product',
292
+ name: name || 'Product Name',
293
+ description: description || 'Product description goes here. Aim for 150-300 characters to help AI agents understand your product.',
294
+ };
295
+
296
+ if (image) {
297
+ product.image = image;
298
+ }
299
+
300
+ if (sku) {
301
+ product.sku = sku;
302
+ }
303
+
304
+ if (brand) {
305
+ product.brand = {
306
+ '@type': 'Brand',
307
+ name: brand,
308
+ };
309
+ }
310
+
311
+ product.offers = {
312
+ '@type': 'Offer',
313
+ price: price || 29.99,
314
+ priceCurrency: currency,
315
+ availability: `https://schema.org/${availability}`,
316
+ itemCondition: `https://schema.org/${condition}`,
317
+ };
318
+
319
+ if (includeReturnPolicy) {
320
+ product.offers.hasMerchantReturnPolicy = { '@id': returnPolicyRef };
321
+ }
322
+
323
+ if (includeShipping) {
324
+ product.offers.shippingDetails = { '@id': shippingRef };
325
+ }
326
+
327
+ return product;
328
+ }
329
+
330
+ function generateCompleteSchema(options) {
331
+ const {
332
+ returnPolicy = '30-day-free',
333
+ shipping = 'us-standard',
334
+ customReturn,
335
+ customShipping,
336
+ product,
337
+ } = options;
338
+
339
+ const schemas = [];
340
+
341
+ // Add return policy
342
+ if (customReturn) {
343
+ schemas.push(generateCustomReturnPolicy(customReturn));
344
+ } else if (RETURN_POLICY_TEMPLATES[returnPolicy]) {
345
+ schemas.push(RETURN_POLICY_TEMPLATES[returnPolicy]);
346
+ }
347
+
348
+ // Add shipping
349
+ if (customShipping) {
350
+ schemas.push(generateCustomShipping(customShipping));
351
+ } else if (SHIPPING_TEMPLATES[shipping]) {
352
+ schemas.push(SHIPPING_TEMPLATES[shipping]);
353
+ }
354
+
355
+ // Add product if provided
356
+ if (product) {
357
+ schemas.push(generateProductSchema(product));
358
+ }
359
+
360
+ return schemas;
361
+ }
362
+
363
+ export default async function handler(req, res) {
364
+ // CORS headers
365
+ res.setHeader('Access-Control-Allow-Origin', '*');
366
+ res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
367
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
368
+
369
+ if (req.method === 'OPTIONS') {
370
+ return res.status(200).end();
371
+ }
372
+
373
+ // GET: Return available templates
374
+ if (req.method === 'GET') {
375
+ return res.status(200).json({
376
+ templates: {
377
+ returnPolicy: Object.keys(RETURN_POLICY_TEMPLATES),
378
+ shipping: Object.keys(SHIPPING_TEMPLATES),
379
+ },
380
+ description: 'Use POST to generate custom Schema.org snippets',
381
+ });
382
+ }
383
+
384
+ if (req.method !== 'POST') {
385
+ return res.status(405).json({ error: 'Method not allowed' });
386
+ }
387
+
388
+ const {
389
+ type, // 'return-policy', 'shipping', 'product', 'complete'
390
+ template, // template name for quick generation
391
+ options = {}, // custom options
392
+ } = req.body;
393
+
394
+ try {
395
+ let schema;
396
+ let embedCode;
397
+
398
+ switch (type) {
399
+ case 'return-policy':
400
+ if (template && RETURN_POLICY_TEMPLATES[template]) {
401
+ schema = RETURN_POLICY_TEMPLATES[template];
402
+ } else {
403
+ schema = generateCustomReturnPolicy(options);
404
+ }
405
+ break;
406
+
407
+ case 'shipping':
408
+ if (template && SHIPPING_TEMPLATES[template]) {
409
+ schema = SHIPPING_TEMPLATES[template];
410
+ } else {
411
+ schema = generateCustomShipping(options);
412
+ }
413
+ break;
414
+
415
+ case 'product':
416
+ schema = generateProductSchema(options);
417
+ break;
418
+
419
+ case 'complete':
420
+ schema = generateCompleteSchema(options);
421
+ break;
422
+
423
+ default:
424
+ return res.status(400).json({
425
+ error: 'Invalid type. Use: return-policy, shipping, product, or complete',
426
+ availableTemplates: {
427
+ returnPolicy: Object.keys(RETURN_POLICY_TEMPLATES),
428
+ shipping: Object.keys(SHIPPING_TEMPLATES),
429
+ },
430
+ });
431
+ }
432
+
433
+ // Generate embed code
434
+ const schemaJson = JSON.stringify(schema, null, 2);
435
+ embedCode = `<script type="application/ld+json">
436
+ ${schemaJson}
437
+ </script>`;
438
+
439
+ return res.status(200).json({
440
+ success: true,
441
+ type,
442
+ schema,
443
+ embedCode,
444
+ instructions: [
445
+ 'Copy the embedCode and paste it into your HTML <head> section',
446
+ 'For products, add the schema to each product page',
447
+ 'Use the @id references to link return policy and shipping to products',
448
+ 'Validate at https://validator.schema.org/',
449
+ ],
450
+ });
451
+ } catch (error) {
452
+ return res.status(500).json({
453
+ error: 'Failed to generate schema',
454
+ message: error.message,
455
+ });
456
+ }
457
+ }
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Vercel Serverless Function: Generate UCP Profile
3
+ * POST /api/generate
4
+ */
5
+
6
+ function generateProfile({ domain, endpoint, capabilities }) {
7
+ const today = new Date().toISOString().split('T')[0];
8
+
9
+ const profile = {
10
+ ucp: {
11
+ version: today,
12
+ services: {
13
+ shopping: {
14
+ version: "1.0",
15
+ spec: "https://ucp.dev/specs/shopping/1.0",
16
+ rest: {
17
+ endpoint: endpoint,
18
+ auth: {
19
+ type: "oauth2",
20
+ flows: ["client_credentials"],
21
+ token_url: `${endpoint}/oauth/token`
22
+ }
23
+ }
24
+ }
25
+ },
26
+ capabilities: []
27
+ }
28
+ };
29
+
30
+ // Always add checkout (required)
31
+ profile.ucp.capabilities.push({
32
+ name: "dev.ucp.shopping.checkout",
33
+ version: "1.0",
34
+ spec: "https://ucp.dev/specs/shopping/checkout/1.0",
35
+ schema: "https://ucp.dev/schemas/shopping/checkout/1.0.json"
36
+ });
37
+
38
+ // Add optional capabilities
39
+ if (capabilities?.order) {
40
+ profile.ucp.capabilities.push({
41
+ name: "dev.ucp.shopping.order",
42
+ version: "1.0",
43
+ spec: "https://ucp.dev/specs/shopping/order/1.0",
44
+ schema: "https://ucp.dev/schemas/shopping/order/1.0.json"
45
+ });
46
+
47
+ // Add signing_keys for order capability
48
+ profile.signing_keys = [
49
+ {
50
+ kid: `${domain.replace(/\./g, '-')}-key-1`,
51
+ alg: "ES256",
52
+ use: "sig",
53
+ kty: "EC",
54
+ crv: "P-256",
55
+ x: "PLACEHOLDER_X_COORDINATE",
56
+ y: "PLACEHOLDER_Y_COORDINATE"
57
+ }
58
+ ];
59
+ }
60
+
61
+ if (capabilities?.fulfillment) {
62
+ profile.ucp.capabilities.push({
63
+ name: "dev.ucp.shopping.fulfillment",
64
+ version: "1.0",
65
+ spec: "https://ucp.dev/specs/shopping/fulfillment/1.0",
66
+ schema: "https://ucp.dev/schemas/shopping/fulfillment/1.0.json"
67
+ });
68
+ }
69
+
70
+ if (capabilities?.discount) {
71
+ profile.ucp.capabilities.push({
72
+ name: "dev.ucp.shopping.discount",
73
+ version: "1.0",
74
+ spec: "https://ucp.dev/specs/shopping/discount/1.0",
75
+ schema: "https://ucp.dev/schemas/shopping/discount/1.0.json"
76
+ });
77
+ }
78
+
79
+ return profile;
80
+ }
81
+
82
+ export default async function handler(req, res) {
83
+ // CORS headers
84
+ res.setHeader('Access-Control-Allow-Origin', '*');
85
+ res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
86
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
87
+
88
+ if (req.method === 'OPTIONS') {
89
+ return res.status(200).end();
90
+ }
91
+
92
+ if (req.method !== 'POST') {
93
+ return res.status(405).json({ error: 'Method not allowed' });
94
+ }
95
+
96
+ const { domain, endpoint, capabilities } = req.body;
97
+
98
+ if (!domain) {
99
+ return res.status(400).json({ error: 'Missing required field: domain' });
100
+ }
101
+
102
+ if (!endpoint) {
103
+ return res.status(400).json({ error: 'Missing required field: endpoint' });
104
+ }
105
+
106
+ if (!endpoint.startsWith('https://')) {
107
+ return res.status(400).json({ error: 'Endpoint must use HTTPS' });
108
+ }
109
+
110
+ const cleanDomain = domain.replace(/^https?:\/\//, '').replace(/\/$/, '');
111
+ const cleanEndpoint = endpoint.replace(/\/$/, '');
112
+
113
+ const profile = generateProfile({
114
+ domain: cleanDomain,
115
+ endpoint: cleanEndpoint,
116
+ capabilities: capabilities || { checkout: true }
117
+ });
118
+
119
+ return res.status(200).json({
120
+ ok: true,
121
+ domain: cleanDomain,
122
+ profile,
123
+ deploy_to: `https://${cleanDomain}/.well-known/ucp`,
124
+ instructions: [
125
+ 'Save this JSON as "ucp" (no extension) or "ucp.json"',
126
+ `Upload to your server at /.well-known/ucp`,
127
+ 'Set Content-Type: application/json',
128
+ 'Add CORS header: Access-Control-Allow-Origin: *',
129
+ profile.signing_keys ? 'Replace PLACEHOLDER values in signing_keys with your actual EC key' : null
130
+ ].filter(Boolean)
131
+ });
132
+ }