spaps 0.7.2 → 0.7.4
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 +10 -11
- package/README.md +267 -110
- package/assets/local-runtime/Dockerfile +28 -0
- package/assets/local-runtime/alembic/env.py +101 -0
- package/assets/local-runtime/alembic/path_bootstrap.py +71 -0
- package/assets/local-runtime/alembic/versions/000000000001_baseline_consolidated_schema.py +1076 -0
- package/assets/local-runtime/alembic/versions/000000000002_fix_column_types_to_match_prod.py +83 -0
- package/assets/local-runtime/alembic/versions/000000000003_fix_email_template_key_uniqueness.py +49 -0
- package/assets/local-runtime/alembic/versions/000000000004_add_hold_duration_minutes_to_dayrate_config.py +30 -0
- package/assets/local-runtime/alembic/versions/000000000005_resource_scoped_entitlements.py +77 -0
- package/assets/local-runtime/alembic/versions/000000000006_cfo_rbac_add_is_admin.py +37 -0
- package/assets/local-runtime/alembic/versions/000000000007_agent_approvals.py +158 -0
- package/assets/local-runtime/alembic/versions/000000000008_add_company_id_to_cfo_connections.py +35 -0
- package/assets/local-runtime/alembic/versions/000000000009_tx_signing.py +62 -0
- package/assets/local-runtime/alembic/versions/000000000010_affiliate_referrals.py +235 -0
- package/assets/local-runtime/alembic/versions/000000000011_checkin_call_booking.py +137 -0
- package/assets/local-runtime/alembic/versions/000000000012_subscription_application_scoping.py +55 -0
- package/assets/local-runtime/alembic/versions/000000000013_refresh_token_anomaly_context.py +61 -0
- package/assets/local-runtime/alembic/versions/000000000014_buildooor_dayrate_hire_schedule.py +39 -0
- package/assets/local-runtime/alembic/versions/000000000015_support_telemetry_platform.py +112 -0
- package/assets/local-runtime/alembic/versions/000000000016_issue_reporting_platform.py +54 -0
- package/assets/local-runtime/alembic/versions/000000000017_issue_reporting_platform_import_tracking.py +44 -0
- package/assets/local-runtime/alembic/versions/000000000018_authorization_policy_engine.py +76 -0
- package/assets/local-runtime/alembic.ini +47 -0
- package/assets/local-runtime/docker-compose.yml +61 -0
- package/assets/local-runtime/manifest.json +8 -0
- package/assets/local-runtime/scripts/container-entrypoint.sh +13 -0
- package/assets/local-runtime/scripts/fetch-prod-db.sh +112 -0
- package/assets/local-runtime/scripts/run-migrations.sh +96 -0
- package/package.json +5 -4
- package/src/ai-helper.js +176 -234
- package/src/ai-tool-spec.js +52 -20
- package/src/auth/api-key.js +119 -0
- package/src/auth/client-id.js +136 -0
- package/src/auth/client.js +169 -0
- package/src/auth/credentials.js +110 -0
- package/src/auth/device-flow.js +159 -0
- package/src/auth/env.js +57 -0
- package/src/auth/handlers.js +462 -0
- package/src/auth/http.js +74 -0
- package/src/cli-dispatcher.js +155 -24
- package/src/docs-system.js +7 -7
- package/src/error-handler.js +42 -0
- package/src/fixture-kernel.js +1143 -0
- package/src/handlers.js +252 -15
- package/src/help-system.js +3 -1
- package/src/local-runtime.js +258 -0
- package/src/local-server.js +597 -199
- package/src/project-scaffolder.js +441 -0
package/src/handlers.js
CHANGED
|
@@ -7,8 +7,27 @@ const { showInteractiveDocs, showQuickReference, searchDocs } = require('./docs-
|
|
|
7
7
|
const { getQuickStartInstructions, getServerStatus, runQuickTest } = require('./ai-helper');
|
|
8
8
|
const { buildToolSpec } = require('./ai-tool-spec');
|
|
9
9
|
const { runDoctor } = require('./doctor');
|
|
10
|
+
const {
|
|
11
|
+
applyFixtures,
|
|
12
|
+
exportStorageState,
|
|
13
|
+
initFixtureKernel,
|
|
14
|
+
resetFixtures,
|
|
15
|
+
} = require('./fixture-kernel');
|
|
16
|
+
const { createProjectStarter } = require('./project-scaffolder');
|
|
17
|
+
const {
|
|
18
|
+
loginHandler,
|
|
19
|
+
logoutHandler,
|
|
20
|
+
whoamiHandler,
|
|
21
|
+
tokenHandler,
|
|
22
|
+
} = require('./auth/handlers');
|
|
10
23
|
|
|
11
24
|
function createHandlers(version, logo) {
|
|
25
|
+
function invalidArgument(message) {
|
|
26
|
+
const error = new Error(message);
|
|
27
|
+
error.code = 'EINVAL';
|
|
28
|
+
return error;
|
|
29
|
+
}
|
|
30
|
+
|
|
12
31
|
return {
|
|
13
32
|
local: async ({ options }) => {
|
|
14
33
|
const isJson = options.json;
|
|
@@ -18,7 +37,13 @@ function createHandlers(version, logo) {
|
|
|
18
37
|
if (options.stop) {
|
|
19
38
|
try {
|
|
20
39
|
const LocalServer = require('./local-server.js');
|
|
21
|
-
const server = new LocalServer({
|
|
40
|
+
const server = new LocalServer({
|
|
41
|
+
port: options.port,
|
|
42
|
+
runtimeDir: options.runtimeDir,
|
|
43
|
+
runtimeSource: options.runtimeSource,
|
|
44
|
+
dataSource: options.dataSource,
|
|
45
|
+
json: isJson,
|
|
46
|
+
});
|
|
22
47
|
server.stop();
|
|
23
48
|
return;
|
|
24
49
|
} catch (error) {
|
|
@@ -31,6 +56,9 @@ function createHandlers(version, logo) {
|
|
|
31
56
|
const LocalServer = require('./local-server.js');
|
|
32
57
|
const server = new LocalServer({
|
|
33
58
|
port: options.port,
|
|
59
|
+
runtimeDir: options.runtimeDir,
|
|
60
|
+
runtimeSource: options.runtimeSource,
|
|
61
|
+
dataSource: options.dataSource,
|
|
34
62
|
json: isJson,
|
|
35
63
|
detach: options.detach,
|
|
36
64
|
fresh: options.fresh,
|
|
@@ -47,23 +75,31 @@ function createHandlers(version, logo) {
|
|
|
47
75
|
}
|
|
48
76
|
|
|
49
77
|
// Set up shutdown handler
|
|
50
|
-
|
|
78
|
+
const shutdown = async () => {
|
|
51
79
|
await server.shutdown();
|
|
52
80
|
process.exit(0);
|
|
53
|
-
}
|
|
81
|
+
};
|
|
82
|
+
process.on('SIGINT', shutdown);
|
|
83
|
+
process.on('SIGTERM', shutdown);
|
|
54
84
|
} catch (error) {
|
|
55
85
|
handleError(error, { port: options.port, command: 'local' }, { json: isJson });
|
|
56
86
|
}
|
|
57
87
|
},
|
|
58
88
|
quickstart: async ({ options }) => {
|
|
59
|
-
const instructions = getQuickStartInstructions(options.port);
|
|
89
|
+
const instructions = await getQuickStartInstructions(options.port);
|
|
60
90
|
if (options.json) {
|
|
61
91
|
console.log(JSON.stringify(instructions, null, 2));
|
|
62
92
|
} else {
|
|
63
93
|
console.log(chalk.yellow('\n🍠 SPAPS Quick Start Instructions\n'));
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
94
|
+
const modeSummary = instructions.server.running
|
|
95
|
+
? instructions.auth.local_mode
|
|
96
|
+
? 'Mode: local mode active'
|
|
97
|
+
: 'Mode: provisioned application required'
|
|
98
|
+
: 'Mode: server unreachable';
|
|
99
|
+
console.log(modeSummary);
|
|
100
|
+
instructions.summary.forEach((line, index) => {
|
|
101
|
+
console.log(`${index + 1}. ${line}`);
|
|
102
|
+
});
|
|
67
103
|
console.log('\nFor JSON output: npx spaps quickstart --json');
|
|
68
104
|
}
|
|
69
105
|
},
|
|
@@ -81,6 +117,12 @@ function createHandlers(version, logo) {
|
|
|
81
117
|
console.log(chalk.green('\n✅ SPAPS server is running!\n'));
|
|
82
118
|
console.log(' URL:', chalk.cyan(status.url));
|
|
83
119
|
console.log(' Docs:', chalk.cyan(status.docs));
|
|
120
|
+
if (status.local_mode?.known) {
|
|
121
|
+
console.log(
|
|
122
|
+
' Mode:',
|
|
123
|
+
chalk.cyan(status.local_mode.active ? 'local mode active' : 'application key required')
|
|
124
|
+
);
|
|
125
|
+
}
|
|
84
126
|
console.log();
|
|
85
127
|
}
|
|
86
128
|
}
|
|
@@ -114,12 +156,66 @@ function createHandlers(version, logo) {
|
|
|
114
156
|
console.log(chalk.cyan(' 3. Start coding!'));
|
|
115
157
|
}
|
|
116
158
|
},
|
|
117
|
-
create: () => {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
159
|
+
create: async ({ options }) => {
|
|
160
|
+
const isJson = options.json;
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
const result = await createProjectStarter({
|
|
164
|
+
name: options.name,
|
|
165
|
+
template: options.template,
|
|
166
|
+
dir: options.dir,
|
|
167
|
+
port: options.port,
|
|
168
|
+
force: options.force,
|
|
169
|
+
version,
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
if (isJson) {
|
|
173
|
+
console.log(JSON.stringify(result, null, 2));
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
console.log(chalk.green(`\n✨ Created ${result.project_name} (${result.template})`));
|
|
178
|
+
console.log(chalk.cyan(` ${result.target_dir}`));
|
|
179
|
+
console.log(chalk.gray(` provisioning: ${result.provisioning.status}`));
|
|
180
|
+
|
|
181
|
+
if (result.files_created.length > 0) {
|
|
182
|
+
console.log(chalk.green('\nFiles created:'));
|
|
183
|
+
result.files_created.forEach((file) => {
|
|
184
|
+
console.log(chalk.gray(` • ${file}`));
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (result.files_overwritten.length > 0) {
|
|
189
|
+
console.log(chalk.yellow('\nFiles overwritten:'));
|
|
190
|
+
result.files_overwritten.forEach((file) => {
|
|
191
|
+
console.log(chalk.gray(` • ${file}`));
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (result.warnings.length > 0) {
|
|
196
|
+
console.log(chalk.yellow('\nWarnings:'));
|
|
197
|
+
result.warnings.forEach((warning) => {
|
|
198
|
+
console.log(chalk.gray(` • ${warning}`));
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
console.log(chalk.green('\nNext steps:'));
|
|
203
|
+
result.next_steps.forEach((step, index) => {
|
|
204
|
+
console.log(chalk.cyan(` ${index + 1}. ${step}`));
|
|
205
|
+
});
|
|
206
|
+
console.log();
|
|
207
|
+
} catch (error) {
|
|
208
|
+
handleError(
|
|
209
|
+
error,
|
|
210
|
+
{
|
|
211
|
+
command: 'create',
|
|
212
|
+
name: options.name,
|
|
213
|
+
template: options.template,
|
|
214
|
+
dir: options.dir,
|
|
215
|
+
},
|
|
216
|
+
{ json: isJson }
|
|
217
|
+
);
|
|
218
|
+
}
|
|
123
219
|
},
|
|
124
220
|
types: () => {
|
|
125
221
|
console.log(chalk.yellow('🍠 SPAPS'));
|
|
@@ -160,12 +256,22 @@ function createHandlers(version, logo) {
|
|
|
160
256
|
}
|
|
161
257
|
},
|
|
162
258
|
tools: async ({ options }) => {
|
|
163
|
-
const spec = buildToolSpec({
|
|
259
|
+
const spec = await buildToolSpec({
|
|
260
|
+
format: options.format || 'openai',
|
|
261
|
+
port: options.port,
|
|
262
|
+
version,
|
|
263
|
+
});
|
|
164
264
|
if (options.json) {
|
|
165
265
|
console.log(JSON.stringify(spec, null, 2));
|
|
166
266
|
} else {
|
|
167
267
|
console.log(chalk.yellow('\n🍠 SPAPS AI Tool Spec (OpenAI-style)\n'));
|
|
168
268
|
console.log('Base URL:', spec.base_url);
|
|
269
|
+
if (spec.auth && typeof spec.auth.local_mode === 'boolean') {
|
|
270
|
+
console.log(
|
|
271
|
+
'Auth:',
|
|
272
|
+
spec.auth.local_mode ? 'local mode active' : 'application key required'
|
|
273
|
+
);
|
|
274
|
+
}
|
|
169
275
|
console.log('Tools:');
|
|
170
276
|
spec.tools.forEach((t, i) => {
|
|
171
277
|
console.log(chalk.green(` ${i + 1}. ${t.name}`), '-', t.description);
|
|
@@ -174,9 +280,140 @@ function createHandlers(version, logo) {
|
|
|
174
280
|
console.log('\nTip: npx spaps tools --json > spaps-tools.json');
|
|
175
281
|
}
|
|
176
282
|
},
|
|
283
|
+
fixtures: async ({ options }) => {
|
|
284
|
+
const isJson = options.json;
|
|
285
|
+
|
|
286
|
+
try {
|
|
287
|
+
if (options.format && options.format !== 'playwright') {
|
|
288
|
+
throw invalidArgument(`Unsupported fixture format "${options.format}". Only "playwright" is currently supported.`);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
let result;
|
|
292
|
+
switch (options.subcommand) {
|
|
293
|
+
case 'init':
|
|
294
|
+
result = await initFixtureKernel({
|
|
295
|
+
dir: options.dir,
|
|
296
|
+
port: options.port,
|
|
297
|
+
baseUrl: options.baseUrl,
|
|
298
|
+
version,
|
|
299
|
+
force: options.force,
|
|
300
|
+
});
|
|
301
|
+
break;
|
|
302
|
+
case 'apply':
|
|
303
|
+
result = await applyFixtures({
|
|
304
|
+
dir: options.dir,
|
|
305
|
+
port: options.port,
|
|
306
|
+
baseUrl: options.baseUrl,
|
|
307
|
+
version,
|
|
308
|
+
});
|
|
309
|
+
break;
|
|
310
|
+
case 'reset':
|
|
311
|
+
result = await resetFixtures({
|
|
312
|
+
dir: options.dir,
|
|
313
|
+
port: options.port,
|
|
314
|
+
baseUrl: options.baseUrl,
|
|
315
|
+
version,
|
|
316
|
+
});
|
|
317
|
+
break;
|
|
318
|
+
case 'storage-state':
|
|
319
|
+
if (!options.persona) {
|
|
320
|
+
throw invalidArgument('The fixtures storage-state command requires --persona.');
|
|
321
|
+
}
|
|
322
|
+
result = await exportStorageState({
|
|
323
|
+
dir: options.dir,
|
|
324
|
+
port: options.port,
|
|
325
|
+
baseUrl: options.baseUrl,
|
|
326
|
+
version,
|
|
327
|
+
persona: options.persona,
|
|
328
|
+
});
|
|
329
|
+
break;
|
|
330
|
+
default:
|
|
331
|
+
throw invalidArgument(
|
|
332
|
+
`Unsupported fixtures subcommand "${options.subcommand}". Use init, apply, reset, or storage-state.`
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (isJson) {
|
|
337
|
+
console.log(JSON.stringify(result, null, 2));
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
console.log(chalk.green(`\n✨ .spaps fixtures ${result.subcommand} complete`));
|
|
342
|
+
console.log(chalk.cyan(` ${result.fixture_dir}`));
|
|
343
|
+
|
|
344
|
+
if (Array.isArray(result.files_created) && result.files_created.length > 0) {
|
|
345
|
+
console.log(chalk.green('\nFiles created:'));
|
|
346
|
+
result.files_created.forEach((file) => {
|
|
347
|
+
console.log(chalk.gray(` • ${file}`));
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (Array.isArray(result.files_overwritten) && result.files_overwritten.length > 0) {
|
|
352
|
+
console.log(chalk.yellow('\nFiles overwritten:'));
|
|
353
|
+
result.files_overwritten.forEach((file) => {
|
|
354
|
+
console.log(chalk.gray(` • ${file}`));
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (Array.isArray(result.removed) && result.removed.length > 0) {
|
|
359
|
+
console.log(chalk.yellow('\nRemoved stale artifacts:'));
|
|
360
|
+
result.removed.forEach((file) => {
|
|
361
|
+
console.log(chalk.gray(` • ${file}`));
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (result.generated?.personas?.length) {
|
|
366
|
+
console.log(chalk.green('\nGenerated personas:'));
|
|
367
|
+
result.generated.personas.forEach((entry) => {
|
|
368
|
+
console.log(chalk.gray(` • ${entry.persona}`));
|
|
369
|
+
console.log(chalk.gray(` storageState: ${entry.storage_state_path}`));
|
|
370
|
+
console.log(chalk.gray(` headers: ${entry.headers_path}`));
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (result.generated?.bridge?.script_path) {
|
|
375
|
+
console.log(chalk.green('\nFrontend bridge:'));
|
|
376
|
+
console.log(chalk.gray(` script: ${result.generated.bridge.script_path}`));
|
|
377
|
+
console.log(chalk.gray(` include: <script src="${result.generated.bridge.public_url}"></script>`));
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (result.storage_state_path) {
|
|
381
|
+
console.log(chalk.green('\nStorage state:'));
|
|
382
|
+
console.log(chalk.gray(` ${result.storage_state_path}`));
|
|
383
|
+
console.log(chalk.gray(` headers: ${result.headers_path}`));
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (result.bridge?.script_path) {
|
|
387
|
+
console.log(chalk.green('\nFrontend bridge:'));
|
|
388
|
+
console.log(chalk.gray(` script: ${result.bridge.script_path}`));
|
|
389
|
+
console.log(chalk.gray(` include: <script src="${result.bridge.public_url}"></script>`));
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
console.log(chalk.green('\nNext steps:'));
|
|
393
|
+
(result.next_steps || []).forEach((step, index) => {
|
|
394
|
+
console.log(chalk.cyan(` ${index + 1}. ${step}`));
|
|
395
|
+
});
|
|
396
|
+
console.log();
|
|
397
|
+
} catch (error) {
|
|
398
|
+
handleError(
|
|
399
|
+
error,
|
|
400
|
+
{
|
|
401
|
+
command: 'fixtures',
|
|
402
|
+
subcommand: options.subcommand,
|
|
403
|
+
dir: options.dir,
|
|
404
|
+
persona: options.persona,
|
|
405
|
+
},
|
|
406
|
+
{ json: isJson }
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
},
|
|
177
410
|
doctor: async ({ options }) => {
|
|
178
411
|
await runDoctor({ port: options.port || DEFAULT_PORT, stripe: options.stripe || null, json: options.json });
|
|
179
|
-
}
|
|
412
|
+
},
|
|
413
|
+
login: loginHandler,
|
|
414
|
+
logout: logoutHandler,
|
|
415
|
+
whoami: whoamiHandler,
|
|
416
|
+
token: tokenHandler,
|
|
180
417
|
};
|
|
181
418
|
}
|
|
182
419
|
|
package/src/help-system.js
CHANGED
|
@@ -454,7 +454,9 @@ function showQuickHelp() {
|
|
|
454
454
|
console.log(chalk.green('Common Commands:'));
|
|
455
455
|
console.log(' npx spaps local ' + chalk.gray('# Start local server'));
|
|
456
456
|
console.log(' npx spaps init ' + chalk.gray('# Initialize in project'));
|
|
457
|
-
console.log(' npx spaps create <name> ' + chalk.gray('#
|
|
457
|
+
console.log(' npx spaps create <name> ' + chalk.gray('# Scaffold a SPAPS starter project'));
|
|
458
|
+
console.log(' npx spaps fixtures apply ' + chalk.gray('# Generate repo-local auth fixtures'));
|
|
459
|
+
console.log(' npx spaps fixtures storage-state ' + chalk.gray('# Export one persona artifact'));
|
|
458
460
|
console.log(' npx spaps types ' + chalk.gray('# Generate types (v0.4.0)'));
|
|
459
461
|
console.log();
|
|
460
462
|
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
const axios = require('axios');
|
|
2
|
+
|
|
3
|
+
const { DEFAULT_PORT } = require('./config');
|
|
4
|
+
|
|
5
|
+
const REQUEST_TIMEOUT_MS = 1200;
|
|
6
|
+
|
|
7
|
+
function buildBaseUrl(port = DEFAULT_PORT) {
|
|
8
|
+
return `http://localhost:${port}`;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function buildDocsUrl(port = DEFAULT_PORT) {
|
|
12
|
+
return `${buildBaseUrl(port)}/docs`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function unwrapEnvelope(payload) {
|
|
16
|
+
if (
|
|
17
|
+
payload &&
|
|
18
|
+
typeof payload === 'object' &&
|
|
19
|
+
payload.success === true &&
|
|
20
|
+
Object.prototype.hasOwnProperty.call(payload, 'data')
|
|
21
|
+
) {
|
|
22
|
+
return payload.data;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return payload;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function extractError(payload, status) {
|
|
29
|
+
if (payload && typeof payload === 'object') {
|
|
30
|
+
if (payload.error && typeof payload.error === 'object') {
|
|
31
|
+
return {
|
|
32
|
+
code: payload.error.code || `HTTP_${status}`,
|
|
33
|
+
message: payload.error.message || `Request failed with status ${status}`,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
code: payload.code || payload.error || `HTTP_${status}`,
|
|
39
|
+
message: payload.message || payload.detail || `Request failed with status ${status}`,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
code: `HTTP_${status}`,
|
|
45
|
+
message: `Request failed with status ${status}`,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function requestJson({ method, url, data = null, headers = {}, timeoutMs = REQUEST_TIMEOUT_MS }) {
|
|
50
|
+
try {
|
|
51
|
+
const response = await axios({
|
|
52
|
+
method,
|
|
53
|
+
url,
|
|
54
|
+
data,
|
|
55
|
+
headers,
|
|
56
|
+
timeout: timeoutMs,
|
|
57
|
+
validateStatus: () => true,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const ok = response.status >= 200 && response.status < 300;
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
ok,
|
|
64
|
+
status: response.status,
|
|
65
|
+
raw: response.data,
|
|
66
|
+
data: unwrapEnvelope(response.data),
|
|
67
|
+
error: ok ? null : extractError(response.data, response.status),
|
|
68
|
+
network_error: false,
|
|
69
|
+
};
|
|
70
|
+
} catch (error) {
|
|
71
|
+
return {
|
|
72
|
+
ok: false,
|
|
73
|
+
status: null,
|
|
74
|
+
raw: null,
|
|
75
|
+
data: null,
|
|
76
|
+
error: {
|
|
77
|
+
code: error.code || 'REQUEST_FAILED',
|
|
78
|
+
message: error.message || 'Request failed',
|
|
79
|
+
},
|
|
80
|
+
network_error: true,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function getServerRuntime({ port = DEFAULT_PORT, timeoutMs = REQUEST_TIMEOUT_MS } = {}) {
|
|
86
|
+
const url = buildBaseUrl(port);
|
|
87
|
+
const docs = buildDocsUrl(port);
|
|
88
|
+
|
|
89
|
+
const health = await requestJson({
|
|
90
|
+
method: 'GET',
|
|
91
|
+
url: `${url}/health`,
|
|
92
|
+
timeoutMs,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
if (!health.ok) {
|
|
96
|
+
return {
|
|
97
|
+
running: false,
|
|
98
|
+
port,
|
|
99
|
+
url,
|
|
100
|
+
docs,
|
|
101
|
+
message: health.network_error ? 'Server not running' : 'Health endpoint returned an error',
|
|
102
|
+
start_command: `npx spaps local --port ${port}`,
|
|
103
|
+
error: health.error,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const localMode = await requestJson({
|
|
108
|
+
method: 'GET',
|
|
109
|
+
url: `${url}/health/local-mode`,
|
|
110
|
+
timeoutMs,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const localModeData = localMode.ok && localMode.data && typeof localMode.data === 'object'
|
|
114
|
+
? localMode.data
|
|
115
|
+
: null;
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
running: true,
|
|
119
|
+
port,
|
|
120
|
+
url,
|
|
121
|
+
docs,
|
|
122
|
+
health: health.data || health.raw,
|
|
123
|
+
local_mode: {
|
|
124
|
+
known: localMode.ok,
|
|
125
|
+
active: localModeData ? Boolean(localModeData.local_mode_active) : null,
|
|
126
|
+
environment: localModeData?.environment || null,
|
|
127
|
+
spaps_local_mode_env:
|
|
128
|
+
typeof localModeData?.spaps_local_mode_env === 'boolean'
|
|
129
|
+
? localModeData.spaps_local_mode_env
|
|
130
|
+
: null,
|
|
131
|
+
test_users: Array.isArray(localModeData?.test_users) ? localModeData.test_users : [],
|
|
132
|
+
test_application: localModeData?.test_application || null,
|
|
133
|
+
hints: localModeData?.hints || {},
|
|
134
|
+
raw: localModeData,
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function readSelfServicePassword() {
|
|
140
|
+
return process.env.SELF_SERVICE_PASSWORD || process.env.SELF_SERVICE_ADMIN_PASSWORD || '';
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function provisionStarterApplication({
|
|
144
|
+
port = DEFAULT_PORT,
|
|
145
|
+
name,
|
|
146
|
+
slug,
|
|
147
|
+
blueprintKey,
|
|
148
|
+
allowedOrigins = [],
|
|
149
|
+
}) {
|
|
150
|
+
const runtime = await getServerRuntime({ port });
|
|
151
|
+
|
|
152
|
+
if (!runtime.running) {
|
|
153
|
+
return {
|
|
154
|
+
status: 'scaffold_only',
|
|
155
|
+
reason: 'server_unreachable',
|
|
156
|
+
runtime,
|
|
157
|
+
warnings: [
|
|
158
|
+
`Local server was unreachable at ${runtime.url}. Starter files were created without provisioning.`,
|
|
159
|
+
],
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (runtime.local_mode.active) {
|
|
164
|
+
return {
|
|
165
|
+
status: 'local_mode',
|
|
166
|
+
reason: null,
|
|
167
|
+
runtime,
|
|
168
|
+
application: {
|
|
169
|
+
id: runtime.local_mode.test_application?.id || null,
|
|
170
|
+
slug: runtime.local_mode.test_application?.slug || slug,
|
|
171
|
+
},
|
|
172
|
+
warnings: [
|
|
173
|
+
`Local mode is active on ${runtime.url}. Starter files were created without provisioning because API key validation is bypassed.`,
|
|
174
|
+
],
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const password = readSelfServicePassword();
|
|
179
|
+
if (!password) {
|
|
180
|
+
return {
|
|
181
|
+
status: 'scaffold_only',
|
|
182
|
+
reason: 'self_service_password_missing',
|
|
183
|
+
runtime,
|
|
184
|
+
warnings: [
|
|
185
|
+
`The server at ${runtime.url} requires a provisioned application key. Set SELF_SERVICE_PASSWORD and re-run this command to provision one automatically.`,
|
|
186
|
+
],
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const auth = await requestJson({
|
|
191
|
+
method: 'POST',
|
|
192
|
+
url: `${runtime.url}/api/self-service/auth`,
|
|
193
|
+
data: { password },
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
if (!auth.ok || !auth.data?.token) {
|
|
197
|
+
return {
|
|
198
|
+
status: 'scaffold_only',
|
|
199
|
+
reason: 'self_service_auth_failed',
|
|
200
|
+
runtime,
|
|
201
|
+
warnings: [
|
|
202
|
+
`Self-service authentication failed: ${auth.error?.message || 'no token returned'}. Starter files were created without provisioning.`,
|
|
203
|
+
],
|
|
204
|
+
error: auth.error,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const creation = await requestJson({
|
|
209
|
+
method: 'POST',
|
|
210
|
+
url: `${runtime.url}/api/self-service/applications`,
|
|
211
|
+
headers: {
|
|
212
|
+
Authorization: `Bearer ${auth.data.token}`,
|
|
213
|
+
},
|
|
214
|
+
data: {
|
|
215
|
+
name,
|
|
216
|
+
slug,
|
|
217
|
+
blueprint_key: blueprintKey,
|
|
218
|
+
allowed_origins: allowedOrigins,
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
if (!creation.ok || !creation.data?.application) {
|
|
223
|
+
return {
|
|
224
|
+
status: 'scaffold_only',
|
|
225
|
+
reason: 'application_provision_failed',
|
|
226
|
+
runtime,
|
|
227
|
+
warnings: [
|
|
228
|
+
`Application provisioning failed: ${creation.error?.message || 'unexpected response'}. Starter files were created without provisioning.`,
|
|
229
|
+
],
|
|
230
|
+
error: creation.error,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
status: 'provisioned',
|
|
236
|
+
reason: null,
|
|
237
|
+
runtime,
|
|
238
|
+
application: {
|
|
239
|
+
id: creation.data.application.id,
|
|
240
|
+
slug: creation.data.application.slug,
|
|
241
|
+
},
|
|
242
|
+
keys: {
|
|
243
|
+
publishable: creation.data.publishable_key || null,
|
|
244
|
+
secret: creation.data.secret_key || creation.data.api_key || null,
|
|
245
|
+
},
|
|
246
|
+
warnings: [],
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
module.exports = {
|
|
251
|
+
buildBaseUrl,
|
|
252
|
+
buildDocsUrl,
|
|
253
|
+
getServerRuntime,
|
|
254
|
+
provisionStarterApplication,
|
|
255
|
+
readSelfServicePassword,
|
|
256
|
+
requestJson,
|
|
257
|
+
unwrapEnvelope,
|
|
258
|
+
};
|