ucpify 1.0.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +30 -2
- package/dist/db.js +35 -0
- package/dist/express-app.js +188 -11
- package/dist/oauth.d.ts +60 -0
- package/dist/oauth.js +211 -0
- package/dist/schema.d.ts +21 -0
- package/dist/schema.js +16 -1
- package/dist/ucp-server-db.d.ts +4 -0
- package/dist/ucp-server-db.js +5 -0
- package/dist/ucp-server.d.ts +6 -0
- package/dist/ucp-server.js +64 -14
- package/package.json +8 -3
- package/src/cli.ts +32 -2
- package/src/db.ts +35 -0
- package/src/express-app.ts +231 -23
- package/src/oauth.ts +270 -0
- package/src/schema.ts +18 -0
- package/src/ucp-server-db.ts +10 -5
- package/src/ucp-server.ts +92 -44
- package/vitest.config.ts +7 -0
package/dist/schema.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.UpdateCheckoutRequestSchema = exports.CreateCheckoutRequestSchema = exports.MerchantConfigSchema = exports.PaymentHandlerSchema = exports.ShippingOptionSchema = exports.ItemSchema = exports.UCP_VERSION = void 0;
|
|
3
|
+
exports.UpdateCheckoutRequestSchema = exports.CreateCheckoutRequestSchema = exports.MerchantConfigSchema = exports.OAuthConfigSchema = exports.PaymentHandlerSchema = exports.ShippingOptionSchema = exports.ItemSchema = exports.UCP_VERSION = void 0;
|
|
4
4
|
const zod_1 = require("zod");
|
|
5
5
|
// UCP Version
|
|
6
6
|
exports.UCP_VERSION = '2026-01-11';
|
|
@@ -28,6 +28,20 @@ exports.PaymentHandlerSchema = zod_1.z.object({
|
|
|
28
28
|
id: zod_1.z.string(),
|
|
29
29
|
config: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()).optional(),
|
|
30
30
|
});
|
|
31
|
+
// OAuth Configuration Schema
|
|
32
|
+
exports.OAuthConfigSchema = zod_1.z.discriminatedUnion('provider', [
|
|
33
|
+
zod_1.z.object({
|
|
34
|
+
provider: zod_1.z.literal('built-in'),
|
|
35
|
+
}),
|
|
36
|
+
zod_1.z.object({
|
|
37
|
+
provider: zod_1.z.literal('external'),
|
|
38
|
+
issuer: zod_1.z.string().url(),
|
|
39
|
+
authorization_endpoint: zod_1.z.string().url(),
|
|
40
|
+
token_endpoint: zod_1.z.string().url(),
|
|
41
|
+
revocation_endpoint: zod_1.z.string().url().optional(),
|
|
42
|
+
jwks_uri: zod_1.z.string().url().optional(),
|
|
43
|
+
}),
|
|
44
|
+
]);
|
|
31
45
|
// Merchant Configuration Schema
|
|
32
46
|
exports.MerchantConfigSchema = zod_1.z.object({
|
|
33
47
|
name: zod_1.z.string(),
|
|
@@ -40,6 +54,7 @@ exports.MerchantConfigSchema = zod_1.z.object({
|
|
|
40
54
|
payment_handlers: zod_1.z.array(exports.PaymentHandlerSchema).default([]),
|
|
41
55
|
tax_rate: zod_1.z.number().min(0).max(1).default(0), // e.g., 0.08 for 8%
|
|
42
56
|
port: zod_1.z.number().default(3000),
|
|
57
|
+
oauth: exports.OAuthConfigSchema.optional(),
|
|
43
58
|
});
|
|
44
59
|
// Request validation schemas
|
|
45
60
|
exports.CreateCheckoutRequestSchema = zod_1.z.object({
|
package/dist/ucp-server-db.d.ts
CHANGED
package/dist/ucp-server-db.js
CHANGED
|
@@ -45,6 +45,11 @@ class UCPServerDB {
|
|
|
45
45
|
'dev.ucp.shopping.order': [
|
|
46
46
|
{ version: schema_1.UCP_VERSION, spec: 'https://ucp.dev/specification/order', schema: 'https://ucp.dev/schemas/shopping/order.json' },
|
|
47
47
|
],
|
|
48
|
+
...(this.config.oauth ? {
|
|
49
|
+
'dev.ucp.identity_linking': [
|
|
50
|
+
{ version: schema_1.UCP_VERSION, spec: 'https://ucp.dev/specification/identity-linking' },
|
|
51
|
+
],
|
|
52
|
+
} : {}),
|
|
48
53
|
},
|
|
49
54
|
payment_handlers: paymentHandlers,
|
|
50
55
|
},
|
package/dist/ucp-server.d.ts
CHANGED
|
@@ -2,6 +2,8 @@ import { MerchantConfig, CheckoutSession, Order, Buyer, Fulfillment } from './sc
|
|
|
2
2
|
export declare class UCPServer {
|
|
3
3
|
private config;
|
|
4
4
|
private itemsMap;
|
|
5
|
+
private checkoutSessions;
|
|
6
|
+
private orders;
|
|
5
7
|
constructor(config: MerchantConfig);
|
|
6
8
|
getProfile(): {
|
|
7
9
|
ucp: {
|
|
@@ -16,6 +18,10 @@ export declare class UCPServer {
|
|
|
16
18
|
}[];
|
|
17
19
|
};
|
|
18
20
|
capabilities: {
|
|
21
|
+
'dev.ucp.identity_linking'?: {
|
|
22
|
+
version: string;
|
|
23
|
+
spec: string;
|
|
24
|
+
}[] | undefined;
|
|
19
25
|
'dev.ucp.shopping.checkout': {
|
|
20
26
|
version: string;
|
|
21
27
|
spec: string;
|
package/dist/ucp-server.js
CHANGED
|
@@ -3,12 +3,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.UCPServer = void 0;
|
|
4
4
|
const uuid_1 = require("uuid");
|
|
5
5
|
const schema_1 = require("./schema");
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
const
|
|
6
|
+
const stripe_1 = require("./stripe");
|
|
7
|
+
const paypal_1 = require("./paypal");
|
|
8
|
+
const logger_1 = require("./logger");
|
|
9
9
|
class UCPServer {
|
|
10
10
|
config;
|
|
11
11
|
itemsMap;
|
|
12
|
+
checkoutSessions = new Map();
|
|
13
|
+
orders = new Map();
|
|
12
14
|
constructor(config) {
|
|
13
15
|
this.config = config;
|
|
14
16
|
this.itemsMap = new Map(config.items.map((item) => [item.id, item]));
|
|
@@ -62,6 +64,14 @@ class UCPServer {
|
|
|
62
64
|
schema: 'https://ucp.dev/schemas/shopping/order.json',
|
|
63
65
|
},
|
|
64
66
|
],
|
|
67
|
+
...(this.config.oauth ? {
|
|
68
|
+
'dev.ucp.identity_linking': [
|
|
69
|
+
{
|
|
70
|
+
version: schema_1.UCP_VERSION,
|
|
71
|
+
spec: 'https://ucp.dev/specification/identity-linking',
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
} : {}),
|
|
65
75
|
},
|
|
66
76
|
payment_handlers: paymentHandlers,
|
|
67
77
|
},
|
|
@@ -156,16 +166,16 @@ class UCPServer {
|
|
|
156
166
|
continue_url: `${this.config.domain}/checkout/${id}`,
|
|
157
167
|
expires_at: expiresAt,
|
|
158
168
|
};
|
|
159
|
-
checkoutSessions.set(id, session);
|
|
169
|
+
this.checkoutSessions.set(id, session);
|
|
160
170
|
return session;
|
|
161
171
|
}
|
|
162
172
|
// Get checkout session
|
|
163
173
|
getCheckout(id) {
|
|
164
|
-
return checkoutSessions.get(id);
|
|
174
|
+
return this.checkoutSessions.get(id);
|
|
165
175
|
}
|
|
166
176
|
// Update checkout session
|
|
167
177
|
updateCheckout(id, data) {
|
|
168
|
-
const session = checkoutSessions.get(id);
|
|
178
|
+
const session = this.checkoutSessions.get(id);
|
|
169
179
|
if (!session)
|
|
170
180
|
return undefined;
|
|
171
181
|
// Update buyer
|
|
@@ -251,18 +261,58 @@ class UCPServer {
|
|
|
251
261
|
const newMessages = this.validateCheckout(session);
|
|
252
262
|
session.messages = newMessages.length > 0 ? newMessages : undefined;
|
|
253
263
|
session.status = newMessages.length > 0 ? 'incomplete' : 'ready_for_complete';
|
|
254
|
-
checkoutSessions.set(id, session);
|
|
264
|
+
this.checkoutSessions.set(id, session);
|
|
255
265
|
return session;
|
|
256
266
|
}
|
|
257
267
|
// Complete checkout (create order)
|
|
258
268
|
async completeCheckout(id) {
|
|
259
|
-
const session = checkoutSessions.get(id);
|
|
269
|
+
const session = this.checkoutSessions.get(id);
|
|
260
270
|
if (!session)
|
|
261
271
|
return { error: 'Checkout session not found' };
|
|
262
272
|
if (session.status !== 'ready_for_complete') {
|
|
263
273
|
return { error: 'Checkout is not ready for completion', messages: session.messages };
|
|
264
274
|
}
|
|
265
275
|
const orderId = `order_${(0, uuid_1.v4)().replace(/-/g, '').slice(0, 12)}`;
|
|
276
|
+
// Get total amount for payment
|
|
277
|
+
const totalAmount = session.totals.find(t => t.type === 'total')?.amount || 0;
|
|
278
|
+
// Payment processing - try Stripe first, then PayPal
|
|
279
|
+
if (totalAmount > 0) {
|
|
280
|
+
if ((0, stripe_1.isStripeConfigured)()) {
|
|
281
|
+
try {
|
|
282
|
+
const paymentIntent = await (0, stripe_1.createPaymentIntent)({
|
|
283
|
+
amount: totalAmount,
|
|
284
|
+
currency: session.currency,
|
|
285
|
+
checkoutId: id,
|
|
286
|
+
customerEmail: session.buyer?.email,
|
|
287
|
+
metadata: { order_id: orderId },
|
|
288
|
+
});
|
|
289
|
+
if (paymentIntent) {
|
|
290
|
+
logger_1.logger.info({ checkoutId: id, paymentIntentId: paymentIntent.id, amount: totalAmount }, 'Stripe payment intent created');
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
catch (error) {
|
|
294
|
+
logger_1.logger.error({ error, checkoutId: id }, 'Failed to create Stripe payment intent');
|
|
295
|
+
return { error: 'Payment processing failed' };
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
else if ((0, paypal_1.isPayPalConfigured)()) {
|
|
299
|
+
try {
|
|
300
|
+
const ppOrder = await (0, paypal_1.createPayPalOrder)({
|
|
301
|
+
amount: totalAmount,
|
|
302
|
+
currency: session.currency,
|
|
303
|
+
checkoutId: id,
|
|
304
|
+
description: `Order ${orderId}`,
|
|
305
|
+
});
|
|
306
|
+
if (ppOrder) {
|
|
307
|
+
logger_1.logger.info({ checkoutId: id, paypalOrderId: ppOrder.id, amount: totalAmount }, 'PayPal order created');
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
catch (error) {
|
|
311
|
+
logger_1.logger.error({ error, checkoutId: id }, 'Failed to create PayPal order');
|
|
312
|
+
return { error: 'Payment processing failed' };
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
266
316
|
const order = {
|
|
267
317
|
ucp: {
|
|
268
318
|
version: schema_1.UCP_VERSION,
|
|
@@ -294,7 +344,7 @@ class UCPServer {
|
|
|
294
344
|
: undefined,
|
|
295
345
|
created_at: new Date().toISOString(),
|
|
296
346
|
};
|
|
297
|
-
orders.set(orderId, order);
|
|
347
|
+
this.orders.set(orderId, order);
|
|
298
348
|
// Update session to completed state with order confirmation
|
|
299
349
|
session.status = 'completed';
|
|
300
350
|
session.order = {
|
|
@@ -303,26 +353,26 @@ class UCPServer {
|
|
|
303
353
|
};
|
|
304
354
|
delete session.continue_url;
|
|
305
355
|
delete session.messages;
|
|
306
|
-
checkoutSessions.set(id, session);
|
|
356
|
+
this.checkoutSessions.set(id, session);
|
|
307
357
|
return session;
|
|
308
358
|
}
|
|
309
359
|
// Cancel checkout
|
|
310
360
|
cancelCheckout(id) {
|
|
311
|
-
const session = checkoutSessions.get(id);
|
|
361
|
+
const session = this.checkoutSessions.get(id);
|
|
312
362
|
if (!session)
|
|
313
363
|
return undefined;
|
|
314
364
|
session.status = 'canceled';
|
|
315
365
|
delete session.continue_url;
|
|
316
|
-
checkoutSessions.set(id, session);
|
|
366
|
+
this.checkoutSessions.set(id, session);
|
|
317
367
|
return session;
|
|
318
368
|
}
|
|
319
369
|
// Get order
|
|
320
370
|
getOrder(id) {
|
|
321
|
-
return orders.get(id);
|
|
371
|
+
return this.orders.get(id);
|
|
322
372
|
}
|
|
323
373
|
// List orders
|
|
324
374
|
listOrders() {
|
|
325
|
-
return Array.from(orders.values());
|
|
375
|
+
return Array.from(this.orders.values());
|
|
326
376
|
}
|
|
327
377
|
getConfig() {
|
|
328
378
|
return this.config;
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.0
|
|
2
|
+
"version": "1.2.0",
|
|
3
3
|
"description": "Generate UCP-compliant servers for merchants from a simple schema",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
@@ -23,8 +23,13 @@
|
|
|
23
23
|
"agentic",
|
|
24
24
|
"ai-commerce"
|
|
25
25
|
],
|
|
26
|
-
"author": "",
|
|
26
|
+
"author": "Hemanth HM <hemanth@h3manth.com>",
|
|
27
27
|
"license": "MIT",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "git+https://github.com/hemanth/ucpify.git"
|
|
31
|
+
},
|
|
32
|
+
"homepage": "https://github.com/hemanth/ucpify#readme",
|
|
28
33
|
"type": "commonjs",
|
|
29
34
|
"dependencies": {
|
|
30
35
|
"@paypal/paypal-server-sdk": "^2.2.0",
|
|
@@ -50,4 +55,4 @@
|
|
|
50
55
|
"vitest": "^4.0.18"
|
|
51
56
|
},
|
|
52
57
|
"name": "ucpify"
|
|
53
|
-
}
|
|
58
|
+
}
|
package/src/cli.ts
CHANGED
|
@@ -7,11 +7,12 @@ import { MerchantConfigSchema, MerchantConfig } from './schema';
|
|
|
7
7
|
import { createExpressApp } from './express-app';
|
|
8
8
|
|
|
9
9
|
const program = new Command();
|
|
10
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf-8'));
|
|
10
11
|
|
|
11
12
|
program
|
|
12
13
|
.name('ucpify')
|
|
13
14
|
.description('Generate and run a UCP-compliant server for merchants')
|
|
14
|
-
.version(
|
|
15
|
+
.version(pkg.version);
|
|
15
16
|
|
|
16
17
|
program
|
|
17
18
|
.command('init')
|
|
@@ -114,6 +115,9 @@ program
|
|
|
114
115
|
const app = createExpressApp(config, { useDb });
|
|
115
116
|
const port = config.port;
|
|
116
117
|
const storageType = useDb ? 'SQLite' : 'In-memory';
|
|
118
|
+
const oauthStatus = config.oauth
|
|
119
|
+
? config.oauth.provider === 'built-in' ? 'Built-in' : `External (${config.oauth.issuer})`
|
|
120
|
+
: 'Disabled';
|
|
117
121
|
|
|
118
122
|
app.listen(port, () => {
|
|
119
123
|
console.log(`
|
|
@@ -124,6 +128,7 @@ program
|
|
|
124
128
|
║ Domain: ${config.domain.padEnd(49)}║
|
|
125
129
|
║ Port: ${String(port).padEnd(49)}║
|
|
126
130
|
║ Storage: ${storageType.padEnd(49)}║
|
|
131
|
+
║ OAuth: ${oauthStatus.padEnd(49)}║
|
|
127
132
|
╠══════════════════════════════════════════════════════════════╣
|
|
128
133
|
║ Endpoints: ║
|
|
129
134
|
║ • GET /.well-known/ucp - UCP Profile ║
|
|
@@ -135,7 +140,11 @@ program
|
|
|
135
140
|
║ • GET /ucp/v1/orders - List Orders ║
|
|
136
141
|
║ • GET /ucp/v1/orders/:id - Get Order ║
|
|
137
142
|
║ • GET /ucp/v1/items - Product Catalog ║
|
|
138
|
-
║ • GET /health - Health Check
|
|
143
|
+
║ • GET /health - Health Check ║${config.oauth ? `
|
|
144
|
+
║ • GET /.well-known/oauth-... - OAuth Metadata ║` : ''}${config.oauth?.provider === 'built-in' ? `
|
|
145
|
+
║ • GET /oauth2/authorize - Authorization ║
|
|
146
|
+
║ • POST /oauth2/token - Token Exchange ║
|
|
147
|
+
║ • POST /oauth2/revoke - Token Revocation ║` : ''}
|
|
139
148
|
╚══════════════════════════════════════════════════════════════╝
|
|
140
149
|
|
|
141
150
|
🔗 UCP Profile: http://localhost:${port}/.well-known/ucp
|
|
@@ -143,6 +152,7 @@ program
|
|
|
143
152
|
🚚 Shipping: ${config.shipping_options.length} options available
|
|
144
153
|
💳 Payment Handlers: ${config.payment_handlers.length} configured
|
|
145
154
|
💾 Storage: ${storageType}${useDb ? ' (./data/ucp.db)' : ''}
|
|
155
|
+
🔐 OAuth: ${oauthStatus}
|
|
146
156
|
`);
|
|
147
157
|
});
|
|
148
158
|
} catch (error) {
|
|
@@ -175,10 +185,30 @@ program
|
|
|
175
185
|
console.log(` • Products: ${config.items.length}`);
|
|
176
186
|
console.log(` • Shipping Options: ${config.shipping_options.length}`);
|
|
177
187
|
console.log(` • Payment Handlers: ${config.payment_handlers.length}`);
|
|
188
|
+
console.log(` • OAuth: ${config.oauth ? config.oauth.provider : 'Disabled'}`);
|
|
178
189
|
} catch (error) {
|
|
179
190
|
console.error('❌ Validation failed:', error);
|
|
180
191
|
process.exit(1);
|
|
181
192
|
}
|
|
182
193
|
});
|
|
183
194
|
|
|
195
|
+
program
|
|
196
|
+
.command('oauth:add-client')
|
|
197
|
+
.description('Register a new OAuth client (built-in provider only)')
|
|
198
|
+
.argument('<name>', 'Client application name')
|
|
199
|
+
.option('-r, --redirect-uri <uri>', 'Redirect URI', 'http://localhost:3000/callback')
|
|
200
|
+
.action((name, options) => {
|
|
201
|
+
const { OAuthManager } = require('./oauth');
|
|
202
|
+
const oauthManager = new OAuthManager();
|
|
203
|
+
const client = oauthManager.createClient(name, [options.redirectUri]);
|
|
204
|
+
|
|
205
|
+
console.log(`\n✅ OAuth client created!\n`);
|
|
206
|
+
console.log(` Client ID: ${client.client_id}`);
|
|
207
|
+
console.log(` Client Secret: ${client.client_secret}`);
|
|
208
|
+
console.log(` Redirect URI: ${options.redirectUri}`);
|
|
209
|
+
console.log(`\n⚠️ Save the client secret. It cannot be retrieved later.`);
|
|
210
|
+
console.log(`\n📝 Add to your merchant-config.json:`);
|
|
211
|
+
console.log(JSON.stringify({ oauth: { provider: 'built-in' } }, null, 2));
|
|
212
|
+
});
|
|
213
|
+
|
|
184
214
|
program.parse();
|
package/src/db.ts
CHANGED
|
@@ -71,9 +71,44 @@ CREATE TABLE IF NOT EXISTS inventory (
|
|
|
71
71
|
updated_at TEXT NOT NULL
|
|
72
72
|
);
|
|
73
73
|
|
|
74
|
+
CREATE TABLE IF NOT EXISTS oauth_clients (
|
|
75
|
+
client_id TEXT PRIMARY KEY,
|
|
76
|
+
client_secret_hash TEXT NOT NULL,
|
|
77
|
+
name TEXT NOT NULL,
|
|
78
|
+
redirect_uris_json TEXT NOT NULL,
|
|
79
|
+
created_at TEXT NOT NULL
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
CREATE TABLE IF NOT EXISTS oauth_codes (
|
|
83
|
+
code TEXT PRIMARY KEY,
|
|
84
|
+
client_id TEXT NOT NULL,
|
|
85
|
+
user_id TEXT NOT NULL,
|
|
86
|
+
scope TEXT NOT NULL,
|
|
87
|
+
redirect_uri TEXT NOT NULL,
|
|
88
|
+
code_challenge TEXT,
|
|
89
|
+
code_challenge_method TEXT,
|
|
90
|
+
expires_at INTEGER NOT NULL,
|
|
91
|
+
used INTEGER NOT NULL DEFAULT 0,
|
|
92
|
+
FOREIGN KEY (client_id) REFERENCES oauth_clients(client_id)
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
CREATE TABLE IF NOT EXISTS oauth_tokens (
|
|
96
|
+
token TEXT PRIMARY KEY,
|
|
97
|
+
token_type TEXT NOT NULL,
|
|
98
|
+
client_id TEXT NOT NULL,
|
|
99
|
+
user_id TEXT NOT NULL,
|
|
100
|
+
scope TEXT NOT NULL,
|
|
101
|
+
parent_token TEXT,
|
|
102
|
+
expires_at INTEGER NOT NULL,
|
|
103
|
+
revoked INTEGER NOT NULL DEFAULT 0,
|
|
104
|
+
FOREIGN KEY (client_id) REFERENCES oauth_clients(client_id)
|
|
105
|
+
);
|
|
106
|
+
|
|
74
107
|
CREATE INDEX IF NOT EXISTS idx_line_items_checkout ON line_items(checkout_id);
|
|
75
108
|
CREATE INDEX IF NOT EXISTS idx_orders_checkout ON orders(checkout_id);
|
|
76
109
|
CREATE INDEX IF NOT EXISTS idx_checkouts_status ON checkouts(status);
|
|
110
|
+
CREATE INDEX IF NOT EXISTS idx_oauth_tokens_client ON oauth_tokens(client_id);
|
|
111
|
+
CREATE INDEX IF NOT EXISTS idx_oauth_codes_client ON oauth_codes(client_id);
|
|
77
112
|
`;
|
|
78
113
|
|
|
79
114
|
db.exec(schema);
|