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.
package/README.md CHANGED
@@ -4,6 +4,8 @@ Test SPAPS auth in a new app before you build auth backend infrastructure.
4
4
 
5
5
  `spaps` is the shortest path from "I have a new frontend" to "this app can hit authenticated SPAPS routes locally." It wraps the local SPAPS server, tells you what auth mode the server is actually in, scaffolds starter code, and provisions a real local application when the server requires one.
6
6
 
7
+ Run `spaps` with no arguments to get the current operator session, app context, runtime mode, and the next recommended command.
8
+
7
9
  ## TL;DR
8
10
 
9
11
  ### The problem
@@ -28,6 +30,7 @@ Run SPAPS locally, inspect the current auth mode, then either use local mode dir
28
30
  ## No Backend Yet? Start Here
29
31
 
30
32
  ```bash
33
+ npx spaps
31
34
  npx spaps local
32
35
  npx spaps quickstart --json | jq '.auth'
33
36
  SELF_SERVICE_PASSWORD=your-password npx spaps create demo-app --template react
@@ -74,9 +77,11 @@ Restore order is always: restore base dump first, then let the API container run
74
77
  Run it without installing:
75
78
 
76
79
  ```bash
77
- npx spaps help
80
+ npx --yes spaps
78
81
  ```
79
82
 
83
+ For CI, SSH automation, or the first run on a fresh box, prefer `npx --yes spaps ...` so npm does not block on its install confirmation prompt.
84
+
80
85
  Install globally:
81
86
 
82
87
  ```bash
@@ -93,49 +98,94 @@ npm install spaps
93
98
 
94
99
  | Step | Command | Why |
95
100
  | --- | --- |
96
- | 1 | `npx spaps local` | Start the local SPAPS stack |
97
- | 1b | `npx spaps local --data-source prod-cache` | Start the local stack with a cached prod-backed base DB |
98
- | 2 | `npx spaps status --json` | Confirm the server is reachable |
99
- | 3 | `npx spaps quickstart --json` | See whether local mode is active or a real app key is required |
100
- | 4 | `npx spaps create my-app --template react` | Scaffold a starter and, when possible, provision a real local app |
101
- | 5 | `npx spaps tools --json` | Export the current AI tool contract for agents or tests |
102
- | 6 | `npx spaps fixtures apply` | Materialize repo-local personas into Playwright/browser artifacts |
101
+ | 1 | `npx spaps` | Inspect operator session, detected app context, runtime mode, and the next command |
102
+ | 2 | `npx spaps local` | Start the local SPAPS stack when the runtime is not up yet |
103
+ | 2b | `npx spaps local --data-source prod-cache` | Start the local stack with a cached prod-backed base DB |
104
+ | 3 | `npx spaps connect` | Authenticate the CLI with the active SPAPS server |
105
+ | 4 | `npx spaps verify --json` | Confirm the runtime and auth path end to end |
106
+ | 5 | `npx spaps create my-app --template react` | Scaffold a starter and, when possible, provision a real local app |
107
+ | 6 | `npx spaps tools --json` | Export the current AI tool contract for agents or tests |
108
+ | 7 | `npx spaps fixtures apply` | Materialize repo-local personas into Playwright/browser artifacts |
103
109
 
104
110
  ## CLI Surface
105
111
 
106
112
  | Command | Purpose | Common flags |
107
113
  | --- | --- | --- |
114
+ | `spaps` / `spaps home` | Show operator session, app context, runtime mode, and the next recommended action | `--port`, `--server-url`, `--json` |
108
115
  | `spaps local [stop]` | Start or stop the local server workflow | `--port`, `--runtime-dir`, `--runtime-source`, `--data-source`, `--detach`, `--fresh`, `--from-backup`, `--open`, `--json` |
109
116
  | `spaps status` | Check whether the local server is running | `--port`, `--json` |
117
+ | `spaps verify` | Run a compact runtime and auth verification pass | `--port`, `--server-url`, `--json` |
110
118
  | `spaps quickstart` | Print quick-start instructions | `--port`, `--json` |
111
119
  | `spaps init` | Create a starter `.env.local` | `--json` |
112
120
  | `spaps create <name>` | Scaffold a SPAPS starter project directory and try local provisioning | `--template`, `--dir`, `--port`, `--force`, `--json` |
113
- | `spaps fixtures <subcommand>` | Manage repo-local `.spaps` auth fixtures | `--dir`, `--port`, `--base-url`, `--persona`, `--format`, `--force`, `--json` |
121
+ | `spaps fixtures <subcommand>` | Manage repo-local `.spaps` auth fixtures | `--dir`, `--port`, `--base-url`, `--persona`, `--seed`, `--format`, `--force`, `--json` |
114
122
  | `spaps docs` | Browse or search bundled docs | `--interactive`, `--search`, `--json` |
115
- | `spaps tools` | Emit the AI tool spec | `--port`, `--format`, `--json` |
116
- | `spaps doctor` | Diagnose local environment problems | `--port`, `--stripe`, `--json` |
123
+ | `spaps tools` | Emit the AI tool spec (covers auth, stripe, dayrate, email, webhooks, policies, issue-reporting) | `--port`, `--format`, `--json` |
124
+ | `spaps doctor` | Diagnose local environment problems and probe that every domain router is mounted | `--port`, `--stripe`, `--json` |
125
+ | `spaps dayrate config` | Fetch the active dayrate admin config (requires admin JWT) | `--server-url`, `--port`, `--json` |
126
+ | `spaps email <verb>` | Thin email control-plane commands over the existing SPAPS email API | `--server-url`, `--port`, `--json`; run `spaps email --help` for verb-specific flags |
127
+ | `spaps policy list\|create\|delete` | List, create, or delete authorization policies (admin) | `--name`, `--effect`, `--conditions`, `--id`, `--is-active`, `--limit`, `--json` |
128
+ | `spaps webhook list\|register` | List registered webhooks or register a new outbound webhook | `--url`, `--events`, `--json` |
129
+ | `spaps issue-reports list-mine` | List issue reports created by the authenticated caller | `--status`, `--limit`, `--offset`, `--json` |
130
+
131
+ ### Email Commands
132
+
133
+ Use nested help for the exact flag surface:
134
+
135
+ ```bash
136
+ spaps email --help
137
+ spaps email send --help
138
+ spaps email preview --help
139
+ ```
140
+
141
+ | Verb | Auth | Purpose |
142
+ | --- | --- | --- |
143
+ | `send` | API key | Send a transactional email by template key |
144
+ | `get-template` | API key + JWT | Fetch one template definition |
145
+ | `preview` | API key + JWT | Render a preview with sample or custom context |
146
+ | `logs` | API key + JWT | List email logs for the caller's scope |
147
+ | `list-templates` | API key + JWT + admin | List all templates for the application |
148
+ | `create-template` | API key + JWT + admin | Create a template |
149
+ | `update-template` | API key + JWT + admin | Update a template |
150
+ | `get-override` | API key + JWT | Read the current template override |
151
+ | `set-override` | API key + JWT + admin | Create or update an override |
152
+ | `clear-override` | API key + JWT + admin | Delete an override |
117
153
 
118
154
  Example command usage:
119
155
 
120
156
  ```bash
157
+ spaps --json
121
158
  spaps local --port 3400 --detach
122
159
  spaps local --runtime-source bundle --runtime-dir ./.skillbox/spaps-local
123
160
  spaps local --runtime-source repo --data-source prod-cache
124
161
  spaps local --runtime-source repo --fresh --data-source prod-fresh
125
162
  spaps local --from-backup ~/.cache/spaps/db/prod.sql.gz
126
163
  spaps status --json
164
+ spaps verify --json
127
165
  spaps create my-app --template react
128
- spaps login --json
166
+ spaps connect --json
129
167
  spaps fixtures apply --base-url http://localhost:5173
168
+ spaps fixtures apply --seed --persona dayrate-entitled --base-url http://localhost:5173
130
169
  spaps fixtures storage-state --persona admin
131
170
  spaps docs --search secure-messages
132
171
  spaps doctor --stripe mock
133
172
  spaps local stop
173
+ spaps dayrate config --json
174
+ spaps email --help
175
+ spaps email send --help
176
+ spaps email send --template-key welcome --to user@example.com --context '{"user_name":"Jane"}' --json
177
+ spaps email preview --template-key welcome
178
+ spaps email logs --owner-id owner-123 --limit 25 --json
179
+ spaps email set-override --template-key welcome --subject-override "New subject" --json
180
+ spaps policy list --json
181
+ spaps policy create --name allow_admin --effect allow --conditions '{"role":"admin"}' --json
182
+ spaps webhook register --url https://example.com/hook --events user.created,user.deleted --json
183
+ spaps issue-reports list-mine --status open --json
134
184
  ```
135
185
 
136
186
  ## CLI Auth
137
187
 
138
- `spaps login` uses the active FastAPI device-flow contract:
188
+ `spaps connect` is an alias for `spaps login`, and both use the active FastAPI device-flow contract:
139
189
 
140
190
  - device authorization at `/api/cli/device/*`
141
191
  - authenticated follow-up calls at `/api/auth/*`
@@ -160,6 +210,7 @@ Authenticated follow-up calls such as `whoami`, `logout`, and token refresh stil
160
210
  Examples:
161
211
 
162
212
  ```bash
213
+ npx spaps connect --client-id my-app
163
214
  npx spaps login --client-id my-app
164
215
  SPAPS_CLI_CLIENT_ID=my-app npx spaps login
165
216
  VITE_SPAPS_API_KEY=spaps_pub_demo npx spaps whoami --json
@@ -217,9 +268,12 @@ Then run:
217
268
 
218
269
  ```bash
219
270
  npx spaps fixtures apply --base-url http://localhost:5173
271
+ npx spaps fixtures apply --seed --persona issue-reporter-blocked --base-url http://localhost:5173
220
272
  npx spaps fixtures storage-state --persona admin
221
273
  ```
222
274
 
275
+ Use `--seed` when a persona declares backend seed steps in `.spaps/users.json`. Seeding is opt-in, only runs against a reachable local-mode SPAPS server, and reuses the persona selectors already defined in the fixture kernel instead of adding fixture-only backend routes.
276
+
223
277
  What `apply` emits:
224
278
 
225
279
  - `browser/<persona>.storage-state.json` for Playwright `storageState`
@@ -14,6 +14,19 @@ RUN apt-get update && \
14
14
 
15
15
  ARG SPAPS_SERVER_QUICKSTART_VERSION=0.5.0
16
16
  RUN pip install --no-cache-dir "spaps-server-quickstart==${SPAPS_SERVER_QUICKSTART_VERSION}"
17
+ RUN python - <<'PY'
18
+ from pathlib import Path
19
+
20
+ from spaps_server_quickstart.domains.auth import schemas
21
+
22
+ schema_path = Path(schemas.__file__)
23
+ text = schema_path.read_text(encoding="utf-8")
24
+ text = text.replace(
25
+ " refresh_token: str\n",
26
+ " refresh_token: str | None = None\n",
27
+ )
28
+ schema_path.write_text(text, encoding="utf-8")
29
+ PY
17
30
 
18
31
  COPY alembic.ini /app/alembic.ini
19
32
  COPY alembic/ /app/alembic/
@@ -20,7 +20,8 @@ services:
20
20
  LOG_LEVEL: DEBUG
21
21
  LOG_FORMAT: console
22
22
  AUTH_BASE_URL: "${SPAPS_AUTH_BASE_URL:-http://localhost:5173}"
23
- CORS_ALLOW_ORIGINS: "${CORS_ALLOW_ORIGINS:-*}"
23
+ CORS_ALLOW_ORIGINS: "${CORS_ALLOW_ORIGINS:-http://localhost:3000,http://127.0.0.1:3000,http://localhost:3001,http://127.0.0.1:3001,http://localhost:5173,http://127.0.0.1:5173,http://localhost:30000,http://127.0.0.1:30000}"
24
+ CORS_ALLOW_HEADERS: "${CORS_ALLOW_HEADERS:-Accept,Accept-Language,Authorization,Content-Language,Content-Type,X-API-Key,X-Request-ID,X-SPAPS-Signature,X-SPAPS-Use-Refresh-Cookie,X-Test-User}"
24
25
  LEGACY_API_KEY_AUTH_ENABLED: "${LEGACY_API_KEY_AUTH_ENABLED:-true}"
25
26
  depends_on:
26
27
  spaps-dev-db:
@@ -3,6 +3,8 @@
3
3
  "portable_runtime": {
4
4
  "default_local_mode": true,
5
5
  "default_auth_base_url": "http://localhost:5173",
6
- "default_self_service_password": "spaps_local_self_service_password"
6
+ "default_self_service_password": "spaps_local_self_service_password",
7
+ "default_cors_allow_origins": "http://localhost:3000,http://127.0.0.1:3000,http://localhost:3001,http://127.0.0.1:3001,http://localhost:5173,http://127.0.0.1:5173,http://localhost:30000,http://127.0.0.1:30000",
8
+ "default_cors_allow_headers": "Accept,Accept-Language,Authorization,Content-Language,Content-Type,X-API-Key,X-Request-ID,X-SPAPS-Signature,X-SPAPS-Use-Refresh-Cookie,X-Test-User"
7
9
  }
8
10
  }
package/bin/spaps.js CHANGED
@@ -5,7 +5,6 @@
5
5
  */
6
6
 
7
7
  const chalk = require('chalk');
8
- const fs = require('fs');
9
8
  const { buildProgram } = require('../src/cli-dispatcher');
10
9
  const { createHandlers } = require('../src/handlers');
11
10
 
@@ -18,13 +17,40 @@ ${chalk.yellow('🍠 SPAPS')} - Sweet Potato Authentication & Payment Service
18
17
 
19
18
  const handlers = createHandlers(version, logo);
20
19
  const program = buildProgram({ handlers, dryRun: false, version, logo });
20
+ const argv = process.argv.slice(2);
21
21
 
22
- // Show help if no command provided
23
- if (!process.argv.slice(2).length) {
24
- console.log(logo);
25
- program.outputHelp();
26
- console.log();
27
- console.log(chalk.yellow('💡 Try: npx spaps help --interactive'));
22
+ function shouldRouteToHome(args) {
23
+ if (!args.length) {
24
+ return true;
25
+ }
26
+
27
+ const flagsWithValues = new Set(['-p', '--port', '--server-url']);
28
+ const bareFlags = new Set(['--json']);
29
+
30
+ for (let index = 0; index < args.length; index += 1) {
31
+ const arg = args[index];
32
+
33
+ if (arg === '--help' || arg === '-h' || arg === '--version' || arg === '-V') {
34
+ return false;
35
+ }
36
+
37
+ if (bareFlags.has(arg)) {
38
+ continue;
39
+ }
40
+
41
+ if (flagsWithValues.has(arg)) {
42
+ index += 1;
43
+ continue;
44
+ }
45
+
46
+ return false;
47
+ }
48
+
49
+ return true;
28
50
  }
29
51
 
30
- program.parse(process.argv);
52
+ if (shouldRouteToHome(argv)) {
53
+ program.parse([process.argv[0], process.argv[1], 'home', ...argv]);
54
+ } else {
55
+ program.parse(process.argv);
56
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spaps",
3
- "version": "0.7.6",
3
+ "version": "0.7.8",
4
4
  "description": "Sweet Potato Authentication & Payment Service CLI - Docker Compose orchestrator for local Python/FastAPI SPAPS server with built-in admin middleware",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -39,11 +39,10 @@
39
39
  },
40
40
  "homepage": "https://www.buildooor.com/services",
41
41
  "dependencies": {
42
- "axios": "^1.12.2",
42
+ "axios": "^1.15.1",
43
43
  "chalk": "^4.1.2",
44
44
  "commander": "^11.1.0",
45
- "js-yaml": "^4.1.0",
46
- "ora": "^5.4.1",
45
+ "js-yaml": "^4.1.1",
47
46
  "prompts": "^2.4.2"
48
47
  },
49
48
  "engines": {
package/src/ai-helper.js CHANGED
@@ -1,5 +1,35 @@
1
1
  const { DEFAULT_PORT } = require('./config');
2
2
  const { getServerRuntime, readSelfServicePassword } = require('./local-runtime');
3
+ const { resolveAuthApiKey } = require('./auth/api-key');
4
+
5
+ function normalizeRuntimeArgs(input = DEFAULT_PORT) {
6
+ if (typeof input === 'number') {
7
+ return {
8
+ port: input,
9
+ serverUrl: null,
10
+ cwd: process.cwd(),
11
+ };
12
+ }
13
+
14
+ if (input && typeof input === 'object') {
15
+ return {
16
+ port: Number(input.port) || DEFAULT_PORT,
17
+ serverUrl: input.serverUrl || resolveEnvServerUrl(),
18
+ cwd: input.cwd || process.cwd(),
19
+ };
20
+ }
21
+
22
+ return {
23
+ port: DEFAULT_PORT,
24
+ serverUrl: null,
25
+ cwd: process.cwd(),
26
+ };
27
+ }
28
+
29
+ function resolveEnvServerUrl() {
30
+ const raw = String(process.env.SPAPS_API_URL || '').trim();
31
+ return raw ? raw : null;
32
+ }
3
33
 
4
34
  function buildProvisioningCommand(template, port) {
5
35
  return `SELF_SERVICE_PASSWORD=your-password npx spaps create demo-${template === 'node' ? 'api' : 'app'} --template ${template} --port ${port}`;
@@ -127,8 +157,9 @@ main().catch(console.error);
127
157
  };
128
158
  }
129
159
 
130
- async function getQuickStartInstructions(port = DEFAULT_PORT) {
131
- const runtime = await getServerRuntime({ port });
160
+ async function getQuickStartInstructions(input = DEFAULT_PORT) {
161
+ const { port, serverUrl } = normalizeRuntimeArgs(input);
162
+ const runtime = await getServerRuntime({ port, serverUrl });
132
163
 
133
164
  if (!runtime.running) {
134
165
  return {
@@ -154,12 +185,14 @@ async function getQuickStartInstructions(port = DEFAULT_PORT) {
154
185
  return buildProvisionedModeInstructions(runtime, port);
155
186
  }
156
187
 
157
- async function getServerStatus(port = DEFAULT_PORT) {
158
- return getServerRuntime({ port });
188
+ async function getServerStatus(input = DEFAULT_PORT) {
189
+ const { port, serverUrl } = normalizeRuntimeArgs(input);
190
+ return getServerRuntime({ port, serverUrl });
159
191
  }
160
192
 
161
- async function runQuickTest(port = DEFAULT_PORT) {
162
- const runtime = await getServerRuntime({ port });
193
+ async function runQuickTest(input = DEFAULT_PORT) {
194
+ const { port, serverUrl, cwd } = normalizeRuntimeArgs(input);
195
+ const runtime = await getServerRuntime({ port, serverUrl });
163
196
  const results = [
164
197
  {
165
198
  test: 'server_status',
@@ -173,7 +206,7 @@ async function runQuickTest(port = DEFAULT_PORT) {
173
206
  success: false,
174
207
  summary: '0/1 tests passed',
175
208
  results,
176
- next_steps: [`Start the server with: npx spaps local --port ${port}`],
209
+ next_steps: [runtime.start_command || `Check the server at ${runtime.url} and rerun spaps verify.`],
177
210
  };
178
211
  }
179
212
 
@@ -185,12 +218,13 @@ async function runQuickTest(port = DEFAULT_PORT) {
185
218
  });
186
219
 
187
220
  if (!runtime.local_mode.active) {
188
- const hasKey = Boolean(process.env.SPAPS_API_KEY);
221
+ const apiKey = resolveAuthApiKey({ cwd });
222
+ const hasKey = Boolean(apiKey.apiKey);
189
223
  results.push({
190
224
  test: 'api_key_wiring',
191
225
  success: hasKey,
192
226
  message: hasKey
193
- ? 'SPAPS_API_KEY is set for non-local-mode testing'
227
+ ? `API key is configured via ${apiKey.source}`
194
228
  : 'SPAPS_API_KEY is required when local mode is disabled',
195
229
  fix: hasKey ? null : buildProvisioningCommand('node', port),
196
230
  });
@@ -204,7 +238,7 @@ async function runQuickTest(port = DEFAULT_PORT) {
204
238
  results,
205
239
  next_steps: allSuccess
206
240
  ? [`See docs at ${runtime.docs}`]
207
- : ['Fix the failing checks above and re-run: npx spaps test --json'],
241
+ : ['Fix the failing checks above and re-run: spaps verify --json'],
208
242
  };
209
243
  }
210
244
 
@@ -8,6 +8,7 @@ const { DEFAULT_PORT } = require('./config');
8
8
  const fs = require('fs');
9
9
  const path = require('path');
10
10
  const { getServerRuntime } = require('./local-runtime');
11
+ const { buildDomainTools } = require('./domains');
11
12
 
12
13
  function tryLoadManifest() {
13
14
  const candidates = [
@@ -74,7 +75,7 @@ function buildAuthSection(runtime) {
74
75
  };
75
76
  }
76
77
 
77
- async function buildOpenAIToolSpec({ port = DEFAULT_PORT, version = '0.0.0' } = {}) {
78
+ async function buildOpenAIToolSpec({ port = DEFAULT_PORT, version = '0.0.0', include_non_agent = false } = {}) {
78
79
  const baseUrl = `http://localhost:${port}`;
79
80
  const runtime = await getServerRuntime({ port });
80
81
  const auth = buildAuthSection(runtime);
@@ -190,6 +191,14 @@ async function buildOpenAIToolSpec({ port = DEFAULT_PORT, version = '0.0.0' } =
190
191
  ]
191
192
  };
192
193
 
194
+ // Merge domain registry tools (dayrate, email, webhooks, policies, issue_reporting).
195
+ // Admin routes carry admin_required:true; non-agent routes (e.g. mailgun inbound)
196
+ // are excluded unless include_non_agent is set.
197
+ const domainTools = buildDomainTools({ includeNonAgent: include_non_agent });
198
+ for (const dt of domainTools) {
199
+ spec.tools.push(dt);
200
+ }
201
+
193
202
  // Default error shapes used for enrichment/merging
194
203
  const defaultErrors = {
195
204
  '400': {
@@ -305,7 +314,13 @@ async function buildOpenAIToolSpec({ port = DEFAULT_PORT, version = '0.0.0' } =
305
314
  setResponses('list_products', 'GET', '/api/stripe/products');
306
315
  setResponses('request_magic_link', 'POST', '/api/auth/magic-link');
307
316
  setResponses('get_wallet_nonce', 'POST', '/api/auth/nonce');
308
- // For GET endpoints with query/headers, leave minimal schema for simplicity
317
+ // Enrich every domain-registry tool opportunistically. Skips silently
318
+ // if the operation isn\u2019t in the OpenAPI doc.
319
+ for (const t of spec.tools) {
320
+ if (!t.domain) continue;
321
+ setBodySchema(t.name, t.method, t.path);
322
+ setResponses(t.name, t.method, t.path);
323
+ }
309
324
  }
310
325
  } catch {
311
326
  // Ignore enrichment errors
@@ -319,11 +334,11 @@ async function buildOpenAIToolSpec({ port = DEFAULT_PORT, version = '0.0.0' } =
319
334
  return spec;
320
335
  }
321
336
 
322
- async function buildToolSpec({ format = 'openai', port = DEFAULT_PORT, version = '0.0.0' } = {}) {
337
+ async function buildToolSpec({ format = 'openai', port = DEFAULT_PORT, version = '0.0.0', include_non_agent = false } = {}) {
323
338
  switch (format) {
324
339
  case 'openai':
325
340
  default:
326
- return buildOpenAIToolSpec({ port, version });
341
+ return buildOpenAIToolSpec({ port, version, include_non_agent });
327
342
  }
328
343
  }
329
344
 
package/src/auth/env.js CHANGED
@@ -23,6 +23,10 @@ function isHeadless(env = process.env, platform = process.platform) {
23
23
  return isSsh(env) || !hasGui(env, platform);
24
24
  }
25
25
 
26
+ function hasInteractiveTerminal(stdin = process.stdin, stdout = process.stdout) {
27
+ return Boolean(stdin && stdin.isTTY && stdout && stdout.isTTY);
28
+ }
29
+
26
30
  function tryOpenBrowser(url, { env = process.env, platform = process.platform } = {}) {
27
31
  if (isHeadless(env, platform)) return false;
28
32
  try {
@@ -53,5 +57,6 @@ module.exports = {
53
57
  isSsh,
54
58
  hasGui,
55
59
  isHeadless,
60
+ hasInteractiveTerminal,
56
61
  tryOpenBrowser,
57
62
  };