spaps 0.7.6 → 0.7.8

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.
@@ -0,0 +1,42 @@
1
+ const chalk = require('chalk');
2
+
3
+ function showQuickHelp() {
4
+ const output = [
5
+ '',
6
+ chalk.yellow('šŸ  SPAPS Quick Reference'),
7
+ '',
8
+ chalk.green('Common Commands:'),
9
+ ` npx spaps local ${chalk.gray('# Start local server')}`,
10
+ ` npx spaps init ${chalk.gray('# Initialize in project')}`,
11
+ ` npx spaps create <name> ${chalk.gray('# Scaffold a SPAPS starter project')}`,
12
+ ` npx spaps fixtures apply ${chalk.gray('# Generate repo-local auth fixtures')}`,
13
+ ` npx spaps fixtures storage-state ${chalk.gray('# Export one persona artifact')}`,
14
+ ` npx spaps types ${chalk.gray('# Generate types (v0.4.0)')}`,
15
+ '',
16
+ chalk.green('Local Development:'),
17
+ ` npx spaps local --port 3001 ${chalk.gray('# Custom port')}`,
18
+ ` npx spaps local --open ${chalk.gray('# Open browser')}`,
19
+ ` npx spaps local --json ${chalk.gray('# JSON output')}`,
20
+ '',
21
+ chalk.green('SDK Usage:'),
22
+ ` npm install spaps-sdk ${chalk.gray('# Install SDK')}`,
23
+ '',
24
+ chalk.gray(' import { SPAPSClient } from "spaps-sdk"'),
25
+ chalk.gray(' const spaps = new SPAPSClient()'),
26
+ chalk.gray(' await spaps.login(email, password)'),
27
+ '',
28
+ chalk.green('Debugging:'),
29
+ ` curl http://localhost:3301/health ${chalk.gray('# Check server')}`,
30
+ ` npx spaps help --interactive ${chalk.gray('# Interactive help')}`,
31
+ '',
32
+ chalk.blue('šŸ“š Docs: https://sweetpotato.dev'),
33
+ chalk.blue('šŸ’¬ Discord: https://discord.gg/sweetpotato'),
34
+ '',
35
+ ].join('\n');
36
+
37
+ process.stdout.write(`${output}\n`);
38
+ }
39
+
40
+ module.exports = {
41
+ showQuickHelp,
42
+ };
@@ -7,6 +7,7 @@ const chalk = require('chalk');
7
7
  const prompts = require('prompts');
8
8
 
9
9
  const { DEFAULT_PORT } = require('./config');
10
+ const { showQuickHelp } = require('./help-quick');
10
11
 
11
12
  const HELP_TREE = {
12
13
  root: {
@@ -448,42 +449,6 @@ async function showInteractiveHelp() {
448
449
  }
449
450
  }
450
451
 
451
- function showQuickHelp() {
452
- console.log(chalk.yellow('\nšŸ  SPAPS Quick Reference\n'));
453
-
454
- console.log(chalk.green('Common Commands:'));
455
- console.log(' npx spaps local ' + chalk.gray('# Start local server'));
456
- console.log(' npx spaps init ' + chalk.gray('# Initialize in project'));
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'));
460
- console.log(' npx spaps types ' + chalk.gray('# Generate types (v0.4.0)'));
461
- console.log();
462
-
463
- console.log(chalk.green('Local Development:'));
464
- console.log(' npx spaps local --port 3001 ' + chalk.gray('# Custom port'));
465
- console.log(' npx spaps local --open ' + chalk.gray('# Open browser'));
466
- console.log(' npx spaps local --json ' + chalk.gray('# JSON output'));
467
- console.log();
468
-
469
- console.log(chalk.green('SDK Usage:'));
470
- console.log(' npm install spaps-sdk ' + chalk.gray('# Install SDK'));
471
- console.log();
472
- console.log(chalk.gray(' import { SPAPSClient } from "spaps-sdk"'));
473
- console.log(chalk.gray(' const spaps = new SPAPSClient()'));
474
- console.log(chalk.gray(' await spaps.login(email, password)'));
475
- console.log();
476
-
477
- console.log(chalk.green('Debugging:'));
478
- console.log(' curl http://localhost:3301/health ' + chalk.gray('# Check server'));
479
- console.log(' npx spaps help --interactive ' + chalk.gray('# Interactive help'));
480
- console.log();
481
-
482
- console.log(chalk.blue('šŸ“š Docs: https://sweetpotato.dev'));
483
- console.log(chalk.blue('šŸ’¬ Discord: https://discord.gg/sweetpotato'));
484
- console.log();
485
- }
486
-
487
452
  module.exports = {
488
453
  showInteractiveHelp,
489
454
  showQuickHelp,
@@ -0,0 +1,200 @@
1
+ const chalk = require('chalk');
2
+
3
+ const { DEFAULT_PORT } = require('./config');
4
+ const { getServerRuntime } = require('./local-runtime');
5
+ const { resolveServerUrl } = require('./auth/client');
6
+ const { getCredentials, CREDENTIALS_PATH } = require('./auth/credentials');
7
+ const { findUpContract } = require('./auth/client-id');
8
+ const { resolveAuthApiKey } = require('./auth/api-key');
9
+
10
+ function resolveAppContext({ cwd, runtime }) {
11
+ if (typeof process.env.SPAPS_CLI_CLIENT_ID === 'string' && process.env.SPAPS_CLI_CLIENT_ID.trim()) {
12
+ return {
13
+ clientId: process.env.SPAPS_CLI_CLIENT_ID.trim(),
14
+ source: 'SPAPS_CLI_CLIENT_ID',
15
+ path: null,
16
+ };
17
+ }
18
+
19
+ const contractHit = findUpContract(cwd);
20
+ if (contractHit) {
21
+ return contractHit;
22
+ }
23
+
24
+ const runtimeClientId = runtime?.local_mode?.test_application?.slug || null;
25
+ if (runtimeClientId) {
26
+ return {
27
+ clientId: runtimeClientId,
28
+ source: '/health/local-mode',
29
+ path: null,
30
+ };
31
+ }
32
+
33
+ return {
34
+ clientId: null,
35
+ source: null,
36
+ path: null,
37
+ };
38
+ }
39
+
40
+ function isLocalhostUrl(url) {
41
+ try {
42
+ const parsed = new URL(url);
43
+ return parsed.hostname === 'localhost' || parsed.hostname === '127.0.0.1';
44
+ } catch {
45
+ return false;
46
+ }
47
+ }
48
+
49
+ function formatSource(source, filePath) {
50
+ if (!source) {
51
+ return null;
52
+ }
53
+ return filePath ? `${source} (${filePath})` : source;
54
+ }
55
+
56
+ function formatMode(runtime) {
57
+ if (!runtime.running) {
58
+ return 'server unreachable';
59
+ }
60
+ if (!runtime.local_mode?.known) {
61
+ return 'mode unknown';
62
+ }
63
+ return runtime.local_mode.active ? 'local mode active' : 'application key required';
64
+ }
65
+
66
+ function buildRecommendedNext({ operator, runtime, port }) {
67
+ if (!operator.connected) {
68
+ return {
69
+ command: 'spaps connect',
70
+ reason: `No saved operator session for ${operator.server_url}.`,
71
+ };
72
+ }
73
+
74
+ if (!runtime.running) {
75
+ return {
76
+ command: isLocalhostUrl(runtime.url)
77
+ ? `spaps local${port !== DEFAULT_PORT ? ` --port ${port}` : ''}`
78
+ : 'spaps verify',
79
+ reason: isLocalhostUrl(runtime.url)
80
+ ? `No SPAPS runtime is reachable at ${runtime.url}.`
81
+ : `No SPAPS runtime is reachable at ${runtime.url}; verify the configured server URL.`,
82
+ };
83
+ }
84
+
85
+ return {
86
+ command: 'spaps verify',
87
+ reason: runtime.local_mode?.active
88
+ ? 'Confirm the local auth path and runtime hints end to end.'
89
+ : 'Confirm the provisioned auth path and API key wiring end to end.',
90
+ };
91
+ }
92
+
93
+ async function buildHomeView({ port = DEFAULT_PORT, serverUrl = null, cwd = process.cwd() } = {}) {
94
+ const resolvedPort = Number(port) || DEFAULT_PORT;
95
+ const resolvedServerUrl = resolveServerUrl({ port: resolvedPort, serverUrl });
96
+ const runtime = await getServerRuntime({ port: resolvedPort, serverUrl: resolvedServerUrl });
97
+ const storedCreds = getCredentials(resolvedServerUrl);
98
+ const envAccessToken =
99
+ typeof process.env.SPAPS_ACCESS_TOKEN === 'string' && process.env.SPAPS_ACCESS_TOKEN.trim()
100
+ ? process.env.SPAPS_ACCESS_TOKEN.trim()
101
+ : null;
102
+ const appContext = resolveAppContext({ cwd, runtime });
103
+ const apiKey = resolveAuthApiKey({ cwd });
104
+
105
+ const operator = {
106
+ server_url: resolvedServerUrl,
107
+ connected: Boolean(envAccessToken || storedCreds?.access_token),
108
+ source: envAccessToken ? 'SPAPS_ACCESS_TOKEN' : storedCreds?.access_token ? 'credentials' : null,
109
+ credentials_path: CREDENTIALS_PATH,
110
+ user_id: storedCreds?.user_id || null,
111
+ client_id: storedCreds?.client_id || null,
112
+ expires_at: storedCreds?.expires_at || null,
113
+ };
114
+
115
+ const app = {
116
+ client_id: appContext.clientId,
117
+ client_id_source: appContext.source,
118
+ client_id_path: appContext.path,
119
+ api_key_configured: Boolean(apiKey.apiKey),
120
+ api_key_source: apiKey.source,
121
+ api_key_path: apiKey.path,
122
+ api_key_env: apiKey.envVar || null,
123
+ };
124
+
125
+ return {
126
+ success: true,
127
+ command: 'home',
128
+ operator,
129
+ app,
130
+ runtime,
131
+ recommended_next: buildRecommendedNext({
132
+ operator,
133
+ runtime,
134
+ port: resolvedPort,
135
+ }),
136
+ };
137
+ }
138
+
139
+ function renderHomeView(view, { logo = '' } = {}) {
140
+ if (logo) {
141
+ console.log(logo);
142
+ }
143
+
144
+ console.log(chalk.bold('Operator'));
145
+ console.log(' Server:', chalk.cyan(view.operator.server_url));
146
+ console.log(
147
+ ' Session:',
148
+ view.operator.connected
149
+ ? chalk.green(`connected via ${view.operator.source || 'credentials'}`)
150
+ : chalk.yellow('not connected')
151
+ );
152
+ if (view.operator.client_id) {
153
+ console.log(' Session app:', chalk.cyan(view.operator.client_id));
154
+ }
155
+ if (view.operator.user_id) {
156
+ console.log(' User:', chalk.cyan(view.operator.user_id));
157
+ }
158
+
159
+ console.log();
160
+ console.log(chalk.bold('App'));
161
+ if (view.app.client_id) {
162
+ console.log(' Client id:', chalk.cyan(view.app.client_id));
163
+ if (view.app.client_id_source) {
164
+ console.log(chalk.gray(` source: ${formatSource(view.app.client_id_source, view.app.client_id_path)}`));
165
+ }
166
+ } else {
167
+ console.log(' Client id:', chalk.yellow('not detected'));
168
+ }
169
+ console.log(
170
+ ' API key:',
171
+ view.app.api_key_configured
172
+ ? chalk.green(`configured via ${formatSource(view.app.api_key_source, view.app.api_key_path)}`)
173
+ : chalk.yellow('not found')
174
+ );
175
+
176
+ console.log();
177
+ console.log(chalk.bold('Runtime'));
178
+ if (!view.runtime.running) {
179
+ console.log(' Status:', chalk.red('unreachable'));
180
+ console.log(' Target:', chalk.cyan(view.runtime.url));
181
+ if (view.runtime.error?.message) {
182
+ console.log(chalk.gray(` ${view.runtime.error.message}`));
183
+ }
184
+ } else {
185
+ console.log(' Status:', chalk.green('running'));
186
+ console.log(' URL:', chalk.cyan(view.runtime.url));
187
+ console.log(' Mode:', chalk.cyan(formatMode(view.runtime)));
188
+ }
189
+
190
+ console.log();
191
+ console.log(chalk.bold('Next'));
192
+ console.log(' ' + chalk.cyan(view.recommended_next.command));
193
+ console.log(chalk.gray(` ${view.recommended_next.reason}`));
194
+ console.log();
195
+ }
196
+
197
+ module.exports = {
198
+ buildHomeView,
199
+ renderHomeView,
200
+ };
@@ -12,6 +12,19 @@ function buildDocsUrl(port = DEFAULT_PORT) {
12
12
  return `${buildBaseUrl(port)}/docs`;
13
13
  }
14
14
 
15
+ function normalizeServerUrl(url) {
16
+ return String(url || '').trim().replace(/\/+$/, '');
17
+ }
18
+
19
+ function isLocalhostUrl(url) {
20
+ try {
21
+ const parsed = new URL(url);
22
+ return parsed.hostname === 'localhost' || parsed.hostname === '127.0.0.1';
23
+ } catch {
24
+ return false;
25
+ }
26
+ }
27
+
15
28
  function unwrapEnvelope(payload) {
16
29
  if (
17
30
  payload &&
@@ -82,9 +95,9 @@ async function requestJson({ method, url, data = null, headers = {}, timeoutMs =
82
95
  }
83
96
  }
84
97
 
85
- async function getServerRuntime({ port = DEFAULT_PORT, timeoutMs = REQUEST_TIMEOUT_MS } = {}) {
86
- const url = buildBaseUrl(port);
87
- const docs = buildDocsUrl(port);
98
+ async function getServerRuntime({ port = DEFAULT_PORT, serverUrl = null, timeoutMs = REQUEST_TIMEOUT_MS } = {}) {
99
+ const url = serverUrl ? normalizeServerUrl(serverUrl) : buildBaseUrl(port);
100
+ const docs = `${url}/docs`;
88
101
 
89
102
  const health = await requestJson({
90
103
  method: 'GET',
@@ -99,7 +112,7 @@ async function getServerRuntime({ port = DEFAULT_PORT, timeoutMs = REQUEST_TIMEO
99
112
  url,
100
113
  docs,
101
114
  message: health.network_error ? 'Server not running' : 'Health endpoint returned an error',
102
- start_command: `npx spaps local --port ${port}`,
115
+ start_command: isLocalhostUrl(url) ? `npx spaps local --port ${port}` : null,
103
116
  error: health.error,
104
117
  };
105
118
  }
@@ -251,6 +264,8 @@ module.exports = {
251
264
  buildBaseUrl,
252
265
  buildDocsUrl,
253
266
  getServerRuntime,
267
+ isLocalhostUrl,
268
+ normalizeServerUrl,
254
269
  provisionStarterApplication,
255
270
  readSelfServicePassword,
256
271
  requestJson,
@@ -20,6 +20,28 @@ const DEFAULT_RUNTIME_SOURCE = 'auto';
20
20
  const DEFAULT_AUTH_BASE_URL = 'http://localhost:5173';
21
21
  const DEFAULT_DATA_SOURCE = 'empty';
22
22
  const EXTERNAL_NETWORKS = ['reverse-proxy'];
23
+ const DEFAULT_CORS_ALLOW_ORIGINS = [
24
+ 'http://localhost:3000',
25
+ 'http://127.0.0.1:3000',
26
+ 'http://localhost:3001',
27
+ 'http://127.0.0.1:3001',
28
+ 'http://localhost:5173',
29
+ 'http://127.0.0.1:5173',
30
+ 'http://localhost:30000',
31
+ 'http://127.0.0.1:30000',
32
+ ].join(',');
33
+ const DEFAULT_CORS_ALLOW_HEADERS = [
34
+ 'Accept',
35
+ 'Accept-Language',
36
+ 'Authorization',
37
+ 'Content-Language',
38
+ 'Content-Type',
39
+ 'X-API-Key',
40
+ 'X-Request-ID',
41
+ 'X-SPAPS-Signature',
42
+ 'X-SPAPS-Use-Refresh-Cookie',
43
+ 'X-Test-User',
44
+ ].join(',');
23
45
 
24
46
  function readBundledRuntimeManifest() {
25
47
  if (!fs.existsSync(BUNDLED_RUNTIME_MANIFEST)) {
@@ -123,7 +145,14 @@ function buildComposeEnv({ port, authBaseUrl }) {
123
145
  process.env.SELF_SERVICE_PASSWORD ||
124
146
  portableRuntime.default_self_service_password ||
125
147
  'spaps_local_self_service_password',
126
- CORS_ALLOW_ORIGINS: process.env.CORS_ALLOW_ORIGINS || '*',
148
+ CORS_ALLOW_ORIGINS:
149
+ process.env.CORS_ALLOW_ORIGINS ||
150
+ portableRuntime.default_cors_allow_origins ||
151
+ DEFAULT_CORS_ALLOW_ORIGINS,
152
+ CORS_ALLOW_HEADERS:
153
+ process.env.CORS_ALLOW_HEADERS ||
154
+ portableRuntime.default_cors_allow_headers ||
155
+ DEFAULT_CORS_ALLOW_HEADERS,
127
156
  LEGACY_API_KEY_AUTH_ENABLED: process.env.LEGACY_API_KEY_AUTH_ENABLED || 'true',
128
157
  SPAPS_SERVER_QUICKSTART_VERSION:
129
158
  process.env.SPAPS_SERVER_QUICKSTART_VERSION || manifest.spaps_server_quickstart_version,