ucpify 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/.env.example ADDED
@@ -0,0 +1,23 @@
1
+ # UCP Server Configuration
2
+
3
+ # Server
4
+ PORT=3000
5
+ NODE_ENV=production
6
+
7
+ # Database (SQLite path)
8
+ DATABASE_PATH=./data/ucp.db
9
+
10
+ # Logging
11
+ LOG_LEVEL=info
12
+
13
+ # UCP (your public domain)
14
+ UCP_DOMAIN=https://your-store.com
15
+
16
+ # Stripe (optional - for real payments)
17
+ STRIPE_SECRET_KEY=sk_test_...
18
+ STRIPE_WEBHOOK_SECRET=whsec_...
19
+
20
+ # PayPal (optional - alternative to Stripe)
21
+ PAYPAL_CLIENT_ID=...
22
+ PAYPAL_CLIENT_SECRET=...
23
+ PAYPAL_MODE=sandbox
package/Dockerfile ADDED
@@ -0,0 +1,35 @@
1
+ FROM node:20-alpine
2
+
3
+ # Install build dependencies for better-sqlite3
4
+ RUN apk add --no-cache python3 make g++
5
+
6
+ WORKDIR /app
7
+
8
+ # Copy package files
9
+ COPY package*.json ./
10
+
11
+ # Install dependencies
12
+ RUN npm ci --only=production
13
+
14
+ # Copy source
15
+ COPY . .
16
+
17
+ # Build TypeScript
18
+ RUN npm run build
19
+
20
+ # Create data directory
21
+ RUN mkdir -p /app/data
22
+
23
+ # Expose port
24
+ EXPOSE 3000
25
+
26
+ # Set environment
27
+ ENV NODE_ENV=production
28
+ ENV DATABASE_PATH=/app/data/ucp.db
29
+
30
+ # Health check
31
+ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
32
+ CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
33
+
34
+ # Run server
35
+ CMD ["node", "dist/cli.js", "serve", "/app/config/merchant-config.json"]
package/README.md ADDED
@@ -0,0 +1,320 @@
1
+ # ucpify
2
+
3
+ Generate UCP-compliant (Universal Commerce Protocol) servers for merchants from a simple schema.
4
+
5
+ ## What is UCP?
6
+
7
+ The [Universal Commerce Protocol](https://ucp.dev) is an open standard for agentic commerce, enabling AI agents, apps, businesses, and payment providers to interact seamlessly. UCP was co-developed by Google, Shopify, Etsy, Wayfair, Target, and Walmart.
8
+
9
+ ## Quick Start
10
+
11
+ ```bash
12
+ # Initialize a sample merchant config
13
+ npx ts-node src/cli.ts init
14
+
15
+ # Edit merchant-config.json to add your products, shipping, payments
16
+
17
+ # Start the UCP server
18
+ npx ts-node src/cli.ts serve merchant-config.json
19
+ ```
20
+
21
+ ## Merchant Configuration Schema
22
+
23
+ ```json
24
+ {
25
+ "name": "My Store",
26
+ "domain": "http://localhost:3000",
27
+ "currency": "USD",
28
+ "tax_rate": 0.08,
29
+ "terms_url": "https://example.com/terms",
30
+ "port": 3000,
31
+ "items": [
32
+ {
33
+ "id": "item_001",
34
+ "title": "Classic T-Shirt",
35
+ "description": "A comfortable cotton t-shirt",
36
+ "price": 2500
37
+ }
38
+ ],
39
+ "shipping_options": [
40
+ {
41
+ "id": "standard",
42
+ "title": "Standard Shipping",
43
+ "price": 500,
44
+ "estimated_days": "5-7 business days"
45
+ }
46
+ ],
47
+ "payment_handlers": [
48
+ {
49
+ "namespace": "com.stripe",
50
+ "id": "stripe_handler",
51
+ "config": { "publishable_key": "pk_test_..." }
52
+ }
53
+ ]
54
+ }
55
+ ```
56
+
57
+ ## UCP Endpoints
58
+
59
+ Once running, your server exposes these UCP-compliant endpoints:
60
+
61
+ | Endpoint | Method | Description |
62
+ |----------|--------|-------------|
63
+ | `/.well-known/ucp` | GET | UCP Profile (discovery) |
64
+ | `/ucp/v1/checkout-sessions` | POST | Create checkout session |
65
+ | `/ucp/v1/checkout-sessions/:id` | GET | Get checkout session |
66
+ | `/ucp/v1/checkout-sessions/:id` | PUT | Update checkout session |
67
+ | `/ucp/v1/checkout-sessions/:id/complete` | POST | Complete checkout (create order) |
68
+ | `/ucp/v1/checkout-sessions/:id/cancel` | POST | Cancel checkout |
69
+ | `/ucp/v1/orders` | GET | List orders |
70
+ | `/ucp/v1/orders/:id` | GET | Get order |
71
+ | `/ucp/v1/items` | GET | Product catalog |
72
+
73
+ ## Example Usage
74
+
75
+ ### 1. Create a checkout session
76
+
77
+ ```bash
78
+ curl -X POST http://localhost:3000/ucp/v1/checkout-sessions \
79
+ -H "Content-Type: application/json" \
80
+ -d '{
81
+ "line_items": [
82
+ {
83
+ "item": { "id": "item_001" },
84
+ "quantity": 2
85
+ }
86
+ ]
87
+ }'
88
+ ```
89
+
90
+ ### 2. Update with buyer info and shipping
91
+
92
+ ```bash
93
+ curl -X PUT http://localhost:3000/ucp/v1/checkout-sessions/chk_xxx \
94
+ -H "Content-Type: application/json" \
95
+ -d '{
96
+ "buyer": {
97
+ "email": "customer@example.com",
98
+ "first_name": "Jane",
99
+ "last_name": "Doe"
100
+ },
101
+ "line_items": [{ "item": { "id": "item_001" }, "quantity": 2 }],
102
+ "fulfillment": {
103
+ "methods": [{
104
+ "type": "shipping",
105
+ "destinations": [{
106
+ "street_address": "123 Main St",
107
+ "address_locality": "Springfield",
108
+ "address_region": "IL",
109
+ "postal_code": "62701",
110
+ "address_country": "US"
111
+ }]
112
+ }]
113
+ }
114
+ }'
115
+ ```
116
+
117
+ ### 3. Complete checkout
118
+
119
+ ```bash
120
+ curl -X POST http://localhost:3000/ucp/v1/checkout-sessions/chk_xxx/complete
121
+ ```
122
+
123
+ ## CLI Commands
124
+
125
+ ```bash
126
+ # Create sample configuration
127
+ ucpify init --output my-store.json
128
+
129
+ # Validate configuration
130
+ ucpify validate my-store.json
131
+
132
+ # Start server
133
+ ucpify serve my-store.json --port 8080
134
+ ```
135
+
136
+ ## Programmatic Usage
137
+
138
+ ```typescript
139
+ import { createExpressApp, MerchantConfigSchema } from 'ucpify';
140
+
141
+ const config = MerchantConfigSchema.parse({
142
+ name: 'My Store',
143
+ domain: 'http://localhost:3000',
144
+ currency: 'USD',
145
+ items: [
146
+ { id: 'prod_1', title: 'Widget', price: 1999 }
147
+ ],
148
+ shipping_options: [
149
+ { id: 'standard', title: 'Standard', price: 500 }
150
+ ]
151
+ });
152
+
153
+ const app = createExpressApp(config);
154
+ app.listen(3000);
155
+ ```
156
+
157
+ ## License
158
+
159
+ MIT
160
+
161
+ ---
162
+
163
+ ## Identity Linking (Bring Your Own OAuth)
164
+
165
+ UCP uses OAuth 2.0 for identity linking between agents and merchants. ucpify does **not** include a built-in OAuth server—instead, integrate your existing OAuth provider.
166
+
167
+ ### Required OAuth Endpoints
168
+
169
+ Your OAuth provider must expose these endpoints:
170
+
171
+ | Endpoint | Method | Description |
172
+ |----------|--------|-------------|
173
+ | `/oauth2/authorize` | GET | Authorization page (user consent) |
174
+ | `/oauth2/token` | POST | Token exchange |
175
+ | `/oauth2/revoke` | POST | Token revocation |
176
+
177
+ ### OAuth Discovery Endpoint
178
+
179
+ Create `/.well-known/oauth-authorization-server` returning:
180
+
181
+ ```json
182
+ {
183
+ "issuer": "https://your-store.com",
184
+ "authorization_endpoint": "https://your-store.com/oauth2/authorize",
185
+ "token_endpoint": "https://your-store.com/oauth2/token",
186
+ "revocation_endpoint": "https://your-store.com/oauth2/revoke",
187
+ "scopes_supported": [
188
+ "ucp:scopes:checkout_session",
189
+ "ucp:scopes:order_read",
190
+ "ucp:scopes:order_manage"
191
+ ],
192
+ "response_types_supported": ["code"],
193
+ "grant_types_supported": ["authorization_code", "refresh_token"],
194
+ "token_endpoint_auth_methods_supported": ["client_secret_basic"],
195
+ "service_documentation": "https://your-store.com/docs/oauth"
196
+ }
197
+ ```
198
+
199
+ ### UCP OAuth Scopes
200
+
201
+ | Scope | Description |
202
+ |-------|-------------|
203
+ | `ucp:scopes:checkout_session` | Create and manage checkout sessions |
204
+ | `ucp:scopes:order_read` | Read order information |
205
+ | `ucp:scopes:order_manage` | Manage orders (cancel, refund) |
206
+
207
+ ### Integration Examples
208
+
209
+ #### Using Auth0
210
+
211
+ ```javascript
212
+ // Add to your Express app
213
+ app.get('/.well-known/oauth-authorization-server', (req, res) => {
214
+ res.json({
215
+ issuer: 'https://your-tenant.auth0.com',
216
+ authorization_endpoint: 'https://your-tenant.auth0.com/authorize',
217
+ token_endpoint: 'https://your-tenant.auth0.com/oauth/token',
218
+ revocation_endpoint: 'https://your-tenant.auth0.com/oauth/revoke',
219
+ scopes_supported: ['ucp:scopes:checkout_session'],
220
+ response_types_supported: ['code'],
221
+ grant_types_supported: ['authorization_code', 'refresh_token'],
222
+ token_endpoint_auth_methods_supported: ['client_secret_basic'],
223
+ });
224
+ });
225
+ ```
226
+
227
+ #### Using Keycloak
228
+
229
+ ```python
230
+ # Add to your Flask app
231
+ @app.route('/.well-known/oauth-authorization-server')
232
+ def oauth_discovery():
233
+ return jsonify({
234
+ "issuer": "https://keycloak.example.com/realms/merchant",
235
+ "authorization_endpoint": "https://keycloak.example.com/realms/merchant/protocol/openid-connect/auth",
236
+ "token_endpoint": "https://keycloak.example.com/realms/merchant/protocol/openid-connect/token",
237
+ "revocation_endpoint": "https://keycloak.example.com/realms/merchant/protocol/openid-connect/revoke",
238
+ "scopes_supported": ["ucp:scopes:checkout_session"],
239
+ "response_types_supported": ["code"],
240
+ "grant_types_supported": ["authorization_code", "refresh_token"],
241
+ "token_endpoint_auth_methods_supported": ["client_secret_basic"],
242
+ })
243
+ ```
244
+
245
+ #### Using AWS Cognito
246
+
247
+ ```javascript
248
+ app.get('/.well-known/oauth-authorization-server', (req, res) => {
249
+ const cognitoDomain = 'https://your-domain.auth.us-east-1.amazoncognito.com';
250
+ res.json({
251
+ issuer: cognitoDomain,
252
+ authorization_endpoint: `${cognitoDomain}/oauth2/authorize`,
253
+ token_endpoint: `${cognitoDomain}/oauth2/token`,
254
+ revocation_endpoint: `${cognitoDomain}/oauth2/revoke`,
255
+ scopes_supported: ['ucp:scopes:checkout_session'],
256
+ response_types_supported: ['code'],
257
+ grant_types_supported: ['authorization_code', 'refresh_token'],
258
+ token_endpoint_auth_methods_supported: ['client_secret_basic'],
259
+ });
260
+ });
261
+ ```
262
+
263
+ ### Protecting UCP Endpoints
264
+
265
+ Add middleware to validate tokens on protected endpoints:
266
+
267
+ ```javascript
268
+ // JavaScript/Express
269
+ const validateToken = async (req, res, next) => {
270
+ const authHeader = req.headers.authorization;
271
+ if (!authHeader?.startsWith('Bearer ')) {
272
+ return res.status(401).json({ error: 'Missing token' });
273
+ }
274
+
275
+ const token = authHeader.slice(7);
276
+ // Validate with your OAuth provider
277
+ const valid = await verifyTokenWithProvider(token);
278
+ if (!valid) {
279
+ return res.status(401).json({ error: 'Invalid token' });
280
+ }
281
+ next();
282
+ };
283
+
284
+ // Apply to checkout endpoints
285
+ app.post('/ucp/v1/checkout-sessions', validateToken, createCheckout);
286
+ ```
287
+
288
+ ```python
289
+ # Python/Flask
290
+ from functools import wraps
291
+
292
+ def require_oauth(f):
293
+ @wraps(f)
294
+ def decorated(*args, **kwargs):
295
+ auth_header = request.headers.get('Authorization', '')
296
+ if not auth_header.startswith('Bearer '):
297
+ return jsonify({'error': 'Missing token'}), 401
298
+
299
+ token = auth_header[7:]
300
+ # Validate with your OAuth provider
301
+ if not verify_token_with_provider(token):
302
+ return jsonify({'error': 'Invalid token'}), 401
303
+ return f(*args, **kwargs)
304
+ return decorated
305
+
306
+ @app.route('/ucp/v1/checkout-sessions', methods=['POST'])
307
+ @require_oauth
308
+ def create_checkout():
309
+ ...
310
+ ```
311
+
312
+ ### Agent Platform Flow
313
+
314
+ 1. Agent discovers OAuth endpoints via `/.well-known/oauth-authorization-server`
315
+ 2. Agent redirects user to `authorization_endpoint` with scopes
316
+ 3. User consents on merchant's OAuth page
317
+ 4. Agent receives authorization code
318
+ 5. Agent exchanges code for access token at `token_endpoint`
319
+ 6. Agent uses access token in `Authorization: Bearer <token>` header
320
+ 7. Agent refreshes token when expired using `refresh_token` grant
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,121 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const vitest_1 = require("vitest");
7
+ const supertest_1 = __importDefault(require("supertest"));
8
+ const express_app_1 = require("../express-app");
9
+ const testConfig = {
10
+ name: 'Test Store',
11
+ domain: 'https://test.example.com',
12
+ currency: 'USD',
13
+ port: 3000,
14
+ items: [
15
+ { id: 'item-1', title: 'Test Item', price: 1000 },
16
+ { id: 'item-2', title: 'Another Item', price: 2500 },
17
+ ],
18
+ shipping_options: [
19
+ { id: 'standard', title: 'Standard Shipping', price: 500, estimated_days: '5-7 days' },
20
+ ],
21
+ payment_handlers: [
22
+ { namespace: 'com.stripe', id: 'stripe-test' },
23
+ ],
24
+ tax_rate: 0.08,
25
+ };
26
+ (0, vitest_1.describe)('UCP Server API', () => {
27
+ const app = (0, express_app_1.createExpressApp)(testConfig, { useDb: false });
28
+ (0, vitest_1.describe)('GET /.well-known/ucp', () => {
29
+ (0, vitest_1.it)('returns UCP profile', async () => {
30
+ const res = await (0, supertest_1.default)(app).get('/.well-known/ucp');
31
+ (0, vitest_1.expect)(res.status).toBe(200);
32
+ (0, vitest_1.expect)(res.body.ucp.version).toBe('2026-01-11');
33
+ (0, vitest_1.expect)(res.body.ucp.services['dev.ucp.shopping']).toBeDefined();
34
+ });
35
+ });
36
+ (0, vitest_1.describe)('GET /health', () => {
37
+ (0, vitest_1.it)('returns health status', async () => {
38
+ const res = await (0, supertest_1.default)(app).get('/health');
39
+ (0, vitest_1.expect)(res.status).toBe(200);
40
+ (0, vitest_1.expect)(res.body.status).toBeDefined();
41
+ });
42
+ });
43
+ (0, vitest_1.describe)('POST /ucp/v1/checkout-sessions', () => {
44
+ (0, vitest_1.it)('creates a checkout session', async () => {
45
+ const res = await (0, supertest_1.default)(app)
46
+ .post('/ucp/v1/checkout-sessions')
47
+ .send({
48
+ line_items: [{ item: { id: 'item-1' }, quantity: 2 }],
49
+ });
50
+ (0, vitest_1.expect)(res.status).toBe(201);
51
+ (0, vitest_1.expect)(res.body.id).toBeDefined();
52
+ (0, vitest_1.expect)(res.body.status).toBe('incomplete');
53
+ (0, vitest_1.expect)(res.body.line_items).toHaveLength(1);
54
+ (0, vitest_1.expect)(res.body.line_items[0].quantity).toBe(2);
55
+ });
56
+ (0, vitest_1.it)('rejects invalid requests', async () => {
57
+ const res = await (0, supertest_1.default)(app)
58
+ .post('/ucp/v1/checkout-sessions')
59
+ .send({ line_items: [] }); // Empty array
60
+ (0, vitest_1.expect)(res.status).toBe(400);
61
+ (0, vitest_1.expect)(res.body.error).toBe('Validation failed');
62
+ });
63
+ (0, vitest_1.it)('rejects excessive quantity', async () => {
64
+ const res = await (0, supertest_1.default)(app)
65
+ .post('/ucp/v1/checkout-sessions')
66
+ .send({
67
+ line_items: [{ item: { id: 'item-1' }, quantity: 999 }],
68
+ });
69
+ (0, vitest_1.expect)(res.status).toBe(400);
70
+ });
71
+ });
72
+ (0, vitest_1.describe)('Checkout flow', () => {
73
+ let checkoutId;
74
+ (0, vitest_1.it)('creates checkout', async () => {
75
+ const res = await (0, supertest_1.default)(app)
76
+ .post('/ucp/v1/checkout-sessions')
77
+ .send({
78
+ line_items: [{ item: { id: 'item-1' }, quantity: 1 }],
79
+ });
80
+ checkoutId = res.body.id;
81
+ (0, vitest_1.expect)(res.body.status).toBe('incomplete');
82
+ });
83
+ (0, vitest_1.it)('updates with buyer and fulfillment', async () => {
84
+ const res = await (0, supertest_1.default)(app)
85
+ .put(`/ucp/v1/checkout-sessions/${checkoutId}`)
86
+ .send({
87
+ buyer: { email: 'test@example.com' },
88
+ fulfillment: {
89
+ methods: [{
90
+ type: 'shipping',
91
+ destinations: [{
92
+ street_address: '123 Main St',
93
+ address_locality: 'Anytown',
94
+ address_region: 'CA',
95
+ postal_code: '12345',
96
+ address_country: 'US',
97
+ }],
98
+ }],
99
+ },
100
+ });
101
+ (0, vitest_1.expect)(res.status).toBe(200);
102
+ (0, vitest_1.expect)(res.body.status).toBe('ready_for_complete');
103
+ });
104
+ (0, vitest_1.it)('completes checkout', async () => {
105
+ const res = await (0, supertest_1.default)(app)
106
+ .post(`/ucp/v1/checkout-sessions/${checkoutId}/complete`);
107
+ (0, vitest_1.expect)(res.status).toBe(201);
108
+ (0, vitest_1.expect)(res.body.status).toBe('completed');
109
+ (0, vitest_1.expect)(res.body.order).toBeDefined();
110
+ (0, vitest_1.expect)(res.body.order.id).toMatch(/^order_/);
111
+ });
112
+ });
113
+ (0, vitest_1.describe)('GET /ucp/v1/items', () => {
114
+ (0, vitest_1.it)('returns product catalog', async () => {
115
+ const res = await (0, supertest_1.default)(app).get('/ucp/v1/items');
116
+ (0, vitest_1.expect)(res.status).toBe(200);
117
+ (0, vitest_1.expect)(res.body).toHaveLength(2);
118
+ (0, vitest_1.expect)(res.body[0].id).toBe('item-1');
119
+ });
120
+ });
121
+ });
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};