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/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({
@@ -16,6 +16,10 @@ export declare class UCPServerDB {
16
16
  }[];
17
17
  };
18
18
  capabilities: {
19
+ 'dev.ucp.identity_linking'?: {
20
+ version: string;
21
+ spec: string;
22
+ }[] | undefined;
19
23
  'dev.ucp.shopping.checkout': {
20
24
  version: string;
21
25
  spec: string;
@@ -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
  },
@@ -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;
@@ -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
- // In-memory stores
7
- const checkoutSessions = new Map();
8
- const orders = new Map();
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.1",
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('1.0.0');
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);