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/AI_TOOLS.json
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spaps",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Auth + payments via SPAPS
|
|
3
|
+
"version": "0.7.4",
|
|
4
|
+
"description": "Auth + payments via SPAPS. Runtime auth requirements come from /health/local-mode or `npx spaps tools --json`.",
|
|
5
5
|
"base_url": "http://localhost:3301",
|
|
6
6
|
"auth": {
|
|
7
|
-
"local_mode":
|
|
8
|
-
"
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
"local_mode": null,
|
|
8
|
+
"mode_source": "/health/local-mode",
|
|
9
|
+
"api_key_required_when_local_mode_disabled": true,
|
|
10
|
+
"header": "X-API-Key",
|
|
11
|
+
"env": "SPAPS_API_KEY"
|
|
12
12
|
},
|
|
13
13
|
"tools": [
|
|
14
14
|
{
|
|
15
15
|
"name": "login",
|
|
16
|
-
"description": "Login with email/password.
|
|
16
|
+
"description": "Login with email/password. Send X-API-Key when local mode is disabled.",
|
|
17
17
|
"method": "POST",
|
|
18
18
|
"path": "/api/auth/login",
|
|
19
19
|
"parameters": {
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
},
|
|
72
72
|
{
|
|
73
73
|
"name": "list_products",
|
|
74
|
-
"description": "List products
|
|
74
|
+
"description": "List products exposed by the SPAPS server.",
|
|
75
75
|
"method": "GET",
|
|
76
76
|
"path": "/api/stripe/products",
|
|
77
77
|
"parameters": {
|
|
@@ -84,7 +84,7 @@
|
|
|
84
84
|
},
|
|
85
85
|
{
|
|
86
86
|
"name": "request_magic_link",
|
|
87
|
-
"description": "Send a magic link for passwordless login
|
|
87
|
+
"description": "Send a magic link for passwordless login.",
|
|
88
88
|
"method": "POST",
|
|
89
89
|
"path": "/api/auth/magic-link",
|
|
90
90
|
"parameters": {
|
|
@@ -111,4 +111,3 @@
|
|
|
111
111
|
}
|
|
112
112
|
]
|
|
113
113
|
}
|
|
114
|
-
|
package/README.md
CHANGED
|
@@ -1,8 +1,73 @@
|
|
|
1
1
|
# spaps
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
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
|
+
|
|
7
|
+
## TL;DR
|
|
8
|
+
|
|
9
|
+
### The problem
|
|
10
|
+
|
|
11
|
+
You want to wire auth into a new app, but you do not want to design and ship your own auth backend first.
|
|
12
|
+
|
|
13
|
+
### The shortcut
|
|
14
|
+
|
|
15
|
+
Run SPAPS locally, inspect the current auth mode, then either use local mode directly or provision a real local app with one command. The CLI tells you which mode the server is actually in, so you do not have to guess.
|
|
16
|
+
|
|
17
|
+
### Why Use `spaps`?
|
|
18
|
+
|
|
19
|
+
| Need | `spaps` gives you |
|
|
20
|
+
| --- | --- |
|
|
21
|
+
| "Can I test auth in this app today?" | `spaps local`, `status`, and `quickstart` |
|
|
22
|
+
| "Do I need a real app key or not?" | Runtime truth from `/health/local-mode` |
|
|
23
|
+
| "Can you scaffold the wiring for me?" | `spaps create <name> --template ...` |
|
|
24
|
+
| "I want a working local app, not a fake contract" | Self-service provisioning when `SELF_SERVICE_PASSWORD` is available |
|
|
25
|
+
| "If provisioning fails, tell me exactly why" | Explicit `scaffold_only` fallback and warnings |
|
|
26
|
+
| "I want persistent local personas, roles, and entitlements in this repo" | `spaps fixtures ...` and a repo-local `.spaps/` kernel |
|
|
27
|
+
|
|
28
|
+
## No Backend Yet? Start Here
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npx spaps local
|
|
32
|
+
npx spaps quickstart --json | jq '.auth'
|
|
33
|
+
SELF_SERVICE_PASSWORD=your-password npx spaps create demo-app --template react
|
|
34
|
+
cd demo-app
|
|
35
|
+
npm install
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
What happens next depends on the server mode:
|
|
39
|
+
|
|
40
|
+
| Server state | What `spaps` does | What you do |
|
|
41
|
+
| --- | --- | --- |
|
|
42
|
+
| `local_mode_active: true` | Exposes local-mode hints and test personas | Use the starter against `localhost`; no app key provisioning required |
|
|
43
|
+
| `local_mode_active: false` and `SELF_SERVICE_PASSWORD` is set | Provisions a real local SPAPS app and writes the working key into `.env.local` | Start wiring auth flows in your app immediately |
|
|
44
|
+
| Server unreachable or no self-service password | Scaffolds files only and tells you why provisioning was skipped | Bring the server up or rerun with `SELF_SERVICE_PASSWORD` later |
|
|
45
|
+
|
|
46
|
+
This package targets `Node.js >=22`.
|
|
47
|
+
|
|
48
|
+
## Local Runtime Modes
|
|
49
|
+
|
|
50
|
+
`spaps local` now has two runtime paths:
|
|
51
|
+
|
|
52
|
+
| Runtime path | When it is used | What it does |
|
|
53
|
+
| --- | --- | --- |
|
|
54
|
+
| `repo` | You are running from the `sweet-potato` checkout | Uses the repo's Docker Compose stack and source-mounted assets |
|
|
55
|
+
| `bundle` | You installed `spaps` from npm elsewhere | Uses bundled Docker assets plus the published `spaps-server-quickstart` package |
|
|
56
|
+
|
|
57
|
+
The default is `auto`: use repo assets when they exist, otherwise fall back to the bundled runtime. The bundled runtime defaults to `SPAPS_LOCAL_MODE=true`, so RBAC fixtures and test personas work out of the box.
|
|
58
|
+
|
|
59
|
+
## Local Data Sources
|
|
60
|
+
|
|
61
|
+
`spaps local` separates runtime selection from base data selection:
|
|
62
|
+
|
|
63
|
+
| Data source | What it does |
|
|
64
|
+
| --- | --- |
|
|
65
|
+
| `empty` | Boot an empty local DB and let migrations create schema |
|
|
66
|
+
| `prod-cache` | Restore the cached SPAPS production dump before the API starts, then reuse it until the cached dump changes |
|
|
67
|
+
| `prod-fresh` | Force a fresh production dump fetch, restore it, then boot the API |
|
|
68
|
+
| `--from-backup <path>` | Restore a specific `.sql.gz` dump file before boot |
|
|
69
|
+
|
|
70
|
+
Restore order is always: restore base dump first, then let the API container run migrations to head on top of it.
|
|
6
71
|
|
|
7
72
|
## Install
|
|
8
73
|
|
|
@@ -24,42 +89,28 @@ Add it to a project:
|
|
|
24
89
|
npm install spaps
|
|
25
90
|
```
|
|
26
91
|
|
|
27
|
-
This package targets `Node.js >=22`.
|
|
28
|
-
|
|
29
|
-
## When It Fits
|
|
30
|
-
|
|
31
|
-
| Need | Package gives you |
|
|
32
|
-
| --- | --- |
|
|
33
|
-
| A local SPAPS control surface | `local`, `status`, `doctor`, and `quickstart` commands |
|
|
34
|
-
| Project bootstrap | `spaps init` creates a starter `.env.local` |
|
|
35
|
-
| AI tooling integration | `spaps tools` emits an OpenAI-style tool spec |
|
|
36
|
-
| Lightweight Node middleware | Admin and permission helpers for Express-style apps |
|
|
37
|
-
|
|
38
92
|
## Quick Start
|
|
39
93
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
npx spaps local
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
npx spaps
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
npx spaps
|
|
49
|
-
|
|
50
|
-
# Emit the local tool spec as JSON
|
|
51
|
-
npx spaps tools --json
|
|
52
|
-
```
|
|
94
|
+
| Step | Command | Why |
|
|
95
|
+
| --- | --- |
|
|
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 |
|
|
53
103
|
|
|
54
104
|
## CLI Surface
|
|
55
105
|
|
|
56
106
|
| Command | Purpose | Common flags |
|
|
57
107
|
| --- | --- | --- |
|
|
58
|
-
| `spaps local [stop]` | Start or stop the local server workflow | `--port`, `--detach`, `--fresh`, `--from-backup`, `--open`, `--json` |
|
|
108
|
+
| `spaps local [stop]` | Start or stop the local server workflow | `--port`, `--runtime-dir`, `--runtime-source`, `--data-source`, `--detach`, `--fresh`, `--from-backup`, `--open`, `--json` |
|
|
59
109
|
| `spaps status` | Check whether the local server is running | `--port`, `--json` |
|
|
60
110
|
| `spaps quickstart` | Print quick-start instructions | `--port`, `--json` |
|
|
61
111
|
| `spaps init` | Create a starter `.env.local` | `--json` |
|
|
62
|
-
| `spaps create <name>` | Scaffold a SPAPS starter project directory | `--template`, `--dir`, `--force`, `--json` |
|
|
112
|
+
| `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` |
|
|
63
114
|
| `spaps docs` | Browse or search bundled docs | `--interactive`, `--search`, `--json` |
|
|
64
115
|
| `spaps tools` | Emit the AI tool spec | `--port`, `--format`, `--json` |
|
|
65
116
|
| `spaps doctor` | Diagnose local environment problems | `--port`, `--stripe`, `--json` |
|
|
@@ -68,13 +119,52 @@ Example command usage:
|
|
|
68
119
|
|
|
69
120
|
```bash
|
|
70
121
|
spaps local --port 3400 --detach
|
|
122
|
+
spaps local --runtime-source bundle --runtime-dir ./.skillbox/spaps-local
|
|
123
|
+
spaps local --runtime-source repo --data-source prod-cache
|
|
124
|
+
spaps local --runtime-source repo --fresh --data-source prod-fresh
|
|
125
|
+
spaps local --from-backup ~/.cache/spaps/db/prod.sql.gz
|
|
71
126
|
spaps status --json
|
|
72
127
|
spaps create my-app --template react
|
|
128
|
+
spaps login --json
|
|
129
|
+
spaps fixtures apply --base-url http://localhost:5173
|
|
130
|
+
spaps fixtures storage-state --persona admin
|
|
73
131
|
spaps docs --search secure-messages
|
|
74
132
|
spaps doctor --stripe mock
|
|
75
133
|
spaps local stop
|
|
76
134
|
```
|
|
77
135
|
|
|
136
|
+
## CLI Auth
|
|
137
|
+
|
|
138
|
+
`spaps login` uses the active FastAPI device-flow contract:
|
|
139
|
+
|
|
140
|
+
- device authorization at `/api/cli/device/*`
|
|
141
|
+
- authenticated follow-up calls at `/api/auth/*`
|
|
142
|
+
- standard SPAPS response envelopes
|
|
143
|
+
|
|
144
|
+
The CLI no longer assumes a built-in `spaps-cli` application slug. Resolve the client id in this order:
|
|
145
|
+
|
|
146
|
+
- `--client-id <app-slug>`
|
|
147
|
+
- `SPAPS_CLI_CLIENT_ID`
|
|
148
|
+
- repo-local `spaps.app.json`
|
|
149
|
+
- repo-local `.spaps/app.json`
|
|
150
|
+
- `/health/local-mode` test application metadata
|
|
151
|
+
|
|
152
|
+
Authenticated follow-up calls such as `whoami`, `logout`, and token refresh still need a normal SPAPS app key when the server is not in local mode. The CLI resolves that key from:
|
|
153
|
+
|
|
154
|
+
- `SPAPS_API_KEY`
|
|
155
|
+
- `NEXT_PUBLIC_SPAPS_API_KEY`
|
|
156
|
+
- `VITE_SPAPS_API_KEY`
|
|
157
|
+
- repo-local `.env.local`
|
|
158
|
+
- repo-local `.env`
|
|
159
|
+
|
|
160
|
+
Examples:
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
npx spaps login --client-id my-app
|
|
164
|
+
SPAPS_CLI_CLIENT_ID=my-app npx spaps login
|
|
165
|
+
VITE_SPAPS_API_KEY=spaps_pub_demo npx spaps whoami --json
|
|
166
|
+
```
|
|
167
|
+
|
|
78
168
|
Still reserved and not finished:
|
|
79
169
|
|
|
80
170
|
- `spaps types`
|
|
@@ -84,10 +174,12 @@ Still reserved and not finished:
|
|
|
84
174
|
`spaps create` ships a local-first starter kit rather than a full framework generator. It creates a new directory with:
|
|
85
175
|
|
|
86
176
|
- `spaps.app.json` for the machine-readable SPAPS app contract
|
|
87
|
-
- `.env.local` pointing at local SPAPS
|
|
177
|
+
- `.env.local` pointing at local SPAPS and, when available, a working template-appropriate key
|
|
88
178
|
- `package.json` with `spaps-sdk`
|
|
89
179
|
- a small template-specific integration starter
|
|
90
180
|
|
|
181
|
+
When the local server is reachable and `SELF_SERVICE_PASSWORD` is set, `create` also provisions a real local SPAPS application. When that is not possible, it falls back to scaffold-only mode and tells you why.
|
|
182
|
+
|
|
91
183
|
Supported templates:
|
|
92
184
|
|
|
93
185
|
- `nextjs`
|
|
@@ -95,13 +187,60 @@ Supported templates:
|
|
|
95
187
|
- `node`
|
|
96
188
|
- `vanilla`
|
|
97
189
|
|
|
98
|
-
|
|
190
|
+
Examples:
|
|
99
191
|
|
|
100
192
|
```bash
|
|
101
193
|
npx spaps create my-app --template react
|
|
102
194
|
npx spaps create my-api --template node --dir ./services/my-api
|
|
195
|
+
SELF_SERVICE_PASSWORD=your-password npx spaps create my-app --template react
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
The generated starter is template-aware:
|
|
199
|
+
|
|
200
|
+
- browser templates write a publishable key when provisioning succeeds
|
|
201
|
+
- server templates write a server key when provisioning succeeds
|
|
202
|
+
- `spaps.app.json` records provisioning status and application id, but never stores raw keys
|
|
203
|
+
|
|
204
|
+
## Repo-Local Fixtures
|
|
205
|
+
|
|
206
|
+
Use `.spaps/` when you want persistent local personas for clicking around, Playwright, or app-level auth tests without inventing ad hoc storage keys in each repo.
|
|
207
|
+
|
|
208
|
+
`spaps fixtures init` creates:
|
|
209
|
+
|
|
210
|
+
- `.spaps/app.json` for runtime and browser target settings
|
|
211
|
+
- `.spaps/users.json` for personas and profile data
|
|
212
|
+
- `.spaps/roles.json` for RBAC grants
|
|
213
|
+
- `.spaps/entitlements.json` for entitlement grants
|
|
214
|
+
- `.spaps/browser/` for generated storage-state and header artifacts
|
|
215
|
+
|
|
216
|
+
Then run:
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
npx spaps fixtures apply --base-url http://localhost:5173
|
|
220
|
+
npx spaps fixtures storage-state --persona admin
|
|
103
221
|
```
|
|
104
222
|
|
|
223
|
+
What `apply` emits:
|
|
224
|
+
|
|
225
|
+
- `browser/<persona>.storage-state.json` for Playwright `storageState`
|
|
226
|
+
- `browser/<persona>.headers.json` for `extraHTTPHeaders`
|
|
227
|
+
- `browser/<persona>.context.json` with merged persona/runtime metadata
|
|
228
|
+
- `public/spaps-dev-auth.js` for frontend-only auth/RBAC dev mode
|
|
229
|
+
|
|
230
|
+
This does not try to import browser password-manager state. It writes deterministic, repo-local test artifacts instead.
|
|
231
|
+
|
|
232
|
+
If you want a human to launch the frontend and click around without a mock backend, include the generated bridge before your app boots:
|
|
233
|
+
|
|
234
|
+
```html
|
|
235
|
+
<script src="/spaps-dev-auth.js"></script>
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
That bridge does three things:
|
|
239
|
+
|
|
240
|
+
- seeds SDK-compatible localStorage keys like `sweet_potato_user` and `sweet_potato_access_token`
|
|
241
|
+
- intercepts `/api/auth/user`, `/api/auth/login`, `/api/auth/logout`, `/api/entitlements`, and `/api/entitlements/check`
|
|
242
|
+
- renders a tiny persona switcher so you can flip between `user`, `admin`, and `premium`
|
|
243
|
+
|
|
105
244
|
## Middleware Example
|
|
106
245
|
|
|
107
246
|
The main module exports admin and permission helpers for Express-style apps.
|
|
@@ -144,9 +283,19 @@ Start on another port:
|
|
|
144
283
|
npx spaps local --port 3400
|
|
145
284
|
```
|
|
146
285
|
|
|
286
|
+
### I need deterministic portable runtime files
|
|
287
|
+
|
|
288
|
+
Pin the bundled runtime directory explicitly:
|
|
289
|
+
|
|
290
|
+
```bash
|
|
291
|
+
npx spaps local --runtime-source bundle --runtime-dir ./.skillbox/spaps-local
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
This is the recommended path for managed environments such as skillbox.
|
|
295
|
+
|
|
147
296
|
### I need machine-readable output
|
|
148
297
|
|
|
149
|
-
Use `--json` on commands that support it, including `local`, `status`, `quickstart`, `init`, `docs`, `tools`, and `doctor`.
|
|
298
|
+
Use `--json` on commands that support it, including `local`, `status`, `quickstart`, `init`, `fixtures`, `docs`, `tools`, and `doctor`.
|
|
150
299
|
|
|
151
300
|
### The local server is not responding
|
|
152
301
|
|
|
@@ -162,6 +311,37 @@ If needed, restart with a clean boot:
|
|
|
162
311
|
npx spaps local --fresh
|
|
163
312
|
```
|
|
164
313
|
|
|
314
|
+
If you want the old "db=fresh from prod" behavior through the portable CLI:
|
|
315
|
+
|
|
316
|
+
```bash
|
|
317
|
+
npx spaps local --fresh --data-source prod-fresh
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
If you want to keep a cached prod-backed base DB around for day-to-day work:
|
|
321
|
+
|
|
322
|
+
```bash
|
|
323
|
+
npx spaps local --data-source prod-cache
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
### `spaps create` only scaffolded files
|
|
327
|
+
|
|
328
|
+
That means one of two things:
|
|
329
|
+
|
|
330
|
+
- the local server was unreachable
|
|
331
|
+
- the server is not in local mode and `SELF_SERVICE_PASSWORD` was missing or invalid
|
|
332
|
+
|
|
333
|
+
Check the current mode first:
|
|
334
|
+
|
|
335
|
+
```bash
|
|
336
|
+
npx spaps quickstart --json
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
If the server requires provisioning, rerun with:
|
|
340
|
+
|
|
341
|
+
```bash
|
|
342
|
+
SELF_SERVICE_PASSWORD=your-password npx spaps create my-app --template react --force
|
|
343
|
+
```
|
|
344
|
+
|
|
165
345
|
## Limitations
|
|
166
346
|
|
|
167
347
|
- The CLI is aimed at local workflows. It is not a full deployment or hosting tool.
|
|
@@ -171,6 +351,10 @@ npx spaps local --fresh
|
|
|
171
351
|
|
|
172
352
|
## FAQ
|
|
173
353
|
|
|
354
|
+
### What is this package actually for?
|
|
355
|
+
|
|
356
|
+
Use it when you want to prove auth wiring in a new app before you build backend auth infrastructure yourself.
|
|
357
|
+
|
|
174
358
|
### Does this package include the TypeScript SDK?
|
|
175
359
|
|
|
176
360
|
No. The SDK is published separately as `spaps-sdk`.
|
|
@@ -185,16 +369,12 @@ No. It skips `.env.local` if the file already exists.
|
|
|
185
369
|
|
|
186
370
|
### What does `spaps tools` output?
|
|
187
371
|
|
|
188
|
-
An OpenAI-style tool spec for the local SPAPS surface.
|
|
372
|
+
An OpenAI-style tool spec for the local SPAPS surface. The auth section is derived from `/health/local-mode` when the local server is reachable.
|
|
189
373
|
|
|
190
374
|
### Is the middleware available from a subpath?
|
|
191
375
|
|
|
192
376
|
Yes. You can import from the main module or `spaps/middleware`.
|
|
193
377
|
|
|
194
|
-
## About Contributions
|
|
195
|
-
|
|
196
|
-
> *About Contributions:* Please don't take this the wrong way, but I do not accept outside contributions for any of my projects. I simply don't have the mental bandwidth to review anything, and it's my name on the thing, so I'm responsible for any problems it causes; thus, the risk-reward is highly asymmetric from my perspective. I'd also have to worry about other "stakeholders," which seems unwise for tools I mostly make for myself for free. Feel free to submit issues, and even PRs if you want to illustrate a proposed fix, but know I won't merge them directly. Instead, I'll have Claude or Codex review submissions via `gh` and independently decide whether and how to address them. Bug reports in particular are welcome. Sorry if this offends, but I want to avoid wasted time and hurt feelings. I understand this isn't in sync with the prevailing open-source ethos that seeks community contributions, but it's the only way I can move at this velocity and keep my sanity.
|
|
197
|
-
|
|
198
378
|
## License
|
|
199
379
|
|
|
200
380
|
MIT
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Portable SPAPS local runtime image.
|
|
2
|
+
# Built from bundled assets shipped inside the npm package.
|
|
3
|
+
|
|
4
|
+
FROM python:3.12-slim AS base
|
|
5
|
+
|
|
6
|
+
WORKDIR /app
|
|
7
|
+
|
|
8
|
+
RUN apt-get update && \
|
|
9
|
+
apt-get install -y --no-install-recommends \
|
|
10
|
+
libpq-dev \
|
|
11
|
+
gcc \
|
|
12
|
+
curl \
|
|
13
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
14
|
+
|
|
15
|
+
ARG SPAPS_SERVER_QUICKSTART_VERSION=0.5.0
|
|
16
|
+
RUN pip install --no-cache-dir "spaps-server-quickstart==${SPAPS_SERVER_QUICKSTART_VERSION}"
|
|
17
|
+
|
|
18
|
+
COPY alembic.ini /app/alembic.ini
|
|
19
|
+
COPY alembic/ /app/alembic/
|
|
20
|
+
COPY scripts/ /app/scripts/
|
|
21
|
+
|
|
22
|
+
ENV APP_ROOT=/app
|
|
23
|
+
ENV PYTHONUNBUFFERED=1
|
|
24
|
+
|
|
25
|
+
EXPOSE 8000
|
|
26
|
+
|
|
27
|
+
ENTRYPOINT ["/app/scripts/container-entrypoint.sh"]
|
|
28
|
+
CMD ["uvicorn", "spaps_server_quickstart.spaps_app:app", "--host", "0.0.0.0", "--port", "8000"]
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Alembic environment for SPAPS.
|
|
3
|
+
|
|
4
|
+
Imports all domain models from spaps_server_quickstart.domains so that
|
|
5
|
+
``Base.metadata`` contains every table. Alembic uses this metadata for
|
|
6
|
+
autogenerate and schema diffing.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import sys
|
|
13
|
+
from logging.config import fileConfig
|
|
14
|
+
|
|
15
|
+
from alembic import context
|
|
16
|
+
from sqlalchemy import engine_from_config, pool
|
|
17
|
+
|
|
18
|
+
# Ensure Alembic helper modules are importable, then prefer the active
|
|
19
|
+
# quickstart source tree (for local Docker this should be the live /app/src bind
|
|
20
|
+
# mount, not the packaged fallback copied into the image).
|
|
21
|
+
sys.path.insert(0, os.path.dirname(__file__))
|
|
22
|
+
|
|
23
|
+
from path_bootstrap import ensure_quickstart_src_path # noqa: E402
|
|
24
|
+
|
|
25
|
+
ensure_quickstart_src_path(__file__, sys.path)
|
|
26
|
+
|
|
27
|
+
from spaps_server_quickstart.domains._db.base import Base # noqa: E402
|
|
28
|
+
|
|
29
|
+
# Phase 1+: import domain models here so Base.metadata picks them up
|
|
30
|
+
# from spaps_server_quickstart.domains.users.models import User # noqa: E402, F401
|
|
31
|
+
# from spaps_server_quickstart.domains.auth.models import ... # noqa: E402, F401
|
|
32
|
+
from spaps_server_quickstart.domains.approval_authz_prod_hardening.models import GovernanceAuthzAuditEvent # noqa: E402, F401
|
|
33
|
+
from spaps_server_quickstart.domains.support_telemetry_platform.models import SupportCase, SupportCaseEvent # noqa: E402, F401
|
|
34
|
+
from spaps_server_quickstart.domains.issue_reporting_platform.models import IssueReport # noqa: E402, F401
|
|
35
|
+
|
|
36
|
+
config = context.config
|
|
37
|
+
|
|
38
|
+
if config.config_file_name is not None:
|
|
39
|
+
fileConfig(config.config_file_name)
|
|
40
|
+
|
|
41
|
+
target_metadata = Base.metadata
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def get_database_url() -> str:
|
|
45
|
+
"""Resolve the database URL from environment or settings."""
|
|
46
|
+
url = os.environ.get("DATABASE_URL", "")
|
|
47
|
+
if url:
|
|
48
|
+
# Alembic runs synchronously — convert asyncpg to psycopg
|
|
49
|
+
if "asyncpg" in url:
|
|
50
|
+
url = url.replace("postgresql+asyncpg", "postgresql+psycopg")
|
|
51
|
+
return url
|
|
52
|
+
|
|
53
|
+
# Fallback: try loading from settings
|
|
54
|
+
try:
|
|
55
|
+
from spaps_server_quickstart.spaps_settings import get_spaps_settings
|
|
56
|
+
|
|
57
|
+
settings = get_spaps_settings()
|
|
58
|
+
return settings.sync_database_url
|
|
59
|
+
except Exception:
|
|
60
|
+
return "postgresql+psycopg://postgres:postgres@localhost:5432/spaps"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def run_migrations_offline() -> None:
|
|
64
|
+
"""Run migrations in 'offline' mode (generate SQL without DB connection)."""
|
|
65
|
+
url = get_database_url()
|
|
66
|
+
context.configure(
|
|
67
|
+
url=url,
|
|
68
|
+
target_metadata=target_metadata,
|
|
69
|
+
literal_binds=True,
|
|
70
|
+
dialect_opts={"paramstyle": "named"},
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
with context.begin_transaction():
|
|
74
|
+
context.run_migrations()
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def run_migrations_online() -> None:
|
|
78
|
+
"""Run migrations with a live database connection."""
|
|
79
|
+
alembic_config = config.get_section(config.config_ini_section, {})
|
|
80
|
+
alembic_config["sqlalchemy.url"] = get_database_url()
|
|
81
|
+
|
|
82
|
+
connectable = engine_from_config(
|
|
83
|
+
alembic_config,
|
|
84
|
+
prefix="sqlalchemy.",
|
|
85
|
+
poolclass=pool.NullPool,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
with connectable.connect() as connection:
|
|
89
|
+
context.configure(
|
|
90
|
+
connection=connection,
|
|
91
|
+
target_metadata=target_metadata,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
with context.begin_transaction():
|
|
95
|
+
context.run_migrations()
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
if context.is_offline_mode():
|
|
99
|
+
run_migrations_offline()
|
|
100
|
+
else:
|
|
101
|
+
run_migrations_online()
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Helpers for locating the active quickstart source tree for Alembic."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import MutableSequence, Sequence
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
_PACKAGE_DIRNAME = "spaps_server_quickstart"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _dedupe_paths(raw_paths: Sequence[str | Path]) -> list[Path]:
|
|
13
|
+
"""Normalize candidate paths while preserving first-seen order."""
|
|
14
|
+
seen: set[str] = set()
|
|
15
|
+
candidates: list[Path] = []
|
|
16
|
+
for raw_path in raw_paths:
|
|
17
|
+
if not raw_path:
|
|
18
|
+
continue
|
|
19
|
+
path = Path(raw_path).resolve()
|
|
20
|
+
normalized = str(path)
|
|
21
|
+
if normalized in seen:
|
|
22
|
+
continue
|
|
23
|
+
seen.add(normalized)
|
|
24
|
+
candidates.append(path)
|
|
25
|
+
return candidates
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def resolve_quickstart_src_path(
|
|
29
|
+
alembic_env_path: str | Path,
|
|
30
|
+
sys_path: Sequence[str],
|
|
31
|
+
) -> str | None:
|
|
32
|
+
"""Return the best source root that contains the quickstart package.
|
|
33
|
+
|
|
34
|
+
Preference order:
|
|
35
|
+
1. Existing `sys.path` entries that already expose the package. This keeps
|
|
36
|
+
Docker bind mounts like `/app/src` ahead of any packaged fallback.
|
|
37
|
+
2. Known fallback roots relative to `alembic/env.py`.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
alembic_dir = Path(alembic_env_path).resolve().parent
|
|
41
|
+
fallback_paths = [
|
|
42
|
+
Path("/app/src"),
|
|
43
|
+
alembic_dir.parent / "packages" / "python-server-quickstart" / "src",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
for candidate in _dedupe_paths([*sys_path, *fallback_paths]):
|
|
47
|
+
if (candidate / _PACKAGE_DIRNAME).exists():
|
|
48
|
+
return str(candidate)
|
|
49
|
+
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def ensure_quickstart_src_path(
|
|
54
|
+
alembic_env_path: str | Path,
|
|
55
|
+
sys_path: MutableSequence[str],
|
|
56
|
+
) -> str | None:
|
|
57
|
+
"""Move the resolved quickstart source root to the front of sys.path."""
|
|
58
|
+
|
|
59
|
+
resolved = resolve_quickstart_src_path(alembic_env_path, list(sys_path))
|
|
60
|
+
if resolved is None:
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
resolved_path = Path(resolved).resolve()
|
|
64
|
+
filtered = [
|
|
65
|
+
entry
|
|
66
|
+
for entry in sys_path
|
|
67
|
+
if not entry or Path(entry).resolve() != resolved_path
|
|
68
|
+
]
|
|
69
|
+
sys_path[:] = filtered
|
|
70
|
+
sys_path.insert(0, resolved)
|
|
71
|
+
return resolved
|