@ucptools/validator 1.0.0 → 1.0.1
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/settings.local.json +60 -0
- package/.vercel/README.txt +11 -0
- package/.vercel/project.json +1 -0
- package/dist/cli/index.d.ts +6 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +279 -0
- package/dist/cli/index.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 +11 -0
- package/dist/db/index.d.ts.map +1 -0
- package/dist/db/index.js +63 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/schema.d.ts +444 -0
- package/dist/db/schema.d.ts.map +1 -0
- package/dist/db/schema.js +65 -0
- package/dist/db/schema.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} +642 -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 +204 -0
- package/dist/feed-analyzer/types.d.ts.map +1 -0
- package/dist/feed-analyzer/types.js +162 -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/{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 +541 -0
- package/dist/security/security-scanner.js.map +1 -0
- package/dist/security/types.d.ts +48 -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/directory.d.ts +104 -0
- package/dist/services/directory.d.ts.map +1 -0
- package/dist/services/directory.js +333 -0
- package/dist/services/directory.js.map +1 -0
- package/dist/simulator/agent-simulator.d.ts +69 -0
- package/dist/simulator/agent-simulator.d.ts.map +1 -0
- package/{src/simulator/agent-simulator.ts → dist/simulator/agent-simulator.js} +650 -941
- 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} +145 -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/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 +103 -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 +68 -0
- package/dist/types/validation.d.ts.map +1 -0
- package/dist/types/validation.js +32 -0
- package/dist/types/validation.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 +11 -0
- package/dist/validator/rules-validator.d.ts.map +1 -0
- package/dist/validator/rules-validator.js +257 -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 +415 -0
- package/dist/validator/structural-validator.js.map +1 -0
- package/package.json +1 -1
- package/publish-output.txt +0 -0
- 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/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/vercel.json +0 -34
- package/vitest.config.ts +0 -22
|
@@ -1,434 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Directory Service
|
|
3
|
-
* Business logic for the UCP Merchant Directory
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { eq, ilike, sql, desc, asc, and, count, SQL } from 'drizzle-orm';
|
|
7
|
-
import { getDb, merchants, type Merchant, type NewMerchant } from '../db/index.js';
|
|
8
|
-
|
|
9
|
-
// Types
|
|
10
|
-
export interface ListMerchantsParams {
|
|
11
|
-
page?: number;
|
|
12
|
-
limit?: number;
|
|
13
|
-
category?: string;
|
|
14
|
-
country?: string;
|
|
15
|
-
search?: string;
|
|
16
|
-
sort?: 'score' | 'domain' | 'displayName' | 'createdAt';
|
|
17
|
-
order?: 'asc' | 'desc';
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface ListMerchantsResult {
|
|
21
|
-
merchants: Merchant[];
|
|
22
|
-
pagination: {
|
|
23
|
-
page: number;
|
|
24
|
-
limit: number;
|
|
25
|
-
total: number;
|
|
26
|
-
totalPages: number;
|
|
27
|
-
};
|
|
28
|
-
filters: {
|
|
29
|
-
categories: { name: string; count: number }[];
|
|
30
|
-
countries: { code: string; count: number }[];
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export interface SubmitMerchantParams {
|
|
35
|
-
domain: string;
|
|
36
|
-
displayName?: string;
|
|
37
|
-
description?: string;
|
|
38
|
-
logoUrl?: string;
|
|
39
|
-
websiteUrl?: string;
|
|
40
|
-
category?: string;
|
|
41
|
-
countryCode?: string;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export interface DirectoryStats {
|
|
45
|
-
totalMerchants: number;
|
|
46
|
-
verifiedMerchants: number;
|
|
47
|
-
avgScore: number;
|
|
48
|
-
totalCategories: number;
|
|
49
|
-
totalCountries: number;
|
|
50
|
-
gradeDistribution: { grade: string; count: number }[];
|
|
51
|
-
topCategories: { name: string; count: number }[];
|
|
52
|
-
recentAdditions: { domain: string; displayName: string; grade: string | null; addedAt: Date }[];
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export interface ValidationResult {
|
|
56
|
-
valid: boolean;
|
|
57
|
-
error?: string;
|
|
58
|
-
score?: number;
|
|
59
|
-
grade?: string;
|
|
60
|
-
transports?: string;
|
|
61
|
-
ucpVersion?: string;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// UCP Profile structure (minimal typing for validation)
|
|
65
|
-
interface UcpProfile {
|
|
66
|
-
ucp?: {
|
|
67
|
-
version?: string;
|
|
68
|
-
services?: Record<string, { rest?: unknown; mcp?: unknown; a2a?: unknown; embedded?: unknown }>;
|
|
69
|
-
capabilities?: Array<{ name: string }>;
|
|
70
|
-
};
|
|
71
|
-
signing_keys?: unknown[];
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Fetch UCP profile from a URL, return null if not valid JSON
|
|
76
|
-
*/
|
|
77
|
-
async function tryFetchProfile(url: string): Promise<UcpProfile | null> {
|
|
78
|
-
try {
|
|
79
|
-
const res = await fetch(url, {
|
|
80
|
-
headers: {
|
|
81
|
-
Accept: 'application/json',
|
|
82
|
-
'User-Agent': 'UCP-Directory/1.0 (https://ucptools.dev)',
|
|
83
|
-
},
|
|
84
|
-
signal: AbortSignal.timeout(10000),
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
if (!res.ok) return null;
|
|
88
|
-
|
|
89
|
-
const text = await res.text();
|
|
90
|
-
// Check if response looks like JSON (not HTML)
|
|
91
|
-
if (text.trim().startsWith('<')) return null;
|
|
92
|
-
|
|
93
|
-
return JSON.parse(text) as UcpProfile;
|
|
94
|
-
} catch {
|
|
95
|
-
return null;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Validate a domain's UCP profile
|
|
101
|
-
*/
|
|
102
|
-
export async function validateDomain(domain: string): Promise<ValidationResult> {
|
|
103
|
-
// Try both /.well-known/ucp and /.well-known/ucp.json
|
|
104
|
-
const urls = [
|
|
105
|
-
`https://${domain}/.well-known/ucp`,
|
|
106
|
-
`https://${domain}/.well-known/ucp.json`,
|
|
107
|
-
];
|
|
108
|
-
|
|
109
|
-
let profile: UcpProfile | null = null;
|
|
110
|
-
|
|
111
|
-
for (const url of urls) {
|
|
112
|
-
profile = await tryFetchProfile(url);
|
|
113
|
-
if (profile) break;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
try {
|
|
117
|
-
if (!profile) {
|
|
118
|
-
return { valid: false, error: 'No UCP profile found at /.well-known/ucp or /.well-known/ucp.json' };
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// Basic validation
|
|
122
|
-
if (!profile?.ucp?.version || !profile?.ucp?.services) {
|
|
123
|
-
return { valid: false, error: 'Invalid UCP profile structure' };
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// Extract transports
|
|
127
|
-
const transports = new Set<string>();
|
|
128
|
-
for (const svc of Object.values(profile.ucp.services || {})) {
|
|
129
|
-
if (svc.rest) transports.add('REST');
|
|
130
|
-
if (svc.mcp) transports.add('MCP');
|
|
131
|
-
if (svc.a2a) transports.add('A2A');
|
|
132
|
-
if (svc.embedded) transports.add('Embedded');
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// Simple scoring
|
|
136
|
-
let score = 50;
|
|
137
|
-
const capabilities = profile.ucp.capabilities || [];
|
|
138
|
-
if (capabilities.some((c) => c.name === 'dev.ucp.shopping.checkout')) score += 20;
|
|
139
|
-
if (capabilities.some((c) => c.name === 'dev.ucp.shopping.cart')) score += 10;
|
|
140
|
-
if (capabilities.some((c) => c.name === 'dev.ucp.shopping.order')) score += 10;
|
|
141
|
-
if (profile.signing_keys && profile.signing_keys.length > 0) score += 10;
|
|
142
|
-
|
|
143
|
-
const grade = score >= 90 ? 'A' : score >= 80 ? 'B' : score >= 70 ? 'C' : score >= 60 ? 'D' : 'F';
|
|
144
|
-
|
|
145
|
-
return {
|
|
146
|
-
valid: true,
|
|
147
|
-
score,
|
|
148
|
-
grade,
|
|
149
|
-
transports: Array.from(transports).join(','),
|
|
150
|
-
ucpVersion: profile.ucp.version,
|
|
151
|
-
};
|
|
152
|
-
} catch (e: unknown) {
|
|
153
|
-
const message = e instanceof Error ? e.message : 'Failed to fetch profile';
|
|
154
|
-
return { valid: false, error: message };
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* List merchants with pagination and filters
|
|
160
|
-
*/
|
|
161
|
-
export async function listMerchants(params: ListMerchantsParams): Promise<ListMerchantsResult> {
|
|
162
|
-
const db = getDb();
|
|
163
|
-
|
|
164
|
-
const page = Math.max(1, params.page || 1);
|
|
165
|
-
const limit = Math.min(100, Math.max(1, params.limit || 20));
|
|
166
|
-
const offset = (page - 1) * limit;
|
|
167
|
-
|
|
168
|
-
// Build where conditions
|
|
169
|
-
const conditions: SQL[] = [eq(merchants.isPublic, true)];
|
|
170
|
-
|
|
171
|
-
if (params.category) {
|
|
172
|
-
conditions.push(eq(merchants.category, params.category));
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
if (params.country) {
|
|
176
|
-
conditions.push(eq(merchants.countryCode, params.country.toUpperCase()));
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
if (params.search) {
|
|
180
|
-
conditions.push(
|
|
181
|
-
sql`(${merchants.domain} ILIKE ${`%${params.search}%`} OR ${merchants.displayName} ILIKE ${`%${params.search}%`})`
|
|
182
|
-
);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
const whereClause = and(...conditions);
|
|
186
|
-
|
|
187
|
-
// Determine sort column and order
|
|
188
|
-
const sortColumn =
|
|
189
|
-
params.sort === 'domain'
|
|
190
|
-
? merchants.domain
|
|
191
|
-
: params.sort === 'displayName'
|
|
192
|
-
? merchants.displayName
|
|
193
|
-
: params.sort === 'createdAt'
|
|
194
|
-
? merchants.createdAt
|
|
195
|
-
: merchants.ucpScore;
|
|
196
|
-
|
|
197
|
-
const orderFn = params.order === 'asc' ? asc : desc;
|
|
198
|
-
|
|
199
|
-
// Get total count
|
|
200
|
-
const countResult = await db
|
|
201
|
-
.select({ total: count() })
|
|
202
|
-
.from(merchants)
|
|
203
|
-
.where(whereClause);
|
|
204
|
-
|
|
205
|
-
const total = countResult[0]?.total || 0;
|
|
206
|
-
|
|
207
|
-
// Get merchants
|
|
208
|
-
const merchantList = await db
|
|
209
|
-
.select()
|
|
210
|
-
.from(merchants)
|
|
211
|
-
.where(whereClause)
|
|
212
|
-
.orderBy(orderFn(sortColumn))
|
|
213
|
-
.limit(limit)
|
|
214
|
-
.offset(offset);
|
|
215
|
-
|
|
216
|
-
// Get category counts
|
|
217
|
-
const categoryResult = await db
|
|
218
|
-
.select({
|
|
219
|
-
category: merchants.category,
|
|
220
|
-
count: count(),
|
|
221
|
-
})
|
|
222
|
-
.from(merchants)
|
|
223
|
-
.where(and(eq(merchants.isPublic, true), sql`${merchants.category} IS NOT NULL`))
|
|
224
|
-
.groupBy(merchants.category)
|
|
225
|
-
.orderBy(desc(count()));
|
|
226
|
-
|
|
227
|
-
// Get country counts
|
|
228
|
-
const countryResult = await db
|
|
229
|
-
.select({
|
|
230
|
-
countryCode: merchants.countryCode,
|
|
231
|
-
count: count(),
|
|
232
|
-
})
|
|
233
|
-
.from(merchants)
|
|
234
|
-
.where(and(eq(merchants.isPublic, true), sql`${merchants.countryCode} IS NOT NULL`))
|
|
235
|
-
.groupBy(merchants.countryCode)
|
|
236
|
-
.orderBy(desc(count()));
|
|
237
|
-
|
|
238
|
-
return {
|
|
239
|
-
merchants: merchantList,
|
|
240
|
-
pagination: {
|
|
241
|
-
page,
|
|
242
|
-
limit,
|
|
243
|
-
total,
|
|
244
|
-
totalPages: Math.ceil(total / limit),
|
|
245
|
-
},
|
|
246
|
-
filters: {
|
|
247
|
-
categories: categoryResult.map((r) => ({ name: r.category!, count: r.count })),
|
|
248
|
-
countries: countryResult.map((r) => ({ code: r.countryCode!, count: r.count })),
|
|
249
|
-
},
|
|
250
|
-
};
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
/**
|
|
254
|
-
* Get a single merchant by domain
|
|
255
|
-
*/
|
|
256
|
-
export async function getMerchantByDomain(domain: string): Promise<Merchant | null> {
|
|
257
|
-
const db = getDb();
|
|
258
|
-
const result = await db.select().from(merchants).where(eq(merchants.domain, domain.toLowerCase())).limit(1);
|
|
259
|
-
return result[0] || null;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
/**
|
|
263
|
-
* Submit a new merchant to the directory
|
|
264
|
-
*/
|
|
265
|
-
export async function submitMerchant(
|
|
266
|
-
params: SubmitMerchantParams
|
|
267
|
-
): Promise<{ success: boolean; merchant?: Merchant; error?: string; details?: string }> {
|
|
268
|
-
const db = getDb();
|
|
269
|
-
|
|
270
|
-
// Clean domain
|
|
271
|
-
const cleanDomain = params.domain
|
|
272
|
-
.replace(/^https?:\/\//, '')
|
|
273
|
-
.replace(/\/$/, '')
|
|
274
|
-
.split('/')[0]
|
|
275
|
-
.toLowerCase();
|
|
276
|
-
|
|
277
|
-
// Check if domain already exists
|
|
278
|
-
const existing = await getMerchantByDomain(cleanDomain);
|
|
279
|
-
if (existing) {
|
|
280
|
-
return {
|
|
281
|
-
success: false,
|
|
282
|
-
error: 'Domain already registered',
|
|
283
|
-
details: `Merchant ID: ${existing.id}`,
|
|
284
|
-
};
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
// Validate the domain has a valid UCP profile
|
|
288
|
-
const validation = await validateDomain(cleanDomain);
|
|
289
|
-
|
|
290
|
-
if (!validation.valid) {
|
|
291
|
-
return {
|
|
292
|
-
success: false,
|
|
293
|
-
error: 'Invalid UCP profile',
|
|
294
|
-
details: validation.error,
|
|
295
|
-
};
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
// Insert merchant
|
|
299
|
-
const newMerchant: NewMerchant = {
|
|
300
|
-
domain: cleanDomain,
|
|
301
|
-
displayName: params.displayName || cleanDomain,
|
|
302
|
-
description: params.description || null,
|
|
303
|
-
logoUrl: params.logoUrl || null,
|
|
304
|
-
websiteUrl: params.websiteUrl || `https://${cleanDomain}`,
|
|
305
|
-
category: params.category || null,
|
|
306
|
-
countryCode: params.countryCode?.toUpperCase() || null,
|
|
307
|
-
ucpScore: validation.score || null,
|
|
308
|
-
ucpGrade: validation.grade || null,
|
|
309
|
-
transports: validation.transports || null,
|
|
310
|
-
isPublic: true,
|
|
311
|
-
isVerified: false,
|
|
312
|
-
lastValidatedAt: new Date(),
|
|
313
|
-
};
|
|
314
|
-
|
|
315
|
-
const result = await db.insert(merchants).values(newMerchant).returning();
|
|
316
|
-
|
|
317
|
-
return {
|
|
318
|
-
success: true,
|
|
319
|
-
merchant: result[0],
|
|
320
|
-
};
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
/**
|
|
324
|
-
* Get directory statistics
|
|
325
|
-
*/
|
|
326
|
-
export async function getDirectoryStats(): Promise<DirectoryStats> {
|
|
327
|
-
const db = getDb();
|
|
328
|
-
|
|
329
|
-
// Overall stats
|
|
330
|
-
const statsResult = await db
|
|
331
|
-
.select({
|
|
332
|
-
totalMerchants: count(),
|
|
333
|
-
verifiedMerchants: sql<number>`COUNT(*) FILTER (WHERE ${merchants.isVerified} = true)`,
|
|
334
|
-
avgScore: sql<number>`COALESCE(AVG(${merchants.ucpScore}), 0)`,
|
|
335
|
-
})
|
|
336
|
-
.from(merchants)
|
|
337
|
-
.where(eq(merchants.isPublic, true));
|
|
338
|
-
|
|
339
|
-
// Count distinct categories and countries
|
|
340
|
-
const categoryCountResult = await db
|
|
341
|
-
.select({ count: sql<number>`COUNT(DISTINCT ${merchants.category})` })
|
|
342
|
-
.from(merchants)
|
|
343
|
-
.where(and(eq(merchants.isPublic, true), sql`${merchants.category} IS NOT NULL`));
|
|
344
|
-
|
|
345
|
-
const countryCountResult = await db
|
|
346
|
-
.select({ count: sql<number>`COUNT(DISTINCT ${merchants.countryCode})` })
|
|
347
|
-
.from(merchants)
|
|
348
|
-
.where(and(eq(merchants.isPublic, true), sql`${merchants.countryCode} IS NOT NULL`));
|
|
349
|
-
|
|
350
|
-
// Grade distribution
|
|
351
|
-
const gradeResult = await db
|
|
352
|
-
.select({
|
|
353
|
-
grade: merchants.ucpGrade,
|
|
354
|
-
count: count(),
|
|
355
|
-
})
|
|
356
|
-
.from(merchants)
|
|
357
|
-
.where(and(eq(merchants.isPublic, true), sql`${merchants.ucpGrade} IS NOT NULL`))
|
|
358
|
-
.groupBy(merchants.ucpGrade)
|
|
359
|
-
.orderBy(merchants.ucpGrade);
|
|
360
|
-
|
|
361
|
-
// Top categories
|
|
362
|
-
const topCategoriesResult = await db
|
|
363
|
-
.select({
|
|
364
|
-
name: merchants.category,
|
|
365
|
-
count: count(),
|
|
366
|
-
})
|
|
367
|
-
.from(merchants)
|
|
368
|
-
.where(and(eq(merchants.isPublic, true), sql`${merchants.category} IS NOT NULL`))
|
|
369
|
-
.groupBy(merchants.category)
|
|
370
|
-
.orderBy(desc(count()))
|
|
371
|
-
.limit(10);
|
|
372
|
-
|
|
373
|
-
// Recent additions
|
|
374
|
-
const recentResult = await db
|
|
375
|
-
.select({
|
|
376
|
-
domain: merchants.domain,
|
|
377
|
-
displayName: merchants.displayName,
|
|
378
|
-
grade: merchants.ucpGrade,
|
|
379
|
-
addedAt: merchants.createdAt,
|
|
380
|
-
})
|
|
381
|
-
.from(merchants)
|
|
382
|
-
.where(eq(merchants.isPublic, true))
|
|
383
|
-
.orderBy(desc(merchants.createdAt))
|
|
384
|
-
.limit(5);
|
|
385
|
-
|
|
386
|
-
const stats = statsResult[0];
|
|
387
|
-
|
|
388
|
-
return {
|
|
389
|
-
totalMerchants: stats?.totalMerchants || 0,
|
|
390
|
-
verifiedMerchants: stats?.verifiedMerchants || 0,
|
|
391
|
-
avgScore: Math.round(stats?.avgScore || 0),
|
|
392
|
-
totalCategories: categoryCountResult[0]?.count || 0,
|
|
393
|
-
totalCountries: countryCountResult[0]?.count || 0,
|
|
394
|
-
gradeDistribution: gradeResult.map((r) => ({ grade: r.grade!, count: r.count })),
|
|
395
|
-
topCategories: topCategoriesResult.map((r) => ({ name: r.name!, count: r.count })),
|
|
396
|
-
recentAdditions: recentResult.map((r) => ({
|
|
397
|
-
domain: r.domain,
|
|
398
|
-
displayName: r.displayName,
|
|
399
|
-
grade: r.grade,
|
|
400
|
-
addedAt: r.addedAt,
|
|
401
|
-
})),
|
|
402
|
-
};
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
/**
|
|
406
|
-
* Re-validate a merchant's UCP profile and update their record
|
|
407
|
-
*/
|
|
408
|
-
export async function revalidateMerchant(domain: string): Promise<{ success: boolean; error?: string }> {
|
|
409
|
-
const db = getDb();
|
|
410
|
-
|
|
411
|
-
const merchant = await getMerchantByDomain(domain);
|
|
412
|
-
if (!merchant) {
|
|
413
|
-
return { success: false, error: 'Merchant not found' };
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
const validation = await validateDomain(domain);
|
|
417
|
-
|
|
418
|
-
if (!validation.valid) {
|
|
419
|
-
return { success: false, error: validation.error };
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
await db
|
|
423
|
-
.update(merchants)
|
|
424
|
-
.set({
|
|
425
|
-
ucpScore: validation.score || null,
|
|
426
|
-
ucpGrade: validation.grade || null,
|
|
427
|
-
transports: validation.transports || null,
|
|
428
|
-
lastValidatedAt: new Date(),
|
|
429
|
-
updatedAt: new Date(),
|
|
430
|
-
})
|
|
431
|
-
.where(eq(merchants.domain, domain.toLowerCase()));
|
|
432
|
-
|
|
433
|
-
return { success: true };
|
|
434
|
-
}
|
package/src/types/generator.ts
DELETED
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Generator Types for UCP Profile Generator
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import type { Environment, ProfileStatus } from './ucp-profile.js';
|
|
6
|
-
|
|
7
|
-
// Merchant information
|
|
8
|
-
export interface MerchantInfo {
|
|
9
|
-
merchantId: string;
|
|
10
|
-
primaryDomain: string;
|
|
11
|
-
displayName?: string;
|
|
12
|
-
environment?: Environment;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
// Transport configuration inputs
|
|
16
|
-
export interface TransportConfig {
|
|
17
|
-
rest?: {
|
|
18
|
-
endpoint: string;
|
|
19
|
-
schemaUrl?: string; // Override default
|
|
20
|
-
};
|
|
21
|
-
mcp?: {
|
|
22
|
-
endpoint: string;
|
|
23
|
-
schemaUrl?: string;
|
|
24
|
-
};
|
|
25
|
-
a2a?: {
|
|
26
|
-
agentCardUrl: string;
|
|
27
|
-
};
|
|
28
|
-
embedded?: {
|
|
29
|
-
schemaUrl: string;
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Capability selection (based on official UCP spec)
|
|
34
|
-
export interface CapabilitySelection {
|
|
35
|
-
checkout: boolean; // Default: true
|
|
36
|
-
order: boolean; // Requires signing keys
|
|
37
|
-
fulfillment: boolean; // Extension of order
|
|
38
|
-
discount: boolean; // Extension
|
|
39
|
-
payment?: boolean; // Payment capability
|
|
40
|
-
buyerConsent?: boolean; // Buyer consent capability
|
|
41
|
-
customCapabilities?: CustomCapability[];
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// Custom/vendor capability
|
|
45
|
-
export interface CustomCapability {
|
|
46
|
-
namespace: string; // e.g., "com.myvendor"
|
|
47
|
-
name: string; // e.g., "custom-feature"
|
|
48
|
-
version: string;
|
|
49
|
-
specUrl: string;
|
|
50
|
-
schemaUrl: string;
|
|
51
|
-
extendsCapability?: string;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Security configuration
|
|
55
|
-
export interface SecurityConfig {
|
|
56
|
-
generateSigningKeys: boolean;
|
|
57
|
-
signingKeyAlgorithm?: 'ES256' | 'RS256';
|
|
58
|
-
uploadedPublicKey?: string; // PEM or JWK format
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Complete generator input
|
|
62
|
-
export interface GeneratorInput {
|
|
63
|
-
merchant: MerchantInfo;
|
|
64
|
-
transport: TransportConfig;
|
|
65
|
-
capabilities: CapabilitySelection;
|
|
66
|
-
security?: SecurityConfig;
|
|
67
|
-
ucpVersion?: string; // Override default version
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Generator output artifacts
|
|
71
|
-
export interface GeneratorOutput {
|
|
72
|
-
profile: object; // The UCP profile JSON
|
|
73
|
-
profileJson: string; // Formatted JSON string
|
|
74
|
-
installInstructions: string; // Markdown instructions
|
|
75
|
-
validationReport?: object; // Initial validation
|
|
76
|
-
signingKeyPair?: { // If keys were generated
|
|
77
|
-
publicKey: object; // JWK public key
|
|
78
|
-
privateKey: string; // PEM private key (for merchant to store securely)
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Hosting mode options
|
|
83
|
-
export type HostingMode = 'static' | 'edge-worker' | 'reverse-proxy';
|
|
84
|
-
|
|
85
|
-
// Hosting target platforms
|
|
86
|
-
export type HostingPlatform =
|
|
87
|
-
| 'nginx'
|
|
88
|
-
| 'apache'
|
|
89
|
-
| 'vercel'
|
|
90
|
-
| 'netlify'
|
|
91
|
-
| 'cloudflare-worker'
|
|
92
|
-
| 'cloudflare-pages'
|
|
93
|
-
| 's3-cloudfront'
|
|
94
|
-
| 'generic';
|
|
95
|
-
|
|
96
|
-
// Hosting configuration
|
|
97
|
-
export interface HostingConfig {
|
|
98
|
-
mode: HostingMode;
|
|
99
|
-
platform?: HostingPlatform;
|
|
100
|
-
merchantId: string;
|
|
101
|
-
merchantDomain: string;
|
|
102
|
-
hostedProfileUrl?: string; // For edge/proxy modes
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Install artifact
|
|
106
|
-
export interface InstallArtifact {
|
|
107
|
-
filename: string;
|
|
108
|
-
content: string;
|
|
109
|
-
contentType: 'json' | 'javascript' | 'nginx' | 'apache' | 'markdown';
|
|
110
|
-
description: string;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// Database models (for API service)
|
|
114
|
-
export interface MerchantRecord {
|
|
115
|
-
id: string;
|
|
116
|
-
primaryDomain: string;
|
|
117
|
-
displayName?: string;
|
|
118
|
-
createdAt: Date;
|
|
119
|
-
updatedAt: Date;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
export interface ProfileRecord {
|
|
123
|
-
id: string;
|
|
124
|
-
merchantId: string;
|
|
125
|
-
versionTag: string;
|
|
126
|
-
ucpVersion: string;
|
|
127
|
-
jsonBody: object;
|
|
128
|
-
status: ProfileStatus;
|
|
129
|
-
createdAt: Date;
|
|
130
|
-
updatedAt: Date;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
export interface ValidationRunRecord {
|
|
134
|
-
id: string;
|
|
135
|
-
profileId: string;
|
|
136
|
-
mode: 'draft' | 'remote';
|
|
137
|
-
result: object;
|
|
138
|
-
ok: boolean;
|
|
139
|
-
createdAt: Date;
|
|
140
|
-
}
|
package/src/types/ucp-profile.ts
DELETED
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* UCP (Universal Commerce Protocol) Profile Types
|
|
3
|
-
* Based on https://ucp.dev/specification/overview/
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
// Current UCP version
|
|
7
|
-
export const CURRENT_UCP_VERSION = '2026-01-11';
|
|
8
|
-
|
|
9
|
-
// Transport binding types
|
|
10
|
-
export interface RestTransport {
|
|
11
|
-
schema: string; // OpenAPI schema URL
|
|
12
|
-
endpoint: string; // REST API endpoint (https, no trailing slash)
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface McpTransport {
|
|
16
|
-
schema: string; // MCP schema URL
|
|
17
|
-
endpoint: string; // MCP endpoint URL
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface A2aTransport {
|
|
21
|
-
agentCard: string; // A2A agent card URL
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export interface EmbeddedTransport {
|
|
25
|
-
schema: string; // Embedded schema URL
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// Service definition with transport bindings
|
|
29
|
-
export interface UcpService {
|
|
30
|
-
version: string;
|
|
31
|
-
spec: string;
|
|
32
|
-
rest?: RestTransport;
|
|
33
|
-
mcp?: McpTransport;
|
|
34
|
-
a2a?: A2aTransport;
|
|
35
|
-
embedded?: EmbeddedTransport;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Capability definition
|
|
39
|
-
export interface UcpCapability {
|
|
40
|
-
name: string; // e.g., "dev.ucp.shopping.checkout"
|
|
41
|
-
version: string; // e.g., "2026-01-11"
|
|
42
|
-
spec: string; // Specification URL
|
|
43
|
-
schema: string; // JSON Schema URL
|
|
44
|
-
extends?: string; // Parent capability for extensions
|
|
45
|
-
config?: Record<string, unknown>; // Capability-specific settings
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// JWK (JSON Web Key) for signing
|
|
49
|
-
export interface JwkKey {
|
|
50
|
-
kty: string; // Key type (e.g., "EC", "RSA")
|
|
51
|
-
kid: string; // Key ID
|
|
52
|
-
use?: string; // Key use (e.g., "sig")
|
|
53
|
-
alg?: string; // Algorithm (e.g., "ES256")
|
|
54
|
-
crv?: string; // Curve (for EC keys)
|
|
55
|
-
x?: string; // X coordinate (for EC keys)
|
|
56
|
-
y?: string; // Y coordinate (for EC keys)
|
|
57
|
-
n?: string; // Modulus (for RSA keys)
|
|
58
|
-
e?: string; // Exponent (for RSA keys)
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Signing keys - array of JWK public keys at root level
|
|
62
|
-
export type SigningKeys = JwkKey[];
|
|
63
|
-
|
|
64
|
-
// Payment handler definition
|
|
65
|
-
export interface PaymentHandler {
|
|
66
|
-
id: string; // Handler identifier
|
|
67
|
-
name: string; // Display name
|
|
68
|
-
version: string; // Handler version (YYYY-MM-DD)
|
|
69
|
-
spec: string; // Handler specification URL
|
|
70
|
-
config_schema?: string; // Configuration schema URL
|
|
71
|
-
instrument_schemas?: string[]; // Payment instrument schemas
|
|
72
|
-
config?: Record<string, unknown>; // Handler-specific config
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Payment configuration
|
|
76
|
-
export interface PaymentConfig {
|
|
77
|
-
handlers: PaymentHandler[];
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Main UCP object within profile
|
|
81
|
-
export interface UcpObject {
|
|
82
|
-
version: string;
|
|
83
|
-
services: Record<string, UcpService>;
|
|
84
|
-
capabilities: UcpCapability[];
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Complete UCP Business Profile (/.well-known/ucp)
|
|
88
|
-
export interface UcpProfile {
|
|
89
|
-
ucp: UcpObject;
|
|
90
|
-
payment?: PaymentConfig; // Payment handlers configuration
|
|
91
|
-
signing_keys?: SigningKeys; // JWK public keys for webhook verification
|
|
92
|
-
// Additional vendor extensions can be added as siblings
|
|
93
|
-
[key: string]: unknown;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Known capability namespaces
|
|
97
|
-
export const CAPABILITY_NAMESPACES = {
|
|
98
|
-
UCP_OFFICIAL: 'dev.ucp.',
|
|
99
|
-
VENDOR_PREFIX: 'com.',
|
|
100
|
-
} as const;
|
|
101
|
-
|
|
102
|
-
// Known UCP capabilities (from official spec)
|
|
103
|
-
export const KNOWN_CAPABILITIES = {
|
|
104
|
-
CHECKOUT: 'dev.ucp.shopping.checkout',
|
|
105
|
-
ORDER: 'dev.ucp.shopping.order',
|
|
106
|
-
PAYMENT: 'dev.ucp.shopping.payment',
|
|
107
|
-
PAYMENT_DATA: 'dev.ucp.shopping.payment_data',
|
|
108
|
-
FULFILLMENT: 'dev.ucp.shopping.fulfillment',
|
|
109
|
-
DISCOUNT: 'dev.ucp.shopping.discount',
|
|
110
|
-
BUYER_CONSENT: 'dev.ucp.shopping.buyer_consent',
|
|
111
|
-
} as const;
|
|
112
|
-
|
|
113
|
-
// Known UCP services
|
|
114
|
-
export const KNOWN_SERVICES = {
|
|
115
|
-
SHOPPING: 'dev.ucp.shopping',
|
|
116
|
-
} as const;
|
|
117
|
-
|
|
118
|
-
// Default URLs for UCP official resources
|
|
119
|
-
export const UCP_DEFAULTS = {
|
|
120
|
-
SPEC_BASE: 'https://ucp.dev/specification/',
|
|
121
|
-
SCHEMA_BASE: 'https://ucp.dev/schemas/',
|
|
122
|
-
SERVICE_SCHEMA_BASE: 'https://ucp.dev/services/',
|
|
123
|
-
|
|
124
|
-
// Default schema URLs
|
|
125
|
-
SHOPPING_REST_SCHEMA: 'https://ucp.dev/services/shopping/rest.openapi.json',
|
|
126
|
-
CHECKOUT_SPEC: 'https://ucp.dev/specification/checkout/',
|
|
127
|
-
CHECKOUT_SCHEMA: 'https://ucp.dev/schemas/shopping/checkout.json',
|
|
128
|
-
ORDER_SPEC: 'https://ucp.dev/specification/order/',
|
|
129
|
-
ORDER_SCHEMA: 'https://ucp.dev/schemas/shopping/order.json',
|
|
130
|
-
FULFILLMENT_SPEC: 'https://ucp.dev/specification/fulfillment/',
|
|
131
|
-
FULFILLMENT_SCHEMA: 'https://ucp.dev/schemas/shopping/fulfillment.json',
|
|
132
|
-
DISCOUNT_SPEC: 'https://ucp.dev/specification/discount/',
|
|
133
|
-
DISCOUNT_SCHEMA: 'https://ucp.dev/schemas/shopping/discount.json',
|
|
134
|
-
} as const;
|
|
135
|
-
|
|
136
|
-
// Environment types
|
|
137
|
-
export type Environment = 'production' | 'staging' | 'development';
|
|
138
|
-
|
|
139
|
-
// Profile status
|
|
140
|
-
export type ProfileStatus = 'draft' | 'published';
|