spaps 0.5.1 ā 0.5.2
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/AI_TOOLS.json +114 -0
- package/README.md +33 -6
- package/package.json +8 -5
- package/src/ai-tool-spec.js +298 -0
- package/src/cli-dispatcher.js +107 -13
- package/src/docs-html.js +3 -2
- package/src/doctor.js +217 -0
- package/src/handlers.js +22 -3
- package/src/local-server.js +181 -16
package/AI_TOOLS.json
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "spaps",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Auth + payments via SPAPS (local by default). Start with: npx spaps local",
|
|
5
|
+
"base_url": "http://localhost:3300",
|
|
6
|
+
"auth": {
|
|
7
|
+
"local_mode": true,
|
|
8
|
+
"production": {
|
|
9
|
+
"header": "X-API-Key",
|
|
10
|
+
"env": "SPAPS_API_KEY"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"tools": [
|
|
14
|
+
{
|
|
15
|
+
"name": "login",
|
|
16
|
+
"description": "Login with email/password. Local mode accepts any values.",
|
|
17
|
+
"method": "POST",
|
|
18
|
+
"path": "/api/auth/login",
|
|
19
|
+
"parameters": {
|
|
20
|
+
"type": "object",
|
|
21
|
+
"required": ["email", "password"],
|
|
22
|
+
"properties": {
|
|
23
|
+
"email": { "type": "string" },
|
|
24
|
+
"password": { "type": "string" }
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"name": "register",
|
|
30
|
+
"description": "Register a new user with email/password.",
|
|
31
|
+
"method": "POST",
|
|
32
|
+
"path": "/api/auth/register",
|
|
33
|
+
"parameters": {
|
|
34
|
+
"type": "object",
|
|
35
|
+
"required": ["email", "password"],
|
|
36
|
+
"properties": {
|
|
37
|
+
"email": { "type": "string" },
|
|
38
|
+
"password": { "type": "string" }
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"name": "get_current_user",
|
|
44
|
+
"description": "Get the currently authenticated user.",
|
|
45
|
+
"method": "GET",
|
|
46
|
+
"path": "/api/auth/user",
|
|
47
|
+
"parameters": {
|
|
48
|
+
"type": "object",
|
|
49
|
+
"properties": {
|
|
50
|
+
"authorization": { "type": "string", "description": "Bearer <access_token>" }
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"name": "create_checkout_session",
|
|
56
|
+
"description": "Create a Stripe Checkout session.",
|
|
57
|
+
"method": "POST",
|
|
58
|
+
"path": "/api/stripe/checkout-sessions",
|
|
59
|
+
"parameters": {
|
|
60
|
+
"type": "object",
|
|
61
|
+
"required": ["success_url", "cancel_url"],
|
|
62
|
+
"properties": {
|
|
63
|
+
"price_id": { "type": "string" },
|
|
64
|
+
"product_name": { "type": "string" },
|
|
65
|
+
"amount": { "type": "number" },
|
|
66
|
+
"currency": { "type": "string", "default": "usd" },
|
|
67
|
+
"success_url": { "type": "string" },
|
|
68
|
+
"cancel_url": { "type": "string" }
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"name": "list_products",
|
|
74
|
+
"description": "List products (Stripe-backed or local).",
|
|
75
|
+
"method": "GET",
|
|
76
|
+
"path": "/api/stripe/products",
|
|
77
|
+
"parameters": {
|
|
78
|
+
"type": "object",
|
|
79
|
+
"properties": {
|
|
80
|
+
"active": { "type": "boolean" },
|
|
81
|
+
"limit": { "type": "number" }
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"name": "request_magic_link",
|
|
87
|
+
"description": "Send a magic link for passwordless login (simulated locally).",
|
|
88
|
+
"method": "POST",
|
|
89
|
+
"path": "/api/auth/magic-link",
|
|
90
|
+
"parameters": {
|
|
91
|
+
"type": "object",
|
|
92
|
+
"required": ["email"],
|
|
93
|
+
"properties": {
|
|
94
|
+
"email": { "type": "string" }
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"name": "get_wallet_nonce",
|
|
100
|
+
"description": "Get a nonce to sign for wallet authentication.",
|
|
101
|
+
"method": "POST",
|
|
102
|
+
"path": "/api/auth/nonce",
|
|
103
|
+
"parameters": {
|
|
104
|
+
"type": "object",
|
|
105
|
+
"required": ["wallet_address"],
|
|
106
|
+
"properties": {
|
|
107
|
+
"wallet_address": { "type": "string" },
|
|
108
|
+
"chain_type": { "type": "string", "enum": ["solana", "ethereum", "bitcoin", "base"] }
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
]
|
|
113
|
+
}
|
|
114
|
+
|
package/README.md
CHANGED
|
@@ -25,9 +25,9 @@ npm install spaps-sdk
|
|
|
25
25
|
Minimal init (works for both local and prod):
|
|
26
26
|
|
|
27
27
|
```ts
|
|
28
|
-
import {
|
|
28
|
+
import { SPAPSClient } from 'spaps-sdk'
|
|
29
29
|
|
|
30
|
-
export const sdk = new
|
|
30
|
+
export const sdk = new SPAPSClient({
|
|
31
31
|
apiUrl: process.env.SPAPS_API_URL || 'http://localhost:3300',
|
|
32
32
|
apiKey: process.env.SPAPS_API_KEY, // not required in local mode
|
|
33
33
|
})
|
|
@@ -46,6 +46,9 @@ spaps local
|
|
|
46
46
|
|
|
47
47
|
Your local SPAPS server runs at `http://localhost:3300` š
|
|
48
48
|
|
|
49
|
+
- API docs (Swagger UI): `http://localhost:3300/docs`
|
|
50
|
+
- OpenAPI JSON: `http://localhost:3300/openapi.json`
|
|
51
|
+
|
|
49
52
|
Point your app (via `SPAPS_API_URL`) to that URL and use `spaps-sdk` for calls.
|
|
50
53
|
|
|
51
54
|
## Local ā Prod
|
|
@@ -83,9 +86,11 @@ Perfect for **rapid prototyping**, **hackathons**, and **local development**.
|
|
|
83
86
|
Start a full-featured local server with zero configuration:
|
|
84
87
|
|
|
85
88
|
```bash
|
|
86
|
-
spaps local
|
|
87
|
-
spaps local --port 3000
|
|
88
|
-
spaps local --
|
|
89
|
+
spaps local # Default: http://localhost:3300
|
|
90
|
+
spaps local --port 3000 # Custom port
|
|
91
|
+
spaps local --stripe mock|real # Choose Stripe mode (default: mock)
|
|
92
|
+
spaps local --seed demo # Seed demo products/customers/orders
|
|
93
|
+
spaps local --json # JSON output (CI-friendly)
|
|
89
94
|
```
|
|
90
95
|
|
|
91
96
|
Includes:
|
|
@@ -98,7 +103,9 @@ Includes:
|
|
|
98
103
|
|
|
99
104
|
Flags:
|
|
100
105
|
|
|
101
|
-
- `--port <number>`: Set a custom port (default:
|
|
106
|
+
- `--port <number>`: Set a custom port (default: 3300)
|
|
107
|
+
- `--stripe <mode>`: Stripe mode `mock` (offline, default) or `real` (test API)
|
|
108
|
+
- `--seed <preset>`: Seed local data; supported: `demo`
|
|
102
109
|
- `--open`: Open docs in your browser after start
|
|
103
110
|
- `--json`: JSON machine-readable output (ideal for CI)
|
|
104
111
|
|
|
@@ -127,6 +134,8 @@ spaps status
|
|
|
127
134
|
- `spaps help` ā Quick help; `spaps help --interactive` for guided setup
|
|
128
135
|
- `spaps docs` ā SDK docs; `spaps docs --interactive` or `--search "query"`
|
|
129
136
|
- `spaps quickstart` ā Minimal SDK usage instructions
|
|
137
|
+
- `spaps tools` ā Output AI tool spec (use `--json` to save)
|
|
138
|
+
- `spaps doctor` ā Diagnose local environment and config
|
|
130
139
|
|
|
131
140
|
### JSON Mode (CI)
|
|
132
141
|
|
|
@@ -136,6 +145,24 @@ All commands that support `--json` will print machine-readable output. Example:
|
|
|
136
145
|
npx spaps local --port 0 --json | jq '.'
|
|
137
146
|
```
|
|
138
147
|
|
|
148
|
+
AI tool spec (OpenAI-style):
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
npx spaps tools --json > spaps-tools.json
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Run diagnostics:
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
npx spaps doctor --json
|
|
158
|
+
|
|
159
|
+
OpenAPI JSON:
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
curl http://localhost:3300/openapi.json | jq '.'
|
|
163
|
+
```
|
|
164
|
+
```
|
|
165
|
+
|
|
139
166
|
## šÆ Key Features
|
|
140
167
|
|
|
141
168
|
### š§ **Zero Configuration**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spaps",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"description": "Sweet Potato Authentication & Payment Service CLI - Zero-config local development with built-in admin middleware and permission utilities",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -29,21 +29,23 @@
|
|
|
29
29
|
"ethereum"
|
|
30
30
|
],
|
|
31
31
|
"author": "buildooor",
|
|
32
|
-
"license": "
|
|
32
|
+
"license": "UNLICENSED",
|
|
33
33
|
"repository": {
|
|
34
34
|
"type": "git",
|
|
35
|
-
"url": "https://github.com/
|
|
35
|
+
"url": "https://github.com/build000r"
|
|
36
36
|
},
|
|
37
37
|
"bugs": {
|
|
38
|
-
"
|
|
38
|
+
"email": "buildooor@gmail.com"
|
|
39
39
|
},
|
|
40
|
-
"homepage": "https://
|
|
40
|
+
"homepage": "https://www.buildooor.com/services",
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"axios": "^1.6.0",
|
|
43
43
|
"chalk": "^4.1.2",
|
|
44
44
|
"commander": "^11.1.0",
|
|
45
45
|
"cors": "^2.8.5",
|
|
46
46
|
"express": "^4.18.2",
|
|
47
|
+
"js-yaml": "^4.1.0",
|
|
48
|
+
"swagger-ui-dist": "^5.17.14",
|
|
47
49
|
"ora": "^5.4.1",
|
|
48
50
|
"prompts": "^2.4.2",
|
|
49
51
|
"stripe": "^18.5.0",
|
|
@@ -55,6 +57,7 @@
|
|
|
55
57
|
"files": [
|
|
56
58
|
"bin",
|
|
57
59
|
"src",
|
|
60
|
+
"AI_TOOLS.json",
|
|
58
61
|
"client.js",
|
|
59
62
|
"README.md"
|
|
60
63
|
]
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Tool Spec generator for SPAPS
|
|
3
|
+
* - Produces OpenAI-style function schemas for common SPAPS actions
|
|
4
|
+
* - Keeps defaults safe for local development (no API key required)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { DEFAULT_PORT } = require('./config');
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
|
|
11
|
+
function tryLoadManifest() {
|
|
12
|
+
const candidates = [
|
|
13
|
+
path.resolve(process.cwd(), 'docs/manifest.json'),
|
|
14
|
+
path.resolve(__dirname, '../../../docs/manifest.json')
|
|
15
|
+
];
|
|
16
|
+
for (const p of candidates) {
|
|
17
|
+
try {
|
|
18
|
+
if (fs.existsSync(p)) {
|
|
19
|
+
const raw = fs.readFileSync(p, 'utf8');
|
|
20
|
+
return JSON.parse(raw);
|
|
21
|
+
}
|
|
22
|
+
} catch {}
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function tryLoadOpenAPI() {
|
|
28
|
+
const candidates = [
|
|
29
|
+
path.resolve(process.cwd(), 'docs/api-reference.yaml'),
|
|
30
|
+
path.resolve(__dirname, '../../../docs/api-reference.yaml')
|
|
31
|
+
];
|
|
32
|
+
for (const p of candidates) {
|
|
33
|
+
try {
|
|
34
|
+
if (fs.existsSync(p)) {
|
|
35
|
+
const yaml = require('js-yaml');
|
|
36
|
+
const raw = fs.readFileSync(p, 'utf8');
|
|
37
|
+
return yaml.load(raw);
|
|
38
|
+
}
|
|
39
|
+
} catch {}
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function buildOpenAIToolSpec({ port = DEFAULT_PORT } = {}) {
|
|
45
|
+
const baseUrl = `http://localhost:${port}`;
|
|
46
|
+
const spec = {
|
|
47
|
+
name: 'spaps',
|
|
48
|
+
version: '0.1.0',
|
|
49
|
+
description: 'Auth + payments via SPAPS (local by default). Start with: npx spaps local',
|
|
50
|
+
base_url: baseUrl,
|
|
51
|
+
auth: {
|
|
52
|
+
local_mode: true,
|
|
53
|
+
production: {
|
|
54
|
+
header: 'X-API-Key',
|
|
55
|
+
env: 'SPAPS_API_KEY'
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
tools: [
|
|
59
|
+
{
|
|
60
|
+
name: 'login',
|
|
61
|
+
description: 'Login with email/password. Local mode accepts any values.',
|
|
62
|
+
method: 'POST',
|
|
63
|
+
path: '/api/auth/login',
|
|
64
|
+
parameters: {
|
|
65
|
+
type: 'object',
|
|
66
|
+
required: ['email', 'password'],
|
|
67
|
+
properties: {
|
|
68
|
+
email: { type: 'string', description: 'Email address' },
|
|
69
|
+
password: { type: 'string', description: 'Plain text password' }
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: 'register',
|
|
75
|
+
description: 'Register a new user with email/password.',
|
|
76
|
+
method: 'POST',
|
|
77
|
+
path: '/api/auth/register',
|
|
78
|
+
parameters: {
|
|
79
|
+
type: 'object',
|
|
80
|
+
required: ['email', 'password'],
|
|
81
|
+
properties: {
|
|
82
|
+
email: { type: 'string' },
|
|
83
|
+
password: { type: 'string' }
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: 'get_current_user',
|
|
89
|
+
description: 'Get the currently authenticated user. Uses bearer token from previous login.',
|
|
90
|
+
method: 'GET',
|
|
91
|
+
path: '/api/auth/user',
|
|
92
|
+
parameters: {
|
|
93
|
+
type: 'object',
|
|
94
|
+
properties: {
|
|
95
|
+
authorization: { type: 'string', description: 'Bearer <access_token>' }
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
name: 'create_checkout_session',
|
|
101
|
+
description: 'Create a Stripe Checkout session. In local mode uses Stripe test or mock based on USE_REAL_STRIPE.',
|
|
102
|
+
method: 'POST',
|
|
103
|
+
path: '/api/stripe/checkout-sessions',
|
|
104
|
+
parameters: {
|
|
105
|
+
type: 'object',
|
|
106
|
+
required: ['success_url', 'cancel_url'],
|
|
107
|
+
properties: {
|
|
108
|
+
price_id: { type: 'string', description: 'Existing Stripe price ID (preferred)' },
|
|
109
|
+
product_name: { type: 'string', description: 'Used when price_id not provided' },
|
|
110
|
+
amount: { type: 'number', description: 'Amount in cents if creating ad-hoc price' },
|
|
111
|
+
currency: { type: 'string', default: 'usd' },
|
|
112
|
+
success_url: { type: 'string' },
|
|
113
|
+
cancel_url: { type: 'string' }
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
name: 'list_products',
|
|
119
|
+
description: 'List products (Stripe-backed or local).',
|
|
120
|
+
method: 'GET',
|
|
121
|
+
path: '/api/stripe/products',
|
|
122
|
+
parameters: {
|
|
123
|
+
type: 'object',
|
|
124
|
+
properties: {
|
|
125
|
+
active: { type: 'boolean' },
|
|
126
|
+
limit: { type: 'number' }
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
name: 'request_magic_link',
|
|
132
|
+
description: 'Send a magic link for passwordless login (local mode simulates delivery).',
|
|
133
|
+
method: 'POST',
|
|
134
|
+
path: '/api/auth/magic-link',
|
|
135
|
+
parameters: {
|
|
136
|
+
type: 'object',
|
|
137
|
+
required: ['email'],
|
|
138
|
+
properties: {
|
|
139
|
+
email: { type: 'string' }
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
name: 'get_wallet_nonce',
|
|
145
|
+
description: 'Get a nonce to sign for wallet authentication.',
|
|
146
|
+
method: 'POST',
|
|
147
|
+
path: '/api/auth/nonce',
|
|
148
|
+
parameters: {
|
|
149
|
+
type: 'object',
|
|
150
|
+
required: ['wallet_address'],
|
|
151
|
+
properties: {
|
|
152
|
+
wallet_address: { type: 'string' },
|
|
153
|
+
chain_type: { type: 'string', enum: ['solana', 'ethereum', 'bitcoin', 'base'] }
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
]
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
// Default error shapes used for enrichment/merging
|
|
161
|
+
const defaultErrors = {
|
|
162
|
+
'400': {
|
|
163
|
+
type: 'object',
|
|
164
|
+
properties: {
|
|
165
|
+
success: { type: 'boolean' },
|
|
166
|
+
error: { type: 'object', properties: { code: { type: 'string' }, message: { type: 'string' } }, required: ['message'] }
|
|
167
|
+
},
|
|
168
|
+
required: ['error']
|
|
169
|
+
},
|
|
170
|
+
'401': {
|
|
171
|
+
type: 'object',
|
|
172
|
+
properties: { error: { type: 'string', enum: ['unauthorized'] }, message: { type: 'string' } },
|
|
173
|
+
required: ['error']
|
|
174
|
+
},
|
|
175
|
+
'403': {
|
|
176
|
+
type: 'object',
|
|
177
|
+
properties: { error: { type: 'string', enum: ['forbidden'] }, message: { type: 'string' } },
|
|
178
|
+
required: ['error']
|
|
179
|
+
},
|
|
180
|
+
'429': {
|
|
181
|
+
type: 'object',
|
|
182
|
+
properties: { error: { type: 'string', enum: ['rate_limited'] }, message: { type: 'string' } },
|
|
183
|
+
required: ['error']
|
|
184
|
+
},
|
|
185
|
+
'500': {
|
|
186
|
+
type: 'object',
|
|
187
|
+
properties: { error: { type: 'string', enum: ['server_error'] }, message: { type: 'string' } },
|
|
188
|
+
required: ['error']
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
// Attempt to align paths/methods with docs/manifest.json if available
|
|
193
|
+
try {
|
|
194
|
+
const manifest = tryLoadManifest();
|
|
195
|
+
if (manifest && Array.isArray(manifest.endpoints)) {
|
|
196
|
+
const find = (method, pathStr) => manifest.endpoints.find(e => e.method === method && e.path === pathStr);
|
|
197
|
+
const patchTool = (toolName, method, pathStr) => {
|
|
198
|
+
const t = spec.tools.find(x => x.name === toolName);
|
|
199
|
+
const ep = find(method, pathStr);
|
|
200
|
+
if (t && ep) {
|
|
201
|
+
t.method = ep.method;
|
|
202
|
+
t.path = ep.path;
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
patchTool('login', 'POST', '/api/auth/login');
|
|
206
|
+
patchTool('register', 'POST', '/api/auth/register');
|
|
207
|
+
patchTool('get_current_user', 'GET', '/api/auth/user');
|
|
208
|
+
patchTool('create_checkout_session', 'POST', '/api/stripe/checkout-sessions');
|
|
209
|
+
patchTool('list_products', 'GET', '/api/stripe/products');
|
|
210
|
+
patchTool('request_magic_link', 'POST', '/api/auth/magic-link');
|
|
211
|
+
patchTool('get_wallet_nonce', 'POST', '/api/auth/nonce');
|
|
212
|
+
}
|
|
213
|
+
} catch {
|
|
214
|
+
// Best-effort alignment only
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Attempt to enrich parameter schemas from OpenAPI
|
|
218
|
+
try {
|
|
219
|
+
const openapi = tryLoadOpenAPI();
|
|
220
|
+
if (openapi && openapi.paths) {
|
|
221
|
+
const findOp = (method, pathStr) => {
|
|
222
|
+
const ops = openapi.paths[pathStr];
|
|
223
|
+
if (!ops) return null;
|
|
224
|
+
return ops[String(method).toLowerCase()] || null;
|
|
225
|
+
};
|
|
226
|
+
const setBodySchema = (toolName, method, pathStr) => {
|
|
227
|
+
const t = spec.tools.find(x => x.name === toolName);
|
|
228
|
+
const op = findOp(method, pathStr);
|
|
229
|
+
const schema = op?.requestBody?.content?.['application/json']?.schema;
|
|
230
|
+
if (t && schema) {
|
|
231
|
+
t.parameters = schema;
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
const setResponses = (toolName, method, pathStr) => {
|
|
235
|
+
const t = spec.tools.find(x => x.name === toolName);
|
|
236
|
+
const op = findOp(method, pathStr);
|
|
237
|
+
if (t && op && op.responses) {
|
|
238
|
+
const responses = {};
|
|
239
|
+
const examples = {};
|
|
240
|
+
for (const [code, obj] of Object.entries(op.responses)) {
|
|
241
|
+
const schema = obj?.content?.['application/json']?.schema;
|
|
242
|
+
if (schema) responses[code] = schema;
|
|
243
|
+
const content = obj?.content?.['application/json'];
|
|
244
|
+
if (content?.example !== undefined) {
|
|
245
|
+
examples[code] = content.example;
|
|
246
|
+
} else if (content?.examples && typeof content.examples === 'object') {
|
|
247
|
+
const ex = {};
|
|
248
|
+
for (const [name, val] of Object.entries(content.examples)) {
|
|
249
|
+
if (val && typeof val === 'object') {
|
|
250
|
+
if ('value' in val) ex[name] = val.value;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (Object.keys(ex).length) examples[code] = ex;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (Object.keys(responses).length) {
|
|
257
|
+
// Merge with default errors for completeness
|
|
258
|
+
const merged = { ...defaultErrors, ...responses };
|
|
259
|
+
t.responses = merged;
|
|
260
|
+
}
|
|
261
|
+
if (Object.keys(examples).length) t.examples = examples;
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
setBodySchema('login', 'POST', '/api/auth/login');
|
|
265
|
+
setBodySchema('register', 'POST', '/api/auth/register');
|
|
266
|
+
setBodySchema('create_checkout_session', 'POST', '/api/stripe/checkout-sessions');
|
|
267
|
+
setBodySchema('request_magic_link', 'POST', '/api/auth/magic-link');
|
|
268
|
+
setBodySchema('get_wallet_nonce', 'POST', '/api/auth/nonce');
|
|
269
|
+
setResponses('login', 'POST', '/api/auth/login');
|
|
270
|
+
setResponses('register', 'POST', '/api/auth/register');
|
|
271
|
+
setResponses('get_current_user', 'GET', '/api/auth/user');
|
|
272
|
+
setResponses('create_checkout_session', 'POST', '/api/stripe/checkout-sessions');
|
|
273
|
+
setResponses('list_products', 'GET', '/api/stripe/products');
|
|
274
|
+
setResponses('request_magic_link', 'POST', '/api/auth/magic-link');
|
|
275
|
+
setResponses('get_wallet_nonce', 'POST', '/api/auth/nonce');
|
|
276
|
+
// For GET endpoints with query/headers, leave minimal schema for simplicity
|
|
277
|
+
}
|
|
278
|
+
} catch {
|
|
279
|
+
// Ignore enrichment errors
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Add default error shapes if responses missing
|
|
283
|
+
spec.tools.forEach(t => {
|
|
284
|
+
if (!t.responses) t.responses = defaultErrors;
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
return spec;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function buildToolSpec({ format = 'openai', port = DEFAULT_PORT } = {}) {
|
|
291
|
+
switch (format) {
|
|
292
|
+
case 'openai':
|
|
293
|
+
default:
|
|
294
|
+
return buildOpenAIToolSpec({ port });
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
module.exports = { buildToolSpec };
|
package/src/cli-dispatcher.js
CHANGED
|
@@ -10,6 +10,10 @@ function defineProgram({ handlers = {}, dryRun = false, version = '0.0.0', logo
|
|
|
10
10
|
|
|
11
11
|
if (dryRun) {
|
|
12
12
|
program.allowUnknownOption(true);
|
|
13
|
+
// Tolerate stray operands during unit tests (Commander normally errors)
|
|
14
|
+
if (typeof program.allowExcessArguments === 'function') {
|
|
15
|
+
program.allowExcessArguments(true);
|
|
16
|
+
}
|
|
13
17
|
}
|
|
14
18
|
|
|
15
19
|
program
|
|
@@ -44,16 +48,37 @@ function defineProgram({ handlers = {}, dryRun = false, version = '0.0.0', logo
|
|
|
44
48
|
.command('local')
|
|
45
49
|
.description('Start local SPAPS server (no API keys required!)')
|
|
46
50
|
.option('-p, --port <port>', 'Port to run on', String(DEFAULT_PORT))
|
|
51
|
+
.option('-s, --stripe <mode>', 'Stripe mode: mock|real', 'mock')
|
|
52
|
+
.option('--seed <preset>', 'Seed local data: demo', 'none')
|
|
47
53
|
.option('-o, --open', 'Open browser automatically', false)
|
|
48
54
|
.option('--json', 'Output in JSON format')
|
|
49
55
|
.action(
|
|
50
|
-
makeAction('local', (opts,
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
56
|
+
makeAction('local', (opts, cmd, isJson) => {
|
|
57
|
+
const out = {
|
|
58
|
+
port: Number(opts.port),
|
|
59
|
+
open: Boolean(opts.open),
|
|
60
|
+
json: isJson,
|
|
61
|
+
};
|
|
62
|
+
// Include optional flags only if explicitly provided by user
|
|
63
|
+
try {
|
|
64
|
+
const srcStripe = typeof cmd.getOptionValueSource === 'function' ? cmd.getOptionValueSource('stripe') : null;
|
|
65
|
+
const srcSeed = typeof cmd.getOptionValueSource === 'function' ? cmd.getOptionValueSource('seed') : null;
|
|
66
|
+
if (srcStripe === 'cli') out.stripe = String(opts.stripe || '').toLowerCase();
|
|
67
|
+
if (srcSeed === 'cli') out.seed = String(opts.seed || '').toLowerCase();
|
|
68
|
+
} catch (_) {
|
|
69
|
+
// Commander versions without getOptionValueSource; fall back to only including when present
|
|
70
|
+
if (typeof opts.stripe !== 'undefined') out.stripe = String(opts.stripe).toLowerCase();
|
|
71
|
+
if (typeof opts.seed !== 'undefined') out.seed = String(opts.seed).toLowerCase();
|
|
72
|
+
}
|
|
73
|
+
return out;
|
|
74
|
+
})
|
|
55
75
|
);
|
|
56
|
-
if (dryRun)
|
|
76
|
+
if (dryRun) {
|
|
77
|
+
cmdLocal.allowUnknownOption(true);
|
|
78
|
+
if (typeof cmdLocal.allowExcessArguments === 'function') {
|
|
79
|
+
cmdLocal.allowExcessArguments(true);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
57
82
|
|
|
58
83
|
// spaps quickstart
|
|
59
84
|
const cmdQuick = program
|
|
@@ -62,7 +87,12 @@ function defineProgram({ handlers = {}, dryRun = false, version = '0.0.0', logo
|
|
|
62
87
|
.option('-p, --port <port>', 'Port to check', String(DEFAULT_PORT))
|
|
63
88
|
.option('--json', 'Output in JSON format')
|
|
64
89
|
.action(makeAction('quickstart', (opts, _cmd, isJson) => ({ port: Number(opts.port), json: isJson })));
|
|
65
|
-
if (dryRun)
|
|
90
|
+
if (dryRun) {
|
|
91
|
+
cmdQuick.allowUnknownOption(true);
|
|
92
|
+
if (typeof cmdQuick.allowExcessArguments === 'function') {
|
|
93
|
+
cmdQuick.allowExcessArguments(true);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
66
96
|
|
|
67
97
|
// spaps status
|
|
68
98
|
const cmdStatus = program
|
|
@@ -71,7 +101,12 @@ function defineProgram({ handlers = {}, dryRun = false, version = '0.0.0', logo
|
|
|
71
101
|
.option('-p, --port <port>', 'Port to check', String(DEFAULT_PORT))
|
|
72
102
|
.option('--json', 'Output in JSON format')
|
|
73
103
|
.action(makeAction('status', (opts, _cmd, isJson) => ({ port: Number(opts.port), json: isJson })));
|
|
74
|
-
if (dryRun)
|
|
104
|
+
if (dryRun) {
|
|
105
|
+
cmdStatus.allowUnknownOption(true);
|
|
106
|
+
if (typeof cmdStatus.allowExcessArguments === 'function') {
|
|
107
|
+
cmdStatus.allowExcessArguments(true);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
75
110
|
|
|
76
111
|
// spaps init
|
|
77
112
|
const cmdInit = program
|
|
@@ -79,21 +114,36 @@ function defineProgram({ handlers = {}, dryRun = false, version = '0.0.0', logo
|
|
|
79
114
|
.description('Initialize SPAPS in current project')
|
|
80
115
|
.option('--json', 'Output in JSON format')
|
|
81
116
|
.action(makeAction('init', (_opts, _cmd, isJson) => ({ json: isJson })));
|
|
82
|
-
if (dryRun)
|
|
117
|
+
if (dryRun) {
|
|
118
|
+
cmdInit.allowUnknownOption(true);
|
|
119
|
+
if (typeof cmdInit.allowExcessArguments === 'function') {
|
|
120
|
+
cmdInit.allowExcessArguments(true);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
83
123
|
|
|
84
124
|
// spaps create <name>
|
|
85
125
|
const cmdCreate = program
|
|
86
126
|
.command('create <name>')
|
|
87
127
|
.description('Create a new project with SPAPS (coming soon)')
|
|
88
128
|
.action(makeAction('create', (optsOrName, cmd) => ({ name: typeof optsOrName === 'string' ? optsOrName : cmd.args[0] })));
|
|
89
|
-
if (dryRun)
|
|
129
|
+
if (dryRun) {
|
|
130
|
+
cmdCreate.allowUnknownOption(true);
|
|
131
|
+
if (typeof cmdCreate.allowExcessArguments === 'function') {
|
|
132
|
+
cmdCreate.allowExcessArguments(true);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
90
135
|
|
|
91
136
|
// spaps types
|
|
92
137
|
const cmdTypes = program
|
|
93
138
|
.command('types')
|
|
94
139
|
.description('Generate TypeScript types (coming soon)')
|
|
95
140
|
.action(makeAction('types', () => ({})));
|
|
96
|
-
if (dryRun)
|
|
141
|
+
if (dryRun) {
|
|
142
|
+
cmdTypes.allowUnknownOption(true);
|
|
143
|
+
if (typeof cmdTypes.allowExcessArguments === 'function') {
|
|
144
|
+
cmdTypes.allowExcessArguments(true);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
97
147
|
|
|
98
148
|
// spaps help
|
|
99
149
|
const cmdHelp = program
|
|
@@ -104,7 +154,12 @@ function defineProgram({ handlers = {}, dryRun = false, version = '0.0.0', logo
|
|
|
104
154
|
.action(
|
|
105
155
|
makeAction('help', (opts) => ({ interactive: Boolean(opts.interactive), quick: Boolean(opts.quick) }))
|
|
106
156
|
);
|
|
107
|
-
if (dryRun)
|
|
157
|
+
if (dryRun) {
|
|
158
|
+
cmdHelp.allowUnknownOption(true);
|
|
159
|
+
if (typeof cmdHelp.allowExcessArguments === 'function') {
|
|
160
|
+
cmdHelp.allowExcessArguments(true);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
108
163
|
|
|
109
164
|
// spaps docs
|
|
110
165
|
const cmdDocs = program
|
|
@@ -116,7 +171,46 @@ function defineProgram({ handlers = {}, dryRun = false, version = '0.0.0', logo
|
|
|
116
171
|
.action(
|
|
117
172
|
makeAction('docs', (opts, _cmd, isJson) => ({ interactive: Boolean(opts.interactive), search: opts.search || null, json: isJson }))
|
|
118
173
|
);
|
|
119
|
-
if (dryRun)
|
|
174
|
+
if (dryRun) {
|
|
175
|
+
cmdDocs.allowUnknownOption(true);
|
|
176
|
+
if (typeof cmdDocs.allowExcessArguments === 'function') {
|
|
177
|
+
cmdDocs.allowExcessArguments(true);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// spaps tools
|
|
182
|
+
const cmdTools = program
|
|
183
|
+
.command('tools')
|
|
184
|
+
.description('Output AI tool spec (OpenAI-style)')
|
|
185
|
+
.option('-p, --port <port>', 'Port to use for base_url', String(DEFAULT_PORT))
|
|
186
|
+
.option('-f, --format <format>', 'Spec format (openai)', 'openai')
|
|
187
|
+
.option('--json', 'Output in JSON format')
|
|
188
|
+
.action(
|
|
189
|
+
makeAction('tools', (opts, _cmd, isJson) => ({ port: Number(opts.port), format: String(opts.format || 'openai'), json: isJson }))
|
|
190
|
+
);
|
|
191
|
+
if (dryRun) {
|
|
192
|
+
cmdTools.allowUnknownOption(true);
|
|
193
|
+
if (typeof cmdTools.allowExcessArguments === 'function') {
|
|
194
|
+
cmdTools.allowExcessArguments(true);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// spaps doctor
|
|
199
|
+
const cmdDoctor = program
|
|
200
|
+
.command('doctor')
|
|
201
|
+
.description('Diagnose local environment and config')
|
|
202
|
+
.option('-p, --port <port>', 'Port to check', String(DEFAULT_PORT))
|
|
203
|
+
.option('-s, --stripe <mode>', 'Stripe mode: mock|real')
|
|
204
|
+
.option('--json', 'Output in JSON format')
|
|
205
|
+
.action(
|
|
206
|
+
makeAction('doctor', (opts, _cmd, isJson) => ({ port: Number(opts.port), stripe: opts.stripe || null, json: isJson }))
|
|
207
|
+
);
|
|
208
|
+
if (dryRun) {
|
|
209
|
+
cmdDoctor.allowUnknownOption(true);
|
|
210
|
+
if (typeof cmdDoctor.allowExcessArguments === 'function') {
|
|
211
|
+
cmdDoctor.allowExcessArguments(true);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
120
214
|
|
|
121
215
|
return { program, getIntents: () => intents };
|
|
122
216
|
}
|
package/src/docs-html.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Generates comprehensive documentation page
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
function generateDocsHTML(port = 3300) {
|
|
6
|
+
function generateDocsHTML(port = 3300, notice) {
|
|
7
7
|
return `<!DOCTYPE html>
|
|
8
8
|
<html lang="en">
|
|
9
9
|
<head>
|
|
@@ -277,6 +277,7 @@ function generateDocsHTML(port = 3300) {
|
|
|
277
277
|
<p class="subtitle">Sweet Potato Authentication & Payment Service</p>
|
|
278
278
|
<span class="status-badge">ā
Local Mode Active - Port ${port}</span>
|
|
279
279
|
</div>
|
|
280
|
+
${notice ? `<div class="alert">${notice}</div>` : ''}
|
|
280
281
|
|
|
281
282
|
<nav class="nav">
|
|
282
283
|
<ul>
|
|
@@ -807,4 +808,4 @@ test().catch(console.error);</code></pre>
|
|
|
807
808
|
</html>`;
|
|
808
809
|
}
|
|
809
810
|
|
|
810
|
-
module.exports = { generateDocsHTML };
|
|
811
|
+
module.exports = { generateDocsHTML };
|
package/src/doctor.js
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const net = require('net');
|
|
5
|
+
const chalk = require('chalk');
|
|
6
|
+
|
|
7
|
+
const { getServerStatus } = require('./ai-helper');
|
|
8
|
+
const { DEFAULT_PORT } = require('./config');
|
|
9
|
+
|
|
10
|
+
function checkNodeVersion() {
|
|
11
|
+
const version = process.versions.node || '0.0.0';
|
|
12
|
+
const major = parseInt(version.split('.')[0], 10) || 0;
|
|
13
|
+
const ok = major >= 16;
|
|
14
|
+
return {
|
|
15
|
+
check: 'node_version',
|
|
16
|
+
success: ok,
|
|
17
|
+
details: { version, requirement: '>=16' },
|
|
18
|
+
fix: ok ? null : 'Upgrade Node.js to v18+ (recommended)'
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function checkPort(port) {
|
|
23
|
+
// If server is running, we consider port check OK
|
|
24
|
+
const status = await getServerStatus(port);
|
|
25
|
+
if (status.running) {
|
|
26
|
+
return {
|
|
27
|
+
check: 'port',
|
|
28
|
+
success: true,
|
|
29
|
+
details: { port, running: true, url: status.url },
|
|
30
|
+
fix: null
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
// Otherwise ensure port is free to bind
|
|
34
|
+
const free = await new Promise((resolve) => {
|
|
35
|
+
const tester = net.createServer()
|
|
36
|
+
.once('error', () => resolve(false))
|
|
37
|
+
.once('listening', () => tester.once('close', () => resolve(true)).close())
|
|
38
|
+
.listen(port, '127.0.0.1');
|
|
39
|
+
});
|
|
40
|
+
return {
|
|
41
|
+
check: 'port',
|
|
42
|
+
success: free,
|
|
43
|
+
details: { port, running: false, free },
|
|
44
|
+
fix: free ? null : `Use a different port: npx spaps local --port ${port + 1}`
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function checkEnvFile() {
|
|
49
|
+
const envPath = path.resolve(process.cwd(), '.env.local');
|
|
50
|
+
const exists = fs.existsSync(envPath);
|
|
51
|
+
let hasApiUrl = false;
|
|
52
|
+
if (exists) {
|
|
53
|
+
try {
|
|
54
|
+
const content = fs.readFileSync(envPath, 'utf8');
|
|
55
|
+
hasApiUrl = /SPAPS_API_URL\s*=/.test(content);
|
|
56
|
+
} catch {}
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
check: 'env_file',
|
|
60
|
+
success: exists && hasApiUrl,
|
|
61
|
+
details: { path: envPath, exists, hasApiUrl },
|
|
62
|
+
fix: exists ? (hasApiUrl ? null : 'Add SPAPS_API_URL to .env.local (http://localhost:3300)') : 'Run: npx spaps init'
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function checkWritePermissions() {
|
|
67
|
+
const dir = path.resolve(process.cwd(), '.spaps');
|
|
68
|
+
try {
|
|
69
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
70
|
+
const tmp = path.join(dir, '_doctor.tmp');
|
|
71
|
+
fs.writeFileSync(tmp, 'ok');
|
|
72
|
+
fs.unlinkSync(tmp);
|
|
73
|
+
return { check: 'write_permissions', success: true, details: { dir }, fix: null };
|
|
74
|
+
} catch (e) {
|
|
75
|
+
return { check: 'write_permissions', success: false, details: { dir, error: e.message }, fix: `Make directory writable: chmod -R u+rw ${dir}` };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function checkSDKInstalled() {
|
|
80
|
+
try {
|
|
81
|
+
require.resolve('spaps-sdk', { paths: [process.cwd()] });
|
|
82
|
+
return { check: 'sdk_installed', success: true, details: { package: 'spaps-sdk' }, fix: null };
|
|
83
|
+
} catch {
|
|
84
|
+
return { check: 'sdk_installed', success: false, details: { package: 'spaps-sdk' }, fix: 'npm install spaps-sdk' };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function checkStripeMode(stripeModeOpt) {
|
|
89
|
+
const mode = (stripeModeOpt || (process.env.USE_REAL_STRIPE === 'false' ? 'mock' : 'real')).toLowerCase();
|
|
90
|
+
const needsKey = mode === 'real';
|
|
91
|
+
const hasKey = Boolean(process.env.STRIPE_SECRET_KEY);
|
|
92
|
+
const ok = mode === 'mock' || (mode === 'real' && hasKey);
|
|
93
|
+
return {
|
|
94
|
+
check: 'stripe_mode',
|
|
95
|
+
success: ok,
|
|
96
|
+
details: { mode, needsKey, hasKey },
|
|
97
|
+
fix: ok ? null : (mode === 'real' ? 'Set STRIPE_SECRET_KEY or run with --stripe mock' : null)
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function checkEnvTest() {
|
|
102
|
+
const envPath = path.resolve(process.cwd(), '.env.test');
|
|
103
|
+
if (!fs.existsSync(envPath)) {
|
|
104
|
+
return {
|
|
105
|
+
check: 'env_test',
|
|
106
|
+
success: false,
|
|
107
|
+
details: { path: envPath, exists: false },
|
|
108
|
+
fix: 'Create .env.test with SPAPS_API_URL=http://localhost:3300 (no real network keys)'
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
const content = fs.readFileSync(envPath, 'utf8');
|
|
113
|
+
const hasLocalUrl = /SPAPS_API_URL\s*=\s*http:\/\/localhost:\d+/.test(content);
|
|
114
|
+
const hasApiKey = /SPAPS_API_KEY\s*=\s*\S+/.test(content);
|
|
115
|
+
const warns = [];
|
|
116
|
+
if (!hasLocalUrl) warns.push('SPAPS_API_URL should point to localhost');
|
|
117
|
+
if (hasApiKey) warns.push('SPAPS_API_KEY should not be set in tests');
|
|
118
|
+
return {
|
|
119
|
+
check: 'env_test',
|
|
120
|
+
success: hasLocalUrl && !hasApiKey,
|
|
121
|
+
details: { path: envPath, hasLocalUrl, hasApiKey },
|
|
122
|
+
fix: warns.length ? warns.join(' | ') : null
|
|
123
|
+
};
|
|
124
|
+
} catch (e) {
|
|
125
|
+
return { check: 'env_test', success: false, details: { error: e.message }, fix: 'Ensure .env.test is readable' };
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function checkNextJsPort() {
|
|
130
|
+
const defaultNextPort = 3000;
|
|
131
|
+
const inUse = await new Promise((resolve) => {
|
|
132
|
+
const tester = net.createServer()
|
|
133
|
+
.once('error', () => resolve(true))
|
|
134
|
+
.once('listening', () => tester.once('close', () => resolve(false)).close())
|
|
135
|
+
.listen(defaultNextPort, '127.0.0.1');
|
|
136
|
+
});
|
|
137
|
+
return {
|
|
138
|
+
check: 'next_port',
|
|
139
|
+
success: true,
|
|
140
|
+
details: { port: defaultNextPort, inUse, note: inUse ? 'Next.js likely running (good)' : 'Port free' },
|
|
141
|
+
fix: null
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function checkWebhook(port) {
|
|
146
|
+
const status = await getServerStatus(port);
|
|
147
|
+
if (!status.running) {
|
|
148
|
+
return {
|
|
149
|
+
check: 'webhook',
|
|
150
|
+
success: false,
|
|
151
|
+
details: { running: false },
|
|
152
|
+
fix: `Start server: npx spaps local --port ${port} --stripe mock`
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
const http = require('http');
|
|
157
|
+
const payload = JSON.stringify({ id: 'evt_doctor_' + Date.now(), type: 'checkout.session.completed', data: { object: { id: 'cs_doctor_' + Date.now() } } });
|
|
158
|
+
const ok = await new Promise((resolve) => {
|
|
159
|
+
const req = http.request({ hostname: 'localhost', port, path: '/api/stripe/webhooks', method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) } }, (res) => {
|
|
160
|
+
resolve(res.statusCode >= 200 && res.statusCode < 300);
|
|
161
|
+
});
|
|
162
|
+
req.on('error', () => resolve(false));
|
|
163
|
+
req.write(payload);
|
|
164
|
+
req.end();
|
|
165
|
+
});
|
|
166
|
+
return { check: 'webhook', success: ok, details: { path: '/api/stripe/webhooks' }, fix: ok ? null : 'Use --stripe mock or ensure webhook handler is reachable' };
|
|
167
|
+
} catch (e) {
|
|
168
|
+
return { check: 'webhook', success: false, details: { error: e.message }, fix: 'Use --stripe mock or ensure server is running' };
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function formatHuman(results) {
|
|
173
|
+
const ok = results.every(r => r.success);
|
|
174
|
+
console.log(chalk.yellow('\nš SPAPS Doctor\n'));
|
|
175
|
+
results.forEach(r => {
|
|
176
|
+
const icon = r.success ? chalk.green('ā') : chalk.red('ā');
|
|
177
|
+
console.log(`${icon} ${r.check} ${chalk.gray(JSON.stringify(r.details))}`);
|
|
178
|
+
if (!r.success && r.fix) console.log(chalk.cyan(` fix: ${r.fix}`));
|
|
179
|
+
});
|
|
180
|
+
console.log();
|
|
181
|
+
console.log(ok ? chalk.green('All checks passed!') : chalk.red('Some checks failed. See fixes above.'));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function runDoctor({ port = DEFAULT_PORT, stripe = null, json = false } = {}) {
|
|
185
|
+
const results = [];
|
|
186
|
+
results.push(checkNodeVersion());
|
|
187
|
+
results.push(await checkPort(port));
|
|
188
|
+
// Warn if using 3000 which often collides with Next.js
|
|
189
|
+
if (port === 3000) {
|
|
190
|
+
results.push({
|
|
191
|
+
check: 'spaps_port_vs_next',
|
|
192
|
+
success: false,
|
|
193
|
+
details: { spaps_port: port, suggestion: 'Use 3300 for SPAPS to avoid Next.js conflicts' },
|
|
194
|
+
fix: 'Run: npx spaps local --port 3300'
|
|
195
|
+
});
|
|
196
|
+
} else {
|
|
197
|
+
results.push({ check: 'spaps_port_vs_next', success: true, details: { spaps_port: port }, fix: null });
|
|
198
|
+
}
|
|
199
|
+
results.push(checkEnvFile());
|
|
200
|
+
results.push(checkWritePermissions());
|
|
201
|
+
results.push(checkSDKInstalled());
|
|
202
|
+
results.push(checkStripeMode(stripe));
|
|
203
|
+
results.push(checkEnvTest());
|
|
204
|
+
results.push(await checkNextJsPort());
|
|
205
|
+
results.push(await checkWebhook(port));
|
|
206
|
+
|
|
207
|
+
const ok = results.every(r => r.success);
|
|
208
|
+
const payload = { success: ok, results, next_steps: ok ? [] : ['Apply suggested fixes and re-run: npx spaps doctor --json'] };
|
|
209
|
+
if (json) {
|
|
210
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
211
|
+
} else {
|
|
212
|
+
formatHuman(results);
|
|
213
|
+
}
|
|
214
|
+
return payload;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
module.exports = { runDoctor };
|
package/src/handlers.js
CHANGED
|
@@ -5,6 +5,8 @@ const { handleError } = require('./error-handler');
|
|
|
5
5
|
const { showInteractiveHelp, showQuickHelp } = require('./help-system');
|
|
6
6
|
const { showInteractiveDocs, showQuickReference, searchDocs } = require('./docs-system');
|
|
7
7
|
const { getQuickStartInstructions, getServerStatus, runQuickTest } = require('./ai-helper');
|
|
8
|
+
const { buildToolSpec } = require('./ai-tool-spec');
|
|
9
|
+
const { runDoctor } = require('./doctor');
|
|
8
10
|
|
|
9
11
|
function createHandlers(version, logo) {
|
|
10
12
|
return {
|
|
@@ -13,7 +15,7 @@ function createHandlers(version, logo) {
|
|
|
13
15
|
if (!isJson) console.log(logo);
|
|
14
16
|
try {
|
|
15
17
|
const LocalServer = require('./local-server.js');
|
|
16
|
-
const server = new LocalServer({ port: options.port, json: isJson });
|
|
18
|
+
const server = new LocalServer({ port: options.port, json: isJson, stripeMode: options.stripe, seedMode: options.seed });
|
|
17
19
|
if (isJson) {
|
|
18
20
|
await server.start();
|
|
19
21
|
console.log(JSON.stringify({
|
|
@@ -24,7 +26,7 @@ function createHandlers(version, logo) {
|
|
|
24
26
|
docs: `http://localhost:${options.port}/docs`,
|
|
25
27
|
mode: 'local-development',
|
|
26
28
|
port: Number(options.port),
|
|
27
|
-
features: { autoAuth: true, corsEnabled: true, testUsers: ['user', 'admin', 'premium'], apiKeyRequired: false }
|
|
29
|
+
features: { autoAuth: true, corsEnabled: true, testUsers: ['user', 'admin', 'premium'], apiKeyRequired: false, stripeMode: options.stripe, seeded: options.seed }
|
|
28
30
|
}
|
|
29
31
|
}));
|
|
30
32
|
} else {
|
|
@@ -147,9 +149,26 @@ function createHandlers(version, logo) {
|
|
|
147
149
|
} else {
|
|
148
150
|
showQuickReference();
|
|
149
151
|
}
|
|
152
|
+
},
|
|
153
|
+
tools: async ({ options }) => {
|
|
154
|
+
const spec = buildToolSpec({ format: options.format || 'openai', port: options.port });
|
|
155
|
+
if (options.json) {
|
|
156
|
+
console.log(JSON.stringify(spec, null, 2));
|
|
157
|
+
} else {
|
|
158
|
+
console.log(chalk.yellow('\nš SPAPS AI Tool Spec (OpenAI-style)\n'));
|
|
159
|
+
console.log('Base URL:', spec.base_url);
|
|
160
|
+
console.log('Tools:');
|
|
161
|
+
spec.tools.forEach((t, i) => {
|
|
162
|
+
console.log(chalk.green(` ${i + 1}. ${t.name}`), '-', t.description);
|
|
163
|
+
console.log(chalk.gray(` ${t.method} ${t.path}`));
|
|
164
|
+
});
|
|
165
|
+
console.log('\nTip: npx spaps tools --json > spaps-tools.json');
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
doctor: async ({ options }) => {
|
|
169
|
+
await runDoctor({ port: options.port || DEFAULT_PORT, stripe: options.stripe || null, json: options.json });
|
|
150
170
|
}
|
|
151
171
|
};
|
|
152
172
|
}
|
|
153
173
|
|
|
154
174
|
module.exports = { createHandlers };
|
|
155
|
-
|
package/src/local-server.js
CHANGED
|
@@ -8,19 +8,24 @@
|
|
|
8
8
|
const express = require('express');
|
|
9
9
|
const cors = require('cors');
|
|
10
10
|
const chalk = require('chalk');
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
11
13
|
const { generateDocsHTML } = require('./docs-html');
|
|
14
|
+
let swaggerUiDist = null;
|
|
15
|
+
try { swaggerUiDist = require('swagger-ui-dist'); } catch {}
|
|
12
16
|
const StripeLocalManager = require('./stripe-local');
|
|
13
17
|
const LocalAdminManager = require('./admin-local');
|
|
14
18
|
|
|
15
19
|
// Stripe configuration for test mode
|
|
16
20
|
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY || 'sk_test_51S1WOy2HT0E1dOewiHvzt7T96PDwjocSDDUuc2ur569AVA5fDj4UpNM66lujrda1tTYrgooG0Z1dNFZfwEZuZdcA00nuVLJW67');
|
|
17
21
|
const STRIPE_PUBLISHABLE_KEY = process.env.STRIPE_PUBLISHABLE_KEY || 'pk_test_51S1WOy2HT0E1dOewb2EkxZIaPkz7v3zMM9VxuBoxgNILYMmS85I4zrAWTkevyUQcaWlWUoC2NYnB8X5ZKd5e7Ifc005IzIW6H2';
|
|
18
|
-
const USE_REAL_STRIPE = process.env.USE_REAL_STRIPE !== 'false'; // Default to true
|
|
19
22
|
|
|
20
23
|
class LocalServer {
|
|
21
24
|
constructor(options = {}) {
|
|
22
25
|
this.port = options.port || process.env.PORT || 3456;
|
|
23
26
|
this.json = options.json || false;
|
|
27
|
+
this.stripeMode = options.stripeMode || (process.env.USE_REAL_STRIPE === 'false' ? 'mock' : 'real');
|
|
28
|
+
this.seedMode = options.seedMode || 'none';
|
|
24
29
|
this.app = express();
|
|
25
30
|
this.stripeManager = null;
|
|
26
31
|
this.adminManager = new LocalAdminManager();
|
|
@@ -29,6 +34,15 @@ class LocalServer {
|
|
|
29
34
|
this.setupStripeRoutes();
|
|
30
35
|
this.setupAdminRoutes();
|
|
31
36
|
this.setupCatchAll();
|
|
37
|
+
|
|
38
|
+
// Optional demo seeding (idempotent)
|
|
39
|
+
if (this.seedMode === 'demo') {
|
|
40
|
+
try {
|
|
41
|
+
this.seedDemoData();
|
|
42
|
+
} catch (e) {
|
|
43
|
+
if (!this.json) console.warn(chalk.yellow(`ā ļø Seed failed: ${e.message}`));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
32
46
|
}
|
|
33
47
|
|
|
34
48
|
setupMiddleware() {
|
|
@@ -44,6 +58,12 @@ class LocalServer {
|
|
|
44
58
|
this.app.use(express.json());
|
|
45
59
|
this.app.use(express.urlencoded({ extended: true }));
|
|
46
60
|
|
|
61
|
+
// Serve Swagger UI assets locally if available
|
|
62
|
+
if (swaggerUiDist && typeof swaggerUiDist.getAbsoluteFSPath === 'function') {
|
|
63
|
+
const uiPath = swaggerUiDist.getAbsoluteFSPath();
|
|
64
|
+
this.app.use('/swagger-ui', express.static(uiPath));
|
|
65
|
+
}
|
|
66
|
+
|
|
47
67
|
// Local mode indicator
|
|
48
68
|
this.app.use((req, res, next) => {
|
|
49
69
|
res.setHeader('X-SPAPS-Mode', 'local-development');
|
|
@@ -67,6 +87,16 @@ class LocalServer {
|
|
|
67
87
|
}
|
|
68
88
|
|
|
69
89
|
setupRoutes() {
|
|
90
|
+
// OpenAPI JSON - try to serve repo spec; fallback to manifest; else minimal stub
|
|
91
|
+
this.app.get('/openapi.json', async (_req, res) => {
|
|
92
|
+
try {
|
|
93
|
+
const spec = await this.loadOpenApiSpec();
|
|
94
|
+
return res.json(spec);
|
|
95
|
+
} catch (e) {
|
|
96
|
+
return res.status(500).json({ error: 'Failed to generate OpenAPI', message: e.message });
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
70
100
|
// Health check
|
|
71
101
|
this.app.get('/health', (req, res) => {
|
|
72
102
|
res.json({
|
|
@@ -167,7 +197,7 @@ class LocalServer {
|
|
|
167
197
|
// Stripe checkout sessions endpoint - REAL or MOCK based on config
|
|
168
198
|
this.app.post('/api/stripe/checkout-sessions', async (req, res) => {
|
|
169
199
|
try {
|
|
170
|
-
if (
|
|
200
|
+
if (this.stripeMode === 'real') {
|
|
171
201
|
// Real Stripe checkout session
|
|
172
202
|
const { product_name, amount, currency = 'usd', success_url, cancel_url, price_id } = req.body;
|
|
173
203
|
|
|
@@ -254,7 +284,7 @@ class LocalServer {
|
|
|
254
284
|
// Stripe products endpoint - REAL or MOCK based on config
|
|
255
285
|
this.app.get('/api/stripe/products', async (req, res) => {
|
|
256
286
|
try {
|
|
257
|
-
if (
|
|
287
|
+
if (this.stripeMode === 'real') {
|
|
258
288
|
// Fetch real Stripe products
|
|
259
289
|
const products = await stripe.products.list({
|
|
260
290
|
active: req.query.active !== undefined ? req.query.active === 'true' : undefined,
|
|
@@ -418,7 +448,7 @@ class LocalServer {
|
|
|
418
448
|
// Admin product sync endpoint - REAL or MOCK based on config
|
|
419
449
|
this.app.post('/api/v1/admin/products/sync', async (req, res) => {
|
|
420
450
|
try {
|
|
421
|
-
if (
|
|
451
|
+
if (this.stripeMode === 'real') {
|
|
422
452
|
// Get local products from admin manager
|
|
423
453
|
const localProducts = this.adminManager.listProducts();
|
|
424
454
|
const syncResults = [];
|
|
@@ -532,19 +562,148 @@ class LocalServer {
|
|
|
532
562
|
});
|
|
533
563
|
});
|
|
534
564
|
|
|
535
|
-
// Documentation endpoint
|
|
565
|
+
// Documentation endpoint: prefer Swagger UI bound to /openapi.json
|
|
536
566
|
this.app.get('/docs', (req, res) => {
|
|
537
|
-
|
|
567
|
+
if (swaggerUiDist && typeof swaggerUiDist.getAbsoluteFSPath === 'function') {
|
|
568
|
+
res.type('html').send(`
|
|
569
|
+
<!DOCTYPE html>
|
|
570
|
+
<html>
|
|
571
|
+
<head>
|
|
572
|
+
<meta charset="utf-8" />
|
|
573
|
+
<title>SPAPS API Docs</title>
|
|
574
|
+
<link rel="stylesheet" href="/swagger-ui/swagger-ui.css" />
|
|
575
|
+
<style>body { margin: 0; } #swagger-ui { max-width: 100%; }</style>
|
|
576
|
+
</head>
|
|
577
|
+
<body>
|
|
578
|
+
<div id="swagger-ui"></div>
|
|
579
|
+
<script src="/swagger-ui/swagger-ui-bundle.js"></script>
|
|
580
|
+
<script src="/swagger-ui/swagger-ui-standalone-preset.js"></script>
|
|
581
|
+
<script>
|
|
582
|
+
window.onload = () => {
|
|
583
|
+
window.ui = SwaggerUIBundle({
|
|
584
|
+
url: '/openapi.json',
|
|
585
|
+
dom_id: '#swagger-ui',
|
|
586
|
+
deepLinking: true,
|
|
587
|
+
presets: [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset],
|
|
588
|
+
layout: 'BaseLayout'
|
|
589
|
+
});
|
|
590
|
+
};
|
|
591
|
+
</script>
|
|
592
|
+
</body>
|
|
593
|
+
</html>`);
|
|
594
|
+
} else {
|
|
595
|
+
// Fallback to the existing docs page if Swagger UI is not available
|
|
596
|
+
const msg = 'Using fallback docs page. Install Swagger UI assets for the full API explorer: npm install swagger-ui-dist';
|
|
597
|
+
res.send(generateDocsHTML(this.port, msg));
|
|
598
|
+
}
|
|
538
599
|
});
|
|
539
600
|
}
|
|
540
601
|
|
|
602
|
+
async loadOpenApiSpec() {
|
|
603
|
+
// Try YAML OpenAPI
|
|
604
|
+
const yamlCandidates = [
|
|
605
|
+
path.resolve(process.cwd(), 'docs/api-reference.yaml'),
|
|
606
|
+
path.resolve(__dirname, '../../../docs/api-reference.yaml')
|
|
607
|
+
];
|
|
608
|
+
for (const p of yamlCandidates) {
|
|
609
|
+
try {
|
|
610
|
+
if (fs.existsSync(p)) {
|
|
611
|
+
let yaml;
|
|
612
|
+
try {
|
|
613
|
+
yaml = require('js-yaml');
|
|
614
|
+
} catch {}
|
|
615
|
+
if (yaml) {
|
|
616
|
+
const content = fs.readFileSync(p, 'utf8');
|
|
617
|
+
const parsed = yaml.load(content);
|
|
618
|
+
// ensure servers list points to local
|
|
619
|
+
parsed.servers = [{ url: `http://localhost:${this.port}` }];
|
|
620
|
+
return parsed;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
} catch {}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// Fallback: build from manifest
|
|
627
|
+
const manifestCandidates = [
|
|
628
|
+
path.resolve(process.cwd(), 'docs/manifest.json'),
|
|
629
|
+
path.resolve(__dirname, '../../../docs/manifest.json')
|
|
630
|
+
];
|
|
631
|
+
for (const p of manifestCandidates) {
|
|
632
|
+
try {
|
|
633
|
+
if (fs.existsSync(p)) {
|
|
634
|
+
const manifest = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
635
|
+
return this.buildOpenApiFromManifest(manifest);
|
|
636
|
+
}
|
|
637
|
+
} catch {}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// Last resort: minimal stub
|
|
641
|
+
return {
|
|
642
|
+
openapi: '3.0.0',
|
|
643
|
+
info: { title: 'SPAPS Local API', version: '0.0.0' },
|
|
644
|
+
servers: [{ url: `http://localhost:${this.port}` }],
|
|
645
|
+
paths: {
|
|
646
|
+
'/health': { get: { summary: 'Health', responses: { '200': { description: 'OK' } } } }
|
|
647
|
+
}
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
buildOpenApiFromManifest(manifest) {
|
|
652
|
+
const spec = {
|
|
653
|
+
openapi: '3.0.0',
|
|
654
|
+
info: { title: 'SPAPS API', version: String(manifest.version || '1.0.0') },
|
|
655
|
+
servers: [{ url: `http://localhost:${this.port}` }],
|
|
656
|
+
paths: {}
|
|
657
|
+
};
|
|
658
|
+
const toPath = (p) => p.replace(/:(\w+)/g, '{$1}');
|
|
659
|
+
for (const ep of manifest.endpoints || []) {
|
|
660
|
+
const pathKey = toPath(ep.path);
|
|
661
|
+
if (!spec.paths[pathKey]) spec.paths[pathKey] = {};
|
|
662
|
+
spec.paths[pathKey][String(ep.method || 'GET').toLowerCase()] = {
|
|
663
|
+
summary: ep.description || `${ep.method} ${ep.path}`,
|
|
664
|
+
tags: ep.tags || [],
|
|
665
|
+
responses: { '200': { description: 'OK' } }
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
return spec;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
seedDemoData() {
|
|
672
|
+
// Add a couple of customers and a completed + pending order if none exist
|
|
673
|
+
const existingCustomers = this.adminManager.listCustomers();
|
|
674
|
+
const products = this.adminManager.listProducts();
|
|
675
|
+
if (existingCustomers.length === 0 && products.length > 0) {
|
|
676
|
+
const alice = this.adminManager.createCustomer({ email: 'alice@example.com', name: 'Alice' });
|
|
677
|
+
const bob = this.adminManager.createCustomer({ email: 'bob@example.com', name: 'Bob' });
|
|
678
|
+
const p = products[0];
|
|
679
|
+
const order1 = this.adminManager.createOrder({
|
|
680
|
+
customer_id: alice.id,
|
|
681
|
+
customer_email: alice.email,
|
|
682
|
+
product_id: p.id,
|
|
683
|
+
price_id: p.price_id,
|
|
684
|
+
amount: p.price,
|
|
685
|
+
currency: p.currency
|
|
686
|
+
});
|
|
687
|
+
this.adminManager.updateOrderStatus(order1.id, 'completed');
|
|
688
|
+
this.adminManager.createOrder({
|
|
689
|
+
customer_id: bob.id,
|
|
690
|
+
customer_email: bob.email,
|
|
691
|
+
product_id: p.id,
|
|
692
|
+
price_id: p.price_id,
|
|
693
|
+
amount: p.price,
|
|
694
|
+
currency: p.currency
|
|
695
|
+
});
|
|
696
|
+
if (!this.json) console.log(chalk.gray('š± Seeded demo customers and orders'));
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
541
700
|
setupStripeRoutes() {
|
|
542
701
|
// Enhanced Stripe Product Management - Full CRUD
|
|
543
702
|
|
|
544
703
|
// GET /api/stripe/products - List all products with filtering
|
|
545
704
|
this.app.get('/api/stripe/products', async (req, res) => {
|
|
546
705
|
try {
|
|
547
|
-
if (
|
|
706
|
+
if (this.stripeMode === 'real') {
|
|
548
707
|
const { active, category, limit = '100' } = req.query;
|
|
549
708
|
|
|
550
709
|
const products = await stripe.products.list({
|
|
@@ -620,7 +779,7 @@ class LocalServer {
|
|
|
620
779
|
});
|
|
621
780
|
}
|
|
622
781
|
|
|
623
|
-
if (
|
|
782
|
+
if (this.stripeMode === 'real') {
|
|
624
783
|
// Create product in Stripe
|
|
625
784
|
const stripeProduct = await stripe.products.create({
|
|
626
785
|
name,
|
|
@@ -718,7 +877,7 @@ class LocalServer {
|
|
|
718
877
|
const { productId } = req.params;
|
|
719
878
|
const { name, description, images, metadata, active } = req.body;
|
|
720
879
|
|
|
721
|
-
if (
|
|
880
|
+
if (this.stripeMode === 'real') {
|
|
722
881
|
// Update in Stripe
|
|
723
882
|
const updateData = {};
|
|
724
883
|
if (name !== undefined) updateData.name = name;
|
|
@@ -780,7 +939,7 @@ class LocalServer {
|
|
|
780
939
|
});
|
|
781
940
|
}
|
|
782
941
|
|
|
783
|
-
if (
|
|
942
|
+
if (this.stripeMode === 'real') {
|
|
784
943
|
const priceData = {
|
|
785
944
|
product: product_id,
|
|
786
945
|
unit_amount: parseInt(unit_amount),
|
|
@@ -852,7 +1011,7 @@ class LocalServer {
|
|
|
852
1011
|
});
|
|
853
1012
|
}
|
|
854
1013
|
|
|
855
|
-
if (
|
|
1014
|
+
if (this.stripeMode === 'real') {
|
|
856
1015
|
const stripeProduct = await stripe.products.update(productId, {
|
|
857
1016
|
default_price: price_id
|
|
858
1017
|
});
|
|
@@ -895,7 +1054,7 @@ class LocalServer {
|
|
|
895
1054
|
try {
|
|
896
1055
|
const { productId } = req.params;
|
|
897
1056
|
|
|
898
|
-
if (
|
|
1057
|
+
if (this.stripeMode === 'real') {
|
|
899
1058
|
// Archive in Stripe (can't truly delete)
|
|
900
1059
|
const stripeProduct = await stripe.products.update(productId, {
|
|
901
1060
|
active: false
|
|
@@ -1058,7 +1217,7 @@ class LocalServer {
|
|
|
1058
1217
|
try {
|
|
1059
1218
|
let event;
|
|
1060
1219
|
|
|
1061
|
-
if (
|
|
1220
|
+
if (this.stripeMode === 'real') {
|
|
1062
1221
|
// Real Stripe webhook verification
|
|
1063
1222
|
const sig = req.headers['stripe-signature'];
|
|
1064
1223
|
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;
|
|
@@ -1076,7 +1235,13 @@ class LocalServer {
|
|
|
1076
1235
|
}
|
|
1077
1236
|
} else {
|
|
1078
1237
|
// Mock mode - accept all webhooks
|
|
1079
|
-
|
|
1238
|
+
if (Buffer.isBuffer(req.body)) {
|
|
1239
|
+
event = JSON.parse(req.body.toString());
|
|
1240
|
+
} else if (typeof req.body === 'string') {
|
|
1241
|
+
event = JSON.parse(req.body);
|
|
1242
|
+
} else {
|
|
1243
|
+
event = req.body;
|
|
1244
|
+
}
|
|
1080
1245
|
}
|
|
1081
1246
|
|
|
1082
1247
|
if (!this.json) {
|
|
@@ -1494,7 +1659,7 @@ class LocalServer {
|
|
|
1494
1659
|
console.log(chalk.yellow('š SPAPS Local Development Server'));
|
|
1495
1660
|
console.log(chalk.green(`⨠Running at: http://localhost:${this.port}`));
|
|
1496
1661
|
console.log(chalk.blue(`š Documentation: http://localhost:${this.port}/docs`));
|
|
1497
|
-
if (
|
|
1662
|
+
if (this.stripeMode === 'real') {
|
|
1498
1663
|
console.log(chalk.magenta('š³ Stripe: Real test mode (live API calls)'));
|
|
1499
1664
|
} else {
|
|
1500
1665
|
console.log(chalk.gray('š³ Stripe: Mock mode (simulated responses)'));
|
|
@@ -1535,4 +1700,4 @@ if (require.main === module) {
|
|
|
1535
1700
|
console.log(chalk.yellow('\nš Shutting down...'));
|
|
1536
1701
|
process.exit(0);
|
|
1537
1702
|
});
|
|
1538
|
-
}
|
|
1703
|
+
}
|