spaps 0.8.0 → 0.8.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 +20 -2
- package/client.js +18 -7
- package/package.json +1 -1
- package/src/cli-dispatcher.js +21 -0
- package/src/docs-html.js +3 -3
- package/src/docs-quick.js +1 -1
- package/src/docs-system.js +2 -2
- package/src/domains.js +19 -4
- package/src/fixture-kernel.js +347 -21
- package/src/handlers.js +60 -0
- package/src/help-system.js +1 -1
package/README.md
CHANGED
|
@@ -118,11 +118,12 @@ npm install spaps
|
|
|
118
118
|
| `spaps quickstart` | Print quick-start instructions | `--port`, `--json` |
|
|
119
119
|
| `spaps init` | Create a starter `.env.local` | `--json` |
|
|
120
120
|
| `spaps create <name>` | Scaffold a SPAPS starter project directory and try local provisioning | `--template`, `--dir`, `--port`, `--force`, `--json` |
|
|
121
|
-
| `spaps fixtures <subcommand>` | Manage repo-local `.spaps` auth fixtures | `--dir`, `--port`, `--base-url`, `--persona`, `--seed`, `--format`, `--force`, `--json` |
|
|
121
|
+
| `spaps fixtures <subcommand>` | Manage repo-local `.spaps` auth fixtures | `--dir`, `--port`, `--base-url`, `--persona`, `--seed`, `--sync-server`, `--format`, `--force`, `--json` |
|
|
122
122
|
| `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, issue-reporting) | `--port`, `--format`, `--json` |
|
|
123
|
+
| `spaps tools` | Emit the AI tool spec (covers auth, stripe, dayrate, email, webhooks, policies, billing, issue-reporting) | `--port`, `--format`, `--json` |
|
|
124
124
|
| `spaps doctor` | Diagnose local environment problems and probe that every domain router is mounted | `--port`, `--stripe`, `--json` |
|
|
125
125
|
| `spaps dayrate config` | Fetch the active dayrate admin config (requires admin JWT) | `--server-url`, `--port`, `--json` |
|
|
126
|
+
| `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` |
|
|
126
127
|
| `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 |
|
|
127
128
|
| `spaps policy list\|create\|delete` | List, create, or delete authorization policies (admin) | `--name`, `--effect`, `--conditions`, `--id`, `--is-active`, `--limit`, `--json` |
|
|
128
129
|
| `spaps webhook list\|register` | List registered webhooks or register a new outbound webhook | `--url`, `--events`, `--json` |
|
|
@@ -165,12 +166,16 @@ spaps verify --json
|
|
|
165
166
|
spaps create my-app --template react
|
|
166
167
|
spaps connect --json
|
|
167
168
|
spaps fixtures apply --base-url http://localhost:5173
|
|
169
|
+
spaps fixtures apply --sync-server --base-url http://localhost:5173
|
|
168
170
|
spaps fixtures apply --seed --persona dayrate-entitled --base-url http://localhost:5173
|
|
169
171
|
spaps fixtures storage-state --persona admin
|
|
170
172
|
spaps docs --search secure-messages
|
|
171
173
|
spaps doctor --stripe mock
|
|
172
174
|
spaps local stop
|
|
173
175
|
spaps dayrate config --json
|
|
176
|
+
spaps billing status --json
|
|
177
|
+
spaps billing attach --billing-account-id 11111111-1111-4111-8111-111111111111 --json
|
|
178
|
+
spaps billing verify --json
|
|
174
179
|
spaps email --help
|
|
175
180
|
spaps email send --help
|
|
176
181
|
spaps email send --template-key welcome --to user@example.com --context '{"user_name":"Jane"}' --json
|
|
@@ -268,10 +273,23 @@ Then run:
|
|
|
268
273
|
|
|
269
274
|
```bash
|
|
270
275
|
npx spaps fixtures apply --base-url http://localhost:5173
|
|
276
|
+
npx spaps fixtures apply --sync-server --base-url http://localhost:5173
|
|
271
277
|
npx spaps fixtures apply --seed --persona issue-reporter-blocked --base-url http://localhost:5173
|
|
272
278
|
npx spaps fixtures storage-state --persona admin
|
|
273
279
|
```
|
|
274
280
|
|
|
281
|
+
Use `--sync-server` when a local app needs real SPAPS DB state. It calls the
|
|
282
|
+
local-only `/api/dev/fixtures/apply` endpoint to upsert fixture users,
|
|
283
|
+
memberships, and entitlements, then writes ownership metadata to
|
|
284
|
+
`.spaps/fixtures.lock.json`. It refuses non-local targets and only runs when
|
|
285
|
+
`/health/local-mode` reports local mode active.
|
|
286
|
+
|
|
287
|
+
The default fixture pack includes shared personas (`user`, `admin`, `premium`),
|
|
288
|
+
CFO-style `cfo-billing-demo` company entitlements, and HTMA-style
|
|
289
|
+
`htma-local-auth` invite/template seed state. Browser artifacts keep
|
|
290
|
+
entitlements as plain keys, while `--sync-server` can send structured grants
|
|
291
|
+
with resource scope metadata to SPAPS.
|
|
292
|
+
|
|
275
293
|
Use `--seed` when a persona declares backend seed steps in `.spaps/users.json`. Seeding is opt-in, only runs against a reachable local-mode SPAPS server, and reuses the persona selectors already defined in the fixture kernel instead of adding fixture-only backend routes.
|
|
276
294
|
|
|
277
295
|
What `apply` emits:
|
package/client.js
CHANGED
|
@@ -70,14 +70,22 @@ try {
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
async createCheckoutSession(priceId, successUrl, cancelUrl) {
|
|
73
|
-
return this.client.post('/api/stripe/
|
|
74
|
-
{
|
|
73
|
+
return this.client.post('/api/stripe/checkout-sessions',
|
|
74
|
+
{
|
|
75
|
+
mode: 'payment',
|
|
76
|
+
line_items: [{ price_id: priceId, quantity: 1 }],
|
|
77
|
+
success_url: successUrl,
|
|
78
|
+
cancel_url: cancelUrl || successUrl
|
|
79
|
+
},
|
|
75
80
|
{ headers: { Authorization: `Bearer ${this.accessToken}` } }
|
|
76
81
|
);
|
|
77
82
|
}
|
|
78
83
|
|
|
79
|
-
async getSubscription() {
|
|
80
|
-
|
|
84
|
+
async getSubscription(subscriptionId) {
|
|
85
|
+
const path = subscriptionId
|
|
86
|
+
? `/api/stripe/subscription/${encodeURIComponent(subscriptionId)}`
|
|
87
|
+
: '/api/stripe/subscriptions';
|
|
88
|
+
return this.client.get(path, {
|
|
81
89
|
headers: { Authorization: `Bearer ${this.accessToken}` }
|
|
82
90
|
});
|
|
83
91
|
}
|
|
@@ -132,8 +140,11 @@ try {
|
|
|
132
140
|
);
|
|
133
141
|
}
|
|
134
142
|
|
|
135
|
-
async cancelSubscription() {
|
|
136
|
-
|
|
143
|
+
async cancelSubscription(subscriptionId, options = {}) {
|
|
144
|
+
if (!subscriptionId) {
|
|
145
|
+
throw new Error('subscriptionId is required to cancel a subscription.');
|
|
146
|
+
}
|
|
147
|
+
return this.client.post(`/api/stripe/subscription/${encodeURIComponent(subscriptionId)}/cancel`, options, {
|
|
137
148
|
headers: { Authorization: `Bearer ${this.accessToken}` }
|
|
138
149
|
});
|
|
139
150
|
}
|
|
@@ -144,4 +155,4 @@ try {
|
|
|
144
155
|
module.exports.default = SPAPSClient;
|
|
145
156
|
module.exports.SPAPS = SPAPSClient;
|
|
146
157
|
module.exports.SweetPotatoSDK = SPAPSClient;
|
|
147
|
-
}
|
|
158
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spaps",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.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/cli-dispatcher.js
CHANGED
|
@@ -229,6 +229,7 @@ function defineProgram({ handlers = {}, dryRun = false, version = '0.0.0', logo
|
|
|
229
229
|
.option('--base-url <url>', 'Browser app base URL for generated storage-state files')
|
|
230
230
|
.option('--persona <persona>', 'Persona code to target for apply or storage-state export')
|
|
231
231
|
.option('--seed', 'Run persona-declared seed requests against the local SPAPS server', false)
|
|
232
|
+
.option('--sync-server', 'Reconcile fixture users, memberships, and entitlements into a local SPAPS server', false)
|
|
232
233
|
.option('-f, --format <format>', 'Artifact format (playwright)', 'playwright')
|
|
233
234
|
.option('--force', 'Overwrite fixture files during init', false)
|
|
234
235
|
.option('--json', 'Output in JSON format')
|
|
@@ -241,6 +242,7 @@ function defineProgram({ handlers = {}, dryRun = false, version = '0.0.0', logo
|
|
|
241
242
|
baseUrl: opts.baseUrl || null,
|
|
242
243
|
persona: opts.persona || null,
|
|
243
244
|
seed: Boolean(opts.seed),
|
|
245
|
+
syncServer: Boolean(opts.syncServer),
|
|
244
246
|
format: String(opts.format || 'playwright'),
|
|
245
247
|
force: Boolean(opts.force),
|
|
246
248
|
json: isJson,
|
|
@@ -345,6 +347,25 @@ function defineProgram({ handlers = {}, dryRun = false, version = '0.0.0', logo
|
|
|
345
347
|
);
|
|
346
348
|
allowDryRun(cmdDayrate);
|
|
347
349
|
|
|
350
|
+
// spaps billing <subcommand>
|
|
351
|
+
const cmdBilling = program
|
|
352
|
+
.command('billing <subcommand>')
|
|
353
|
+
.description('Billing-account operator commands (subcommand: status|attach|verify)')
|
|
354
|
+
.option('-p, --port <port>', 'Port (default: 3301)', String(DEFAULT_PORT))
|
|
355
|
+
.option('--server-url <url>', 'Full server URL (overrides --port and SPAPS_API_URL)')
|
|
356
|
+
.option('--billing-account-id <id>', 'Billing account id (attach)')
|
|
357
|
+
.option('--json', 'Output in JSON format')
|
|
358
|
+
.action(
|
|
359
|
+
makeAction('billing', (opts, cmd, isJson) => ({
|
|
360
|
+
subcommand: cmd.args[0],
|
|
361
|
+
port: Number(opts.port) || DEFAULT_PORT,
|
|
362
|
+
serverUrl: opts.serverUrl || null,
|
|
363
|
+
billingAccountId: opts.billingAccountId || null,
|
|
364
|
+
json: isJson,
|
|
365
|
+
}))
|
|
366
|
+
);
|
|
367
|
+
allowDryRun(cmdBilling);
|
|
368
|
+
|
|
348
369
|
// spaps email
|
|
349
370
|
const cmdEmail = program
|
|
350
371
|
.command('email')
|
package/src/docs-html.js
CHANGED
|
@@ -520,13 +520,13 @@ app.post('/webhook', (req, res) => {
|
|
|
520
520
|
// Visit: http://localhost:${port}/api/stripe/webhooks/test</code></pre>
|
|
521
521
|
|
|
522
522
|
<h3>Subscription Management</h3>
|
|
523
|
-
<pre><code>// Get
|
|
524
|
-
const subscription = await spaps.getSubscription();
|
|
523
|
+
<pre><code>// Get subscription by ID
|
|
524
|
+
const subscription = await spaps.getSubscription(subscriptionId);
|
|
525
525
|
console.log('Status:', subscription.data.status);
|
|
526
526
|
console.log('Plan:', subscription.data.plan);
|
|
527
527
|
|
|
528
528
|
// Cancel subscription
|
|
529
|
-
await spaps.cancelSubscription();</code></pre>
|
|
529
|
+
await spaps.cancelSubscription(subscriptionId);</code></pre>
|
|
530
530
|
|
|
531
531
|
<h3>Usage Tracking</h3>
|
|
532
532
|
<pre><code>// Check balance
|
package/src/docs-quick.js
CHANGED
|
@@ -17,7 +17,7 @@ function showQuickReference() {
|
|
|
17
17
|
' await spaps.register(email, password)',
|
|
18
18
|
' await spaps.getUser()',
|
|
19
19
|
' await spaps.createCheckoutSession(priceId, successUrl)',
|
|
20
|
-
' await spaps.getSubscription()',
|
|
20
|
+
' await spaps.getSubscription(subscriptionId)',
|
|
21
21
|
' await spaps.getUsageBalance()',
|
|
22
22
|
'',
|
|
23
23
|
chalk.green('Helper Methods:'),
|
package/src/docs-system.js
CHANGED
|
@@ -72,7 +72,7 @@ ${chalk.green('Token Management:')}
|
|
|
72
72
|
content: `
|
|
73
73
|
${chalk.green('Stripe Checkout:')}
|
|
74
74
|
${chalk.gray('// Create checkout session')}
|
|
75
|
-
const session = await sdk.payments.
|
|
75
|
+
const session = await sdk.payments.createPaymentCheckout({
|
|
76
76
|
price_id: 'price_123abc',
|
|
77
77
|
success_url: 'http://localhost:3000/success',
|
|
78
78
|
cancel_url: 'http://localhost:3000/cancel'
|
|
@@ -86,7 +86,7 @@ ${chalk.green('Subscription Management:')}
|
|
|
86
86
|
// Example subscription helpers would go here if enabled
|
|
87
87
|
|
|
88
88
|
${chalk.gray('// Cancel subscription')}
|
|
89
|
-
await spaps.cancelSubscription()
|
|
89
|
+
await spaps.cancelSubscription(subscriptionId)
|
|
90
90
|
|
|
91
91
|
${chalk.green('Usage Tracking:')}
|
|
92
92
|
${chalk.gray('// Check balance')}
|
package/src/domains.js
CHANGED
|
@@ -133,6 +133,21 @@ const POLICIES = {
|
|
|
133
133
|
],
|
|
134
134
|
};
|
|
135
135
|
|
|
136
|
+
const BILLING = {
|
|
137
|
+
key: 'billing',
|
|
138
|
+
label: 'Billing Accounts',
|
|
139
|
+
// Admin-gated; 401/403 still proves the operator route is mounted.
|
|
140
|
+
probe: { method: 'GET', path: '/api/admin/billing/status', ok_on_auth_error: true },
|
|
141
|
+
tools: [
|
|
142
|
+
{ name: 'billing_status', method: 'GET', path: '/api/admin/billing/status',
|
|
143
|
+
description: 'Show current application billing-account resolution and redacted Stripe diagnostics.', admin_required: true },
|
|
144
|
+
{ name: 'billing_verify', method: 'GET', path: '/api/admin/billing/verify',
|
|
145
|
+
description: 'Verify that the current application has usable Stripe secret and webhook configuration.', admin_required: true },
|
|
146
|
+
{ name: 'billing_attach', method: 'POST', path: '/api/admin/billing/attach',
|
|
147
|
+
description: 'Bind the current application to an existing active Stripe billing account.', admin_required: true },
|
|
148
|
+
],
|
|
149
|
+
};
|
|
150
|
+
|
|
136
151
|
const ISSUE_REPORTING = {
|
|
137
152
|
key: 'issue_reporting',
|
|
138
153
|
label: 'Issue Reporting',
|
|
@@ -145,16 +160,16 @@ const ISSUE_REPORTING = {
|
|
|
145
160
|
description: 'List the caller\u2019s issue reports.' },
|
|
146
161
|
{ name: 'issue_report_status', method: 'GET', path: '/api/v1/issue-reports/status',
|
|
147
162
|
description: 'Aggregate status for the caller\u2019s issues.' },
|
|
148
|
-
{ name: 'issue_report_get', method: 'GET', path: '/api/v1/issue-reports/{
|
|
163
|
+
{ name: 'issue_report_get', method: 'GET', path: '/api/v1/issue-reports/{issue_report_id}',
|
|
149
164
|
description: 'Fetch a single issue report.' },
|
|
150
|
-
{ name: 'issue_report_update_note', method: 'PATCH', path: '/api/v1/issue-reports/{
|
|
165
|
+
{ name: 'issue_report_update_note', method: 'PATCH', path: '/api/v1/issue-reports/{issue_report_id}',
|
|
151
166
|
description: 'Update the note on an existing report.' },
|
|
152
|
-
{ name: 'issue_report_reply', method: 'POST', path: '/api/v1/issue-reports/{
|
|
167
|
+
{ name: 'issue_report_reply', method: 'POST', path: '/api/v1/issue-reports/{issue_report_id}/replies',
|
|
153
168
|
description: 'Reply to an issue; creates a child report and reopens the linked support case.' },
|
|
154
169
|
],
|
|
155
170
|
};
|
|
156
171
|
|
|
157
|
-
const DOMAINS = [DAYRATE, EMAIL, WEBHOOKS, POLICIES, ISSUE_REPORTING];
|
|
172
|
+
const DOMAINS = [DAYRATE, EMAIL, WEBHOOKS, POLICIES, BILLING, ISSUE_REPORTING];
|
|
158
173
|
|
|
159
174
|
function listDomains() {
|
|
160
175
|
return DOMAINS;
|
package/src/fixture-kernel.js
CHANGED
|
@@ -306,6 +306,58 @@ const DEFAULT_PERSONAS = [
|
|
|
306
306
|
},
|
|
307
307
|
],
|
|
308
308
|
}),
|
|
309
|
+
makeDerivedPersona('cfo-billing-demo', 'CFO Billing Demo', 'admin', {
|
|
310
|
+
permissions: ['view_products', 'manage_company_billing', 'view_company_reports'],
|
|
311
|
+
profile: {
|
|
312
|
+
username: 'cfo-billing-demo',
|
|
313
|
+
},
|
|
314
|
+
scenario: {
|
|
315
|
+
cfo: {
|
|
316
|
+
pack: 'cfo.billing_demo',
|
|
317
|
+
company_id: '00000000-0000-4000-8000-00000000c001',
|
|
318
|
+
company_slug: 'fixture-cfo-company',
|
|
319
|
+
expected_entitlements: [
|
|
320
|
+
'fixture.cfo.company.billing_admin',
|
|
321
|
+
'fixture.cfo.company.reports',
|
|
322
|
+
],
|
|
323
|
+
},
|
|
324
|
+
},
|
|
325
|
+
}),
|
|
326
|
+
makeDerivedPersona('htma-local-auth', 'HTMA Local Auth', 'user', {
|
|
327
|
+
permissions: ['view_products', 'manage_patient_protocols'],
|
|
328
|
+
profile: {
|
|
329
|
+
username: 'htma-local-auth',
|
|
330
|
+
},
|
|
331
|
+
scenario: {
|
|
332
|
+
htma: {
|
|
333
|
+
pack: 'htma.local_auth',
|
|
334
|
+
invite_template_key: 'fixture-htma-invite-{{global.run_id}}',
|
|
335
|
+
invite_action_url: 'http://localhost:5173/htma/invite/{{global.run_id}}',
|
|
336
|
+
expected_entitlements: ['fixture.htma.local_auth'],
|
|
337
|
+
},
|
|
338
|
+
},
|
|
339
|
+
seed: [
|
|
340
|
+
{
|
|
341
|
+
as: 'admin',
|
|
342
|
+
method: 'POST',
|
|
343
|
+
path: '/api/email/templates',
|
|
344
|
+
expected_statuses: [201, 409],
|
|
345
|
+
body: {
|
|
346
|
+
name: 'Fixture HTMA Invite',
|
|
347
|
+
template_key: 'fixture-htma-invite-{{global.run_id}}',
|
|
348
|
+
subject: 'HTMA invite for {{name}}',
|
|
349
|
+
html_body: '<p><a href="{{action_url}}">Open HTMA invite</a></p>',
|
|
350
|
+
text_body: 'Open HTMA invite: {{action_url}}',
|
|
351
|
+
from_email: 'noreply@example.com',
|
|
352
|
+
category: 'fixture',
|
|
353
|
+
sample_context: {
|
|
354
|
+
name: 'Fixture HTMA User',
|
|
355
|
+
action_url: 'http://localhost:5173/htma/invite/{{global.run_id}}',
|
|
356
|
+
},
|
|
357
|
+
},
|
|
358
|
+
},
|
|
359
|
+
],
|
|
360
|
+
}),
|
|
309
361
|
makeDerivedPersona('email-local-safe', 'Email Local Safe', 'user', {
|
|
310
362
|
profile: {
|
|
311
363
|
username: 'email-local-safe',
|
|
@@ -593,6 +645,8 @@ const DEFAULT_ROLE_GRANTS = {
|
|
|
593
645
|
'dayrate-guest': ['user'],
|
|
594
646
|
'dayrate-entitled': ['user'],
|
|
595
647
|
'dayrate-overage': ['user'],
|
|
648
|
+
'cfo-billing-demo': ['admin', 'super_admin'],
|
|
649
|
+
'htma-local-auth': ['user'],
|
|
596
650
|
'email-local-safe': ['user'],
|
|
597
651
|
'webhook-signature-invalid': ['admin', 'super_admin'],
|
|
598
652
|
'webhook-replay': ['admin', 'super_admin'],
|
|
@@ -609,6 +663,27 @@ const DEFAULT_ENTITLEMENT_GRANTS = {
|
|
|
609
663
|
'dayrate-guest': [],
|
|
610
664
|
'dayrate-entitled': ['fixture.dayrate.free.{{global.run_id}}'],
|
|
611
665
|
'dayrate-overage': ['fixture.dayrate.overage.{{global.run_id}}'],
|
|
666
|
+
'cfo-billing-demo': [
|
|
667
|
+
{
|
|
668
|
+
key: 'fixture.cfo.company.billing_admin',
|
|
669
|
+
resource_type: 'company',
|
|
670
|
+
resource_id: '00000000-0000-4000-8000-00000000c001',
|
|
671
|
+
metadata: {
|
|
672
|
+
pack: 'cfo.billing_demo',
|
|
673
|
+
company_slug: 'fixture-cfo-company',
|
|
674
|
+
},
|
|
675
|
+
},
|
|
676
|
+
{
|
|
677
|
+
key: 'fixture.cfo.company.reports',
|
|
678
|
+
resource_type: 'company',
|
|
679
|
+
resource_id: '00000000-0000-4000-8000-00000000c001',
|
|
680
|
+
metadata: {
|
|
681
|
+
pack: 'cfo.billing_demo',
|
|
682
|
+
company_slug: 'fixture-cfo-company',
|
|
683
|
+
},
|
|
684
|
+
},
|
|
685
|
+
],
|
|
686
|
+
'htma-local-auth': ['fixture.htma.local_auth'],
|
|
612
687
|
'email-local-safe': [],
|
|
613
688
|
'webhook-signature-invalid': [],
|
|
614
689
|
'webhook-replay': [],
|
|
@@ -618,6 +693,37 @@ const DEFAULT_ENTITLEMENT_GRANTS = {
|
|
|
618
693
|
'issue-reporter-blocked': [],
|
|
619
694
|
};
|
|
620
695
|
|
|
696
|
+
function normalizeEntitlementGrant(grant) {
|
|
697
|
+
if (typeof grant === 'string') {
|
|
698
|
+
return { key: grant };
|
|
699
|
+
}
|
|
700
|
+
if (!grant || typeof grant !== 'object') {
|
|
701
|
+
return { key: String(grant || '') };
|
|
702
|
+
}
|
|
703
|
+
return {
|
|
704
|
+
key: grant.key || grant.entitlement_key || '',
|
|
705
|
+
entitlement_type: grant.entitlement_type || grant.type || 'access',
|
|
706
|
+
resource_type: grant.resource_type || 'user',
|
|
707
|
+
resource_id: grant.resource_id || null,
|
|
708
|
+
metadata: grant.metadata || {},
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
function entitlementGrantKey(grant) {
|
|
713
|
+
return normalizeEntitlementGrant(grant).key;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
function entitlementGrantKeys(grants = []) {
|
|
717
|
+
return grants.map(entitlementGrantKey).filter(Boolean);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
function serializeEntitlementGrantForSync(grant) {
|
|
721
|
+
if (typeof grant === 'string') {
|
|
722
|
+
return grant;
|
|
723
|
+
}
|
|
724
|
+
return normalizeEntitlementGrant(grant);
|
|
725
|
+
}
|
|
726
|
+
|
|
621
727
|
function createCliError(code, message) {
|
|
622
728
|
const error = new Error(message);
|
|
623
729
|
error.code = code;
|
|
@@ -783,12 +889,14 @@ Common workflow:
|
|
|
783
889
|
1. Run \`npx spaps fixtures init\`
|
|
784
890
|
2. Edit \`.spaps/users.json\`, \`.spaps/roles.json\`, or \`.spaps/entitlements.json\`
|
|
785
891
|
3. Run \`npx spaps fixtures apply\`
|
|
786
|
-
4. Run \`npx spaps fixtures apply --
|
|
787
|
-
5.
|
|
788
|
-
6.
|
|
892
|
+
4. Run \`npx spaps fixtures apply --sync-server\` to reconcile users, memberships, and entitlements into local SPAPS
|
|
893
|
+
5. Run \`npx spaps fixtures apply --seed --persona <code>\` when a domain persona needs extra domain-specific DB state
|
|
894
|
+
6. Point Playwright or local scripts at the generated browser artifacts
|
|
895
|
+
7. Include \`/${DEFAULT_BRIDGE_SCRIPT_NAME}\` before your app boots if you want frontend-only persona switching
|
|
789
896
|
|
|
790
897
|
Notes:
|
|
791
898
|
|
|
899
|
+
- \`--sync-server\` only runs against a reachable localhost SPAPS server with local mode active
|
|
792
900
|
- \`--seed\` only runs against a reachable SPAPS server with local mode active
|
|
793
901
|
- Seed steps use the persona metadata already in \`.spaps/users.json\`; no extra backend-only fixture routes are required
|
|
794
902
|
`;
|
|
@@ -1079,6 +1187,15 @@ function buildSeedUrl(apiUrl, requestPath, queryParams = {}) {
|
|
|
1079
1187
|
return url.toString();
|
|
1080
1188
|
}
|
|
1081
1189
|
|
|
1190
|
+
function isLocalServerUrl(apiUrl) {
|
|
1191
|
+
try {
|
|
1192
|
+
const parsed = new URL(apiUrl);
|
|
1193
|
+
return ['localhost', '127.0.0.1', '::1'].includes(parsed.hostname);
|
|
1194
|
+
} catch {
|
|
1195
|
+
return false;
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1082
1199
|
function assertSeedingRuntime(runtime, port) {
|
|
1083
1200
|
if (!runtime?.running) {
|
|
1084
1201
|
throw createCliError(
|
|
@@ -1095,6 +1212,29 @@ function assertSeedingRuntime(runtime, port) {
|
|
|
1095
1212
|
}
|
|
1096
1213
|
}
|
|
1097
1214
|
|
|
1215
|
+
function assertServerSyncRuntime(runtime, appConfig, port) {
|
|
1216
|
+
if (!runtime?.running) {
|
|
1217
|
+
throw createCliError(
|
|
1218
|
+
'ESYNC',
|
|
1219
|
+
`Fixture server sync requires a running SPAPS server on port ${port}. Start it with "npx spaps local --port ${port}" or omit --sync-server.`
|
|
1220
|
+
);
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
if (runtime.local_mode?.active !== true) {
|
|
1224
|
+
throw createCliError(
|
|
1225
|
+
'ESYNC',
|
|
1226
|
+
'Fixture server sync requires SPAPS local mode. Refusing to sync fixtures into a non-local server.'
|
|
1227
|
+
);
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
if (!isLocalServerUrl(appConfig.server.api_url)) {
|
|
1231
|
+
throw createCliError(
|
|
1232
|
+
'ESYNC',
|
|
1233
|
+
`Fixture server sync refuses non-local SPAPS targets: ${appConfig.server.api_url}`
|
|
1234
|
+
);
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1098
1238
|
async function runSeedSequence({
|
|
1099
1239
|
appConfig,
|
|
1100
1240
|
usersConfig,
|
|
@@ -1238,6 +1378,136 @@ async function runFixtureSeeding({
|
|
|
1238
1378
|
};
|
|
1239
1379
|
}
|
|
1240
1380
|
|
|
1381
|
+
function buildFixtureServerSyncPayload({
|
|
1382
|
+
appConfig,
|
|
1383
|
+
usersConfig,
|
|
1384
|
+
rolesConfig,
|
|
1385
|
+
entitlementsConfig,
|
|
1386
|
+
personaCode = null,
|
|
1387
|
+
}) {
|
|
1388
|
+
const selectedCodes = personaCode
|
|
1389
|
+
? [ensurePersona(usersConfig, personaCode).code]
|
|
1390
|
+
: usersConfig.personas.map((entry) => entry.code);
|
|
1391
|
+
|
|
1392
|
+
return {
|
|
1393
|
+
source: 'spaps-fixtures',
|
|
1394
|
+
application: {
|
|
1395
|
+
id: appConfig.server.application_id || null,
|
|
1396
|
+
slug: appConfig.server.application_slug || 'local-dev',
|
|
1397
|
+
name: appConfig.server.application_slug || 'Local Development',
|
|
1398
|
+
api_key: appConfig.server.api_key || null,
|
|
1399
|
+
allowed_origins: [resolveStorageOrigin(appConfig.browser.base_url)],
|
|
1400
|
+
settings: {
|
|
1401
|
+
spaps_fixture_browser_base_url: appConfig.browser.base_url,
|
|
1402
|
+
},
|
|
1403
|
+
},
|
|
1404
|
+
personas: selectedCodes.map((code) => {
|
|
1405
|
+
const persona = ensurePersona(usersConfig, code);
|
|
1406
|
+
return {
|
|
1407
|
+
code,
|
|
1408
|
+
display_name: persona.display_name || code,
|
|
1409
|
+
profile: persona.profile || {},
|
|
1410
|
+
roles: rolesConfig.grants?.[code] || [],
|
|
1411
|
+
permissions: persona.permissions || [],
|
|
1412
|
+
entitlements: (entitlementsConfig.grants?.[code] || []).map(serializeEntitlementGrantForSync),
|
|
1413
|
+
metadata: {
|
|
1414
|
+
selector: persona.selector || {},
|
|
1415
|
+
scenario: persona.scenario || {},
|
|
1416
|
+
},
|
|
1417
|
+
};
|
|
1418
|
+
}),
|
|
1419
|
+
};
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
async function runFixtureServerSync({
|
|
1423
|
+
appConfig,
|
|
1424
|
+
runtime,
|
|
1425
|
+
usersConfig,
|
|
1426
|
+
rolesConfig,
|
|
1427
|
+
entitlementsConfig,
|
|
1428
|
+
personaCode = null,
|
|
1429
|
+
port,
|
|
1430
|
+
}) {
|
|
1431
|
+
assertServerSyncRuntime(runtime, appConfig, port);
|
|
1432
|
+
const payload = buildFixtureServerSyncPayload({
|
|
1433
|
+
appConfig,
|
|
1434
|
+
usersConfig,
|
|
1435
|
+
rolesConfig,
|
|
1436
|
+
entitlementsConfig,
|
|
1437
|
+
personaCode,
|
|
1438
|
+
});
|
|
1439
|
+
|
|
1440
|
+
let response;
|
|
1441
|
+
try {
|
|
1442
|
+
response = await axios({
|
|
1443
|
+
method: 'POST',
|
|
1444
|
+
url: `${appConfig.server.api_url.replace(/\/+$/, '')}/api/dev/fixtures/apply`,
|
|
1445
|
+
data: payload,
|
|
1446
|
+
headers: {
|
|
1447
|
+
Accept: 'application/json',
|
|
1448
|
+
'Content-Type': 'application/json',
|
|
1449
|
+
},
|
|
1450
|
+
timeout: 10000,
|
|
1451
|
+
validateStatus: () => true,
|
|
1452
|
+
});
|
|
1453
|
+
} catch (error) {
|
|
1454
|
+
throw createCliError(
|
|
1455
|
+
'ESYNC',
|
|
1456
|
+
`Fixture server sync failed at /api/dev/fixtures/apply: ${error.message}`
|
|
1457
|
+
);
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
const body = unwrapEnvelope(response.data);
|
|
1461
|
+
if (response.status < 200 || response.status >= 300) {
|
|
1462
|
+
const message = body?.message || body?.error?.message || `status ${response.status}`;
|
|
1463
|
+
throw createCliError('ESYNC', `Fixture server sync failed: ${message}`);
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
return body;
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
function applyServerSnapshotToUsers(usersConfig, appConfig, serverSync) {
|
|
1470
|
+
if (!serverSync?.application || !Array.isArray(serverSync.personas)) {
|
|
1471
|
+
return usersConfig;
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
const byCode = new Map(serverSync.personas.map((entry) => [entry.code, entry]));
|
|
1475
|
+
appConfig.server.application_id = serverSync.application.id;
|
|
1476
|
+
appConfig.server.application_slug = serverSync.application.slug;
|
|
1477
|
+
appConfig.server.api_key = serverSync.application.api_key;
|
|
1478
|
+
|
|
1479
|
+
return {
|
|
1480
|
+
...usersConfig,
|
|
1481
|
+
personas: usersConfig.personas.map((persona) => {
|
|
1482
|
+
const synced = byCode.get(persona.code);
|
|
1483
|
+
if (!synced) {
|
|
1484
|
+
return persona;
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
return {
|
|
1488
|
+
...persona,
|
|
1489
|
+
profile: {
|
|
1490
|
+
...(persona.profile || {}),
|
|
1491
|
+
user_id: synced.user.id,
|
|
1492
|
+
email: synced.user.email || persona.profile?.email,
|
|
1493
|
+
username: synced.user.username || persona.profile?.username,
|
|
1494
|
+
tier: synced.user.tier || persona.profile?.tier,
|
|
1495
|
+
},
|
|
1496
|
+
application: {
|
|
1497
|
+
...(persona.application || {}),
|
|
1498
|
+
application_id: serverSync.application.id,
|
|
1499
|
+
application_slug: serverSync.application.slug,
|
|
1500
|
+
api_key: serverSync.application.api_key,
|
|
1501
|
+
},
|
|
1502
|
+
server_sync: {
|
|
1503
|
+
membership_created: synced.membership_created,
|
|
1504
|
+
entitlements_created: synced.entitlements_created,
|
|
1505
|
+
},
|
|
1506
|
+
};
|
|
1507
|
+
}),
|
|
1508
|
+
};
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1241
1511
|
function stringifyStorageValue(value) {
|
|
1242
1512
|
if (typeof value === 'string') {
|
|
1243
1513
|
return value;
|
|
@@ -1254,10 +1524,18 @@ function buildRouteHint(appConfig, persona) {
|
|
|
1254
1524
|
}
|
|
1255
1525
|
|
|
1256
1526
|
function buildHeaderArtifact(appConfig, persona) {
|
|
1527
|
+
const application = buildApplicationSummary(appConfig, persona);
|
|
1528
|
+
const headers = {
|
|
1529
|
+
...(persona.selector?.headers || {}),
|
|
1530
|
+
};
|
|
1531
|
+
if (application.api_key && !headers['X-API-Key']) {
|
|
1532
|
+
headers['X-API-Key'] = application.api_key;
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1257
1535
|
return {
|
|
1258
1536
|
format: 'playwright-extra-http-headers',
|
|
1259
1537
|
local_mode_active: appConfig.server.local_mode_active === true,
|
|
1260
|
-
headers
|
|
1538
|
+
headers,
|
|
1261
1539
|
route_hint: buildRouteHint(appConfig, persona),
|
|
1262
1540
|
note:
|
|
1263
1541
|
appConfig.server.local_mode_active === true
|
|
@@ -1298,31 +1576,32 @@ function buildFixtureTokens(appConfig, persona, roles) {
|
|
|
1298
1576
|
}
|
|
1299
1577
|
|
|
1300
1578
|
function buildEntitlementRecords(appConfig, persona, entitlementsConfig) {
|
|
1301
|
-
const
|
|
1579
|
+
const entitlementGrants = (entitlementsConfig.grants?.[persona.code] || []).map(normalizeEntitlementGrant);
|
|
1302
1580
|
const application = buildApplicationSummary(appConfig, persona);
|
|
1303
1581
|
|
|
1304
|
-
return
|
|
1582
|
+
return entitlementGrants.map((grant, index) => ({
|
|
1305
1583
|
id: `fixture-${persona.code}-${index + 1}`,
|
|
1306
1584
|
application_id: application.application_id,
|
|
1307
1585
|
beneficiary_user_id: persona.profile?.user_id || null,
|
|
1308
1586
|
beneficiary_email: persona.profile?.email || null,
|
|
1309
|
-
entitlement_key:
|
|
1310
|
-
entitlement_type: 'manual',
|
|
1587
|
+
entitlement_key: grant.key,
|
|
1588
|
+
entitlement_type: grant.entitlement_type || 'manual',
|
|
1311
1589
|
source: 'fixture',
|
|
1312
|
-
resource_type: 'user',
|
|
1313
|
-
resource_id: null,
|
|
1590
|
+
resource_type: grant.resource_type || 'user',
|
|
1591
|
+
resource_id: grant.resource_id || null,
|
|
1314
1592
|
starts_at: '2026-01-01T00:00:00.000Z',
|
|
1315
1593
|
ends_at: null,
|
|
1316
1594
|
revoked_at: null,
|
|
1317
1595
|
metadata: {
|
|
1318
1596
|
fixture_persona: persona.code,
|
|
1597
|
+
...(grant.metadata || {}),
|
|
1319
1598
|
},
|
|
1320
1599
|
}));
|
|
1321
1600
|
}
|
|
1322
1601
|
|
|
1323
1602
|
function buildCurrentUser(appConfig, persona, rolesConfig, entitlementsConfig) {
|
|
1324
1603
|
const roles = rolesConfig.grants?.[persona.code] || [];
|
|
1325
|
-
const entitlements = entitlementsConfig.grants?.[persona.code] || [];
|
|
1604
|
+
const entitlements = entitlementGrantKeys(entitlementsConfig.grants?.[persona.code] || []);
|
|
1326
1605
|
const permissions = Array.isArray(persona.permissions) ? persona.permissions : [];
|
|
1327
1606
|
const primaryRole = roles[0] || 'user';
|
|
1328
1607
|
const application = buildApplicationSummary(appConfig, persona);
|
|
@@ -1383,7 +1662,7 @@ function buildBridgeConfig(appConfig, users, rolesConfig, entitlementsConfig) {
|
|
|
1383
1662
|
application: buildApplicationSummary(appConfig, persona),
|
|
1384
1663
|
user: buildCurrentUser(appConfig, persona, rolesConfig, entitlementsConfig),
|
|
1385
1664
|
tokens: buildFixtureTokens(appConfig, persona, rolesConfig.grants?.[persona.code] || []),
|
|
1386
|
-
entitlement_keys: entitlementsConfig.grants?.[persona.code] || [],
|
|
1665
|
+
entitlement_keys: entitlementGrantKeys(entitlementsConfig.grants?.[persona.code] || []),
|
|
1387
1666
|
entitlements: buildEntitlementRecords(appConfig, persona, entitlementsConfig),
|
|
1388
1667
|
scenario: persona.scenario || {},
|
|
1389
1668
|
browser: {
|
|
@@ -1767,7 +2046,7 @@ function buildStorageStateArtifact(appConfig, persona, rolesConfig, entitlements
|
|
|
1767
2046
|
},
|
|
1768
2047
|
{
|
|
1769
2048
|
name: FIXTURE_KEYS.entitlements,
|
|
1770
|
-
value: stringifyStorageValue(entitlementsConfig.grants?.[persona.code] || []),
|
|
2049
|
+
value: stringifyStorageValue(entitlementGrantKeys(entitlementsConfig.grants?.[persona.code] || [])),
|
|
1771
2050
|
},
|
|
1772
2051
|
{
|
|
1773
2052
|
name: FIXTURE_KEYS.scenario,
|
|
@@ -1838,7 +2117,7 @@ function buildPersonaContext(appConfig, persona, rolesConfig, entitlementsConfig
|
|
|
1838
2117
|
profile: persona.profile || {},
|
|
1839
2118
|
scenario: persona.scenario || {},
|
|
1840
2119
|
roles: rolesConfig.grants?.[persona.code] || [],
|
|
1841
|
-
entitlements: entitlementsConfig.grants?.[persona.code] || [],
|
|
2120
|
+
entitlements: entitlementGrantKeys(entitlementsConfig.grants?.[persona.code] || []),
|
|
1842
2121
|
user,
|
|
1843
2122
|
tokens,
|
|
1844
2123
|
application,
|
|
@@ -1859,6 +2138,7 @@ function writePersonaArtifacts({
|
|
|
1859
2138
|
entitlements,
|
|
1860
2139
|
personaCode = null,
|
|
1861
2140
|
seedResultsByPersona = {},
|
|
2141
|
+
serverSync = null,
|
|
1862
2142
|
}) {
|
|
1863
2143
|
const selectedCodes = personaCode ? [ensurePersona(users, personaCode).code] : users.personas.map((persona) => persona.code);
|
|
1864
2144
|
const generated = [];
|
|
@@ -1869,6 +2149,7 @@ function writePersonaArtifacts({
|
|
|
1869
2149
|
const headers = buildHeaderArtifact(appConfig, persona);
|
|
1870
2150
|
const context = buildPersonaContext(appConfig, persona, roles, entitlements, paths);
|
|
1871
2151
|
context.seed = seedResultsByPersona[code] || null;
|
|
2152
|
+
context.server_sync = serverSync?.personas?.find((entry) => entry.code === code) || null;
|
|
1872
2153
|
|
|
1873
2154
|
writeJson(context.artifacts.storage_state_path, storageState);
|
|
1874
2155
|
writeJson(context.artifacts.headers_path, headers);
|
|
@@ -1890,6 +2171,20 @@ function writePersonaArtifacts({
|
|
|
1890
2171
|
base_url: appConfig.browser.base_url,
|
|
1891
2172
|
api_url: appConfig.server.api_url,
|
|
1892
2173
|
local_mode_active: appConfig.server.local_mode_active,
|
|
2174
|
+
ownership: serverSync
|
|
2175
|
+
? {
|
|
2176
|
+
source: serverSync.source || 'spaps-fixtures',
|
|
2177
|
+
application: {
|
|
2178
|
+
id: serverSync.application.id,
|
|
2179
|
+
slug: serverSync.application.slug,
|
|
2180
|
+
},
|
|
2181
|
+
personas: serverSync.personas.map((entry) => ({
|
|
2182
|
+
code: entry.code,
|
|
2183
|
+
user_id: entry.user.id,
|
|
2184
|
+
entitlements: entry.entitlements,
|
|
2185
|
+
})),
|
|
2186
|
+
}
|
|
2187
|
+
: null,
|
|
1893
2188
|
});
|
|
1894
2189
|
|
|
1895
2190
|
return generated;
|
|
@@ -1915,6 +2210,7 @@ async function applyFixtures({
|
|
|
1915
2210
|
version = '0.0.0',
|
|
1916
2211
|
persona = null,
|
|
1917
2212
|
seed = false,
|
|
2213
|
+
syncServer = false,
|
|
1918
2214
|
subcommand = 'apply',
|
|
1919
2215
|
} = {}) {
|
|
1920
2216
|
const rootDir = resolveRepoRoot(dir);
|
|
@@ -1946,22 +2242,46 @@ async function applyFixtures({
|
|
|
1946
2242
|
});
|
|
1947
2243
|
|
|
1948
2244
|
const fixtureRunState = buildFixtureRunState(appConfig, runtime);
|
|
2245
|
+
let resolvedUsers = {
|
|
2246
|
+
...kernel.users,
|
|
2247
|
+
personas: kernel.users.personas.map((entry) => resolvePersonaDefinition(appConfig, entry, fixtureRunState)),
|
|
2248
|
+
};
|
|
2249
|
+
const resolvedRoles = resolveConfigTemplates(kernel.roles, fixtureRunState);
|
|
2250
|
+
const resolvedEntitlements = resolveConfigTemplates(kernel.entitlements, fixtureRunState);
|
|
2251
|
+
const serverSync = syncServer
|
|
2252
|
+
? await runFixtureServerSync({
|
|
2253
|
+
appConfig,
|
|
2254
|
+
runtime,
|
|
2255
|
+
usersConfig: resolvedUsers,
|
|
2256
|
+
rolesConfig: resolvedRoles,
|
|
2257
|
+
entitlementsConfig: resolvedEntitlements,
|
|
2258
|
+
personaCode: persona,
|
|
2259
|
+
port,
|
|
2260
|
+
})
|
|
2261
|
+
: null;
|
|
2262
|
+
if (serverSync) {
|
|
2263
|
+
resolvedUsers = applyServerSnapshotToUsers(resolvedUsers, appConfig, serverSync);
|
|
2264
|
+
writeJson(paths.app, appConfig);
|
|
2265
|
+
}
|
|
1949
2266
|
const seeding = seed
|
|
1950
2267
|
? await runFixtureSeeding({
|
|
1951
2268
|
appConfig,
|
|
1952
2269
|
runtime,
|
|
1953
|
-
usersConfig:
|
|
2270
|
+
usersConfig: resolvedUsers,
|
|
1954
2271
|
personaCode: persona,
|
|
1955
2272
|
fixtureRunState,
|
|
1956
2273
|
port,
|
|
1957
2274
|
})
|
|
1958
2275
|
: null;
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
2276
|
+
if (seeding) {
|
|
2277
|
+
resolvedUsers = {
|
|
2278
|
+
...kernel.users,
|
|
2279
|
+
personas: kernel.users.personas.map((entry) => resolvePersonaDefinition(appConfig, entry, fixtureRunState)),
|
|
2280
|
+
};
|
|
2281
|
+
if (serverSync) {
|
|
2282
|
+
resolvedUsers = applyServerSnapshotToUsers(resolvedUsers, appConfig, serverSync);
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
1965
2285
|
const seedResultsByPersona = Object.fromEntries(
|
|
1966
2286
|
(seeding?.personas || []).map((entry) => [entry.persona, entry])
|
|
1967
2287
|
);
|
|
@@ -1975,6 +2295,7 @@ async function applyFixtures({
|
|
|
1975
2295
|
entitlements: resolvedEntitlements,
|
|
1976
2296
|
personaCode: persona,
|
|
1977
2297
|
seedResultsByPersona,
|
|
2298
|
+
serverSync,
|
|
1978
2299
|
});
|
|
1979
2300
|
const bridge = writeDevAuthBridge({
|
|
1980
2301
|
rootDir,
|
|
@@ -1993,6 +2314,7 @@ async function applyFixtures({
|
|
|
1993
2314
|
fixture_dir: paths.fixtureDir,
|
|
1994
2315
|
bootstrapped,
|
|
1995
2316
|
runtime,
|
|
2317
|
+
server_sync: serverSync,
|
|
1996
2318
|
seeding,
|
|
1997
2319
|
generated: {
|
|
1998
2320
|
personas: generated,
|
|
@@ -2028,6 +2350,7 @@ async function exportStorageState(options = {}) {
|
|
|
2028
2350
|
route_hint: personaArtifact.route_hint,
|
|
2029
2351
|
bridge: result.generated.bridge,
|
|
2030
2352
|
runtime: result.runtime,
|
|
2353
|
+
server_sync: result.server_sync,
|
|
2031
2354
|
seeding: result.seeding,
|
|
2032
2355
|
next_steps: result.next_steps,
|
|
2033
2356
|
};
|
|
@@ -2039,6 +2362,7 @@ async function resetFixtures({
|
|
|
2039
2362
|
baseUrl = null,
|
|
2040
2363
|
version = '0.0.0',
|
|
2041
2364
|
seed = false,
|
|
2365
|
+
syncServer = false,
|
|
2042
2366
|
} = {}) {
|
|
2043
2367
|
const rootDir = resolveRepoRoot(dir);
|
|
2044
2368
|
const paths = resolveFixturePaths(rootDir);
|
|
@@ -2056,6 +2380,7 @@ async function resetFixtures({
|
|
|
2056
2380
|
baseUrl,
|
|
2057
2381
|
version,
|
|
2058
2382
|
seed,
|
|
2383
|
+
syncServer,
|
|
2059
2384
|
});
|
|
2060
2385
|
|
|
2061
2386
|
return {
|
|
@@ -2069,6 +2394,7 @@ async function resetFixtures({
|
|
|
2069
2394
|
files_overwritten: initResult.files_overwritten,
|
|
2070
2395
|
generated: applyResult.generated,
|
|
2071
2396
|
runtime: applyResult.runtime,
|
|
2397
|
+
server_sync: applyResult.server_sync,
|
|
2072
2398
|
seeding: applyResult.seeding,
|
|
2073
2399
|
next_steps: applyResult.next_steps,
|
|
2074
2400
|
};
|
package/src/handlers.js
CHANGED
|
@@ -385,6 +385,7 @@ function createHandlers(version, logo) {
|
|
|
385
385
|
version,
|
|
386
386
|
persona: options.persona,
|
|
387
387
|
seed: options.seed,
|
|
388
|
+
syncServer: options.syncServer,
|
|
388
389
|
});
|
|
389
390
|
break;
|
|
390
391
|
case 'reset':
|
|
@@ -394,6 +395,7 @@ function createHandlers(version, logo) {
|
|
|
394
395
|
baseUrl: options.baseUrl,
|
|
395
396
|
version,
|
|
396
397
|
seed: options.seed,
|
|
398
|
+
syncServer: options.syncServer,
|
|
397
399
|
});
|
|
398
400
|
break;
|
|
399
401
|
case 'storage-state':
|
|
@@ -407,6 +409,7 @@ function createHandlers(version, logo) {
|
|
|
407
409
|
version,
|
|
408
410
|
persona: options.persona,
|
|
409
411
|
seed: options.seed,
|
|
412
|
+
syncServer: options.syncServer,
|
|
410
413
|
});
|
|
411
414
|
break;
|
|
412
415
|
default:
|
|
@@ -451,6 +454,14 @@ function createHandlers(version, logo) {
|
|
|
451
454
|
});
|
|
452
455
|
}
|
|
453
456
|
|
|
457
|
+
if (Array.isArray(result.server_sync?.personas) && result.server_sync.personas.length > 0) {
|
|
458
|
+
console.log(chalk.green('\nSynced server fixtures:'));
|
|
459
|
+
console.log(chalk.gray(` app: ${result.server_sync.application.slug}`));
|
|
460
|
+
result.server_sync.personas.forEach((entry) => {
|
|
461
|
+
console.log(chalk.gray(` • ${entry.code} (${entry.user.id})`));
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
|
|
454
465
|
if (result.generated?.personas?.length) {
|
|
455
466
|
console.log(chalk.green('\nGenerated personas:'));
|
|
456
467
|
result.generated.personas.forEach((entry) => {
|
|
@@ -505,6 +516,7 @@ function createHandlers(version, logo) {
|
|
|
505
516
|
logout: async (...args) => loadAuthCommandHandlers().logoutHandler(...args),
|
|
506
517
|
whoami: async (...args) => loadAuthCommandHandlers().whoamiHandler(...args),
|
|
507
518
|
token: async (...args) => loadAuthCommandHandlers().tokenHandler(...args),
|
|
519
|
+
billing: billingHandler,
|
|
508
520
|
dayrate: dayrateHandler,
|
|
509
521
|
email: emailHandler,
|
|
510
522
|
policy: policyHandler,
|
|
@@ -600,6 +612,54 @@ async function dayrateHandler({ options }) {
|
|
|
600
612
|
}
|
|
601
613
|
}
|
|
602
614
|
|
|
615
|
+
async function billingHandler({ options }) {
|
|
616
|
+
const { callEndpoint, emit, emitAuthError } = loadDomainCli();
|
|
617
|
+
const isJson = Boolean(options.json);
|
|
618
|
+
const sub = options.subcommand;
|
|
619
|
+
try {
|
|
620
|
+
if (sub === 'status') {
|
|
621
|
+
const result = await callEndpoint({
|
|
622
|
+
options,
|
|
623
|
+
method: 'GET',
|
|
624
|
+
path: '/api/admin/billing/status',
|
|
625
|
+
});
|
|
626
|
+
emit({ intent: 'billing.status', result, isJson });
|
|
627
|
+
if (!result.ok) process.exitCode = 1;
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
if (sub === 'verify') {
|
|
632
|
+
const result = await callEndpoint({
|
|
633
|
+
options,
|
|
634
|
+
method: 'GET',
|
|
635
|
+
path: '/api/admin/billing/verify',
|
|
636
|
+
});
|
|
637
|
+
emit({ intent: 'billing.verify', result, isJson });
|
|
638
|
+
if (!result.ok || result.data?.valid === false) process.exitCode = 1;
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
if (sub === 'attach') {
|
|
643
|
+
if (!requireOption(options.billingAccountId, 'billing attach: --billing-account-id is required')) return;
|
|
644
|
+
const result = await callEndpoint({
|
|
645
|
+
options,
|
|
646
|
+
method: 'POST',
|
|
647
|
+
path: '/api/admin/billing/attach',
|
|
648
|
+
body: { billing_account_id: options.billingAccountId },
|
|
649
|
+
});
|
|
650
|
+
emit({ intent: 'billing.attach', result, isJson });
|
|
651
|
+
if (!result.ok) process.exitCode = 1;
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
console.error('billing: unknown subcommand. Supported: status, attach, verify');
|
|
656
|
+
process.exitCode = 2;
|
|
657
|
+
} catch (err) {
|
|
658
|
+
emitAuthError(`billing.${sub || 'unknown'}`, err, isJson);
|
|
659
|
+
process.exitCode = 1;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
603
663
|
async function emailHandler({ options }) {
|
|
604
664
|
const { callEndpoint, emit, emitAuthError } = loadDomainCli();
|
|
605
665
|
const isJson = Boolean(options.json);
|
package/src/help-system.js
CHANGED
|
@@ -320,7 +320,7 @@ SPAPS handles two payment types:
|
|
|
320
320
|
const session = await spaps.createCheckoutSession(priceId, successUrl)
|
|
321
321
|
|
|
322
322
|
// Check subscription
|
|
323
|
-
const sub = await spaps.getSubscription()
|
|
323
|
+
const sub = await spaps.getSubscription(subscriptionId)
|
|
324
324
|
\`\`\`
|
|
325
325
|
`
|
|
326
326
|
}
|