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/src/ucp-server-db.ts
CHANGED
|
@@ -57,6 +57,11 @@ export class UCPServerDB {
|
|
|
57
57
|
'dev.ucp.shopping.order': [
|
|
58
58
|
{ version: UCP_VERSION, spec: 'https://ucp.dev/specification/order', schema: 'https://ucp.dev/schemas/shopping/order.json' },
|
|
59
59
|
],
|
|
60
|
+
...(this.config.oauth ? {
|
|
61
|
+
'dev.ucp.identity_linking': [
|
|
62
|
+
{ version: UCP_VERSION, spec: 'https://ucp.dev/specification/identity-linking' },
|
|
63
|
+
],
|
|
64
|
+
} : {}),
|
|
60
65
|
},
|
|
61
66
|
payment_handlers: paymentHandlers,
|
|
62
67
|
},
|
|
@@ -105,9 +110,9 @@ export class UCPServerDB {
|
|
|
105
110
|
|
|
106
111
|
private sessionFromRow(row: any): CheckoutSession | undefined {
|
|
107
112
|
if (!row) return undefined;
|
|
108
|
-
|
|
113
|
+
|
|
109
114
|
const lineItems = db.prepare('SELECT * FROM line_items WHERE checkout_id = ?').all(row.id) as any[];
|
|
110
|
-
|
|
115
|
+
|
|
111
116
|
return {
|
|
112
117
|
ucp: {
|
|
113
118
|
version: UCP_VERSION,
|
|
@@ -169,7 +174,7 @@ export class UCPServerDB {
|
|
|
169
174
|
`);
|
|
170
175
|
|
|
171
176
|
db.transaction(() => {
|
|
172
|
-
insertCheckout.run(id, status, this.config.currency, JSON.stringify(totals), JSON.stringify(links),
|
|
177
|
+
insertCheckout.run(id, status, this.config.currency, JSON.stringify(totals), JSON.stringify(links),
|
|
173
178
|
messages.length > 0 ? JSON.stringify(messages) : null, continueUrl, expiresAt, now, now);
|
|
174
179
|
for (const li of lineItems) {
|
|
175
180
|
insertLineItem.run(li.id, id, JSON.stringify(li.item), li.quantity, JSON.stringify(li.totals));
|
|
@@ -284,13 +289,13 @@ export class UCPServerDB {
|
|
|
284
289
|
|
|
285
290
|
// Get total amount for payment
|
|
286
291
|
const totalAmount = session.totals.find(t => t.type === 'total')?.amount || 0;
|
|
287
|
-
|
|
292
|
+
|
|
288
293
|
// Payment processing - try Stripe first, then PayPal
|
|
289
294
|
let paymentIntentId: string | null = null;
|
|
290
295
|
let paypalOrderId: string | null = null;
|
|
291
296
|
let paymentProvider: string | null = null;
|
|
292
297
|
let paymentStatus = 'none';
|
|
293
|
-
|
|
298
|
+
|
|
294
299
|
if (totalAmount > 0) {
|
|
295
300
|
// Try Stripe first
|
|
296
301
|
if (isStripeConfigured()) {
|
package/src/ucp-server.ts
CHANGED
|
@@ -11,14 +11,15 @@ import {
|
|
|
11
11
|
Item,
|
|
12
12
|
OrderConfirmation,
|
|
13
13
|
} from './schema';
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const orders: Map<string, Order> = new Map();
|
|
14
|
+
import { createPaymentIntent, isStripeConfigured } from './stripe';
|
|
15
|
+
import { createPayPalOrder, isPayPalConfigured } from './paypal';
|
|
16
|
+
import { logger } from './logger';
|
|
18
17
|
|
|
19
18
|
export class UCPServer {
|
|
20
19
|
private config: MerchantConfig;
|
|
21
20
|
private itemsMap: Map<string, Item>;
|
|
21
|
+
private checkoutSessions: Map<string, CheckoutSession> = new Map();
|
|
22
|
+
private orders: Map<string, Order> = new Map();
|
|
22
23
|
|
|
23
24
|
constructor(config: MerchantConfig) {
|
|
24
25
|
this.config = config;
|
|
@@ -75,6 +76,14 @@ export class UCPServer {
|
|
|
75
76
|
schema: 'https://ucp.dev/schemas/shopping/order.json',
|
|
76
77
|
},
|
|
77
78
|
],
|
|
79
|
+
...(this.config.oauth ? {
|
|
80
|
+
'dev.ucp.identity_linking': [
|
|
81
|
+
{
|
|
82
|
+
version: UCP_VERSION,
|
|
83
|
+
spec: 'https://ucp.dev/specification/identity-linking',
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
} : {}),
|
|
78
87
|
},
|
|
79
88
|
payment_handlers: paymentHandlers,
|
|
80
89
|
},
|
|
@@ -122,7 +131,7 @@ export class UCPServer {
|
|
|
122
131
|
// Create checkout session
|
|
123
132
|
createCheckout(data: { line_items: { id?: string; item: { id: string; title?: string; price?: number }; quantity: number }[] }): CheckoutSession {
|
|
124
133
|
const id = `chk_${uuidv4().replace(/-/g, '').slice(0, 12)}`;
|
|
125
|
-
|
|
134
|
+
|
|
126
135
|
// Resolve items from catalog or use provided
|
|
127
136
|
const lineItems: LineItem[] = data.line_items.map((li, idx) => {
|
|
128
137
|
const catalogItem = this.itemsMap.get(li.item.id);
|
|
@@ -131,7 +140,7 @@ export class UCPServer {
|
|
|
131
140
|
title: li.item.title || 'Unknown Item',
|
|
132
141
|
price: li.item.price || 0,
|
|
133
142
|
};
|
|
134
|
-
|
|
143
|
+
|
|
135
144
|
const subtotal = item.price * li.quantity;
|
|
136
145
|
return {
|
|
137
146
|
id: li.id || `li_${idx + 1}`,
|
|
@@ -183,13 +192,13 @@ export class UCPServer {
|
|
|
183
192
|
expires_at: expiresAt,
|
|
184
193
|
};
|
|
185
194
|
|
|
186
|
-
checkoutSessions.set(id, session);
|
|
195
|
+
this.checkoutSessions.set(id, session);
|
|
187
196
|
return session;
|
|
188
197
|
}
|
|
189
198
|
|
|
190
199
|
// Get checkout session
|
|
191
200
|
getCheckout(id: string): CheckoutSession | undefined {
|
|
192
|
-
return checkoutSessions.get(id);
|
|
201
|
+
return this.checkoutSessions.get(id);
|
|
193
202
|
}
|
|
194
203
|
|
|
195
204
|
// Update checkout session
|
|
@@ -198,7 +207,7 @@ export class UCPServer {
|
|
|
198
207
|
line_items?: { id?: string; item: { id: string; title?: string; price?: number }; quantity: number }[];
|
|
199
208
|
fulfillment?: Fulfillment;
|
|
200
209
|
}): CheckoutSession | undefined {
|
|
201
|
-
const session = checkoutSessions.get(id);
|
|
210
|
+
const session = this.checkoutSessions.get(id);
|
|
202
211
|
if (!session) return undefined;
|
|
203
212
|
|
|
204
213
|
// Update buyer
|
|
@@ -239,18 +248,18 @@ export class UCPServer {
|
|
|
239
248
|
const groups = method.groups?.length
|
|
240
249
|
? method.groups
|
|
241
250
|
: [
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
251
|
+
{
|
|
252
|
+
id: `group_${mIdx + 1}`,
|
|
253
|
+
line_item_ids: session.line_items.map((li) => li.id),
|
|
254
|
+
selected_option_id: this.config.shipping_options[0]?.id,
|
|
255
|
+
options: this.config.shipping_options.map((opt) => ({
|
|
256
|
+
id: opt.id,
|
|
257
|
+
title: opt.title,
|
|
258
|
+
description: opt.description || opt.estimated_days,
|
|
259
|
+
totals: [{ type: 'total', amount: opt.price }],
|
|
260
|
+
})),
|
|
261
|
+
},
|
|
262
|
+
];
|
|
254
263
|
|
|
255
264
|
return {
|
|
256
265
|
id: method.id || `method_${mIdx + 1}`,
|
|
@@ -293,20 +302,59 @@ export class UCPServer {
|
|
|
293
302
|
session.messages = newMessages.length > 0 ? newMessages : undefined;
|
|
294
303
|
session.status = newMessages.length > 0 ? 'incomplete' : 'ready_for_complete';
|
|
295
304
|
|
|
296
|
-
checkoutSessions.set(id, session);
|
|
305
|
+
this.checkoutSessions.set(id, session);
|
|
297
306
|
return session;
|
|
298
307
|
}
|
|
299
308
|
|
|
300
309
|
// Complete checkout (create order)
|
|
301
310
|
async completeCheckout(id: string): Promise<CheckoutSession | { error: string }> {
|
|
302
|
-
const session = checkoutSessions.get(id);
|
|
311
|
+
const session = this.checkoutSessions.get(id);
|
|
303
312
|
if (!session) return { error: 'Checkout session not found' };
|
|
304
313
|
if (session.status !== 'ready_for_complete') {
|
|
305
314
|
return { error: 'Checkout is not ready for completion', messages: session.messages };
|
|
306
315
|
}
|
|
307
316
|
|
|
308
317
|
const orderId = `order_${uuidv4().replace(/-/g, '').slice(0, 12)}`;
|
|
309
|
-
|
|
318
|
+
|
|
319
|
+
// Get total amount for payment
|
|
320
|
+
const totalAmount = session.totals.find(t => t.type === 'total')?.amount || 0;
|
|
321
|
+
|
|
322
|
+
// Payment processing - try Stripe first, then PayPal
|
|
323
|
+
if (totalAmount > 0) {
|
|
324
|
+
if (isStripeConfigured()) {
|
|
325
|
+
try {
|
|
326
|
+
const paymentIntent = await createPaymentIntent({
|
|
327
|
+
amount: totalAmount,
|
|
328
|
+
currency: session.currency,
|
|
329
|
+
checkoutId: id,
|
|
330
|
+
customerEmail: session.buyer?.email,
|
|
331
|
+
metadata: { order_id: orderId },
|
|
332
|
+
});
|
|
333
|
+
if (paymentIntent) {
|
|
334
|
+
logger.info({ checkoutId: id, paymentIntentId: paymentIntent.id, amount: totalAmount }, 'Stripe payment intent created');
|
|
335
|
+
}
|
|
336
|
+
} catch (error) {
|
|
337
|
+
logger.error({ error, checkoutId: id }, 'Failed to create Stripe payment intent');
|
|
338
|
+
return { error: 'Payment processing failed' };
|
|
339
|
+
}
|
|
340
|
+
} else if (isPayPalConfigured()) {
|
|
341
|
+
try {
|
|
342
|
+
const ppOrder = await createPayPalOrder({
|
|
343
|
+
amount: totalAmount,
|
|
344
|
+
currency: session.currency,
|
|
345
|
+
checkoutId: id,
|
|
346
|
+
description: `Order ${orderId}`,
|
|
347
|
+
});
|
|
348
|
+
if (ppOrder) {
|
|
349
|
+
logger.info({ checkoutId: id, paypalOrderId: ppOrder.id, amount: totalAmount }, 'PayPal order created');
|
|
350
|
+
}
|
|
351
|
+
} catch (error) {
|
|
352
|
+
logger.error({ error, checkoutId: id }, 'Failed to create PayPal order');
|
|
353
|
+
return { error: 'Payment processing failed' };
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
310
358
|
const order: Order = {
|
|
311
359
|
ucp: {
|
|
312
360
|
version: UCP_VERSION,
|
|
@@ -322,25 +370,25 @@ export class UCPServer {
|
|
|
322
370
|
totals: session.totals,
|
|
323
371
|
fulfillment: session.fulfillment
|
|
324
372
|
? {
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
373
|
+
expectations: session.fulfillment.methods.map((m, idx) => ({
|
|
374
|
+
id: `exp_${idx + 1}`,
|
|
375
|
+
line_items: m.line_item_ids.map((liId) => {
|
|
376
|
+
const li = session.line_items.find((l) => l.id === liId);
|
|
377
|
+
return { id: liId, quantity: li?.quantity || 1 };
|
|
378
|
+
}),
|
|
379
|
+
method_type: m.type,
|
|
380
|
+
destination: m.destinations.find((d) => d.id === m.selected_destination_id),
|
|
381
|
+
description: 'Arrives in 5-7 business days',
|
|
382
|
+
fulfillable_on: 'now',
|
|
383
|
+
})),
|
|
384
|
+
events: [],
|
|
385
|
+
}
|
|
338
386
|
: undefined,
|
|
339
387
|
created_at: new Date().toISOString(),
|
|
340
388
|
};
|
|
341
389
|
|
|
342
|
-
orders.set(orderId, order);
|
|
343
|
-
|
|
390
|
+
this.orders.set(orderId, order);
|
|
391
|
+
|
|
344
392
|
// Update session to completed state with order confirmation
|
|
345
393
|
session.status = 'completed';
|
|
346
394
|
session.order = {
|
|
@@ -349,29 +397,29 @@ export class UCPServer {
|
|
|
349
397
|
};
|
|
350
398
|
delete session.continue_url;
|
|
351
399
|
delete session.messages;
|
|
352
|
-
checkoutSessions.set(id, session);
|
|
400
|
+
this.checkoutSessions.set(id, session);
|
|
353
401
|
|
|
354
402
|
return session;
|
|
355
403
|
}
|
|
356
404
|
|
|
357
405
|
// Cancel checkout
|
|
358
406
|
cancelCheckout(id: string): CheckoutSession | undefined {
|
|
359
|
-
const session = checkoutSessions.get(id);
|
|
407
|
+
const session = this.checkoutSessions.get(id);
|
|
360
408
|
if (!session) return undefined;
|
|
361
409
|
session.status = 'canceled';
|
|
362
410
|
delete session.continue_url;
|
|
363
|
-
checkoutSessions.set(id, session);
|
|
411
|
+
this.checkoutSessions.set(id, session);
|
|
364
412
|
return session;
|
|
365
413
|
}
|
|
366
414
|
|
|
367
415
|
// Get order
|
|
368
416
|
getOrder(id: string): Order | undefined {
|
|
369
|
-
return orders.get(id);
|
|
417
|
+
return this.orders.get(id);
|
|
370
418
|
}
|
|
371
419
|
|
|
372
420
|
// List orders
|
|
373
421
|
listOrders(): Order[] {
|
|
374
|
-
return Array.from(orders.values());
|
|
422
|
+
return Array.from(this.orders.values());
|
|
375
423
|
}
|
|
376
424
|
|
|
377
425
|
getConfig(): MerchantConfig {
|