ucpify 1.2.0 → 1.2.1
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/package.json +18 -18
- package/dist/__tests__/api.test.d.ts +0 -1
- package/dist/__tests__/api.test.js +0 -121
- package/dist/cli.d.ts +0 -2
- package/dist/cli.js +0 -235
- package/dist/config.d.ts +0 -14
- package/dist/config.js +0 -22
- package/dist/db.d.ts +0 -4
- package/dist/db.js +0 -164
- package/dist/express-app.d.ts +0 -5
- package/dist/express-app.js +0 -495
- package/dist/index.d.ts +0 -4
- package/dist/index.js +0 -12
- package/dist/logger.d.ts +0 -3
- package/dist/logger.js +0 -15
- package/dist/oauth.d.ts +0 -60
- package/dist/oauth.js +0 -211
- package/dist/paypal.d.ts +0 -20
- package/dist/paypal.js +0 -111
- package/dist/schema.d.ts +0 -287
- package/dist/schema.js +0 -113
- package/dist/stripe.d.ts +0 -14
- package/dist/stripe.js +0 -103
- package/dist/ucp-server-db.d.ts +0 -76
- package/dist/ucp-server-db.js +0 -369
- package/dist/ucp-server.d.ts +0 -83
- package/dist/ucp-server.js +0 -381
package/dist/express-app.js
DELETED
|
@@ -1,495 +0,0 @@
|
|
|
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
|
-
const oauth_1 = require("./oauth");
|
|
17
|
-
// Rate limiters
|
|
18
|
-
const apiLimiter = (0, express_rate_limit_1.default)({
|
|
19
|
-
windowMs: 60 * 1000, // 1 minute
|
|
20
|
-
max: 100, // 100 requests per minute
|
|
21
|
-
standardHeaders: true,
|
|
22
|
-
legacyHeaders: false,
|
|
23
|
-
message: { error: 'Too many requests, please try again later' },
|
|
24
|
-
});
|
|
25
|
-
const checkoutLimiter = (0, express_rate_limit_1.default)({
|
|
26
|
-
windowMs: 60 * 1000,
|
|
27
|
-
max: 20, // 20 checkout creates per minute
|
|
28
|
-
standardHeaders: true,
|
|
29
|
-
legacyHeaders: false,
|
|
30
|
-
message: { error: 'Too many checkout requests' },
|
|
31
|
-
});
|
|
32
|
-
function createExpressApp(config, options) {
|
|
33
|
-
const app = (0, express_1.default)();
|
|
34
|
-
const ucpServer = options?.useDb
|
|
35
|
-
? new ucp_server_db_1.UCPServerDB(config)
|
|
36
|
-
: new ucp_server_1.UCPServer(config);
|
|
37
|
-
const useDb = options?.useDb ?? false;
|
|
38
|
-
// OAuth setup (built-in or external)
|
|
39
|
-
const oauthEnabled = !!config.oauth;
|
|
40
|
-
const oauthManager = oauthEnabled && config.oauth?.provider === 'built-in'
|
|
41
|
-
? new oauth_1.OAuthManager()
|
|
42
|
-
: null;
|
|
43
|
-
// Apply rate limiting to API routes
|
|
44
|
-
app.use('/ucp/v1/', apiLimiter);
|
|
45
|
-
// Stripe webhook needs raw body - must come before json parser
|
|
46
|
-
app.post('/webhooks/stripe', express_1.default.raw({ type: 'application/json' }), (req, res) => {
|
|
47
|
-
const signature = req.headers['stripe-signature'];
|
|
48
|
-
if (!signature) {
|
|
49
|
-
res.status(400).json({ error: 'Missing stripe-signature header' });
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
const event = (0, stripe_1.constructWebhookEvent)(req.body, signature);
|
|
53
|
-
if (!event) {
|
|
54
|
-
res.status(400).json({ error: 'Invalid webhook signature' });
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
logger_1.logger.info({ type: event.type }, 'Stripe webhook received');
|
|
58
|
-
switch (event.type) {
|
|
59
|
-
case 'payment_intent.succeeded': {
|
|
60
|
-
const paymentIntent = event.data.object;
|
|
61
|
-
const checkoutId = paymentIntent.metadata?.checkout_id;
|
|
62
|
-
const orderId = paymentIntent.metadata?.order_id;
|
|
63
|
-
if (checkoutId && useDb) {
|
|
64
|
-
db_1.db.prepare(`UPDATE checkouts SET payment_status = 'succeeded', updated_at = ? WHERE id = ?`)
|
|
65
|
-
.run(new Date().toISOString(), checkoutId);
|
|
66
|
-
if (orderId) {
|
|
67
|
-
db_1.db.prepare(`UPDATE orders SET payment_status = 'succeeded' WHERE id = ?`).run(orderId);
|
|
68
|
-
}
|
|
69
|
-
logger_1.logger.info({ checkoutId, orderId, paymentIntentId: paymentIntent.id }, 'Payment succeeded');
|
|
70
|
-
}
|
|
71
|
-
break;
|
|
72
|
-
}
|
|
73
|
-
case 'payment_intent.payment_failed': {
|
|
74
|
-
const paymentIntent = event.data.object;
|
|
75
|
-
const checkoutId = paymentIntent.metadata?.checkout_id;
|
|
76
|
-
const orderId = paymentIntent.metadata?.order_id;
|
|
77
|
-
if (checkoutId && useDb) {
|
|
78
|
-
db_1.db.prepare(`UPDATE checkouts SET payment_status = 'failed', updated_at = ? WHERE id = ?`)
|
|
79
|
-
.run(new Date().toISOString(), checkoutId);
|
|
80
|
-
if (orderId) {
|
|
81
|
-
db_1.db.prepare(`UPDATE orders SET payment_status = 'failed' WHERE id = ?`).run(orderId);
|
|
82
|
-
}
|
|
83
|
-
logger_1.logger.error({
|
|
84
|
-
checkoutId, orderId, paymentIntentId: paymentIntent.id,
|
|
85
|
-
error: paymentIntent.last_payment_error?.message
|
|
86
|
-
}, 'Payment failed');
|
|
87
|
-
}
|
|
88
|
-
break;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
res.json({ received: true });
|
|
92
|
-
});
|
|
93
|
-
// PayPal webhook - capture completed orders
|
|
94
|
-
app.post('/webhooks/paypal', express_1.default.json(), async (req, res) => {
|
|
95
|
-
const eventType = req.body.event_type;
|
|
96
|
-
const resource = req.body.resource;
|
|
97
|
-
logger_1.logger.info({ eventType, resourceId: resource?.id }, 'PayPal webhook received');
|
|
98
|
-
if (eventType === 'CHECKOUT.ORDER.APPROVED' && resource?.id) {
|
|
99
|
-
try {
|
|
100
|
-
// Capture the payment
|
|
101
|
-
const capturedOrder = await (0, paypal_1.capturePayPalOrder)(resource.id);
|
|
102
|
-
if (capturedOrder && capturedOrder.status === 'COMPLETED') {
|
|
103
|
-
// Get the checkout ID from the order
|
|
104
|
-
const orderDetails = await (0, paypal_1.getPayPalOrder)(resource.id);
|
|
105
|
-
const checkoutId = orderDetails?.customId;
|
|
106
|
-
if (checkoutId && useDb) {
|
|
107
|
-
const now = new Date().toISOString();
|
|
108
|
-
db_1.db.prepare(`UPDATE checkouts SET payment_status = 'succeeded', updated_at = ? WHERE paypal_order_id = ?`)
|
|
109
|
-
.run(now, resource.id);
|
|
110
|
-
db_1.db.prepare(`UPDATE orders SET payment_status = 'succeeded' WHERE paypal_order_id = ?`)
|
|
111
|
-
.run(resource.id);
|
|
112
|
-
logger_1.logger.info({ checkoutId, paypalOrderId: resource.id }, 'PayPal payment captured and succeeded');
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
catch (error) {
|
|
117
|
-
logger_1.logger.error({ error, paypalOrderId: resource.id }, 'Failed to capture PayPal order');
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
else if (eventType === 'PAYMENT.CAPTURE.DENIED' && resource?.id) {
|
|
121
|
-
if (useDb) {
|
|
122
|
-
const now = new Date().toISOString();
|
|
123
|
-
db_1.db.prepare(`UPDATE checkouts SET payment_status = 'failed', updated_at = ? WHERE paypal_order_id = ?`)
|
|
124
|
-
.run(now, resource.supplementary_data?.related_ids?.order_id);
|
|
125
|
-
db_1.db.prepare(`UPDATE orders SET payment_status = 'failed' WHERE paypal_order_id = ?`)
|
|
126
|
-
.run(resource.supplementary_data?.related_ids?.order_id);
|
|
127
|
-
logger_1.logger.error({ paypalOrderId: resource.id }, 'PayPal payment denied');
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
res.json({ received: true });
|
|
131
|
-
});
|
|
132
|
-
app.use(express_1.default.json());
|
|
133
|
-
// Request logging middleware
|
|
134
|
-
app.use((req, res, next) => {
|
|
135
|
-
const start = Date.now();
|
|
136
|
-
res.on('finish', () => {
|
|
137
|
-
logger_1.logger.info({
|
|
138
|
-
method: req.method,
|
|
139
|
-
path: req.path,
|
|
140
|
-
status: res.statusCode,
|
|
141
|
-
duration: Date.now() - start,
|
|
142
|
-
}, 'Request completed');
|
|
143
|
-
});
|
|
144
|
-
next();
|
|
145
|
-
});
|
|
146
|
-
// CORS middleware
|
|
147
|
-
app.use((_req, res, next) => {
|
|
148
|
-
res.header('Access-Control-Allow-Origin', '*');
|
|
149
|
-
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
|
150
|
-
res.header('Access-Control-Allow-Headers', 'Content-Type, UCP-Agent, Authorization');
|
|
151
|
-
next();
|
|
152
|
-
});
|
|
153
|
-
// ─── OAuth 2.0 Identity Linking ─────────────────────────────────────
|
|
154
|
-
// RFC 8414: OAuth Authorization Server Metadata
|
|
155
|
-
app.get('/.well-known/oauth-authorization-server', (_req, res) => {
|
|
156
|
-
if (!oauthEnabled) {
|
|
157
|
-
res.status(404).json({ error: 'OAuth not configured' });
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
160
|
-
if (config.oauth?.provider === 'external') {
|
|
161
|
-
// Proxy the merchant's external OAuth metadata
|
|
162
|
-
res.json({
|
|
163
|
-
issuer: config.oauth.issuer,
|
|
164
|
-
authorization_endpoint: config.oauth.authorization_endpoint,
|
|
165
|
-
token_endpoint: config.oauth.token_endpoint,
|
|
166
|
-
revocation_endpoint: config.oauth.revocation_endpoint,
|
|
167
|
-
jwks_uri: config.oauth.jwks_uri,
|
|
168
|
-
scopes_supported: ['ucp:scopes:checkout_session'],
|
|
169
|
-
response_types_supported: ['code'],
|
|
170
|
-
grant_types_supported: ['authorization_code', 'refresh_token'],
|
|
171
|
-
token_endpoint_auth_methods_supported: ['client_secret_basic'],
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
else if (oauthManager) {
|
|
175
|
-
res.json(oauthManager.getServerMetadata(config.domain));
|
|
176
|
-
}
|
|
177
|
-
});
|
|
178
|
-
// Built-in OAuth endpoints (only when provider is "built-in")
|
|
179
|
-
if (oauthManager) {
|
|
180
|
-
// Authorization endpoint - renders consent screen
|
|
181
|
-
app.get('/oauth2/authorize', (req, res) => {
|
|
182
|
-
const { client_id, redirect_uri, scope, state, response_type, code_challenge, code_challenge_method } = req.query;
|
|
183
|
-
if (response_type !== 'code') {
|
|
184
|
-
res.status(400).json({ error: 'unsupported_response_type' });
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
187
|
-
const client = oauthManager.getClient(client_id);
|
|
188
|
-
if (!client) {
|
|
189
|
-
res.status(400).json({ error: 'invalid_client' });
|
|
190
|
-
return;
|
|
191
|
-
}
|
|
192
|
-
if (!client.redirect_uris.includes(redirect_uri)) {
|
|
193
|
-
res.status(400).json({ error: 'invalid_redirect_uri' });
|
|
194
|
-
return;
|
|
195
|
-
}
|
|
196
|
-
// Render a minimal consent page
|
|
197
|
-
res.send(`<!DOCTYPE html>
|
|
198
|
-
<html><head><title>Authorize - ${config.name}</title>
|
|
199
|
-
<style>
|
|
200
|
-
body { font-family: system-ui, sans-serif; max-width: 400px; margin: 80px auto; padding: 0 20px; }
|
|
201
|
-
h2 { margin-bottom: 4px; }
|
|
202
|
-
p { color: #666; }
|
|
203
|
-
.scope { background: #f0f0f0; padding: 8px 12px; border-radius: 6px; font-family: monospace; }
|
|
204
|
-
button { padding: 10px 24px; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; margin-right: 8px; }
|
|
205
|
-
.allow { background: #000; color: #fff; }
|
|
206
|
-
.deny { background: #e5e5e5; }
|
|
207
|
-
</style></head>
|
|
208
|
-
<body>
|
|
209
|
-
<h2>${config.name}</h2>
|
|
210
|
-
<p><strong>${client.name}</strong> wants to access your account.</p>
|
|
211
|
-
<div class="scope">${scope || 'ucp:scopes:checkout_session'}</div>
|
|
212
|
-
<p>This will allow the app to manage checkout sessions on your behalf.</p>
|
|
213
|
-
<form method="POST" action="/oauth2/authorize">
|
|
214
|
-
<input type="hidden" name="client_id" value="${client_id}">
|
|
215
|
-
<input type="hidden" name="redirect_uri" value="${redirect_uri}">
|
|
216
|
-
<input type="hidden" name="scope" value="${scope || 'ucp:scopes:checkout_session'}">
|
|
217
|
-
<input type="hidden" name="state" value="${state || ''}">
|
|
218
|
-
<input type="hidden" name="code_challenge" value="${code_challenge || ''}">
|
|
219
|
-
<input type="hidden" name="code_challenge_method" value="${code_challenge_method || ''}">
|
|
220
|
-
<input type="hidden" name="user_id" value="user_1">
|
|
221
|
-
<button type="submit" name="action" value="allow" class="allow">Allow</button>
|
|
222
|
-
<button type="submit" name="action" value="deny" class="deny">Deny</button>
|
|
223
|
-
</form>
|
|
224
|
-
</body></html>`);
|
|
225
|
-
});
|
|
226
|
-
// Handle consent form submission
|
|
227
|
-
app.post('/oauth2/authorize', express_1.default.urlencoded({ extended: false }), (req, res) => {
|
|
228
|
-
const { client_id, redirect_uri, scope, state, action, user_id, code_challenge, code_challenge_method } = req.body;
|
|
229
|
-
if (action === 'deny') {
|
|
230
|
-
const url = new URL(redirect_uri);
|
|
231
|
-
url.searchParams.set('error', 'access_denied');
|
|
232
|
-
if (state)
|
|
233
|
-
url.searchParams.set('state', state);
|
|
234
|
-
res.redirect(url.toString());
|
|
235
|
-
return;
|
|
236
|
-
}
|
|
237
|
-
const code = oauthManager.createAuthorizationCode(client_id, user_id, scope, redirect_uri, code_challenge || undefined, code_challenge_method || undefined);
|
|
238
|
-
const url = new URL(redirect_uri);
|
|
239
|
-
url.searchParams.set('code', code);
|
|
240
|
-
if (state)
|
|
241
|
-
url.searchParams.set('state', state);
|
|
242
|
-
res.redirect(url.toString());
|
|
243
|
-
});
|
|
244
|
-
// Token endpoint
|
|
245
|
-
app.post('/oauth2/token', (req, res) => {
|
|
246
|
-
// Client auth via HTTP Basic (RFC 7617)
|
|
247
|
-
const authHeader = req.headers.authorization;
|
|
248
|
-
let clientId = req.body.client_id;
|
|
249
|
-
let clientSecret = req.body.client_secret;
|
|
250
|
-
if (authHeader && authHeader.startsWith('Basic ')) {
|
|
251
|
-
const decoded = Buffer.from(authHeader.slice(6), 'base64').toString();
|
|
252
|
-
const [id, secret] = decoded.split(':');
|
|
253
|
-
clientId = id;
|
|
254
|
-
clientSecret = secret;
|
|
255
|
-
}
|
|
256
|
-
if (!oauthManager.authenticateClient(clientId, clientSecret)) {
|
|
257
|
-
res.status(401).json({ error: 'invalid_client' });
|
|
258
|
-
return;
|
|
259
|
-
}
|
|
260
|
-
const grantType = req.body.grant_type;
|
|
261
|
-
if (grantType === 'authorization_code') {
|
|
262
|
-
const result = oauthManager.exchangeCode(req.body.code, clientId, req.body.redirect_uri, req.body.code_verifier);
|
|
263
|
-
if ('error' in result) {
|
|
264
|
-
res.status(400).json(result);
|
|
265
|
-
return;
|
|
266
|
-
}
|
|
267
|
-
res.json({ ...result, token_type: 'Bearer' });
|
|
268
|
-
}
|
|
269
|
-
else if (grantType === 'refresh_token') {
|
|
270
|
-
const result = oauthManager.refreshAccessToken(req.body.refresh_token, clientId);
|
|
271
|
-
if ('error' in result) {
|
|
272
|
-
res.status(400).json(result);
|
|
273
|
-
return;
|
|
274
|
-
}
|
|
275
|
-
res.json({ ...result, token_type: 'Bearer' });
|
|
276
|
-
}
|
|
277
|
-
else {
|
|
278
|
-
res.status(400).json({ error: 'unsupported_grant_type' });
|
|
279
|
-
}
|
|
280
|
-
});
|
|
281
|
-
// Token revocation (RFC 7009)
|
|
282
|
-
app.post('/oauth2/revoke', (req, res) => {
|
|
283
|
-
const authHeader = req.headers.authorization;
|
|
284
|
-
let clientId = req.body.client_id;
|
|
285
|
-
let clientSecret = req.body.client_secret;
|
|
286
|
-
if (authHeader && authHeader.startsWith('Basic ')) {
|
|
287
|
-
const decoded = Buffer.from(authHeader.slice(6), 'base64').toString();
|
|
288
|
-
const [id, secret] = decoded.split(':');
|
|
289
|
-
clientId = id;
|
|
290
|
-
clientSecret = secret;
|
|
291
|
-
}
|
|
292
|
-
if (!oauthManager.authenticateClient(clientId, clientSecret)) {
|
|
293
|
-
res.status(401).json({ error: 'invalid_client' });
|
|
294
|
-
return;
|
|
295
|
-
}
|
|
296
|
-
oauthManager.revokeToken(req.body.token);
|
|
297
|
-
// RFC 7009: always respond 200, even if token was invalid
|
|
298
|
-
res.status(200).json({});
|
|
299
|
-
});
|
|
300
|
-
}
|
|
301
|
-
// Bearer token middleware for UCP routes (when OAuth is enabled)
|
|
302
|
-
if (oauthEnabled && oauthManager) {
|
|
303
|
-
app.use('/ucp/v1', (req, res, next) => {
|
|
304
|
-
const authHeader = req.headers.authorization;
|
|
305
|
-
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
306
|
-
res.status(401).json({ error: 'missing_token', message: 'Authorization: Bearer <token> required' });
|
|
307
|
-
return;
|
|
308
|
-
}
|
|
309
|
-
const token = authHeader.slice(7);
|
|
310
|
-
const validToken = oauthManager.validateAccessToken(token);
|
|
311
|
-
if (!validToken) {
|
|
312
|
-
res.status(401).json({ error: 'invalid_token', message: 'Token is expired, revoked, or invalid' });
|
|
313
|
-
return;
|
|
314
|
-
}
|
|
315
|
-
// Attach user info to request
|
|
316
|
-
req.oauthUserId = validToken.user_id;
|
|
317
|
-
req.oauthClientId = validToken.client_id;
|
|
318
|
-
req.oauthScope = validToken.scope;
|
|
319
|
-
next();
|
|
320
|
-
});
|
|
321
|
-
}
|
|
322
|
-
// UCP Discovery endpoint
|
|
323
|
-
app.get('/.well-known/ucp', (_req, res) => {
|
|
324
|
-
res.json(ucpServer.getProfile());
|
|
325
|
-
});
|
|
326
|
-
// Create Checkout Session
|
|
327
|
-
app.post('/ucp/v1/checkout-sessions', checkoutLimiter, (req, res) => {
|
|
328
|
-
const parsed = schema_1.CreateCheckoutRequestSchema.safeParse(req.body);
|
|
329
|
-
if (!parsed.success) {
|
|
330
|
-
res.status(400).json({ error: 'Validation failed', details: parsed.error.issues });
|
|
331
|
-
return;
|
|
332
|
-
}
|
|
333
|
-
try {
|
|
334
|
-
const session = ucpServer.createCheckout(parsed.data);
|
|
335
|
-
res.status(201).json(session);
|
|
336
|
-
}
|
|
337
|
-
catch (error) {
|
|
338
|
-
res.status(400).json({ error: 'Invalid request', details: String(error) });
|
|
339
|
-
}
|
|
340
|
-
});
|
|
341
|
-
// Get Checkout Session
|
|
342
|
-
app.get('/ucp/v1/checkout-sessions/:id', (req, res) => {
|
|
343
|
-
const id = Array.isArray(req.params.id) ? req.params.id[0] : req.params.id;
|
|
344
|
-
const session = ucpServer.getCheckout(id);
|
|
345
|
-
if (!session) {
|
|
346
|
-
res.status(404).json({ error: 'Checkout session not found' });
|
|
347
|
-
return;
|
|
348
|
-
}
|
|
349
|
-
res.json(session);
|
|
350
|
-
});
|
|
351
|
-
// Update Checkout Session
|
|
352
|
-
app.put('/ucp/v1/checkout-sessions/:id', (req, res) => {
|
|
353
|
-
const parsed = schema_1.UpdateCheckoutRequestSchema.safeParse(req.body);
|
|
354
|
-
if (!parsed.success) {
|
|
355
|
-
res.status(400).json({ error: 'Validation failed', details: parsed.error.issues });
|
|
356
|
-
return;
|
|
357
|
-
}
|
|
358
|
-
try {
|
|
359
|
-
const id = Array.isArray(req.params.id) ? req.params.id[0] : req.params.id;
|
|
360
|
-
const session = ucpServer.updateCheckout(id, parsed.data);
|
|
361
|
-
if (!session) {
|
|
362
|
-
res.status(404).json({ error: 'Checkout session not found' });
|
|
363
|
-
return;
|
|
364
|
-
}
|
|
365
|
-
res.json(session);
|
|
366
|
-
}
|
|
367
|
-
catch (error) {
|
|
368
|
-
res.status(400).json({ error: 'Invalid request', details: String(error) });
|
|
369
|
-
}
|
|
370
|
-
});
|
|
371
|
-
// Complete Checkout
|
|
372
|
-
app.post('/ucp/v1/checkout-sessions/:id/complete', async (req, res) => {
|
|
373
|
-
const id = Array.isArray(req.params.id) ? req.params.id[0] : req.params.id;
|
|
374
|
-
try {
|
|
375
|
-
const result = await ucpServer.completeCheckout(id);
|
|
376
|
-
if ('error' in result) {
|
|
377
|
-
res.status(400).json(result);
|
|
378
|
-
return;
|
|
379
|
-
}
|
|
380
|
-
res.status(201).json(result);
|
|
381
|
-
}
|
|
382
|
-
catch (error) {
|
|
383
|
-
logger_1.logger.error({ error, checkoutId: id }, 'Failed to complete checkout');
|
|
384
|
-
res.status(500).json({ error: 'Internal server error' });
|
|
385
|
-
}
|
|
386
|
-
});
|
|
387
|
-
// Cancel Checkout
|
|
388
|
-
app.post('/ucp/v1/checkout-sessions/:id/cancel', (req, res) => {
|
|
389
|
-
const id = Array.isArray(req.params.id) ? req.params.id[0] : req.params.id;
|
|
390
|
-
const session = ucpServer.cancelCheckout(id);
|
|
391
|
-
if (!session) {
|
|
392
|
-
res.status(404).json({ error: 'Checkout session not found' });
|
|
393
|
-
return;
|
|
394
|
-
}
|
|
395
|
-
res.json(session);
|
|
396
|
-
});
|
|
397
|
-
// Get Order
|
|
398
|
-
app.get('/ucp/v1/orders/:id', (req, res) => {
|
|
399
|
-
const id = Array.isArray(req.params.id) ? req.params.id[0] : req.params.id;
|
|
400
|
-
const order = ucpServer.getOrder(id);
|
|
401
|
-
if (!order) {
|
|
402
|
-
res.status(404).json({ error: 'Order not found' });
|
|
403
|
-
return;
|
|
404
|
-
}
|
|
405
|
-
res.json(order);
|
|
406
|
-
});
|
|
407
|
-
// List Orders
|
|
408
|
-
app.get('/ucp/v1/orders', (_req, res) => {
|
|
409
|
-
res.json(ucpServer.listOrders());
|
|
410
|
-
});
|
|
411
|
-
// Product catalog (convenience endpoint)
|
|
412
|
-
app.get('/ucp/v1/items', (_req, res) => {
|
|
413
|
-
res.json(ucpServer.getConfig().items);
|
|
414
|
-
});
|
|
415
|
-
// Admin stats endpoint
|
|
416
|
-
app.get('/admin/stats', (_req, res) => {
|
|
417
|
-
if (!useDb) {
|
|
418
|
-
res.status(501).json({ error: 'Stats require database mode' });
|
|
419
|
-
return;
|
|
420
|
-
}
|
|
421
|
-
try {
|
|
422
|
-
const checkoutStats = db_1.db.prepare(`
|
|
423
|
-
SELECT status, COUNT(*) as count FROM checkouts GROUP BY status
|
|
424
|
-
`).all();
|
|
425
|
-
const orderStats = db_1.db.prepare(`
|
|
426
|
-
SELECT payment_status, payment_provider, COUNT(*) as count
|
|
427
|
-
FROM orders GROUP BY payment_status, payment_provider
|
|
428
|
-
`).all();
|
|
429
|
-
const revenueResult = db_1.db.prepare(`
|
|
430
|
-
SELECT SUM(json_extract(totals_json, '$[?(@.type=="total")].amount')) as total
|
|
431
|
-
FROM orders WHERE payment_status = 'succeeded'
|
|
432
|
-
`).get();
|
|
433
|
-
const todayOrders = db_1.db.prepare(`
|
|
434
|
-
SELECT COUNT(*) as count FROM orders
|
|
435
|
-
WHERE date(created_at) = date('now')
|
|
436
|
-
`).get();
|
|
437
|
-
res.json({
|
|
438
|
-
checkouts: Object.fromEntries(checkoutStats.map(s => [s.status, s.count])),
|
|
439
|
-
orders: {
|
|
440
|
-
by_payment_status: Object.fromEntries(orderStats.map(s => [`${s.payment_provider || 'none'}_${s.payment_status}`, s.count])),
|
|
441
|
-
today: todayOrders.count,
|
|
442
|
-
},
|
|
443
|
-
revenue: {
|
|
444
|
-
total_succeeded: revenueResult.total || 0,
|
|
445
|
-
},
|
|
446
|
-
timestamp: new Date().toISOString(),
|
|
447
|
-
});
|
|
448
|
-
}
|
|
449
|
-
catch (error) {
|
|
450
|
-
logger_1.logger.error({ error }, 'Failed to get admin stats');
|
|
451
|
-
res.status(500).json({ error: 'Failed to get stats' });
|
|
452
|
-
}
|
|
453
|
-
});
|
|
454
|
-
// Admin dashboard HTML
|
|
455
|
-
app.get('/admin', (_req, res) => {
|
|
456
|
-
const statsScript = useDb
|
|
457
|
-
? `fetch('/admin/stats').then(r=>r.json()).then(d=>{
|
|
458
|
-
document.getElementById('stats').innerHTML = \`
|
|
459
|
-
<div class="card"><div class="stat">\${Object.values(d.checkouts).reduce((a,b)=>a+b,0)}</div><div class="label">Total Checkouts</div></div>
|
|
460
|
-
<div class="card"><div class="stat">\${d.checkouts.completed||0}</div><div class="label">Completed</div></div>
|
|
461
|
-
<div class="card"><div class="stat">\${d.orders.today||0}</div><div class="label">Orders Today</div></div>
|
|
462
|
-
<div class="card"><div class="stat">$\${(d.revenue.total_succeeded/100).toFixed(2)}</div><div class="label">Revenue</div></div>
|
|
463
|
-
\`;
|
|
464
|
-
}).catch(e=>document.getElementById('stats').innerHTML='Error loading stats');`
|
|
465
|
-
: `document.getElementById('stats').innerHTML = '<div class="card" style="grid-column:1/-1"><div class="label">Running in <strong>in-memory mode</strong>. Stats require <code>--db</code> mode.</div><div style="margin-top:8px;font-size:0.85em;color:#999">Restart with: <code>ucpify serve config.json</code> (DB is on by default)</div></div>';`;
|
|
466
|
-
res.send(`<!DOCTYPE html>
|
|
467
|
-
<html><head><title>UCP Admin</title>
|
|
468
|
-
<style>
|
|
469
|
-
body { font-family: system-ui; max-width: 800px; margin: 40px auto; padding: 20px; }
|
|
470
|
-
.card { background: #f5f5f5; padding: 20px; border-radius: 8px; margin: 10px 0; }
|
|
471
|
-
.stat { font-size: 2em; font-weight: bold; color: #333; }
|
|
472
|
-
.label { color: #666; font-size: 0.9em; }
|
|
473
|
-
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; }
|
|
474
|
-
h1 { color: #333; }
|
|
475
|
-
code { background: #e5e5e5; padding: 2px 6px; border-radius: 3px; font-size: 0.85em; }
|
|
476
|
-
</style>
|
|
477
|
-
</head><body>
|
|
478
|
-
<h1>🛒 UCP Server Admin</h1>
|
|
479
|
-
<div class="grid" id="stats">Loading...</div>
|
|
480
|
-
<script>${statsScript}</script>
|
|
481
|
-
</body></html>`);
|
|
482
|
-
});
|
|
483
|
-
// Health check
|
|
484
|
-
app.get('/health', (_req, res) => {
|
|
485
|
-
const dbHealthy = (0, db_1.isDbHealthy)();
|
|
486
|
-
const status = dbHealthy ? 'ok' : 'degraded';
|
|
487
|
-
res.status(dbHealthy ? 200 : 503).json({
|
|
488
|
-
status,
|
|
489
|
-
ucp_version: schema_1.UCP_VERSION,
|
|
490
|
-
database: dbHealthy ? 'connected' : 'disconnected',
|
|
491
|
-
timestamp: new Date().toISOString(),
|
|
492
|
-
});
|
|
493
|
-
});
|
|
494
|
-
return app;
|
|
495
|
-
}
|
package/dist/index.d.ts
DELETED
|
@@ -1,4 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
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; } });
|
package/dist/logger.d.ts
DELETED
package/dist/logger.js
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
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: 'ucpify' },
|
|
15
|
-
});
|
package/dist/oauth.d.ts
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
export interface OAuthClient {
|
|
2
|
-
client_id: string;
|
|
3
|
-
client_secret: string;
|
|
4
|
-
client_secret_hash: string;
|
|
5
|
-
name: string;
|
|
6
|
-
redirect_uris: string[];
|
|
7
|
-
created_at: string;
|
|
8
|
-
}
|
|
9
|
-
export interface AuthorizationCode {
|
|
10
|
-
code: string;
|
|
11
|
-
client_id: string;
|
|
12
|
-
user_id: string;
|
|
13
|
-
scope: string;
|
|
14
|
-
redirect_uri: string;
|
|
15
|
-
code_challenge?: string;
|
|
16
|
-
code_challenge_method?: string;
|
|
17
|
-
expires_at: number;
|
|
18
|
-
used: boolean;
|
|
19
|
-
}
|
|
20
|
-
export interface OAuthToken {
|
|
21
|
-
token: string;
|
|
22
|
-
token_type: 'access' | 'refresh';
|
|
23
|
-
client_id: string;
|
|
24
|
-
user_id: string;
|
|
25
|
-
scope: string;
|
|
26
|
-
parent_token?: string;
|
|
27
|
-
expires_at: number;
|
|
28
|
-
revoked: boolean;
|
|
29
|
-
}
|
|
30
|
-
export declare const UCP_SCOPES: readonly ["ucp:scopes:checkout_session"];
|
|
31
|
-
export declare class OAuthManager {
|
|
32
|
-
private clients;
|
|
33
|
-
private codes;
|
|
34
|
-
private accessTokens;
|
|
35
|
-
private refreshTokens;
|
|
36
|
-
createClient(name: string, redirectUris: string[]): OAuthClient;
|
|
37
|
-
getClient(clientId: string): OAuthClient | undefined;
|
|
38
|
-
authenticateClient(clientId: string, clientSecret: string): boolean;
|
|
39
|
-
createAuthorizationCode(clientId: string, userId: string, scope: string, redirectUri: string, codeChallenge?: string, codeChallengeMethod?: string): string;
|
|
40
|
-
exchangeCode(code: string, clientId: string, redirectUri: string, codeVerifier?: string): {
|
|
41
|
-
access_token: string;
|
|
42
|
-
refresh_token: string;
|
|
43
|
-
expires_in: number;
|
|
44
|
-
scope: string;
|
|
45
|
-
} | {
|
|
46
|
-
error: string;
|
|
47
|
-
};
|
|
48
|
-
private createAccessToken;
|
|
49
|
-
private createRefreshToken;
|
|
50
|
-
refreshAccessToken(refreshToken: string, clientId: string): {
|
|
51
|
-
access_token: string;
|
|
52
|
-
expires_in: number;
|
|
53
|
-
scope: string;
|
|
54
|
-
} | {
|
|
55
|
-
error: string;
|
|
56
|
-
};
|
|
57
|
-
validateAccessToken(token: string): OAuthToken | null;
|
|
58
|
-
revokeToken(token: string): boolean;
|
|
59
|
-
getServerMetadata(issuer: string): Record<string, unknown>;
|
|
60
|
-
}
|