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.
Files changed (48) hide show
  1. package/AI_TOOLS.json +10 -11
  2. package/README.md +216 -36
  3. package/assets/local-runtime/Dockerfile +28 -0
  4. package/assets/local-runtime/alembic/env.py +101 -0
  5. package/assets/local-runtime/alembic/path_bootstrap.py +71 -0
  6. package/assets/local-runtime/alembic/versions/000000000001_baseline_consolidated_schema.py +1076 -0
  7. package/assets/local-runtime/alembic/versions/000000000002_fix_column_types_to_match_prod.py +83 -0
  8. package/assets/local-runtime/alembic/versions/000000000003_fix_email_template_key_uniqueness.py +49 -0
  9. package/assets/local-runtime/alembic/versions/000000000004_add_hold_duration_minutes_to_dayrate_config.py +30 -0
  10. package/assets/local-runtime/alembic/versions/000000000005_resource_scoped_entitlements.py +77 -0
  11. package/assets/local-runtime/alembic/versions/000000000006_cfo_rbac_add_is_admin.py +37 -0
  12. package/assets/local-runtime/alembic/versions/000000000007_agent_approvals.py +158 -0
  13. package/assets/local-runtime/alembic/versions/000000000008_add_company_id_to_cfo_connections.py +35 -0
  14. package/assets/local-runtime/alembic/versions/000000000009_tx_signing.py +62 -0
  15. package/assets/local-runtime/alembic/versions/000000000010_affiliate_referrals.py +235 -0
  16. package/assets/local-runtime/alembic/versions/000000000011_checkin_call_booking.py +137 -0
  17. package/assets/local-runtime/alembic/versions/000000000012_subscription_application_scoping.py +55 -0
  18. package/assets/local-runtime/alembic/versions/000000000013_refresh_token_anomaly_context.py +61 -0
  19. package/assets/local-runtime/alembic/versions/000000000014_buildooor_dayrate_hire_schedule.py +39 -0
  20. package/assets/local-runtime/alembic/versions/000000000015_support_telemetry_platform.py +112 -0
  21. package/assets/local-runtime/alembic/versions/000000000016_issue_reporting_platform.py +54 -0
  22. package/assets/local-runtime/alembic/versions/000000000017_issue_reporting_platform_import_tracking.py +44 -0
  23. package/assets/local-runtime/alembic/versions/000000000018_authorization_policy_engine.py +76 -0
  24. package/assets/local-runtime/alembic.ini +47 -0
  25. package/assets/local-runtime/docker-compose.yml +61 -0
  26. package/assets/local-runtime/manifest.json +8 -0
  27. package/assets/local-runtime/scripts/container-entrypoint.sh +13 -0
  28. package/assets/local-runtime/scripts/fetch-prod-db.sh +112 -0
  29. package/assets/local-runtime/scripts/run-migrations.sh +96 -0
  30. package/package.json +2 -1
  31. package/src/ai-helper.js +176 -234
  32. package/src/ai-tool-spec.js +52 -20
  33. package/src/auth/api-key.js +119 -0
  34. package/src/auth/client-id.js +136 -0
  35. package/src/auth/client.js +169 -0
  36. package/src/auth/credentials.js +110 -0
  37. package/src/auth/device-flow.js +159 -0
  38. package/src/auth/env.js +57 -0
  39. package/src/auth/handlers.js +462 -0
  40. package/src/auth/http.js +74 -0
  41. package/src/cli-dispatcher.js +134 -21
  42. package/src/docs-system.js +7 -7
  43. package/src/fixture-kernel.js +1143 -0
  44. package/src/handlers.js +202 -11
  45. package/src/help-system.js +2 -0
  46. package/src/local-runtime.js +258 -0
  47. package/src/local-server.js +597 -199
  48. 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.1.0",
4
- "description": "Auth + payments via SPAPS (local by default). Start with: npx spaps local",
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": true,
8
- "production": {
9
- "header": "X-API-Key",
10
- "env": "SPAPS_API_KEY"
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. Local mode accepts any values.",
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 (Stripe-backed or local).",
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 (simulated locally).",
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
- CLI and middleware package for local SPAPS workflows.
3
+ Test SPAPS auth in a new app before you build auth backend infrastructure.
4
4
 
5
- Examples in this README use placeholder emails and local-only defaults. Replace them with your own values if you wire the middleware into an app.
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
- ```bash
41
- # Start the local server
42
- npx spaps local
43
-
44
- # Confirm it is healthy
45
- npx spaps status
46
-
47
- # Create a starter .env.local in the current project
48
- npx spaps init
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
- Example:
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