spaps 0.9.0 → 0.9.1
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 +2410 -346
- package/README.md +89 -2
- package/client.js +4 -0
- package/package.json +1 -1
- package/src/ai-tool-spec.js +282 -19
- package/src/auth/handlers.js +109 -0
- package/src/auth/surface.js +653 -0
- package/src/cli-dispatcher.js +345 -1
- package/src/doctor.js +97 -5
- package/src/domain-cli.js +1 -0
- package/src/handlers.js +390 -1
- package/src/help-system.js +1 -1
package/README.md
CHANGED
|
@@ -26,6 +26,7 @@ Run SPAPS locally, inspect the current auth mode, then either use local mode dir
|
|
|
26
26
|
| "I want a working local app, not a fake contract" | Self-service provisioning when `SELF_SERVICE_PASSWORD` is available |
|
|
27
27
|
| "If provisioning fails, tell me exactly why" | Explicit `scaffold_only` fallback and warnings |
|
|
28
28
|
| "I want persistent local personas, roles, and entitlements in this repo" | `spaps fixtures ...` and a repo-local `.spaps/` kernel |
|
|
29
|
+
| "Can an agent tell me why this action is blocked?" | `spaps access check`, `journey run`, `graph`, `explain`, and `contract` |
|
|
29
30
|
|
|
30
31
|
## No Backend Yet? Start Here
|
|
31
32
|
|
|
@@ -59,6 +60,12 @@ This package targets `Node.js >=22`.
|
|
|
59
60
|
|
|
60
61
|
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.
|
|
61
62
|
|
|
63
|
+
`SPAPS_LOCAL_MODE` is the SPAPS app's canonical local auth/persona switch.
|
|
64
|
+
`DEVELOPMENT_ENVIRONMENT=local` is only for base quickstart services that wire
|
|
65
|
+
`LocalAuthMiddleware`. The CLI reads runtime truth from `/health/local-mode`;
|
|
66
|
+
servers also expose the resolved value in `/health/ready` and the
|
|
67
|
+
`X-SPAPS-Mode` response header.
|
|
68
|
+
|
|
62
69
|
## Local Data Sources
|
|
63
70
|
|
|
64
71
|
`spaps local` separates runtime selection from base data selection:
|
|
@@ -120,14 +127,66 @@ npm install spaps
|
|
|
120
127
|
| `spaps create <name>` | Scaffold a SPAPS starter project directory and try local provisioning | `--template`, `--dir`, `--port`, `--force`, `--json` |
|
|
121
128
|
| `spaps fixtures <subcommand>` | Manage repo-local `.spaps` auth fixtures | `--dir`, `--port`, `--base-url`, `--persona`, `--seed`, `--sync-server`, `--format`, `--force`, `--json` |
|
|
122
129
|
| `spaps docs` | Browse or search bundled docs | `--interactive`, `--search`, `--json` |
|
|
123
|
-
| `spaps tools` | Emit the AI tool spec (covers auth, stripe, dayrate, email, webhooks, policies, billing, issue-reporting) | `--port`, `--format`, `--json` |
|
|
124
|
-
| `spaps doctor` | Diagnose local environment
|
|
130
|
+
| `spaps tools` | Emit the AI tool spec (covers auth-method discovery, auth, stripe, dayrate, email, webhooks, policies, billing, issue-reporting) | `--port`, `--format`, `--json` |
|
|
131
|
+
| `spaps doctor` | Diagnose local environment, domain mounts, and auth-provider configuration | `--port`, `--server-url`, `--origin`, `--stripe`, `--json` |
|
|
132
|
+
| `spaps auth methods` | Print the active app's auth-method matrix from `/api/auth/methods` | `--server-url`, `--port`, `--origin`, `--json` |
|
|
133
|
+
| `spaps auth mfa-test` | Exercise local TOTP MFA enroll, activate, login challenge, verify, and cleanup | `--email`, `--password`, `--server-url`, `--origin`, `--allow-remote`, `--json` |
|
|
134
|
+
| `spaps auth sms-test` | Request or verify a local console SMS OTP challenge | `--phone-number`, `--challenge-id`, `--code`, `--server-url`, `--origin`, `--allow-remote`, `--json` |
|
|
125
135
|
| `spaps dayrate config` | Fetch the active dayrate admin config (requires admin JWT) | `--server-url`, `--port`, `--json` |
|
|
126
136
|
| `spaps billing status\|attach\|verify` | Inspect, bind, and verify the current app's billing-account resolution (admin/operator) | `--billing-account-id`, `--server-url`, `--port`, `--json` |
|
|
127
137
|
| `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 |
|
|
128
138
|
| `spaps policy list\|create\|delete` | List, create, or delete authorization policies (admin) | `--name`, `--effect`, `--conditions`, `--id`, `--is-active`, `--limit`, `--json` |
|
|
129
139
|
| `spaps webhook list\|register` | List registered webhooks or register a new outbound webhook | `--url`, `--events`, `--json` |
|
|
130
140
|
| `spaps issue-reports list-mine` | List issue reports created by the authenticated caller | `--status`, `--limit`, `--offset`, `--json` |
|
|
141
|
+
| `spaps access check` | Compose an access decision and return reasons, facts, next actions, and sources | `--actor-ref`, `--action`, `--resource-type`, `--resource-ref`, `--entitlement-key`, `--policy-name`, `--usage-feature-key`, `--json` |
|
|
142
|
+
| `spaps journey run` | Prepare the next safe action for an agent and request command templates when the server recognizes trusted operator context | access flags plus `--include-command-templates`, `--operator-gated`, `--operator-labels`, `--environment`, `--json` |
|
|
143
|
+
| `spaps graph nodes\|paths\|impact\|refresh` | Inspect or refresh the materialized capability graph for the current app | `--application-id`, `--node-type`, `--query`, `--from`, `--to`, `--node-key`, `--max-depth`, `--include-stale`, `--correlation-id`, `--json` |
|
|
144
|
+
| `spaps explain <decision-id>` | Fetch a persisted decision trace with graph references | `--server-url`, `--port`, `--json` |
|
|
145
|
+
| `spaps contract` | Fetch the capability graph client contract | `--server-url`, `--port`, `--json` |
|
|
146
|
+
|
|
147
|
+
`spaps contract --json` includes graph vocabulary fields for agents:
|
|
148
|
+
`graph_node_types`, `graph_edge_types`, `graph_source_domains`, and
|
|
149
|
+
`source_domain_notes`.
|
|
150
|
+
|
|
151
|
+
### Agent Decision Commands
|
|
152
|
+
|
|
153
|
+
The capability commands use a stable JSON envelope when `--json` is present:
|
|
154
|
+
|
|
155
|
+
```json
|
|
156
|
+
{
|
|
157
|
+
"schema_version": "spaps.cli.capability.v1",
|
|
158
|
+
"command": "access.check",
|
|
159
|
+
"success": true,
|
|
160
|
+
"status": 200,
|
|
161
|
+
"data": {},
|
|
162
|
+
"diagnostics": [],
|
|
163
|
+
"remediations": [],
|
|
164
|
+
"sources": []
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
`access check` exits `0` for a valid denied decision. Agents should inspect
|
|
169
|
+
`data.allowed`, `data.outcome`, and `remediations` instead of treating denial
|
|
170
|
+
as a shell failure.
|
|
171
|
+
|
|
172
|
+
Failed API responses preserve server diagnostics: `diagnostics[].code`,
|
|
173
|
+
`diagnostics[].status`, `diagnostics[].details`, top-level `request_id`, and
|
|
174
|
+
top-level `remediations` are carried through when the server returns them.
|
|
175
|
+
|
|
176
|
+
`--operator-gated` and `--operator-labels` are compatibility/descriptive inputs.
|
|
177
|
+
They do not grant operator authority. Mutation command templates require a
|
|
178
|
+
server-recognized non-publishable key plus an authenticated admin/operator user
|
|
179
|
+
context; publishable callers cannot self-attest the gate.
|
|
180
|
+
|
|
181
|
+
Exit codes used by the capability commands:
|
|
182
|
+
|
|
183
|
+
| Code | Meaning |
|
|
184
|
+
| --- | --- |
|
|
185
|
+
| `0` | Command completed; the decision may still be `allowed: false` |
|
|
186
|
+
| `2` | Local input or setup error |
|
|
187
|
+
| `10` | Authenticated API request failed |
|
|
188
|
+
| `20` | `spaps contract` failed |
|
|
189
|
+
| `30` | `spaps journey run` failed |
|
|
131
190
|
|
|
132
191
|
### Email Commands
|
|
133
192
|
|
|
@@ -186,6 +245,13 @@ spaps policy list --json
|
|
|
186
245
|
spaps policy create --name allow_admin --effect allow --conditions '{"role":"admin"}' --json
|
|
187
246
|
spaps webhook register --url https://example.com/hook --events user.created,user.deleted --json
|
|
188
247
|
spaps issue-reports list-mine --status open --json
|
|
248
|
+
spaps access check --actor-ref user_123 --action checkout.create --resource-type product --resource-ref dayrate --entitlement-key bookme_paid --json
|
|
249
|
+
spaps journey run --action admin.delete_user --resource-type user --resource-ref user_123 --include-command-templates --json
|
|
250
|
+
spaps graph refresh --correlation-id local-refresh --json
|
|
251
|
+
spaps graph nodes --node-type x402_resource --query dayrate --json
|
|
252
|
+
spaps graph paths --from actor:user_123 --to x402_resource:dayrate --json
|
|
253
|
+
spaps explain 11111111-1111-4111-8111-111111111111 --json
|
|
254
|
+
spaps contract --json
|
|
189
255
|
```
|
|
190
256
|
|
|
191
257
|
## CLI Auth
|
|
@@ -196,6 +262,12 @@ spaps issue-reports list-mine --status open --json
|
|
|
196
262
|
- authenticated follow-up calls at `/api/auth/*`
|
|
197
263
|
- standard SPAPS response envelopes
|
|
198
264
|
|
|
265
|
+
For users with activated TOTP MFA, `spaps connect` keeps polling the device
|
|
266
|
+
token endpoint while the browser approval page completes the second factor. The
|
|
267
|
+
approval page handles `mfa_required` from `/api/cli/device/verify` by retrying
|
|
268
|
+
that endpoint with `challenge_id`, `challenge`, and either `totp_code` or
|
|
269
|
+
`recovery_code`; the CLI does not need an MFA flag.
|
|
270
|
+
|
|
199
271
|
The CLI no longer assumes a built-in `spaps-cli` application slug. Resolve the client id in this order:
|
|
200
272
|
|
|
201
273
|
- `--client-id <app-slug>`
|
|
@@ -221,6 +293,21 @@ SPAPS_CLI_CLIENT_ID=my-app npx spaps login
|
|
|
221
293
|
VITE_SPAPS_API_KEY=spaps_pub_demo npx spaps whoami --json
|
|
222
294
|
```
|
|
223
295
|
|
|
296
|
+
Auth diagnostics:
|
|
297
|
+
|
|
298
|
+
```bash
|
|
299
|
+
npx spaps doctor --server-url http://localhost:3301 --origin http://localhost:3000 --json
|
|
300
|
+
npx spaps auth methods --origin http://localhost:3000 --json
|
|
301
|
+
SPAPS_TEST_EMAIL=user@example.com SPAPS_TEST_PASSWORD=correct-horse npx spaps auth mfa-test --json
|
|
302
|
+
npx spaps auth sms-test --phone-number +15555550100 --json
|
|
303
|
+
npx spaps auth sms-test --phone-number +15555550100 --challenge-id <id> --code <code> --json
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
`mfa-test` and `sms-test` refuse non-local server URLs unless `--allow-remote`
|
|
307
|
+
is set. The SMS command cannot read OTP digits from the server because SPAPS
|
|
308
|
+
does not return them in API responses; with the console provider, read the code
|
|
309
|
+
from local server logs and rerun with `--challenge-id` and `--code`.
|
|
310
|
+
|
|
224
311
|
Still reserved and not finished:
|
|
225
312
|
|
|
226
313
|
- `spaps types`
|
package/client.js
CHANGED
|
@@ -68,6 +68,10 @@ try {
|
|
|
68
68
|
headers: { Authorization: `Bearer ${this.accessToken}` }
|
|
69
69
|
});
|
|
70
70
|
}
|
|
71
|
+
|
|
72
|
+
async getAuthMethods() {
|
|
73
|
+
return this.client.get('/api/auth/methods');
|
|
74
|
+
}
|
|
71
75
|
|
|
72
76
|
async createCheckoutSession(priceId, successUrl, cancelUrl) {
|
|
73
77
|
return this.client.post('/api/stripe/checkout-sessions',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spaps",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.1",
|
|
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": {
|
package/src/ai-tool-spec.js
CHANGED
|
@@ -75,6 +75,87 @@ function buildAuthSection(runtime) {
|
|
|
75
75
|
};
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
const CORE_TOOL_ROUTES = [
|
|
79
|
+
{ name: 'login', method: 'POST', path: '/api/auth/login', requestBody: true },
|
|
80
|
+
{ name: 'register', method: 'POST', path: '/api/auth/register', requestBody: true },
|
|
81
|
+
{ name: 'auth_methods', method: 'GET', path: '/api/auth/methods' },
|
|
82
|
+
{ name: 'get_current_user', method: 'GET', path: '/api/auth/user' },
|
|
83
|
+
{ name: 'create_checkout_session', method: 'POST', path: '/api/stripe/checkout-sessions', requestBody: true },
|
|
84
|
+
{ name: 'list_products', method: 'GET', path: '/api/stripe/products' },
|
|
85
|
+
{ name: 'request_magic_link', method: 'POST', path: '/api/auth/magic-link', requestBody: true },
|
|
86
|
+
{ name: 'get_wallet_nonce', method: 'POST', path: '/api/auth/nonce', requestBody: true },
|
|
87
|
+
{ name: 'wallet_sign_in', method: 'POST', path: '/api/auth/wallet-sign-in', requestBody: true },
|
|
88
|
+
{ name: 'verify_magic_link', method: 'POST', path: '/api/auth/verify-magic-link', requestBody: true },
|
|
89
|
+
{ name: 'oidc_nonce', method: 'POST', path: '/api/auth/oidc/nonce', requestBody: true },
|
|
90
|
+
{ name: 'oidc_sign_in', method: 'POST', path: '/api/auth/oidc/sign-in', requestBody: true },
|
|
91
|
+
{ name: 'sms_request', method: 'POST', path: '/api/auth/sms/request', requestBody: true },
|
|
92
|
+
{ name: 'sms_verify', method: 'POST', path: '/api/auth/sms/verify', requestBody: true },
|
|
93
|
+
{ name: 'webauthn_assertion_options', method: 'POST', path: '/api/auth/webauthn/assertion/options', requestBody: true },
|
|
94
|
+
{ name: 'webauthn_assertion_verify', method: 'POST', path: '/api/auth/webauthn/assertion/verify', requestBody: true },
|
|
95
|
+
{ name: 'webauthn_register_options', method: 'POST', path: '/api/auth/webauthn/register/options', requestBody: true },
|
|
96
|
+
{ name: 'webauthn_register_verify', method: 'POST', path: '/api/auth/webauthn/register/verify', requestBody: true },
|
|
97
|
+
{ name: 'mfa_verify', method: 'POST', path: '/api/auth/mfa/verify', requestBody: true },
|
|
98
|
+
{ name: 'mfa_totp_enroll', method: 'POST', path: '/api/auth/mfa/totp/enroll', requestBody: true },
|
|
99
|
+
{ name: 'mfa_totp_activate', method: 'POST', path: '/api/auth/mfa/totp/activate', requestBody: true },
|
|
100
|
+
{ name: 'mfa_totp_disable', method: 'POST', path: '/api/auth/mfa/totp/disable', requestBody: true },
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
const CLI_AUTH_TOOLS = [
|
|
104
|
+
{
|
|
105
|
+
name: 'spaps_auth_methods',
|
|
106
|
+
description: 'Run `spaps auth methods` to print the configured auth method matrix for an application.',
|
|
107
|
+
command: 'spaps auth methods',
|
|
108
|
+
cli: true,
|
|
109
|
+
parameters: {
|
|
110
|
+
type: 'object',
|
|
111
|
+
properties: {
|
|
112
|
+
server_url: { type: 'string', description: 'SPAPS server URL; maps to --server-url' },
|
|
113
|
+
origin: { type: 'string', description: 'Browser origin to send with publishable-key checks; maps to --origin' },
|
|
114
|
+
json: { type: 'boolean', default: true },
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
name: 'spaps_auth_mfa_test',
|
|
120
|
+
description: 'Run `spaps auth mfa-test` against a local SPAPS stack to exercise TOTP enrollment, activation, MFA challenge verification, and cleanup.',
|
|
121
|
+
command: 'spaps auth mfa-test',
|
|
122
|
+
cli: true,
|
|
123
|
+
local_only_by_default: true,
|
|
124
|
+
parameters: {
|
|
125
|
+
type: 'object',
|
|
126
|
+
required: ['email', 'password'],
|
|
127
|
+
properties: {
|
|
128
|
+
email: { type: 'string', description: 'Local test user email; maps to --email' },
|
|
129
|
+
password: { type: 'string', description: 'Local test user password; maps to --password' },
|
|
130
|
+
server_url: { type: 'string', description: 'SPAPS server URL; maps to --server-url' },
|
|
131
|
+
origin: { type: 'string', description: 'Browser origin to send with publishable-key checks; maps to --origin' },
|
|
132
|
+
allow_remote: { type: 'boolean', default: false, description: 'Maps to --allow-remote' },
|
|
133
|
+
json: { type: 'boolean', default: true },
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
name: 'spaps_auth_sms_test',
|
|
139
|
+
description: 'Run `spaps auth sms-test` against local console SMS to request a challenge, then verify it with a code read from local server logs.',
|
|
140
|
+
command: 'spaps auth sms-test',
|
|
141
|
+
cli: true,
|
|
142
|
+
local_only_by_default: true,
|
|
143
|
+
parameters: {
|
|
144
|
+
type: 'object',
|
|
145
|
+
required: ['phone_number'],
|
|
146
|
+
properties: {
|
|
147
|
+
phone_number: { type: 'string', description: 'E.164 phone number; maps to --phone-number' },
|
|
148
|
+
challenge_id: { type: 'string', description: 'Existing SMS challenge id; maps to --challenge-id' },
|
|
149
|
+
code: { type: 'string', description: 'SMS code from local server logs; maps to --code' },
|
|
150
|
+
server_url: { type: 'string', description: 'SPAPS server URL; maps to --server-url' },
|
|
151
|
+
origin: { type: 'string', description: 'Browser origin to send with publishable-key checks; maps to --origin' },
|
|
152
|
+
allow_remote: { type: 'boolean', default: false, description: 'Maps to --allow-remote' },
|
|
153
|
+
json: { type: 'boolean', default: true },
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
];
|
|
158
|
+
|
|
78
159
|
async function buildOpenAIToolSpec({ port = DEFAULT_PORT, version = '0.0.0', include_non_agent = false } = {}) {
|
|
79
160
|
const baseUrl = `http://localhost:${port}`;
|
|
80
161
|
const runtime = await getServerRuntime({ port });
|
|
@@ -118,6 +199,16 @@ async function buildOpenAIToolSpec({ port = DEFAULT_PORT, version = '0.0.0', inc
|
|
|
118
199
|
}
|
|
119
200
|
}
|
|
120
201
|
},
|
|
202
|
+
{
|
|
203
|
+
name: 'auth_methods',
|
|
204
|
+
description: 'Discover the public auth methods configured for this application before rendering sign-in UI.',
|
|
205
|
+
method: 'GET',
|
|
206
|
+
path: '/api/auth/methods',
|
|
207
|
+
parameters: {
|
|
208
|
+
type: 'object',
|
|
209
|
+
properties: {}
|
|
210
|
+
}
|
|
211
|
+
},
|
|
121
212
|
{
|
|
122
213
|
name: 'get_current_user',
|
|
123
214
|
description: 'Get the currently authenticated user. Uses the bearer token from a previous login.',
|
|
@@ -187,6 +278,185 @@ async function buildOpenAIToolSpec({ port = DEFAULT_PORT, version = '0.0.0', inc
|
|
|
187
278
|
chain_type: { type: 'string', enum: ['solana', 'ethereum', 'bitcoin', 'base'] }
|
|
188
279
|
}
|
|
189
280
|
}
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
name: 'wallet_sign_in',
|
|
284
|
+
description: 'Exchange a signed wallet nonce for SPAPS tokens or an mfa_required challenge.',
|
|
285
|
+
method: 'POST',
|
|
286
|
+
path: '/api/auth/wallet-sign-in',
|
|
287
|
+
parameters: {
|
|
288
|
+
type: 'object',
|
|
289
|
+
required: ['wallet_address', 'signature', 'message'],
|
|
290
|
+
properties: {
|
|
291
|
+
wallet_address: { type: 'string' },
|
|
292
|
+
signature: { type: 'string' },
|
|
293
|
+
message: { type: 'string' },
|
|
294
|
+
chain_type: { type: 'string', enum: ['solana', 'ethereum', 'bitcoin', 'base'] },
|
|
295
|
+
username: { type: 'string' }
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
name: 'verify_magic_link',
|
|
301
|
+
description: 'Exchange a magic-link token for SPAPS tokens or an mfa_required challenge.',
|
|
302
|
+
method: 'POST',
|
|
303
|
+
path: '/api/auth/verify-magic-link',
|
|
304
|
+
parameters: {
|
|
305
|
+
type: 'object',
|
|
306
|
+
required: ['token'],
|
|
307
|
+
properties: {
|
|
308
|
+
token: { type: 'string' },
|
|
309
|
+
type: { type: 'string', default: 'magiclink' },
|
|
310
|
+
state: { type: 'string' }
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
},
|
|
314
|
+
{
|
|
315
|
+
name: 'oidc_nonce',
|
|
316
|
+
description: 'Create a nonce challenge before exchanging an OIDC provider ID token.',
|
|
317
|
+
method: 'POST',
|
|
318
|
+
path: '/api/auth/oidc/nonce',
|
|
319
|
+
parameters: { type: 'object', properties: {} }
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
name: 'oidc_sign_in',
|
|
323
|
+
description: 'Exchange an OIDC ID token and challenge_id for SPAPS tokens or an mfa_required challenge.',
|
|
324
|
+
method: 'POST',
|
|
325
|
+
path: '/api/auth/oidc/sign-in',
|
|
326
|
+
parameters: {
|
|
327
|
+
type: 'object',
|
|
328
|
+
required: ['provider', 'id_token', 'challenge_id'],
|
|
329
|
+
properties: {
|
|
330
|
+
provider: { type: 'string' },
|
|
331
|
+
id_token: { type: 'string' },
|
|
332
|
+
challenge_id: { type: 'string' },
|
|
333
|
+
username: { type: 'string' }
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
name: 'sms_request',
|
|
339
|
+
description: 'Request an enumeration-safe SMS OTP challenge.',
|
|
340
|
+
method: 'POST',
|
|
341
|
+
path: '/api/auth/sms/request',
|
|
342
|
+
parameters: {
|
|
343
|
+
type: 'object',
|
|
344
|
+
required: ['phone_number'],
|
|
345
|
+
properties: {
|
|
346
|
+
phone_number: { type: 'string' }
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
name: 'sms_verify',
|
|
352
|
+
description: 'Verify an SMS OTP challenge for SPAPS tokens or an mfa_required challenge.',
|
|
353
|
+
method: 'POST',
|
|
354
|
+
path: '/api/auth/sms/verify',
|
|
355
|
+
parameters: {
|
|
356
|
+
type: 'object',
|
|
357
|
+
required: ['phone_number', 'code', 'challenge_id'],
|
|
358
|
+
properties: {
|
|
359
|
+
phone_number: { type: 'string' },
|
|
360
|
+
code: { type: 'string' },
|
|
361
|
+
challenge_id: { type: 'string' }
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
},
|
|
365
|
+
{
|
|
366
|
+
name: 'webauthn_assertion_options',
|
|
367
|
+
description: 'Request WebAuthn assertion options before navigator.credentials.get().',
|
|
368
|
+
method: 'POST',
|
|
369
|
+
path: '/api/auth/webauthn/assertion/options',
|
|
370
|
+
parameters: {
|
|
371
|
+
type: 'object',
|
|
372
|
+
properties: {
|
|
373
|
+
email: { type: 'string' }
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
},
|
|
377
|
+
{
|
|
378
|
+
name: 'webauthn_assertion_verify',
|
|
379
|
+
description: 'Verify a browser WebAuthn assertion for SPAPS tokens or an mfa_required challenge.',
|
|
380
|
+
method: 'POST',
|
|
381
|
+
path: '/api/auth/webauthn/assertion/verify',
|
|
382
|
+
parameters: {
|
|
383
|
+
type: 'object',
|
|
384
|
+
required: ['challenge_id', 'credential'],
|
|
385
|
+
properties: {
|
|
386
|
+
challenge_id: { type: 'string' },
|
|
387
|
+
credential: { type: 'object' }
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
},
|
|
391
|
+
{
|
|
392
|
+
name: 'webauthn_register_options',
|
|
393
|
+
description: 'Request WebAuthn passkey registration options for an authenticated user.',
|
|
394
|
+
method: 'POST',
|
|
395
|
+
path: '/api/auth/webauthn/register/options',
|
|
396
|
+
parameters: { type: 'object', properties: {} }
|
|
397
|
+
},
|
|
398
|
+
{
|
|
399
|
+
name: 'webauthn_register_verify',
|
|
400
|
+
description: 'Verify browser WebAuthn registration output for an authenticated user.',
|
|
401
|
+
method: 'POST',
|
|
402
|
+
path: '/api/auth/webauthn/register/verify',
|
|
403
|
+
parameters: {
|
|
404
|
+
type: 'object',
|
|
405
|
+
required: ['challenge_id', 'credential'],
|
|
406
|
+
properties: {
|
|
407
|
+
challenge_id: { type: 'string' },
|
|
408
|
+
credential: { type: 'object' }
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
},
|
|
412
|
+
{
|
|
413
|
+
name: 'mfa_verify',
|
|
414
|
+
description: 'Complete an mfa_required challenge with a TOTP code or recovery code.',
|
|
415
|
+
method: 'POST',
|
|
416
|
+
path: '/api/auth/mfa/verify',
|
|
417
|
+
parameters: {
|
|
418
|
+
type: 'object',
|
|
419
|
+
required: ['challenge_id', 'challenge'],
|
|
420
|
+
properties: {
|
|
421
|
+
challenge_id: { type: 'string' },
|
|
422
|
+
challenge: { type: 'string' },
|
|
423
|
+
code: { type: 'string' },
|
|
424
|
+
recovery_code: { type: 'string' }
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
},
|
|
428
|
+
{
|
|
429
|
+
name: 'mfa_totp_enroll',
|
|
430
|
+
description: 'Enroll TOTP MFA for an authenticated user.',
|
|
431
|
+
method: 'POST',
|
|
432
|
+
path: '/api/auth/mfa/totp/enroll',
|
|
433
|
+
parameters: { type: 'object', properties: {} }
|
|
434
|
+
},
|
|
435
|
+
{
|
|
436
|
+
name: 'mfa_totp_activate',
|
|
437
|
+
description: 'Activate pending TOTP MFA enrollment with a current code.',
|
|
438
|
+
method: 'POST',
|
|
439
|
+
path: '/api/auth/mfa/totp/activate',
|
|
440
|
+
parameters: {
|
|
441
|
+
type: 'object',
|
|
442
|
+
required: ['code'],
|
|
443
|
+
properties: {
|
|
444
|
+
code: { type: 'string' }
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
},
|
|
448
|
+
{
|
|
449
|
+
name: 'mfa_totp_disable',
|
|
450
|
+
description: 'Disable TOTP MFA for an authenticated user with a current code.',
|
|
451
|
+
method: 'POST',
|
|
452
|
+
path: '/api/auth/mfa/totp/disable',
|
|
453
|
+
parameters: {
|
|
454
|
+
type: 'object',
|
|
455
|
+
required: ['code'],
|
|
456
|
+
properties: {
|
|
457
|
+
code: { type: 'string' }
|
|
458
|
+
}
|
|
459
|
+
}
|
|
190
460
|
}
|
|
191
461
|
]
|
|
192
462
|
};
|
|
@@ -194,6 +464,7 @@ async function buildOpenAIToolSpec({ port = DEFAULT_PORT, version = '0.0.0', inc
|
|
|
194
464
|
// Merge domain registry tools (dayrate, email, webhooks, policies, issue_reporting).
|
|
195
465
|
// Admin routes carry admin_required:true; non-agent routes (e.g. mailgun inbound)
|
|
196
466
|
// are excluded unless include_non_agent is set.
|
|
467
|
+
spec.tools.push(...CLI_AUTH_TOOLS.map((tool) => ({ ...tool })));
|
|
197
468
|
const domainTools = buildDomainTools({ includeNonAgent: include_non_agent });
|
|
198
469
|
for (const dt of domainTools) {
|
|
199
470
|
spec.tools.push(dt);
|
|
@@ -242,15 +513,15 @@ async function buildOpenAIToolSpec({ port = DEFAULT_PORT, version = '0.0.0', inc
|
|
|
242
513
|
if (t && ep) {
|
|
243
514
|
t.method = ep.method;
|
|
244
515
|
t.path = ep.path;
|
|
516
|
+
if (ep.auth !== undefined) t.auth = ep.auth;
|
|
517
|
+
if (ep.rate_tier !== undefined) t.rate_tier = ep.rate_tier;
|
|
518
|
+
if (ep.publishable_scope !== undefined) t.publishable_scope = ep.publishable_scope;
|
|
519
|
+
if (ep.entitlement_key !== undefined) t.entitlement_key = ep.entitlement_key;
|
|
245
520
|
}
|
|
246
521
|
};
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
patchTool('create_checkout_session', 'POST', '/api/stripe/checkout-sessions');
|
|
251
|
-
patchTool('list_products', 'GET', '/api/stripe/products');
|
|
252
|
-
patchTool('request_magic_link', 'POST', '/api/auth/magic-link');
|
|
253
|
-
patchTool('get_wallet_nonce', 'POST', '/api/auth/nonce');
|
|
522
|
+
for (const route of CORE_TOOL_ROUTES) {
|
|
523
|
+
patchTool(route.name, route.method, route.path);
|
|
524
|
+
}
|
|
254
525
|
}
|
|
255
526
|
} catch {
|
|
256
527
|
// Best-effort alignment only
|
|
@@ -302,18 +573,10 @@ async function buildOpenAIToolSpec({ port = DEFAULT_PORT, version = '0.0.0', inc
|
|
|
302
573
|
if (Object.keys(examples).length) t.examples = examples;
|
|
303
574
|
}
|
|
304
575
|
};
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
setBodySchema('get_wallet_nonce', 'POST', '/api/auth/nonce');
|
|
310
|
-
setResponses('login', 'POST', '/api/auth/login');
|
|
311
|
-
setResponses('register', 'POST', '/api/auth/register');
|
|
312
|
-
setResponses('get_current_user', 'GET', '/api/auth/user');
|
|
313
|
-
setResponses('create_checkout_session', 'POST', '/api/stripe/checkout-sessions');
|
|
314
|
-
setResponses('list_products', 'GET', '/api/stripe/products');
|
|
315
|
-
setResponses('request_magic_link', 'POST', '/api/auth/magic-link');
|
|
316
|
-
setResponses('get_wallet_nonce', 'POST', '/api/auth/nonce');
|
|
576
|
+
for (const route of CORE_TOOL_ROUTES) {
|
|
577
|
+
if (route.requestBody) setBodySchema(route.name, route.method, route.path);
|
|
578
|
+
setResponses(route.name, route.method, route.path);
|
|
579
|
+
}
|
|
317
580
|
// Enrich every domain-registry tool opportunistically. Skips silently
|
|
318
581
|
// if the operation isn\u2019t in the OpenAPI doc.
|
|
319
582
|
for (const t of spec.tools) {
|
package/src/auth/handlers.js
CHANGED
|
@@ -21,6 +21,11 @@ const {
|
|
|
21
21
|
const { isHeadless, tryOpenBrowser } = require('./env');
|
|
22
22
|
const { authFetch, resolveServerUrl, refreshAccessToken } = require('./client');
|
|
23
23
|
const { resolveLoginClientId } = require('./client-id');
|
|
24
|
+
const {
|
|
25
|
+
fetchAuthMethods,
|
|
26
|
+
runMfaTest,
|
|
27
|
+
runSmsTest,
|
|
28
|
+
} = require('./surface');
|
|
24
29
|
|
|
25
30
|
function emitJsonError(command, err, extra = {}) {
|
|
26
31
|
console.log(
|
|
@@ -454,7 +459,111 @@ async function tokenHandler({ options }) {
|
|
|
454
459
|
process.stdout.write(creds.access_token + '\n');
|
|
455
460
|
}
|
|
456
461
|
|
|
462
|
+
function renderMethods(methods) {
|
|
463
|
+
console.log();
|
|
464
|
+
console.log(chalk.bold('Auth methods'));
|
|
465
|
+
for (const method of methods) {
|
|
466
|
+
const status = method.enabled ? chalk.green('enabled') : chalk.gray('disabled');
|
|
467
|
+
const config = method.config && Object.keys(method.config).length
|
|
468
|
+
? ` ${chalk.gray(JSON.stringify(method.config))}`
|
|
469
|
+
: '';
|
|
470
|
+
console.log(` ${method.method}: ${status}${config}`);
|
|
471
|
+
}
|
|
472
|
+
console.log();
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function renderProbeResult(result) {
|
|
476
|
+
console.log();
|
|
477
|
+
console.log(chalk.green(`✓ ${result.command} completed`));
|
|
478
|
+
if (Array.isArray(result.steps)) {
|
|
479
|
+
for (const step of result.steps) {
|
|
480
|
+
const mark = step.success ? chalk.green('✓') : chalk.yellow('!');
|
|
481
|
+
console.log(` ${mark} ${step.name}`);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
if (result.verification_required) {
|
|
485
|
+
console.log(chalk.yellow(' verification required'));
|
|
486
|
+
console.log(chalk.gray(` challenge_id: ${result.challenge_id}`));
|
|
487
|
+
console.log(chalk.gray(` next: ${result.next_step}`));
|
|
488
|
+
}
|
|
489
|
+
console.log();
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
async function authHandler({ options }) {
|
|
493
|
+
const isJson = Boolean(options.json);
|
|
494
|
+
const serverUrl = resolveServerUrl(options);
|
|
495
|
+
const command = `auth.${options.subcommand || 'unknown'}`;
|
|
496
|
+
|
|
497
|
+
try {
|
|
498
|
+
if (options.subcommand === 'methods') {
|
|
499
|
+
const result = await fetchAuthMethods({
|
|
500
|
+
serverUrl,
|
|
501
|
+
origin: options.origin || null,
|
|
502
|
+
});
|
|
503
|
+
const payload = {
|
|
504
|
+
success: true,
|
|
505
|
+
command: 'auth.methods',
|
|
506
|
+
server_url: serverUrl,
|
|
507
|
+
origin: result.origin,
|
|
508
|
+
api_key_source: result.apiKeySource,
|
|
509
|
+
methods: result.methods,
|
|
510
|
+
};
|
|
511
|
+
if (isJson) {
|
|
512
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
513
|
+
} else {
|
|
514
|
+
renderMethods(result.methods);
|
|
515
|
+
}
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
if (options.subcommand === 'mfa-test') {
|
|
520
|
+
const result = await runMfaTest({
|
|
521
|
+
serverUrl,
|
|
522
|
+
origin: options.origin || null,
|
|
523
|
+
email: options.email || process.env.SPAPS_TEST_EMAIL || null,
|
|
524
|
+
password: options.password || process.env.SPAPS_TEST_PASSWORD || null,
|
|
525
|
+
allowRemote: Boolean(options.allowRemote),
|
|
526
|
+
});
|
|
527
|
+
if (isJson) {
|
|
528
|
+
console.log(JSON.stringify(result, null, 2));
|
|
529
|
+
} else {
|
|
530
|
+
renderProbeResult(result);
|
|
531
|
+
}
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
if (options.subcommand === 'sms-test') {
|
|
536
|
+
const result = await runSmsTest({
|
|
537
|
+
serverUrl,
|
|
538
|
+
origin: options.origin || null,
|
|
539
|
+
phoneNumber: options.phoneNumber || process.env.SPAPS_TEST_PHONE_NUMBER || null,
|
|
540
|
+
challengeId: options.challengeId || null,
|
|
541
|
+
code: options.code || null,
|
|
542
|
+
allowRemote: Boolean(options.allowRemote),
|
|
543
|
+
});
|
|
544
|
+
if (isJson) {
|
|
545
|
+
console.log(JSON.stringify(result, null, 2));
|
|
546
|
+
} else {
|
|
547
|
+
renderProbeResult(result);
|
|
548
|
+
}
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const err = new Error('Unknown auth subcommand. Use `spaps auth --help`.');
|
|
553
|
+
err.code = 'UNKNOWN_AUTH_SUBCOMMAND';
|
|
554
|
+
throw err;
|
|
555
|
+
} catch (err) {
|
|
556
|
+
if (isJson) {
|
|
557
|
+
emitJsonError(command, err, { server_url: serverUrl });
|
|
558
|
+
} else {
|
|
559
|
+
console.error(chalk.red(`\n❌ ${err.message || String(err)}`));
|
|
560
|
+
}
|
|
561
|
+
process.exit(err.code === 'MISSING_ARGUMENT' || err.code === 'REMOTE_REFUSED' ? 2 : 1);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
457
565
|
module.exports = {
|
|
566
|
+
authHandler,
|
|
458
567
|
loginHandler,
|
|
459
568
|
logoutHandler,
|
|
460
569
|
whoamiHandler,
|