spaps 0.7.3 → 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 +216 -36
- 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 +2 -1
- 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 +134 -21
- package/src/docs-system.js +7 -7
- package/src/fixture-kernel.js +1143 -0
- package/src/handlers.js +202 -11
- package/src/help-system.js +2 -0
- package/src/local-runtime.js +258 -0
- package/src/local-server.js +597 -199
- package/src/project-scaffolder.js +185 -45
package/src/handlers.js
CHANGED
|
@@ -7,9 +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');
|
|
10
16
|
const { createProjectStarter } = require('./project-scaffolder');
|
|
17
|
+
const {
|
|
18
|
+
loginHandler,
|
|
19
|
+
logoutHandler,
|
|
20
|
+
whoamiHandler,
|
|
21
|
+
tokenHandler,
|
|
22
|
+
} = require('./auth/handlers');
|
|
11
23
|
|
|
12
24
|
function createHandlers(version, logo) {
|
|
25
|
+
function invalidArgument(message) {
|
|
26
|
+
const error = new Error(message);
|
|
27
|
+
error.code = 'EINVAL';
|
|
28
|
+
return error;
|
|
29
|
+
}
|
|
30
|
+
|
|
13
31
|
return {
|
|
14
32
|
local: async ({ options }) => {
|
|
15
33
|
const isJson = options.json;
|
|
@@ -19,7 +37,13 @@ function createHandlers(version, logo) {
|
|
|
19
37
|
if (options.stop) {
|
|
20
38
|
try {
|
|
21
39
|
const LocalServer = require('./local-server.js');
|
|
22
|
-
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
|
+
});
|
|
23
47
|
server.stop();
|
|
24
48
|
return;
|
|
25
49
|
} catch (error) {
|
|
@@ -32,6 +56,9 @@ function createHandlers(version, logo) {
|
|
|
32
56
|
const LocalServer = require('./local-server.js');
|
|
33
57
|
const server = new LocalServer({
|
|
34
58
|
port: options.port,
|
|
59
|
+
runtimeDir: options.runtimeDir,
|
|
60
|
+
runtimeSource: options.runtimeSource,
|
|
61
|
+
dataSource: options.dataSource,
|
|
35
62
|
json: isJson,
|
|
36
63
|
detach: options.detach,
|
|
37
64
|
fresh: options.fresh,
|
|
@@ -48,23 +75,31 @@ function createHandlers(version, logo) {
|
|
|
48
75
|
}
|
|
49
76
|
|
|
50
77
|
// Set up shutdown handler
|
|
51
|
-
|
|
78
|
+
const shutdown = async () => {
|
|
52
79
|
await server.shutdown();
|
|
53
80
|
process.exit(0);
|
|
54
|
-
}
|
|
81
|
+
};
|
|
82
|
+
process.on('SIGINT', shutdown);
|
|
83
|
+
process.on('SIGTERM', shutdown);
|
|
55
84
|
} catch (error) {
|
|
56
85
|
handleError(error, { port: options.port, command: 'local' }, { json: isJson });
|
|
57
86
|
}
|
|
58
87
|
},
|
|
59
88
|
quickstart: async ({ options }) => {
|
|
60
|
-
const instructions = getQuickStartInstructions(options.port);
|
|
89
|
+
const instructions = await getQuickStartInstructions(options.port);
|
|
61
90
|
if (options.json) {
|
|
62
91
|
console.log(JSON.stringify(instructions, null, 2));
|
|
63
92
|
} else {
|
|
64
93
|
console.log(chalk.yellow('\n🍠 SPAPS Quick Start Instructions\n'));
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
+
});
|
|
68
103
|
console.log('\nFor JSON output: npx spaps quickstart --json');
|
|
69
104
|
}
|
|
70
105
|
},
|
|
@@ -82,6 +117,12 @@ function createHandlers(version, logo) {
|
|
|
82
117
|
console.log(chalk.green('\n✅ SPAPS server is running!\n'));
|
|
83
118
|
console.log(' URL:', chalk.cyan(status.url));
|
|
84
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
|
+
}
|
|
85
126
|
console.log();
|
|
86
127
|
}
|
|
87
128
|
}
|
|
@@ -115,14 +156,15 @@ function createHandlers(version, logo) {
|
|
|
115
156
|
console.log(chalk.cyan(' 3. Start coding!'));
|
|
116
157
|
}
|
|
117
158
|
},
|
|
118
|
-
create: ({ options }) => {
|
|
159
|
+
create: async ({ options }) => {
|
|
119
160
|
const isJson = options.json;
|
|
120
161
|
|
|
121
162
|
try {
|
|
122
|
-
const result = createProjectStarter({
|
|
163
|
+
const result = await createProjectStarter({
|
|
123
164
|
name: options.name,
|
|
124
165
|
template: options.template,
|
|
125
166
|
dir: options.dir,
|
|
167
|
+
port: options.port,
|
|
126
168
|
force: options.force,
|
|
127
169
|
version,
|
|
128
170
|
});
|
|
@@ -134,6 +176,7 @@ function createHandlers(version, logo) {
|
|
|
134
176
|
|
|
135
177
|
console.log(chalk.green(`\n✨ Created ${result.project_name} (${result.template})`));
|
|
136
178
|
console.log(chalk.cyan(` ${result.target_dir}`));
|
|
179
|
+
console.log(chalk.gray(` provisioning: ${result.provisioning.status}`));
|
|
137
180
|
|
|
138
181
|
if (result.files_created.length > 0) {
|
|
139
182
|
console.log(chalk.green('\nFiles created:'));
|
|
@@ -149,6 +192,13 @@ function createHandlers(version, logo) {
|
|
|
149
192
|
});
|
|
150
193
|
}
|
|
151
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
|
+
|
|
152
202
|
console.log(chalk.green('\nNext steps:'));
|
|
153
203
|
result.next_steps.forEach((step, index) => {
|
|
154
204
|
console.log(chalk.cyan(` ${index + 1}. ${step}`));
|
|
@@ -206,12 +256,22 @@ function createHandlers(version, logo) {
|
|
|
206
256
|
}
|
|
207
257
|
},
|
|
208
258
|
tools: async ({ options }) => {
|
|
209
|
-
const spec = buildToolSpec({
|
|
259
|
+
const spec = await buildToolSpec({
|
|
260
|
+
format: options.format || 'openai',
|
|
261
|
+
port: options.port,
|
|
262
|
+
version,
|
|
263
|
+
});
|
|
210
264
|
if (options.json) {
|
|
211
265
|
console.log(JSON.stringify(spec, null, 2));
|
|
212
266
|
} else {
|
|
213
267
|
console.log(chalk.yellow('\n🍠 SPAPS AI Tool Spec (OpenAI-style)\n'));
|
|
214
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
|
+
}
|
|
215
275
|
console.log('Tools:');
|
|
216
276
|
spec.tools.forEach((t, i) => {
|
|
217
277
|
console.log(chalk.green(` ${i + 1}. ${t.name}`), '-', t.description);
|
|
@@ -220,9 +280,140 @@ function createHandlers(version, logo) {
|
|
|
220
280
|
console.log('\nTip: npx spaps tools --json > spaps-tools.json');
|
|
221
281
|
}
|
|
222
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
|
+
},
|
|
223
410
|
doctor: async ({ options }) => {
|
|
224
411
|
await runDoctor({ port: options.port || DEFAULT_PORT, stripe: options.stripe || null, json: options.json });
|
|
225
|
-
}
|
|
412
|
+
},
|
|
413
|
+
login: loginHandler,
|
|
414
|
+
logout: logoutHandler,
|
|
415
|
+
whoami: whoamiHandler,
|
|
416
|
+
token: tokenHandler,
|
|
226
417
|
};
|
|
227
418
|
}
|
|
228
419
|
|
package/src/help-system.js
CHANGED
|
@@ -455,6 +455,8 @@ function showQuickHelp() {
|
|
|
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
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
|
+
};
|