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.
@@ -0,0 +1,318 @@
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
+ exports.createExpressApp = createExpressApp;
7
+ const express_1 = __importDefault(require("express"));
8
+ const express_rate_limit_1 = __importDefault(require("express-rate-limit"));
9
+ const ucp_server_1 = require("./ucp-server");
10
+ const ucp_server_db_1 = require("./ucp-server-db");
11
+ const schema_1 = require("./schema");
12
+ const db_1 = require("./db");
13
+ const logger_1 = require("./logger");
14
+ const stripe_1 = require("./stripe");
15
+ const paypal_1 = require("./paypal");
16
+ // Rate limiters
17
+ const apiLimiter = (0, express_rate_limit_1.default)({
18
+ windowMs: 60 * 1000, // 1 minute
19
+ max: 100, // 100 requests per minute
20
+ standardHeaders: true,
21
+ legacyHeaders: false,
22
+ message: { error: 'Too many requests, please try again later' },
23
+ });
24
+ const checkoutLimiter = (0, express_rate_limit_1.default)({
25
+ windowMs: 60 * 1000,
26
+ max: 20, // 20 checkout creates per minute
27
+ standardHeaders: true,
28
+ legacyHeaders: false,
29
+ message: { error: 'Too many checkout requests' },
30
+ });
31
+ function createExpressApp(config, options) {
32
+ const app = (0, express_1.default)();
33
+ const ucpServer = options?.useDb
34
+ ? new ucp_server_db_1.UCPServerDB(config)
35
+ : new ucp_server_1.UCPServer(config);
36
+ const useDb = options?.useDb ?? false;
37
+ // Apply rate limiting to API routes
38
+ app.use('/ucp/v1/', apiLimiter);
39
+ // Stripe webhook needs raw body - must come before json parser
40
+ app.post('/webhooks/stripe', express_1.default.raw({ type: 'application/json' }), (req, res) => {
41
+ const signature = req.headers['stripe-signature'];
42
+ if (!signature) {
43
+ res.status(400).json({ error: 'Missing stripe-signature header' });
44
+ return;
45
+ }
46
+ const event = (0, stripe_1.constructWebhookEvent)(req.body, signature);
47
+ if (!event) {
48
+ res.status(400).json({ error: 'Invalid webhook signature' });
49
+ return;
50
+ }
51
+ logger_1.logger.info({ eventType: event.type, eventId: event.id }, 'Stripe webhook received');
52
+ switch (event.type) {
53
+ case 'payment_intent.succeeded': {
54
+ const paymentIntent = event.data.object;
55
+ const checkoutId = paymentIntent.metadata?.checkout_id;
56
+ const orderId = paymentIntent.metadata?.order_id;
57
+ if (checkoutId && useDb) {
58
+ db_1.db.prepare(`UPDATE checkouts SET payment_status = 'succeeded', updated_at = ? WHERE id = ?`)
59
+ .run(new Date().toISOString(), checkoutId);
60
+ if (orderId) {
61
+ db_1.db.prepare(`UPDATE orders SET payment_status = 'succeeded' WHERE id = ?`).run(orderId);
62
+ }
63
+ logger_1.logger.info({ checkoutId, orderId, paymentIntentId: paymentIntent.id }, 'Payment succeeded');
64
+ }
65
+ break;
66
+ }
67
+ case 'payment_intent.payment_failed': {
68
+ const paymentIntent = event.data.object;
69
+ const checkoutId = paymentIntent.metadata?.checkout_id;
70
+ const orderId = paymentIntent.metadata?.order_id;
71
+ if (checkoutId && useDb) {
72
+ db_1.db.prepare(`UPDATE checkouts SET payment_status = 'failed', updated_at = ? WHERE id = ?`)
73
+ .run(new Date().toISOString(), checkoutId);
74
+ if (orderId) {
75
+ db_1.db.prepare(`UPDATE orders SET payment_status = 'failed' WHERE id = ?`).run(orderId);
76
+ }
77
+ logger_1.logger.error({
78
+ checkoutId, orderId, paymentIntentId: paymentIntent.id,
79
+ error: paymentIntent.last_payment_error?.message
80
+ }, 'Payment failed');
81
+ }
82
+ break;
83
+ }
84
+ }
85
+ res.json({ received: true });
86
+ });
87
+ // PayPal webhook - capture completed orders
88
+ app.post('/webhooks/paypal', express_1.default.json(), async (req, res) => {
89
+ const eventType = req.body.event_type;
90
+ const resource = req.body.resource;
91
+ logger_1.logger.info({ eventType, resourceId: resource?.id }, 'PayPal webhook received');
92
+ if (eventType === 'CHECKOUT.ORDER.APPROVED' && resource?.id) {
93
+ try {
94
+ // Capture the payment
95
+ const capturedOrder = await (0, paypal_1.capturePayPalOrder)(resource.id);
96
+ if (capturedOrder && capturedOrder.status === 'COMPLETED') {
97
+ // Get the checkout ID from the order
98
+ const orderDetails = await (0, paypal_1.getPayPalOrder)(resource.id);
99
+ const checkoutId = orderDetails?.customId;
100
+ if (checkoutId && useDb) {
101
+ const now = new Date().toISOString();
102
+ db_1.db.prepare(`UPDATE checkouts SET payment_status = 'succeeded', updated_at = ? WHERE paypal_order_id = ?`)
103
+ .run(now, resource.id);
104
+ db_1.db.prepare(`UPDATE orders SET payment_status = 'succeeded' WHERE paypal_order_id = ?`)
105
+ .run(resource.id);
106
+ logger_1.logger.info({ checkoutId, paypalOrderId: resource.id }, 'PayPal payment captured and succeeded');
107
+ }
108
+ }
109
+ }
110
+ catch (error) {
111
+ logger_1.logger.error({ error, paypalOrderId: resource.id }, 'Failed to capture PayPal order');
112
+ }
113
+ }
114
+ else if (eventType === 'PAYMENT.CAPTURE.DENIED' && resource?.id) {
115
+ if (useDb) {
116
+ const now = new Date().toISOString();
117
+ db_1.db.prepare(`UPDATE checkouts SET payment_status = 'failed', updated_at = ? WHERE paypal_order_id = ?`)
118
+ .run(now, resource.supplementary_data?.related_ids?.order_id);
119
+ db_1.db.prepare(`UPDATE orders SET payment_status = 'failed' WHERE paypal_order_id = ?`)
120
+ .run(resource.supplementary_data?.related_ids?.order_id);
121
+ logger_1.logger.error({ paypalOrderId: resource.id }, 'PayPal payment denied');
122
+ }
123
+ }
124
+ res.json({ received: true });
125
+ });
126
+ app.use(express_1.default.json());
127
+ // Request logging middleware
128
+ app.use((req, res, next) => {
129
+ const start = Date.now();
130
+ res.on('finish', () => {
131
+ logger_1.logger.info({
132
+ method: req.method,
133
+ path: req.path,
134
+ status: res.statusCode,
135
+ duration: Date.now() - start,
136
+ }, 'Request completed');
137
+ });
138
+ next();
139
+ });
140
+ // CORS middleware
141
+ app.use((_req, res, next) => {
142
+ res.header('Access-Control-Allow-Origin', '*');
143
+ res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
144
+ res.header('Access-Control-Allow-Headers', 'Content-Type, UCP-Agent, Authorization');
145
+ next();
146
+ });
147
+ // UCP Discovery endpoint
148
+ app.get('/.well-known/ucp', (_req, res) => {
149
+ res.json(ucpServer.getProfile());
150
+ });
151
+ // Create Checkout Session
152
+ app.post('/ucp/v1/checkout-sessions', checkoutLimiter, (req, res) => {
153
+ const parsed = schema_1.CreateCheckoutRequestSchema.safeParse(req.body);
154
+ if (!parsed.success) {
155
+ res.status(400).json({ error: 'Validation failed', details: parsed.error.issues });
156
+ return;
157
+ }
158
+ try {
159
+ const session = ucpServer.createCheckout(parsed.data);
160
+ res.status(201).json(session);
161
+ }
162
+ catch (error) {
163
+ res.status(400).json({ error: 'Invalid request', details: String(error) });
164
+ }
165
+ });
166
+ // Get Checkout Session
167
+ app.get('/ucp/v1/checkout-sessions/:id', (req, res) => {
168
+ const id = Array.isArray(req.params.id) ? req.params.id[0] : req.params.id;
169
+ const session = ucpServer.getCheckout(id);
170
+ if (!session) {
171
+ res.status(404).json({ error: 'Checkout session not found' });
172
+ return;
173
+ }
174
+ res.json(session);
175
+ });
176
+ // Update Checkout Session
177
+ app.put('/ucp/v1/checkout-sessions/:id', (req, res) => {
178
+ const parsed = schema_1.UpdateCheckoutRequestSchema.safeParse(req.body);
179
+ if (!parsed.success) {
180
+ res.status(400).json({ error: 'Validation failed', details: parsed.error.issues });
181
+ return;
182
+ }
183
+ try {
184
+ const id = Array.isArray(req.params.id) ? req.params.id[0] : req.params.id;
185
+ const session = ucpServer.updateCheckout(id, parsed.data);
186
+ if (!session) {
187
+ res.status(404).json({ error: 'Checkout session not found' });
188
+ return;
189
+ }
190
+ res.json(session);
191
+ }
192
+ catch (error) {
193
+ res.status(400).json({ error: 'Invalid request', details: String(error) });
194
+ }
195
+ });
196
+ // Complete Checkout
197
+ app.post('/ucp/v1/checkout-sessions/:id/complete', async (req, res) => {
198
+ const id = Array.isArray(req.params.id) ? req.params.id[0] : req.params.id;
199
+ try {
200
+ const result = await ucpServer.completeCheckout(id);
201
+ if ('error' in result) {
202
+ res.status(400).json(result);
203
+ return;
204
+ }
205
+ res.status(201).json(result);
206
+ }
207
+ catch (error) {
208
+ logger_1.logger.error({ error, checkoutId: id }, 'Failed to complete checkout');
209
+ res.status(500).json({ error: 'Internal server error' });
210
+ }
211
+ });
212
+ // Cancel Checkout
213
+ app.post('/ucp/v1/checkout-sessions/:id/cancel', (req, res) => {
214
+ const id = Array.isArray(req.params.id) ? req.params.id[0] : req.params.id;
215
+ const session = ucpServer.cancelCheckout(id);
216
+ if (!session) {
217
+ res.status(404).json({ error: 'Checkout session not found' });
218
+ return;
219
+ }
220
+ res.json(session);
221
+ });
222
+ // Get Order
223
+ app.get('/ucp/v1/orders/:id', (req, res) => {
224
+ const id = Array.isArray(req.params.id) ? req.params.id[0] : req.params.id;
225
+ const order = ucpServer.getOrder(id);
226
+ if (!order) {
227
+ res.status(404).json({ error: 'Order not found' });
228
+ return;
229
+ }
230
+ res.json(order);
231
+ });
232
+ // List Orders
233
+ app.get('/ucp/v1/orders', (_req, res) => {
234
+ res.json(ucpServer.listOrders());
235
+ });
236
+ // Product catalog (convenience endpoint)
237
+ app.get('/ucp/v1/items', (_req, res) => {
238
+ res.json(ucpServer.getConfig().items);
239
+ });
240
+ // Admin stats endpoint
241
+ app.get('/admin/stats', (_req, res) => {
242
+ if (!useDb) {
243
+ res.status(501).json({ error: 'Stats require database mode' });
244
+ return;
245
+ }
246
+ try {
247
+ const checkoutStats = db_1.db.prepare(`
248
+ SELECT status, COUNT(*) as count FROM checkouts GROUP BY status
249
+ `).all();
250
+ const orderStats = db_1.db.prepare(`
251
+ SELECT payment_status, payment_provider, COUNT(*) as count
252
+ FROM orders GROUP BY payment_status, payment_provider
253
+ `).all();
254
+ const revenueResult = db_1.db.prepare(`
255
+ SELECT SUM(json_extract(totals_json, '$[?(@.type=="total")].amount')) as total
256
+ FROM orders WHERE payment_status = 'succeeded'
257
+ `).get();
258
+ const todayOrders = db_1.db.prepare(`
259
+ SELECT COUNT(*) as count FROM orders
260
+ WHERE date(created_at) = date('now')
261
+ `).get();
262
+ res.json({
263
+ checkouts: Object.fromEntries(checkoutStats.map(s => [s.status, s.count])),
264
+ orders: {
265
+ by_payment_status: Object.fromEntries(orderStats.map(s => [`${s.payment_provider || 'none'}_${s.payment_status}`, s.count])),
266
+ today: todayOrders.count,
267
+ },
268
+ revenue: {
269
+ total_succeeded: revenueResult.total || 0,
270
+ },
271
+ timestamp: new Date().toISOString(),
272
+ });
273
+ }
274
+ catch (error) {
275
+ logger_1.logger.error({ error }, 'Failed to get admin stats');
276
+ res.status(500).json({ error: 'Failed to get stats' });
277
+ }
278
+ });
279
+ // Admin dashboard HTML
280
+ app.get('/admin', (_req, res) => {
281
+ res.send(`<!DOCTYPE html>
282
+ <html><head><title>UCP Admin</title>
283
+ <style>
284
+ body { font-family: system-ui; max-width: 800px; margin: 40px auto; padding: 20px; }
285
+ .card { background: #f5f5f5; padding: 20px; border-radius: 8px; margin: 10px 0; }
286
+ .stat { font-size: 2em; font-weight: bold; color: #333; }
287
+ .label { color: #666; font-size: 0.9em; }
288
+ .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; }
289
+ h1 { color: #333; }
290
+ </style>
291
+ </head><body>
292
+ <h1>🛒 UCP Server Admin</h1>
293
+ <div class="grid" id="stats">Loading...</div>
294
+ <script>
295
+ fetch('/admin/stats').then(r=>r.json()).then(d=>{
296
+ document.getElementById('stats').innerHTML = \`
297
+ <div class="card"><div class="stat">\${Object.values(d.checkouts).reduce((a,b)=>a+b,0)}</div><div class="label">Total Checkouts</div></div>
298
+ <div class="card"><div class="stat">\${d.checkouts.completed||0}</div><div class="label">Completed</div></div>
299
+ <div class="card"><div class="stat">\${d.orders.today||0}</div><div class="label">Orders Today</div></div>
300
+ <div class="card"><div class="stat">$\${(d.revenue.total_succeeded/100).toFixed(2)}</div><div class="label">Revenue</div></div>
301
+ \`;
302
+ }).catch(e=>document.getElementById('stats').innerHTML='Error loading stats');
303
+ </script>
304
+ </body></html>`);
305
+ });
306
+ // Health check
307
+ app.get('/health', (_req, res) => {
308
+ const dbHealthy = (0, db_1.isDbHealthy)();
309
+ const status = dbHealthy ? 'ok' : 'degraded';
310
+ res.status(dbHealthy ? 200 : 503).json({
311
+ status,
312
+ ucp_version: schema_1.UCP_VERSION,
313
+ database: dbHealthy ? 'connected' : 'disconnected',
314
+ timestamp: new Date().toISOString(),
315
+ });
316
+ });
317
+ return app;
318
+ }
@@ -0,0 +1,4 @@
1
+ export { MerchantConfigSchema, ItemSchema, ShippingOptionSchema, PaymentHandlerSchema } from './schema';
2
+ export type { MerchantConfig, Item, ShippingOption, PaymentHandler, CheckoutSession, Order } from './schema';
3
+ export { UCPServer } from './ucp-server';
4
+ export { createExpressApp } from './express-app';
package/dist/index.js ADDED
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createExpressApp = exports.UCPServer = exports.PaymentHandlerSchema = exports.ShippingOptionSchema = exports.ItemSchema = exports.MerchantConfigSchema = void 0;
4
+ var schema_1 = require("./schema");
5
+ Object.defineProperty(exports, "MerchantConfigSchema", { enumerable: true, get: function () { return schema_1.MerchantConfigSchema; } });
6
+ Object.defineProperty(exports, "ItemSchema", { enumerable: true, get: function () { return schema_1.ItemSchema; } });
7
+ Object.defineProperty(exports, "ShippingOptionSchema", { enumerable: true, get: function () { return schema_1.ShippingOptionSchema; } });
8
+ Object.defineProperty(exports, "PaymentHandlerSchema", { enumerable: true, get: function () { return schema_1.PaymentHandlerSchema; } });
9
+ var ucp_server_1 = require("./ucp-server");
10
+ Object.defineProperty(exports, "UCPServer", { enumerable: true, get: function () { return ucp_server_1.UCPServer; } });
11
+ var express_app_1 = require("./express-app");
12
+ Object.defineProperty(exports, "createExpressApp", { enumerable: true, get: function () { return express_app_1.createExpressApp; } });
@@ -0,0 +1,3 @@
1
+ import pino from 'pino';
2
+ export declare const logger: pino.Logger<never, boolean>;
3
+ export type Logger = typeof logger;
package/dist/logger.js ADDED
@@ -0,0 +1,15 @@
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
+ exports.logger = void 0;
7
+ const pino_1 = __importDefault(require("pino"));
8
+ const config_1 = require("./config");
9
+ exports.logger = (0, pino_1.default)({
10
+ level: config_1.config.logLevel,
11
+ transport: config_1.config.nodeEnv === 'development'
12
+ ? { target: 'pino-pretty', options: { colorize: true } }
13
+ : undefined,
14
+ base: { service: 'ucp-server' },
15
+ });
@@ -0,0 +1,20 @@
1
+ export declare function isPayPalConfigured(): boolean;
2
+ export interface CreatePayPalOrderParams {
3
+ amount: number;
4
+ currency: string;
5
+ checkoutId: string;
6
+ description?: string;
7
+ }
8
+ export declare function createPayPalOrder(params: CreatePayPalOrderParams): Promise<{
9
+ id: string;
10
+ status: string;
11
+ } | null>;
12
+ export declare function capturePayPalOrder(orderId: string): Promise<{
13
+ id: string;
14
+ status: string;
15
+ } | null>;
16
+ export declare function getPayPalOrder(orderId: string): Promise<{
17
+ id: string;
18
+ status: string;
19
+ customId?: string;
20
+ } | null>;
package/dist/paypal.js ADDED
@@ -0,0 +1,111 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isPayPalConfigured = isPayPalConfigured;
4
+ exports.createPayPalOrder = createPayPalOrder;
5
+ exports.capturePayPalOrder = capturePayPalOrder;
6
+ exports.getPayPalOrder = getPayPalOrder;
7
+ const paypal_server_sdk_1 = require("@paypal/paypal-server-sdk");
8
+ const config_1 = require("./config");
9
+ const logger_1 = require("./logger");
10
+ let paypalClient = null;
11
+ let ordersController = null;
12
+ function getPayPalClient() {
13
+ if (!config_1.config.paypalClientId || !config_1.config.paypalClientSecret) {
14
+ return null;
15
+ }
16
+ if (!paypalClient) {
17
+ paypalClient = new paypal_server_sdk_1.Client({
18
+ clientCredentialsAuthCredentials: {
19
+ oAuthClientId: config_1.config.paypalClientId,
20
+ oAuthClientSecret: config_1.config.paypalClientSecret,
21
+ },
22
+ environment: config_1.config.paypalMode === 'live' ? paypal_server_sdk_1.Environment.Production : paypal_server_sdk_1.Environment.Sandbox,
23
+ });
24
+ ordersController = new paypal_server_sdk_1.OrdersController(paypalClient);
25
+ logger_1.logger.info('PayPal client initialized');
26
+ }
27
+ return paypalClient;
28
+ }
29
+ function isPayPalConfigured() {
30
+ return !!(config_1.config.paypalClientId && config_1.config.paypalClientSecret);
31
+ }
32
+ async function createPayPalOrder(params) {
33
+ const client = getPayPalClient();
34
+ if (!client || !ordersController) {
35
+ logger_1.logger.warn('PayPal not configured, skipping order creation');
36
+ return null;
37
+ }
38
+ // Convert cents to dollars with 2 decimal places
39
+ const amountValue = (params.amount / 100).toFixed(2);
40
+ try {
41
+ const response = await ordersController.createOrder({
42
+ body: {
43
+ intent: paypal_server_sdk_1.CheckoutPaymentIntent.Capture,
44
+ purchaseUnits: [
45
+ {
46
+ amount: {
47
+ currencyCode: params.currency.toUpperCase(),
48
+ value: amountValue,
49
+ },
50
+ description: params.description || `Checkout ${params.checkoutId}`,
51
+ customId: params.checkoutId,
52
+ },
53
+ ],
54
+ },
55
+ prefer: 'return=representation',
56
+ });
57
+ const order = response.result;
58
+ logger_1.logger.info({
59
+ paypalOrderId: order.id,
60
+ checkoutId: params.checkoutId,
61
+ amount: params.amount,
62
+ }, 'PayPal order created');
63
+ return { id: order.id, status: order.status };
64
+ }
65
+ catch (error) {
66
+ if (error instanceof paypal_server_sdk_1.ApiError) {
67
+ logger_1.logger.error({ error: error.message, checkoutId: params.checkoutId }, 'Failed to create PayPal order');
68
+ }
69
+ throw error;
70
+ }
71
+ }
72
+ async function capturePayPalOrder(orderId) {
73
+ const client = getPayPalClient();
74
+ if (!client || !ordersController)
75
+ return null;
76
+ try {
77
+ const response = await ordersController.captureOrder({
78
+ id: orderId,
79
+ prefer: 'return=representation',
80
+ });
81
+ const order = response.result;
82
+ logger_1.logger.info({ paypalOrderId: orderId, status: order.status }, 'PayPal order captured');
83
+ return { id: order.id, status: order.status };
84
+ }
85
+ catch (error) {
86
+ if (error instanceof paypal_server_sdk_1.ApiError) {
87
+ logger_1.logger.error({ error: error.message, paypalOrderId: orderId }, 'Failed to capture PayPal order');
88
+ }
89
+ throw error;
90
+ }
91
+ }
92
+ async function getPayPalOrder(orderId) {
93
+ const client = getPayPalClient();
94
+ if (!client || !ordersController)
95
+ return null;
96
+ try {
97
+ const response = await ordersController.getOrder({ id: orderId });
98
+ const order = response.result;
99
+ return {
100
+ id: order.id,
101
+ status: order.status,
102
+ customId: order.purchaseUnits?.[0]?.customId,
103
+ };
104
+ }
105
+ catch (error) {
106
+ if (error instanceof paypal_server_sdk_1.ApiError) {
107
+ logger_1.logger.error({ error: error.message, paypalOrderId: orderId }, 'Failed to get PayPal order');
108
+ }
109
+ throw error;
110
+ }
111
+ }