spaps 0.7.6 → 0.7.7

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,7 +77,7 @@ 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 spaps
78
81
  ```
79
82
 
80
83
  Install globally:
@@ -93,49 +96,94 @@ npm install spaps
93
96
 
94
97
  | Step | Command | Why |
95
98
  | --- | --- |
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 |
99
+ | 1 | `npx spaps` | Inspect operator session, detected app context, runtime mode, and the next command |
100
+ | 2 | `npx spaps local` | Start the local SPAPS stack when the runtime is not up yet |
101
+ | 2b | `npx spaps local --data-source prod-cache` | Start the local stack with a cached prod-backed base DB |
102
+ | 3 | `npx spaps connect` | Authenticate the CLI with the active SPAPS server |
103
+ | 4 | `npx spaps verify --json` | Confirm the runtime and auth path end to end |
104
+ | 5 | `npx spaps create my-app --template react` | Scaffold a starter and, when possible, provision a real local app |
105
+ | 6 | `npx spaps tools --json` | Export the current AI tool contract for agents or tests |
106
+ | 7 | `npx spaps fixtures apply` | Materialize repo-local personas into Playwright/browser artifacts |
103
107
 
104
108
  ## CLI Surface
105
109
 
106
110
  | Command | Purpose | Common flags |
107
111
  | --- | --- | --- |
112
+ | `spaps` / `spaps home` | Show operator session, app context, runtime mode, and the next recommended action | `--port`, `--server-url`, `--json` |
108
113
  | `spaps local [stop]` | Start or stop the local server workflow | `--port`, `--runtime-dir`, `--runtime-source`, `--data-source`, `--detach`, `--fresh`, `--from-backup`, `--open`, `--json` |
109
114
  | `spaps status` | Check whether the local server is running | `--port`, `--json` |
115
+ | `spaps verify` | Run a compact runtime and auth verification pass | `--port`, `--server-url`, `--json` |
110
116
  | `spaps quickstart` | Print quick-start instructions | `--port`, `--json` |
111
117
  | `spaps init` | Create a starter `.env.local` | `--json` |
112
118
  | `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` |
119
+ | `spaps fixtures <subcommand>` | Manage repo-local `.spaps` auth fixtures | `--dir`, `--port`, `--base-url`, `--persona`, `--seed`, `--format`, `--force`, `--json` |
114
120
  | `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` |
121
+ | `spaps tools` | Emit the AI tool spec (covers auth, stripe, dayrate, email, webhooks, policies, issue-reporting) | `--port`, `--format`, `--json` |
122
+ | `spaps doctor` | Diagnose local environment problems and probe that every domain router is mounted | `--port`, `--stripe`, `--json` |
123
+ | `spaps dayrate config` | Fetch the active dayrate admin config (requires admin JWT) | `--server-url`, `--port`, `--json` |
124
+ | `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 |
125
+ | `spaps policy list\|create\|delete` | List, create, or delete authorization policies (admin) | `--name`, `--effect`, `--conditions`, `--id`, `--is-active`, `--limit`, `--json` |
126
+ | `spaps webhook list\|register` | List registered webhooks or register a new outbound webhook | `--url`, `--events`, `--json` |
127
+ | `spaps issue-reports list-mine` | List issue reports created by the authenticated caller | `--status`, `--limit`, `--offset`, `--json` |
128
+
129
+ ### Email Commands
130
+
131
+ Use nested help for the exact flag surface:
132
+
133
+ ```bash
134
+ spaps email --help
135
+ spaps email send --help
136
+ spaps email preview --help
137
+ ```
138
+
139
+ | Verb | Auth | Purpose |
140
+ | --- | --- | --- |
141
+ | `send` | API key | Send a transactional email by template key |
142
+ | `get-template` | API key + JWT | Fetch one template definition |
143
+ | `preview` | API key + JWT | Render a preview with sample or custom context |
144
+ | `logs` | API key + JWT | List email logs for the caller's scope |
145
+ | `list-templates` | API key + JWT + admin | List all templates for the application |
146
+ | `create-template` | API key + JWT + admin | Create a template |
147
+ | `update-template` | API key + JWT + admin | Update a template |
148
+ | `get-override` | API key + JWT | Read the current template override |
149
+ | `set-override` | API key + JWT + admin | Create or update an override |
150
+ | `clear-override` | API key + JWT + admin | Delete an override |
117
151
 
118
152
  Example command usage:
119
153
 
120
154
  ```bash
155
+ spaps --json
121
156
  spaps local --port 3400 --detach
122
157
  spaps local --runtime-source bundle --runtime-dir ./.skillbox/spaps-local
123
158
  spaps local --runtime-source repo --data-source prod-cache
124
159
  spaps local --runtime-source repo --fresh --data-source prod-fresh
125
160
  spaps local --from-backup ~/.cache/spaps/db/prod.sql.gz
126
161
  spaps status --json
162
+ spaps verify --json
127
163
  spaps create my-app --template react
128
- spaps login --json
164
+ spaps connect --json
129
165
  spaps fixtures apply --base-url http://localhost:5173
166
+ spaps fixtures apply --seed --persona dayrate-entitled --base-url http://localhost:5173
130
167
  spaps fixtures storage-state --persona admin
131
168
  spaps docs --search secure-messages
132
169
  spaps doctor --stripe mock
133
170
  spaps local stop
171
+ spaps dayrate config --json
172
+ spaps email --help
173
+ spaps email send --help
174
+ spaps email send --template-key welcome --to user@example.com --context '{"user_name":"Jane"}' --json
175
+ spaps email preview --template-key welcome
176
+ spaps email logs --owner-id owner-123 --limit 25 --json
177
+ spaps email set-override --template-key welcome --subject-override "New subject" --json
178
+ spaps policy list --json
179
+ spaps policy create --name allow_admin --effect allow --conditions '{"role":"admin"}' --json
180
+ spaps webhook register --url https://example.com/hook --events user.created,user.deleted --json
181
+ spaps issue-reports list-mine --status open --json
134
182
  ```
135
183
 
136
184
  ## CLI Auth
137
185
 
138
- `spaps login` uses the active FastAPI device-flow contract:
186
+ `spaps connect` is an alias for `spaps login`, and both use the active FastAPI device-flow contract:
139
187
 
140
188
  - device authorization at `/api/cli/device/*`
141
189
  - authenticated follow-up calls at `/api/auth/*`
@@ -160,6 +208,7 @@ Authenticated follow-up calls such as `whoami`, `logout`, and token refresh stil
160
208
  Examples:
161
209
 
162
210
  ```bash
211
+ npx spaps connect --client-id my-app
163
212
  npx spaps login --client-id my-app
164
213
  SPAPS_CLI_CLIENT_ID=my-app npx spaps login
165
214
  VITE_SPAPS_API_KEY=spaps_pub_demo npx spaps whoami --json
@@ -217,9 +266,12 @@ Then run:
217
266
 
218
267
  ```bash
219
268
  npx spaps fixtures apply --base-url http://localhost:5173
269
+ npx spaps fixtures apply --seed --persona issue-reporter-blocked --base-url http://localhost:5173
220
270
  npx spaps fixtures storage-state --persona admin
221
271
  ```
222
272
 
273
+ 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.
274
+
223
275
  What `apply` emits:
224
276
 
225
277
  - `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.7",
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