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/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 problems and probe that every domain router is mounted | `--port`, `--stripe`, `--json` |
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.0",
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": {
@@ -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
- patchTool('login', 'POST', '/api/auth/login');
248
- patchTool('register', 'POST', '/api/auth/register');
249
- patchTool('get_current_user', 'GET', '/api/auth/user');
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
- setBodySchema('login', 'POST', '/api/auth/login');
306
- setBodySchema('register', 'POST', '/api/auth/register');
307
- setBodySchema('create_checkout_session', 'POST', '/api/stripe/checkout-sessions');
308
- setBodySchema('request_magic_link', 'POST', '/api/auth/magic-link');
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) {
@@ -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,