spaps 0.2.7 โ 0.3.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/bin/spaps.js +82 -0
- package/package.json +1 -1
- package/src/ai-helper.js +273 -0
- package/src/docs-html.js +52 -5
- package/src/local-server.js +173 -0
- package/src/stripe-local.js +263 -0
package/bin/spaps.js
CHANGED
|
@@ -15,6 +15,7 @@ const fs = require('fs');
|
|
|
15
15
|
const { handleError } = require('../src/error-handler');
|
|
16
16
|
const { showInteractiveHelp, showQuickHelp } = require('../src/help-system');
|
|
17
17
|
const { showInteractiveDocs, showQuickReference, searchDocs } = require('../src/docs-system');
|
|
18
|
+
const { getQuickStartInstructions, getServerStatus, runQuickTest } = require('../src/ai-helper');
|
|
18
19
|
|
|
19
20
|
const version = require('../package.json').version;
|
|
20
21
|
|
|
@@ -93,6 +94,87 @@ program
|
|
|
93
94
|
}
|
|
94
95
|
});
|
|
95
96
|
|
|
97
|
+
// Quickstart command - For AI agents
|
|
98
|
+
program
|
|
99
|
+
.command('quickstart')
|
|
100
|
+
.description('Get quick start instructions (for AI agents)')
|
|
101
|
+
.option('-p, --port <port>', 'Port to check', '3300')
|
|
102
|
+
.option('--json', 'Output in JSON format')
|
|
103
|
+
.action(async (options) => {
|
|
104
|
+
const instructions = getQuickStartInstructions(options.port);
|
|
105
|
+
|
|
106
|
+
if (options.json === true) {
|
|
107
|
+
console.log(JSON.stringify(instructions, null, 2));
|
|
108
|
+
process.exit(0);
|
|
109
|
+
} else {
|
|
110
|
+
console.log(chalk.yellow('\n๐ SPAPS Quick Start Instructions\n'));
|
|
111
|
+
console.log('1. Install SDK: npm install spaps-sdk');
|
|
112
|
+
console.log('2. Create test file with the code above');
|
|
113
|
+
console.log('3. Run: node test-spaps.js');
|
|
114
|
+
console.log('\nFor JSON output: npx spaps quickstart --json');
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Status command - Check if server is running
|
|
119
|
+
program
|
|
120
|
+
.command('status')
|
|
121
|
+
.description('Check if SPAPS server is running')
|
|
122
|
+
.option('-p, --port <port>', 'Port to check', '3300')
|
|
123
|
+
.option('--json', 'Output in JSON format')
|
|
124
|
+
.action(async (options) => {
|
|
125
|
+
const status = await getServerStatus(options.port);
|
|
126
|
+
|
|
127
|
+
if (options.json) {
|
|
128
|
+
console.log(JSON.stringify(status));
|
|
129
|
+
} else {
|
|
130
|
+
if (status.running) {
|
|
131
|
+
console.log(chalk.green(`โ
SPAPS is running on port ${options.port}`));
|
|
132
|
+
console.log(chalk.blue(` URL: ${status.url}`));
|
|
133
|
+
console.log(chalk.blue(` Docs: ${status.docs}`));
|
|
134
|
+
} else {
|
|
135
|
+
console.log(chalk.red(`โ SPAPS is not running on port ${options.port}`));
|
|
136
|
+
console.log(chalk.yellow(` Start with: ${status.start_command}`));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Test command - Run quick tests
|
|
142
|
+
program
|
|
143
|
+
.command('test')
|
|
144
|
+
.description('Run quick tests to verify SPAPS is working')
|
|
145
|
+
.option('-p, --port <port>', 'Port to test', '3300')
|
|
146
|
+
.option('--json', 'Output in JSON format')
|
|
147
|
+
.action(async (options) => {
|
|
148
|
+
const results = await runQuickTest(options.port);
|
|
149
|
+
|
|
150
|
+
if (options.json) {
|
|
151
|
+
console.log(JSON.stringify(results, null, 2));
|
|
152
|
+
} else {
|
|
153
|
+
console.log(chalk.yellow('\n๐งช Running SPAPS Tests...\n'));
|
|
154
|
+
|
|
155
|
+
results.results.forEach(result => {
|
|
156
|
+
const icon = result.success ? 'โ
' : 'โ';
|
|
157
|
+
console.log(`${icon} ${result.test}`);
|
|
158
|
+
if (!result.success && result.fix) {
|
|
159
|
+
console.log(chalk.yellow(` Fix: ${result.fix}`));
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
console.log();
|
|
164
|
+
console.log(results.success ?
|
|
165
|
+
chalk.green(`โจ ${results.summary}`) :
|
|
166
|
+
chalk.red(`โ ๏ธ ${results.summary}`)
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
if (results.next_steps) {
|
|
170
|
+
console.log('\nNext steps:');
|
|
171
|
+
results.next_steps.forEach(step => {
|
|
172
|
+
console.log(` โข ${step}`);
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
96
178
|
// Init command - Initialize SPAPS in existing project
|
|
97
179
|
program
|
|
98
180
|
.command('init')
|
package/package.json
CHANGED
package/src/ai-helper.js
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPAPS AI Agent Helper
|
|
3
|
+
* Provides AI-friendly outputs and quick commands
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const chalk = require('chalk');
|
|
7
|
+
|
|
8
|
+
function getQuickStartInstructions(port = 3300) {
|
|
9
|
+
return {
|
|
10
|
+
success: true,
|
|
11
|
+
instructions: {
|
|
12
|
+
step1: {
|
|
13
|
+
description: "Install SDK",
|
|
14
|
+
command: "npm install spaps-sdk",
|
|
15
|
+
verify: "npm list spaps-sdk"
|
|
16
|
+
},
|
|
17
|
+
step2: {
|
|
18
|
+
description: "Create test file",
|
|
19
|
+
filename: "test-spaps.js",
|
|
20
|
+
content: `const { SPAPSClient } = require('spaps-sdk');
|
|
21
|
+
|
|
22
|
+
async function test() {
|
|
23
|
+
const spaps = new SPAPSClient({
|
|
24
|
+
apiUrl: 'http://localhost:${port}'
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// Test login
|
|
28
|
+
const { data } = await spaps.login('test@example.com', 'password');
|
|
29
|
+
console.log('โ
Login successful:', data.user.email);
|
|
30
|
+
|
|
31
|
+
// Test authenticated request
|
|
32
|
+
const user = await spaps.getUser();
|
|
33
|
+
console.log('โ
Got user:', user.data.email);
|
|
34
|
+
|
|
35
|
+
return { success: true, user: user.data };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
test().then(console.log).catch(console.error);`
|
|
39
|
+
},
|
|
40
|
+
step3: {
|
|
41
|
+
description: "Run test",
|
|
42
|
+
command: "node test-spaps.js",
|
|
43
|
+
expected_output: {
|
|
44
|
+
success: true,
|
|
45
|
+
user: {
|
|
46
|
+
id: "local-user-123",
|
|
47
|
+
email: "test@example.com"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
endpoints: [
|
|
53
|
+
{
|
|
54
|
+
method: "POST",
|
|
55
|
+
path: "/api/auth/login",
|
|
56
|
+
body: { email: "string", password: "string" },
|
|
57
|
+
response: { access_token: "string", refresh_token: "string", user: "object" }
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
method: "POST",
|
|
61
|
+
path: "/api/auth/register",
|
|
62
|
+
body: { email: "string", password: "string" },
|
|
63
|
+
response: { access_token: "string", refresh_token: "string", user: "object" }
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
method: "GET",
|
|
67
|
+
path: "/api/auth/user",
|
|
68
|
+
headers: { Authorization: "Bearer TOKEN" },
|
|
69
|
+
response: { id: "string", email: "string", role: "string" }
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
method: "POST",
|
|
73
|
+
path: "/api/stripe/create-checkout-session",
|
|
74
|
+
body: { price_id: "string", success_url: "string" },
|
|
75
|
+
response: { sessionId: "string", url: "string" }
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
method: "GET",
|
|
79
|
+
path: "/api/usage/balance",
|
|
80
|
+
headers: { Authorization: "Bearer TOKEN" },
|
|
81
|
+
response: { balance: "number", currency: "string" }
|
|
82
|
+
}
|
|
83
|
+
],
|
|
84
|
+
test_commands: {
|
|
85
|
+
health_check: `curl http://localhost:${port}/health`,
|
|
86
|
+
login: `curl -X POST http://localhost:${port}/api/auth/login -H "Content-Type: application/json" -d '{"email":"test@example.com","password":"password"}'`,
|
|
87
|
+
with_sdk: `node -e "const {SPAPSClient}=require('spaps-sdk');const s=new SPAPSClient();s.login('test@example.com','password').then(r=>console.log(JSON.stringify(r.data))).catch(console.error)"`
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function getServerStatus(port = 3300) {
|
|
93
|
+
const http = require('http');
|
|
94
|
+
|
|
95
|
+
return new Promise((resolve) => {
|
|
96
|
+
const options = {
|
|
97
|
+
hostname: 'localhost',
|
|
98
|
+
port: port,
|
|
99
|
+
path: '/health',
|
|
100
|
+
method: 'GET',
|
|
101
|
+
timeout: 1000
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const req = http.request(options, (res) => {
|
|
105
|
+
let data = '';
|
|
106
|
+
res.on('data', chunk => data += chunk);
|
|
107
|
+
res.on('end', () => {
|
|
108
|
+
try {
|
|
109
|
+
const parsed = JSON.parse(data);
|
|
110
|
+
resolve({
|
|
111
|
+
running: true,
|
|
112
|
+
port: port,
|
|
113
|
+
health: parsed,
|
|
114
|
+
url: `http://localhost:${port}`,
|
|
115
|
+
docs: `http://localhost:${port}/docs`
|
|
116
|
+
});
|
|
117
|
+
} catch {
|
|
118
|
+
resolve({ running: true, port: port, error: 'Invalid response' });
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
req.on('error', () => {
|
|
124
|
+
resolve({
|
|
125
|
+
running: false,
|
|
126
|
+
port: port,
|
|
127
|
+
message: 'Server not running',
|
|
128
|
+
start_command: `npx spaps local --port ${port}`
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
req.on('timeout', () => {
|
|
133
|
+
req.destroy();
|
|
134
|
+
resolve({
|
|
135
|
+
running: false,
|
|
136
|
+
port: port,
|
|
137
|
+
message: 'Server timeout',
|
|
138
|
+
start_command: `npx spaps local --port ${port}`
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
req.end();
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function runQuickTest(port = 3300) {
|
|
147
|
+
const results = [];
|
|
148
|
+
|
|
149
|
+
// Check server
|
|
150
|
+
const status = await getServerStatus(port);
|
|
151
|
+
results.push({
|
|
152
|
+
test: 'server_status',
|
|
153
|
+
success: status.running,
|
|
154
|
+
details: status
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
if (!status.running) {
|
|
158
|
+
return {
|
|
159
|
+
success: false,
|
|
160
|
+
message: 'Server not running',
|
|
161
|
+
fix: `npx spaps local --port ${port}`,
|
|
162
|
+
results
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Try HTTP request
|
|
167
|
+
try {
|
|
168
|
+
const http = require('http');
|
|
169
|
+
const loginResult = await new Promise((resolve, reject) => {
|
|
170
|
+
const postData = JSON.stringify({
|
|
171
|
+
email: 'test@example.com',
|
|
172
|
+
password: 'password'
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const options = {
|
|
176
|
+
hostname: 'localhost',
|
|
177
|
+
port: port,
|
|
178
|
+
path: '/api/auth/login',
|
|
179
|
+
method: 'POST',
|
|
180
|
+
headers: {
|
|
181
|
+
'Content-Type': 'application/json',
|
|
182
|
+
'Content-Length': Buffer.byteLength(postData)
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const req = http.request(options, (res) => {
|
|
187
|
+
let data = '';
|
|
188
|
+
res.on('data', chunk => data += chunk);
|
|
189
|
+
res.on('end', () => {
|
|
190
|
+
try {
|
|
191
|
+
resolve(JSON.parse(data));
|
|
192
|
+
} catch {
|
|
193
|
+
reject(new Error('Invalid JSON response'));
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
req.on('error', reject);
|
|
199
|
+
req.write(postData);
|
|
200
|
+
req.end();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
results.push({
|
|
204
|
+
test: 'login_endpoint',
|
|
205
|
+
success: true,
|
|
206
|
+
response: loginResult
|
|
207
|
+
});
|
|
208
|
+
} catch (error) {
|
|
209
|
+
results.push({
|
|
210
|
+
test: 'login_endpoint',
|
|
211
|
+
success: false,
|
|
212
|
+
error: error.message
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Check SDK availability
|
|
217
|
+
try {
|
|
218
|
+
require.resolve('spaps-sdk');
|
|
219
|
+
results.push({
|
|
220
|
+
test: 'sdk_installed',
|
|
221
|
+
success: true,
|
|
222
|
+
message: 'spaps-sdk is installed'
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// Try SDK login
|
|
226
|
+
try {
|
|
227
|
+
const { SPAPSClient } = require('spaps-sdk');
|
|
228
|
+
const spaps = new SPAPSClient({ apiUrl: `http://localhost:${port}` });
|
|
229
|
+
const { data } = await spaps.login('test@example.com', 'password');
|
|
230
|
+
|
|
231
|
+
results.push({
|
|
232
|
+
test: 'sdk_login',
|
|
233
|
+
success: true,
|
|
234
|
+
user: data.user
|
|
235
|
+
});
|
|
236
|
+
} catch (error) {
|
|
237
|
+
results.push({
|
|
238
|
+
test: 'sdk_login',
|
|
239
|
+
success: false,
|
|
240
|
+
error: error.message
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
} catch {
|
|
244
|
+
results.push({
|
|
245
|
+
test: 'sdk_installed',
|
|
246
|
+
success: false,
|
|
247
|
+
message: 'spaps-sdk not installed',
|
|
248
|
+
fix: 'npm install spaps-sdk'
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const allSuccess = results.every(r => r.success);
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
success: allSuccess,
|
|
256
|
+
summary: `${results.filter(r => r.success).length}/${results.length} tests passed`,
|
|
257
|
+
results,
|
|
258
|
+
next_steps: allSuccess ? [
|
|
259
|
+
'Server is running and SDK is working',
|
|
260
|
+
'You can now use SPAPS in your application',
|
|
261
|
+
'See docs at http://localhost:' + port + '/docs'
|
|
262
|
+
] : [
|
|
263
|
+
'Fix the failing tests above',
|
|
264
|
+
'Run: npx spaps test --json to retry'
|
|
265
|
+
]
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
module.exports = {
|
|
270
|
+
getQuickStartInstructions,
|
|
271
|
+
getServerStatus,
|
|
272
|
+
runQuickTest
|
|
273
|
+
};
|
package/src/docs-html.js
CHANGED
|
@@ -402,6 +402,14 @@ NEXT_PUBLIC_SPAPS_API_URL=http://localhost:${port}</code></pre>
|
|
|
402
402
|
<h3>๐ Auto-Refresh</h3>
|
|
403
403
|
<p>Tokens automatically refresh when expired</p>
|
|
404
404
|
</div>
|
|
405
|
+
<div class="feature-card">
|
|
406
|
+
<h3>๐ณ Mock Stripe</h3>
|
|
407
|
+
<p>Checkout & webhooks work instantly - no Stripe account needed!</p>
|
|
408
|
+
</div>
|
|
409
|
+
<div class="feature-card">
|
|
410
|
+
<h3>โก Auto Webhooks</h3>
|
|
411
|
+
<p>Webhooks fire automatically 2 seconds after payment actions</p>
|
|
412
|
+
</div>
|
|
405
413
|
</div>
|
|
406
414
|
</section>
|
|
407
415
|
|
|
@@ -460,16 +468,55 @@ await spaps.refresh();</code></pre>
|
|
|
460
468
|
<section id="payments">
|
|
461
469
|
<h2>Payment Integration</h2>
|
|
462
470
|
|
|
463
|
-
<
|
|
464
|
-
|
|
471
|
+
<div class="alert" style="background: #e0f2fe; border-color: #0369a1; margin-bottom: 2rem;">
|
|
472
|
+
<strong>๐ New in v0.3.1:</strong> Local Stripe testing with automatic webhook simulation!
|
|
473
|
+
<br>โข No Stripe account required for local development
|
|
474
|
+
<br>โข Webhooks fire automatically after payments
|
|
475
|
+
<br>โข Mock checkout page included
|
|
476
|
+
<br>โข Test webhook UI at <code>/api/stripe/webhooks/test</code>
|
|
477
|
+
</div>
|
|
478
|
+
|
|
479
|
+
<h3>Stripe Checkout (Works in Local Mode!)</h3>
|
|
480
|
+
<pre><code>// Create checkout session - works instantly in local mode!
|
|
465
481
|
const session = await spaps.createCheckoutSession(
|
|
466
|
-
'
|
|
482
|
+
'price_local_validate', // Use local price IDs
|
|
467
483
|
'http://localhost:3000/success', // Success URL
|
|
468
484
|
'http://localhost:3000/cancel' // Cancel URL (optional)
|
|
469
485
|
);
|
|
470
486
|
|
|
471
|
-
//
|
|
472
|
-
|
|
487
|
+
// In local mode: Returns mock checkout URL
|
|
488
|
+
// In production: Returns real Stripe URL
|
|
489
|
+
window.location.href = session.data.url;
|
|
490
|
+
|
|
491
|
+
// Webhook fires automatically after 2 seconds in local mode!</code></pre>
|
|
492
|
+
|
|
493
|
+
<h3>Local Mode Test Products</h3>
|
|
494
|
+
<pre><code>// Pre-configured test products (Buildooor tiers)
|
|
495
|
+
const prices = {
|
|
496
|
+
'price_local_validate': '$500 - Validate tier',
|
|
497
|
+
'price_local_prototype': '$2,500 - Prototype tier',
|
|
498
|
+
'price_local_strategy': '$10,000 - Strategy tier',
|
|
499
|
+
'price_local_build': '$25,000 - Build tier'
|
|
500
|
+
};</code></pre>
|
|
501
|
+
|
|
502
|
+
<h3>Webhook Testing</h3>
|
|
503
|
+
<pre><code>// Your webhook handler (same code for local & production!)
|
|
504
|
+
app.post('/webhook', (req, res) => {
|
|
505
|
+
const event = req.body;
|
|
506
|
+
|
|
507
|
+
switch(event.type) {
|
|
508
|
+
case 'checkout.session.completed':
|
|
509
|
+
// In local: fires 2 seconds after checkout
|
|
510
|
+
// In production: fires when payment completes
|
|
511
|
+
console.log('Payment successful!');
|
|
512
|
+
break;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
res.json({ received: true });
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
// Test webhooks via UI (local only)
|
|
519
|
+
// Visit: http://localhost:${port}/api/stripe/webhooks/test</code></pre>
|
|
473
520
|
|
|
474
521
|
<h3>Subscription Management</h3>
|
|
475
522
|
<pre><code>// Get current subscription
|
package/src/local-server.js
CHANGED
|
@@ -9,14 +9,18 @@ const express = require('express');
|
|
|
9
9
|
const cors = require('cors');
|
|
10
10
|
const chalk = require('chalk');
|
|
11
11
|
const { generateDocsHTML } = require('./docs-html');
|
|
12
|
+
const StripeLocalManager = require('./stripe-local');
|
|
12
13
|
|
|
13
14
|
class LocalServer {
|
|
14
15
|
constructor(options = {}) {
|
|
15
16
|
this.port = options.port || process.env.PORT || 3300;
|
|
16
17
|
this.json = options.json || false;
|
|
17
18
|
this.app = express();
|
|
19
|
+
this.stripeManager = null;
|
|
18
20
|
this.setupMiddleware();
|
|
19
21
|
this.setupRoutes();
|
|
22
|
+
this.setupStripeRoutes();
|
|
23
|
+
this.setupCatchAll();
|
|
20
24
|
}
|
|
21
25
|
|
|
22
26
|
setupMiddleware() {
|
|
@@ -170,7 +174,176 @@ class LocalServer {
|
|
|
170
174
|
this.app.get('/docs', (req, res) => {
|
|
171
175
|
res.send(generateDocsHTML(this.port));
|
|
172
176
|
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
setupStripeRoutes() {
|
|
180
|
+
// Mock Stripe checkout session
|
|
181
|
+
this.app.post('/api/stripe/create-checkout-session', async (req, res) => {
|
|
182
|
+
const { price_id, success_url, cancel_url } = req.body;
|
|
183
|
+
const sessionId = 'cs_local_' + Date.now();
|
|
184
|
+
|
|
185
|
+
res.json({
|
|
186
|
+
sessionId,
|
|
187
|
+
url: `http://localhost:${this.port}/checkout/${sessionId}?success=${encodeURIComponent(success_url)}&cancel=${encodeURIComponent(cancel_url)}`
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// Simulate webhook after delay
|
|
191
|
+
setTimeout(async () => {
|
|
192
|
+
try {
|
|
193
|
+
await this.simulateCheckoutWebhook(sessionId, price_id);
|
|
194
|
+
if (!this.json) {
|
|
195
|
+
console.log(chalk.blue(`โก Webhook simulated: checkout.session.completed`));
|
|
196
|
+
}
|
|
197
|
+
} catch (error) {
|
|
198
|
+
console.error(chalk.red('Webhook simulation failed:'), error);
|
|
199
|
+
}
|
|
200
|
+
}, 2000);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// Mock checkout page
|
|
204
|
+
this.app.get('/checkout/:sessionId', (req, res) => {
|
|
205
|
+
const { sessionId } = req.params;
|
|
206
|
+
const { success, cancel } = req.query;
|
|
207
|
+
|
|
208
|
+
res.send(`
|
|
209
|
+
<!DOCTYPE html>
|
|
210
|
+
<html>
|
|
211
|
+
<head>
|
|
212
|
+
<title>SPAPS Local - Mock Checkout</title>
|
|
213
|
+
<style>
|
|
214
|
+
body { font-family: system-ui; max-width: 400px; margin: 100px auto; padding: 2rem; }
|
|
215
|
+
button { width: 100%; padding: 1rem; margin: 0.5rem 0; border: none; border-radius: 8px; cursor: pointer; font-size: 16px; }
|
|
216
|
+
.pay { background: #635bff; color: white; }
|
|
217
|
+
.pay:hover { background: #4b41e0; }
|
|
218
|
+
.cancel { background: #f5f5f5; }
|
|
219
|
+
.cancel:hover { background: #e5e5e5; }
|
|
220
|
+
</style>
|
|
221
|
+
</head>
|
|
222
|
+
<body>
|
|
223
|
+
<h1>๐ Mock Checkout</h1>
|
|
224
|
+
<p>Session: ${sessionId}</p>
|
|
225
|
+
<p>This is a mock checkout page for local development.</p>
|
|
226
|
+
|
|
227
|
+
<button class="pay" onclick="window.location='${success}'">
|
|
228
|
+
๐ณ Complete Payment
|
|
229
|
+
</button>
|
|
230
|
+
|
|
231
|
+
<button class="cancel" onclick="window.location='${cancel}'">
|
|
232
|
+
Cancel
|
|
233
|
+
</button>
|
|
234
|
+
|
|
235
|
+
<p style="margin-top: 2rem; color: #666; font-size: 14px;">
|
|
236
|
+
In production, this would be a real Stripe Checkout page.
|
|
237
|
+
</p>
|
|
238
|
+
</body>
|
|
239
|
+
</html>
|
|
240
|
+
`);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// Mock webhook endpoint
|
|
244
|
+
this.app.post('/api/stripe/webhooks', express.raw({ type: 'application/json' }), (req, res) => {
|
|
245
|
+
// In local mode, accept all webhooks
|
|
246
|
+
const event = typeof req.body === 'string' ? JSON.parse(req.body) : req.body;
|
|
247
|
+
|
|
248
|
+
if (!this.json) {
|
|
249
|
+
console.log(chalk.blue(`โก Webhook received: ${event.type}`));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Store for testing
|
|
253
|
+
this.lastWebhookEvent = event;
|
|
254
|
+
|
|
255
|
+
res.json({ received: true });
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// Webhook testing UI
|
|
259
|
+
this.app.get('/api/stripe/webhooks/test', (req, res) => {
|
|
260
|
+
res.send(`
|
|
261
|
+
<!DOCTYPE html>
|
|
262
|
+
<html>
|
|
263
|
+
<head>
|
|
264
|
+
<title>SPAPS - Stripe Webhook Tester</title>
|
|
265
|
+
<style>
|
|
266
|
+
body { font-family: system-ui; max-width: 800px; margin: 0 auto; padding: 2rem; }
|
|
267
|
+
button { background: #635bff; color: white; border: none; padding: 0.75rem 1.5rem; border-radius: 6px; cursor: pointer; margin: 0.25rem; }
|
|
268
|
+
button:hover { background: #4b41e0; }
|
|
269
|
+
.event { background: #f9f9f9; padding: 1rem; margin: 1rem 0; border-radius: 8px; border-left: 4px solid #635bff; }
|
|
270
|
+
</style>
|
|
271
|
+
</head>
|
|
272
|
+
<body>
|
|
273
|
+
<h1>๐ Stripe Webhook Tester</h1>
|
|
274
|
+
|
|
275
|
+
<h2>Simulate Events</h2>
|
|
276
|
+
<div>
|
|
277
|
+
<button onclick="simulate('checkout.session.completed')">Checkout Completed</button>
|
|
278
|
+
<button onclick="simulate('payment_intent.succeeded')">Payment Success</button>
|
|
279
|
+
<button onclick="simulate('customer.subscription.created')">Subscription Created</button>
|
|
280
|
+
</div>
|
|
281
|
+
|
|
282
|
+
<h2>Last Event</h2>
|
|
283
|
+
<div id="lastEvent">No events yet</div>
|
|
284
|
+
|
|
285
|
+
<script>
|
|
286
|
+
async function simulate(type) {
|
|
287
|
+
const response = await fetch('/api/stripe/webhooks', {
|
|
288
|
+
method: 'POST',
|
|
289
|
+
headers: { 'Content-Type': 'application/json' },
|
|
290
|
+
body: JSON.stringify({
|
|
291
|
+
id: 'evt_local_' + Date.now(),
|
|
292
|
+
type: type,
|
|
293
|
+
data: { object: { id: type.split('.')[0] + '_' + Date.now() } }
|
|
294
|
+
})
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
if (response.ok) {
|
|
298
|
+
document.getElementById('lastEvent').innerHTML =
|
|
299
|
+
'<div class="event">โ
' + type + ' - ' + new Date().toLocaleTimeString() + '</div>';
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
</script>
|
|
303
|
+
</body>
|
|
304
|
+
</html>
|
|
305
|
+
`);
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
async simulateCheckoutWebhook(sessionId, priceId) {
|
|
310
|
+
const event = {
|
|
311
|
+
id: 'evt_local_' + Date.now(),
|
|
312
|
+
type: 'checkout.session.completed',
|
|
313
|
+
data: {
|
|
314
|
+
object: {
|
|
315
|
+
id: sessionId,
|
|
316
|
+
amount_total: this.getPriceAmount(priceId),
|
|
317
|
+
currency: 'usd',
|
|
318
|
+
customer: 'cus_local_' + Date.now(),
|
|
319
|
+
payment_status: 'paid',
|
|
320
|
+
status: 'complete',
|
|
321
|
+
metadata: { app_id: 'local-app-001', price_id: priceId }
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
// Send to webhook endpoint
|
|
327
|
+
const response = await fetch(`http://localhost:${this.port}/api/stripe/webhooks`, {
|
|
328
|
+
method: 'POST',
|
|
329
|
+
headers: { 'Content-Type': 'application/json' },
|
|
330
|
+
body: JSON.stringify(event)
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
return response.ok;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
getPriceAmount(priceId) {
|
|
337
|
+
const prices = {
|
|
338
|
+
'price_local_validate': 50000,
|
|
339
|
+
'price_local_prototype': 250000,
|
|
340
|
+
'price_local_strategy': 1000000,
|
|
341
|
+
'price_local_build': 2500000
|
|
342
|
+
};
|
|
343
|
+
return prices[priceId] || 10000;
|
|
344
|
+
}
|
|
173
345
|
|
|
346
|
+
setupCatchAll() {
|
|
174
347
|
// Catch-all for unimplemented routes
|
|
175
348
|
this.app.use((req, res) => {
|
|
176
349
|
res.status(404).json({
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local Stripe Integration for SPAPS CLI
|
|
3
|
+
* Handles webhook forwarding and testing without Stripe CLI
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { spawn } = require('child_process');
|
|
7
|
+
const chalk = require('chalk');
|
|
8
|
+
const ora = require('ora');
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
class StripeLocalManager {
|
|
13
|
+
constructor(options = {}) {
|
|
14
|
+
this.port = options.port || 3300;
|
|
15
|
+
this.stripeCliProcess = null;
|
|
16
|
+
this.useBuiltInSimulator = options.simulator !== false;
|
|
17
|
+
this.webhookSecret = 'whsec_local_development_secret';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Check if Stripe CLI is installed
|
|
22
|
+
*/
|
|
23
|
+
async checkStripeCLI() {
|
|
24
|
+
try {
|
|
25
|
+
const { execSync } = require('child_process');
|
|
26
|
+
execSync('stripe --version', { stdio: 'ignore' });
|
|
27
|
+
return true;
|
|
28
|
+
} catch (error) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Start Stripe webhook forwarding
|
|
35
|
+
*/
|
|
36
|
+
async startWebhookForwarding() {
|
|
37
|
+
const hasStripeCLI = await this.checkStripeCLI();
|
|
38
|
+
|
|
39
|
+
if (hasStripeCLI && !this.useBuiltInSimulator) {
|
|
40
|
+
return this.startStripeCLI();
|
|
41
|
+
} else {
|
|
42
|
+
return this.startBuiltInSimulator();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Start Stripe CLI webhook forwarding
|
|
48
|
+
*/
|
|
49
|
+
async startStripeCLI() {
|
|
50
|
+
console.log(chalk.blue('\n๐ก Starting Stripe CLI webhook forwarding...'));
|
|
51
|
+
|
|
52
|
+
return new Promise((resolve, reject) => {
|
|
53
|
+
this.stripeCliProcess = spawn('stripe', [
|
|
54
|
+
'listen',
|
|
55
|
+
'--forward-to',
|
|
56
|
+
`localhost:${this.port}/api/stripe/webhooks`,
|
|
57
|
+
'--print-json'
|
|
58
|
+
]);
|
|
59
|
+
|
|
60
|
+
let webhookSecret = null;
|
|
61
|
+
|
|
62
|
+
this.stripeCliProcess.stdout.on('data', (data) => {
|
|
63
|
+
const output = data.toString();
|
|
64
|
+
|
|
65
|
+
// Parse webhook secret from output
|
|
66
|
+
if (!webhookSecret && output.includes('whsec_')) {
|
|
67
|
+
const match = output.match(/whsec_[a-zA-Z0-9]+/);
|
|
68
|
+
if (match) {
|
|
69
|
+
webhookSecret = match[0];
|
|
70
|
+
console.log(chalk.green(`โ
Stripe webhooks connected!`));
|
|
71
|
+
console.log(chalk.gray(` Secret: ${webhookSecret}`));
|
|
72
|
+
console.log(chalk.gray(` Forwarding to: http://localhost:${this.port}/api/stripe/webhooks`));
|
|
73
|
+
|
|
74
|
+
// Save webhook secret to env file
|
|
75
|
+
this.saveWebhookSecret(webhookSecret);
|
|
76
|
+
|
|
77
|
+
resolve({
|
|
78
|
+
type: 'stripe-cli',
|
|
79
|
+
secret: webhookSecret,
|
|
80
|
+
url: `http://localhost:${this.port}/api/stripe/webhooks`
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Log webhook events
|
|
86
|
+
try {
|
|
87
|
+
const json = JSON.parse(output);
|
|
88
|
+
if (json.type) {
|
|
89
|
+
console.log(chalk.blue(`โก Webhook: ${json.type}`));
|
|
90
|
+
}
|
|
91
|
+
} catch (e) {
|
|
92
|
+
// Not JSON, ignore
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
this.stripeCliProcess.stderr.on('data', (data) => {
|
|
97
|
+
const error = data.toString();
|
|
98
|
+
if (error.includes('login')) {
|
|
99
|
+
console.log(chalk.yellow('\nโ ๏ธ Stripe CLI not logged in'));
|
|
100
|
+
console.log(chalk.cyan(' Run: stripe login'));
|
|
101
|
+
reject(new Error('Stripe CLI not authenticated'));
|
|
102
|
+
} else if (error.includes('Error')) {
|
|
103
|
+
console.error(chalk.red(`Stripe CLI error: ${error}`));
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
this.stripeCliProcess.on('close', (code) => {
|
|
108
|
+
if (code !== 0 && code !== null) {
|
|
109
|
+
reject(new Error(`Stripe CLI exited with code ${code}`));
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Start built-in webhook simulator
|
|
117
|
+
*/
|
|
118
|
+
async startBuiltInSimulator() {
|
|
119
|
+
console.log(chalk.blue('\n๐ญ Starting built-in webhook simulator...'));
|
|
120
|
+
console.log(chalk.gray(' (Stripe CLI not found or simulator mode enabled)'));
|
|
121
|
+
|
|
122
|
+
// The local server will handle webhook simulation
|
|
123
|
+
console.log(chalk.green(`โ
Webhook simulator ready!`));
|
|
124
|
+
console.log(chalk.gray(` Test UI: http://localhost:${this.port}/api/stripe/webhooks/test`));
|
|
125
|
+
console.log(chalk.gray(` Endpoint: http://localhost:${this.port}/api/stripe/webhooks`));
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
type: 'simulator',
|
|
129
|
+
secret: this.webhookSecret,
|
|
130
|
+
url: `http://localhost:${this.port}/api/stripe/webhooks`,
|
|
131
|
+
testUI: `http://localhost:${this.port}/api/stripe/webhooks/test`
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Save webhook secret to local env file
|
|
137
|
+
*/
|
|
138
|
+
saveWebhookSecret(secret) {
|
|
139
|
+
const envPath = path.join(process.cwd(), '.env.local');
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
let envContent = '';
|
|
143
|
+
if (fs.existsSync(envPath)) {
|
|
144
|
+
envContent = fs.readFileSync(envPath, 'utf8');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Update or add webhook secret
|
|
148
|
+
if (envContent.includes('STRIPE_WEBHOOK_SECRET=')) {
|
|
149
|
+
envContent = envContent.replace(
|
|
150
|
+
/STRIPE_WEBHOOK_SECRET=.*/,
|
|
151
|
+
`STRIPE_WEBHOOK_SECRET=${secret}`
|
|
152
|
+
);
|
|
153
|
+
} else {
|
|
154
|
+
envContent += `\n# Auto-generated by SPAPS\nSTRIPE_WEBHOOK_SECRET=${secret}\n`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
fs.writeFileSync(envPath, envContent);
|
|
158
|
+
console.log(chalk.gray(` Secret saved to .env.local`));
|
|
159
|
+
} catch (error) {
|
|
160
|
+
console.error(chalk.yellow(` Could not save webhook secret: ${error.message}`));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Stop webhook forwarding
|
|
166
|
+
*/
|
|
167
|
+
stop() {
|
|
168
|
+
if (this.stripeCliProcess) {
|
|
169
|
+
console.log(chalk.yellow('\n๐ Stopping Stripe webhook forwarding...'));
|
|
170
|
+
this.stripeCliProcess.kill();
|
|
171
|
+
this.stripeCliProcess = null;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Create test products for local development
|
|
177
|
+
*/
|
|
178
|
+
async createTestProducts() {
|
|
179
|
+
console.log(chalk.blue('\n๐ฆ Creating test Stripe products...'));
|
|
180
|
+
|
|
181
|
+
const products = [
|
|
182
|
+
{
|
|
183
|
+
id: 'prod_local_validate',
|
|
184
|
+
name: 'Validate Tier',
|
|
185
|
+
description: 'Landing page with data capture',
|
|
186
|
+
price: 50000, // $500
|
|
187
|
+
price_id: 'price_local_validate'
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
id: 'prod_local_prototype',
|
|
191
|
+
name: 'Prototype Tier',
|
|
192
|
+
description: 'Clickable prototype with core flows',
|
|
193
|
+
price: 250000, // $2,500
|
|
194
|
+
price_id: 'price_local_prototype'
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
id: 'prod_local_strategy',
|
|
198
|
+
name: 'Strategy Tier',
|
|
199
|
+
description: 'Technical architecture and roadmap',
|
|
200
|
+
price: 1000000, // $10,000
|
|
201
|
+
price_id: 'price_local_strategy'
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
id: 'prod_local_build',
|
|
205
|
+
name: 'Build Tier',
|
|
206
|
+
description: 'Full application development',
|
|
207
|
+
price: 2500000, // $25,000
|
|
208
|
+
price_id: 'price_local_build'
|
|
209
|
+
}
|
|
210
|
+
];
|
|
211
|
+
|
|
212
|
+
// Store products in local config
|
|
213
|
+
const configPath = path.join(process.cwd(), '.spaps', 'stripe-products.json');
|
|
214
|
+
const configDir = path.dirname(configPath);
|
|
215
|
+
|
|
216
|
+
if (!fs.existsSync(configDir)) {
|
|
217
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
fs.writeFileSync(configPath, JSON.stringify(products, null, 2));
|
|
221
|
+
|
|
222
|
+
console.log(chalk.green('โ
Test products created:'));
|
|
223
|
+
products.forEach(p => {
|
|
224
|
+
console.log(chalk.gray(` - ${p.name}: $${p.price / 100}`));
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
return products;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Show webhook testing instructions
|
|
232
|
+
*/
|
|
233
|
+
showInstructions() {
|
|
234
|
+
console.log(chalk.yellow('\n๐ Webhook Testing Guide:'));
|
|
235
|
+
console.log();
|
|
236
|
+
|
|
237
|
+
if (this.useBuiltInSimulator) {
|
|
238
|
+
console.log('1. Open webhook tester UI:');
|
|
239
|
+
console.log(chalk.cyan(` http://localhost:${this.port}/api/stripe/webhooks/test`));
|
|
240
|
+
console.log();
|
|
241
|
+
console.log('2. Or trigger via code:');
|
|
242
|
+
console.log(chalk.gray(' ```javascript'));
|
|
243
|
+
console.log(chalk.gray(' // Your app code'));
|
|
244
|
+
console.log(chalk.gray(' const result = await spaps.createCheckoutSession(...);'));
|
|
245
|
+
console.log(chalk.gray(' // Webhook fires automatically after 1 second'));
|
|
246
|
+
console.log(chalk.gray(' ```'));
|
|
247
|
+
} else {
|
|
248
|
+
console.log('1. Trigger test events:');
|
|
249
|
+
console.log(chalk.cyan(' stripe trigger payment_intent.succeeded'));
|
|
250
|
+
console.log();
|
|
251
|
+
console.log('2. Or use the Stripe Dashboard:');
|
|
252
|
+
console.log(chalk.cyan(' https://dashboard.stripe.com/test/webhooks'));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
console.log();
|
|
256
|
+
console.log(chalk.blue('๐ก Tips:'));
|
|
257
|
+
console.log(' - Webhooks auto-retry on failure');
|
|
258
|
+
console.log(' - Check logs for webhook events');
|
|
259
|
+
console.log(' - Use webhook secret in your app');
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
module.exports = StripeLocalManager;
|