spaps 0.7.3 → 0.7.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/AI_TOOLS.json +10 -11
  2. package/README.md +216 -36
  3. package/assets/local-runtime/Dockerfile +28 -0
  4. package/assets/local-runtime/alembic/env.py +101 -0
  5. package/assets/local-runtime/alembic/path_bootstrap.py +71 -0
  6. package/assets/local-runtime/alembic/versions/000000000001_baseline_consolidated_schema.py +1076 -0
  7. package/assets/local-runtime/alembic/versions/000000000002_fix_column_types_to_match_prod.py +83 -0
  8. package/assets/local-runtime/alembic/versions/000000000003_fix_email_template_key_uniqueness.py +49 -0
  9. package/assets/local-runtime/alembic/versions/000000000004_add_hold_duration_minutes_to_dayrate_config.py +30 -0
  10. package/assets/local-runtime/alembic/versions/000000000005_resource_scoped_entitlements.py +77 -0
  11. package/assets/local-runtime/alembic/versions/000000000006_cfo_rbac_add_is_admin.py +37 -0
  12. package/assets/local-runtime/alembic/versions/000000000007_agent_approvals.py +158 -0
  13. package/assets/local-runtime/alembic/versions/000000000008_add_company_id_to_cfo_connections.py +35 -0
  14. package/assets/local-runtime/alembic/versions/000000000009_tx_signing.py +62 -0
  15. package/assets/local-runtime/alembic/versions/000000000010_affiliate_referrals.py +235 -0
  16. package/assets/local-runtime/alembic/versions/000000000011_checkin_call_booking.py +137 -0
  17. package/assets/local-runtime/alembic/versions/000000000012_subscription_application_scoping.py +55 -0
  18. package/assets/local-runtime/alembic/versions/000000000013_refresh_token_anomaly_context.py +61 -0
  19. package/assets/local-runtime/alembic/versions/000000000014_buildooor_dayrate_hire_schedule.py +39 -0
  20. package/assets/local-runtime/alembic/versions/000000000015_support_telemetry_platform.py +112 -0
  21. package/assets/local-runtime/alembic/versions/000000000016_issue_reporting_platform.py +54 -0
  22. package/assets/local-runtime/alembic/versions/000000000017_issue_reporting_platform_import_tracking.py +44 -0
  23. package/assets/local-runtime/alembic/versions/000000000018_authorization_policy_engine.py +76 -0
  24. package/assets/local-runtime/alembic.ini +47 -0
  25. package/assets/local-runtime/docker-compose.yml +61 -0
  26. package/assets/local-runtime/manifest.json +8 -0
  27. package/assets/local-runtime/scripts/container-entrypoint.sh +13 -0
  28. package/assets/local-runtime/scripts/fetch-prod-db.sh +112 -0
  29. package/assets/local-runtime/scripts/run-migrations.sh +96 -0
  30. package/package.json +2 -1
  31. package/src/ai-helper.js +176 -234
  32. package/src/ai-tool-spec.js +52 -20
  33. package/src/auth/api-key.js +119 -0
  34. package/src/auth/client-id.js +136 -0
  35. package/src/auth/client.js +169 -0
  36. package/src/auth/credentials.js +110 -0
  37. package/src/auth/device-flow.js +159 -0
  38. package/src/auth/env.js +57 -0
  39. package/src/auth/handlers.js +462 -0
  40. package/src/auth/http.js +74 -0
  41. package/src/cli-dispatcher.js +134 -21
  42. package/src/docs-system.js +7 -7
  43. package/src/fixture-kernel.js +1143 -0
  44. package/src/handlers.js +202 -11
  45. package/src/help-system.js +2 -0
  46. package/src/local-runtime.js +258 -0
  47. package/src/local-server.js +597 -199
  48. package/src/project-scaffolder.js +185 -45
@@ -2,18 +2,27 @@ const fs = require('node:fs');
2
2
  const path = require('node:path');
3
3
 
4
4
  const { DEFAULT_PORT } = require('./config');
5
+ const { provisionStarterApplication } = require('./local-runtime');
5
6
 
6
7
  const SUPPORTED_TEMPLATES = {
7
8
  nextjs: {
8
9
  label: 'Next.js starter',
9
10
  blueprintKey: 'browser_auth',
10
11
  allowedOrigins: ['http://localhost:3000'],
12
+ publicApiUrlEnv: 'NEXT_PUBLIC_SPAPS_API_URL',
13
+ publicApiKeyEnv: 'NEXT_PUBLIC_SPAPS_API_KEY',
11
14
  files: ({ apiUrl }) => ({
12
- 'lib/spaps.ts': `import { SPAPSClient } from 'spaps-sdk';
15
+ 'lib/spaps.ts': `import { createBrowserClient, SPAPSClient } from 'spaps-sdk';
13
16
 
14
- export const spaps = new SPAPSClient({
15
- apiUrl: process.env.NEXT_PUBLIC_SPAPS_API_URL || '${apiUrl}',
16
- });
17
+ const apiUrl = process.env.NEXT_PUBLIC_SPAPS_API_URL || '${apiUrl}';
18
+ const apiKey = process.env.NEXT_PUBLIC_SPAPS_API_KEY;
19
+
20
+ export const spaps = apiKey && apiKey.startsWith('spaps_pub_')
21
+ ? createBrowserClient(apiKey, { apiUrl })
22
+ : new SPAPSClient({
23
+ apiUrl,
24
+ ...(apiKey ? { apiKey } : {}),
25
+ });
17
26
  `,
18
27
  'app/providers.tsx': `'use client';
19
28
 
@@ -31,12 +40,20 @@ export function Providers({ children }: PropsWithChildren) {
31
40
  label: 'React + Vite starter',
32
41
  blueprintKey: 'browser_auth',
33
42
  allowedOrigins: ['http://localhost:5173'],
43
+ publicApiUrlEnv: 'VITE_SPAPS_API_URL',
44
+ publicApiKeyEnv: 'VITE_SPAPS_API_KEY',
34
45
  files: ({ apiUrl }) => ({
35
- 'src/lib/spaps.ts': `import { SPAPSClient } from 'spaps-sdk';
46
+ 'src/lib/spaps.ts': `import { createBrowserClient, SPAPSClient } from 'spaps-sdk';
47
+
48
+ const apiUrl = import.meta.env.VITE_SPAPS_API_URL || '${apiUrl}';
49
+ const apiKey = import.meta.env.VITE_SPAPS_API_KEY;
36
50
 
37
- export const spaps = new SPAPSClient({
38
- apiUrl: import.meta.env.VITE_SPAPS_API_URL || '${apiUrl}',
39
- });
51
+ export const spaps = apiKey && apiKey.startsWith('spaps_pub_')
52
+ ? createBrowserClient(apiKey, { apiUrl })
53
+ : new SPAPSClient({
54
+ apiUrl,
55
+ ...(apiKey ? { apiKey } : {}),
56
+ });
40
57
  `,
41
58
  }),
42
59
  },
@@ -44,13 +61,17 @@ export const spaps = new SPAPSClient({
44
61
  label: 'Node.js starter',
45
62
  blueprintKey: 'default',
46
63
  allowedOrigins: [],
64
+ publicApiUrlEnv: 'SPAPS_API_URL',
65
+ publicApiKeyEnv: 'SPAPS_API_KEY',
47
66
  files: ({ apiUrl }) => ({
48
- 'src/spaps.js': `const { SPAPSClient } = require('spaps-sdk');
67
+ 'src/spaps.js': `const { createServerClient, SPAPSClient } = require('spaps-sdk');
68
+
69
+ const apiUrl = process.env.SPAPS_API_URL || '${apiUrl}';
70
+ const apiKey = process.env.SPAPS_API_KEY;
49
71
 
50
- const spaps = new SPAPSClient({
51
- apiUrl: process.env.SPAPS_API_URL || '${apiUrl}',
52
- apiKey: process.env.SPAPS_API_KEY,
53
- });
72
+ const spaps = apiKey
73
+ ? createServerClient(apiKey, { apiUrl })
74
+ : new SPAPSClient({ apiUrl });
54
75
 
55
76
  module.exports = { spaps };
56
77
  `,
@@ -60,12 +81,20 @@ module.exports = { spaps };
60
81
  label: 'Vanilla JavaScript starter',
61
82
  blueprintKey: 'browser_auth',
62
83
  allowedOrigins: ['http://localhost:8080'],
84
+ publicApiUrlEnv: 'SPAPS_API_URL',
85
+ publicApiKeyEnv: 'SPAPS_API_KEY',
63
86
  files: ({ apiUrl }) => ({
64
- 'src/spaps.js': `import { SPAPSClient } from 'spaps-sdk';
87
+ 'src/spaps.js': `import { createBrowserClient, SPAPSClient } from 'spaps-sdk';
88
+
89
+ const apiUrl = window.SPAPS_API_URL || '${apiUrl}';
90
+ const apiKey = window.SPAPS_API_KEY;
65
91
 
66
- export const spaps = new SPAPSClient({
67
- apiUrl: '${apiUrl}',
68
- });
92
+ export const spaps = apiKey && apiKey.startsWith('spaps_pub_')
93
+ ? createBrowserClient(apiKey, { apiUrl })
94
+ : new SPAPSClient({
95
+ apiUrl,
96
+ ...(apiKey ? { apiKey } : {}),
97
+ });
69
98
  `,
70
99
  }),
71
100
  },
@@ -115,6 +144,12 @@ function ensureWritableTarget(targetDir, force) {
115
144
  }
116
145
  }
117
146
 
147
+ function prepareWritableTarget(targetDir, force) {
148
+ ensureWritableTarget(targetDir, force);
149
+ fs.mkdirSync(targetDir, { recursive: true });
150
+ fs.accessSync(targetDir, fs.constants.W_OK);
151
+ }
152
+
118
153
  function writeManagedFile(targetDir, relativePath, content, bookkeeping) {
119
154
  const fullPath = path.join(targetDir, relativePath);
120
155
  fs.mkdirSync(path.dirname(fullPath), { recursive: true });
@@ -146,7 +181,7 @@ function buildPackageJson(name, template) {
146
181
  return `${JSON.stringify(pkg, null, 2)}\n`;
147
182
  }
148
183
 
149
- function buildContract({ name, slug, template, version, apiUrl, docsUrl }) {
184
+ function buildContract({ name, slug, template, version, apiUrl, docsUrl, provisioning }) {
150
185
  const templateDef = SUPPORTED_TEMPLATES[template];
151
186
  const contract = {
152
187
  name,
@@ -157,12 +192,20 @@ function buildContract({ name, slug, template, version, apiUrl, docsUrl }) {
157
192
  local: {
158
193
  api_url: apiUrl,
159
194
  docs_url: docsUrl,
195
+ local_mode_active: provisioning.runtime?.local_mode?.active ?? null,
160
196
  },
161
197
  application: {
198
+ id: provisioning.application?.id || null,
162
199
  slug,
163
200
  blueprint_key: templateDef.blueprintKey,
164
201
  allowed_origins: templateDef.allowedOrigins,
165
- provisioning_status: 'local_starter_only',
202
+ provisioning_status: provisioning.status,
203
+ provisioned_via:
204
+ provisioning.status === 'provisioned'
205
+ ? 'self_service'
206
+ : provisioning.status === 'local_mode'
207
+ ? 'local_mode'
208
+ : null,
166
209
  },
167
210
  },
168
211
  };
@@ -170,25 +213,73 @@ function buildContract({ name, slug, template, version, apiUrl, docsUrl }) {
170
213
  return `${JSON.stringify(contract, null, 2)}\n`;
171
214
  }
172
215
 
173
- function buildEnvFile(template, apiUrl) {
216
+ function buildEnvFile(template, apiUrl, provisioning) {
217
+ const templateDef = SUPPORTED_TEMPLATES[template];
174
218
  const lines = [
175
219
  '# SPAPS local development',
176
220
  `SPAPS_API_URL=${apiUrl}`,
177
- '# SPAPS_API_KEY=',
178
221
  ];
179
222
 
180
- if (template === 'nextjs') {
181
- lines.push(`NEXT_PUBLIC_SPAPS_API_URL=${apiUrl}`);
223
+ if (templateDef.publicApiUrlEnv !== 'SPAPS_API_URL') {
224
+ lines.push(`${templateDef.publicApiUrlEnv}=${apiUrl}`);
182
225
  }
183
226
 
184
- if (template === 'react') {
185
- lines.push(`VITE_SPAPS_API_URL=${apiUrl}`);
227
+ if (provisioning.status === 'provisioned') {
228
+ const keyValue = template === 'node' ? provisioning.keys?.secret : provisioning.keys?.publishable;
229
+ if (keyValue) {
230
+ lines.push(`${templateDef.publicApiKeyEnv}=${keyValue}`);
231
+ } else {
232
+ lines.push(`# ${templateDef.publicApiKeyEnv}=`);
233
+ }
234
+ } else {
235
+ lines.push(`# ${templateDef.publicApiKeyEnv}=`);
236
+ }
237
+
238
+ if (template !== 'node' && templateDef.publicApiKeyEnv !== 'SPAPS_API_KEY') {
239
+ lines.push('# SPAPS_API_KEY=');
186
240
  }
187
241
 
188
242
  return `${lines.join('\n')}\n`;
189
243
  }
190
244
 
191
- function buildReadme({ name, template, apiUrl }) {
245
+ function buildProvisioningNote({ provisioning, targetDir, template }) {
246
+ if (provisioning.status === 'provisioned') {
247
+ return [
248
+ '## Provisioning Status',
249
+ '',
250
+ `This starter was provisioned against \`${provisioning.runtime.url}\`. The generated \`.env.local\` includes a working ${template === 'node' ? 'server' : 'browser'} key for this template.`,
251
+ '',
252
+ ].join('\n');
253
+ }
254
+
255
+ if (provisioning.status === 'local_mode') {
256
+ return [
257
+ '## Provisioning Status',
258
+ '',
259
+ `The server at \`${provisioning.runtime.url}\` is currently in local mode, so authenticated flows can run without provisioning while that mode stays enabled.`,
260
+ '',
261
+ 'Use `npx spaps quickstart --json` if you need the current local-mode hints and test personas.',
262
+ '',
263
+ ].join('\n');
264
+ }
265
+
266
+ const rerunCommand = `SELF_SERVICE_PASSWORD=your-password npx spaps create ${path.basename(targetDir)} --template ${template} --dir ${targetDir} --force`;
267
+
268
+ return [
269
+ '## Provisioning Status',
270
+ '',
271
+ 'This run only scaffolded files. Authenticated flows will not work until you provision a real SPAPS application or enable server local mode.',
272
+ '',
273
+ 'To provision automatically:',
274
+ '',
275
+ '```bash',
276
+ rerunCommand,
277
+ '```',
278
+ '',
279
+ ].join('\n');
280
+ }
281
+
282
+ function buildReadme({ name, template, apiUrl, targetDir, provisioning }) {
192
283
  const templateDef = SUPPORTED_TEMPLATES[template];
193
284
 
194
285
  return `# ${name}
@@ -203,22 +294,48 @@ It gives you three things immediately:
203
294
 
204
295
  This is not a full framework generator. It does not run \`create-next-app\`, Vite, or Express setup for you.
205
296
 
206
- ## Next Steps
297
+ ${buildProvisioningNote({ provisioning, targetDir, template })}## Next Steps
207
298
 
208
- 1. Start SPAPS locally with \`npx spaps local\`
209
- 2. Install dependencies in this project with \`npm install\`
210
- 3. Copy the generated starter files into your real ${templateDef.label.toLowerCase()} or keep extending this directory
211
- 4. Point your app at \`${apiUrl}\`
299
+ 1. Install dependencies in this project with \`npm install\`
300
+ 2. Copy the generated starter files into your real ${templateDef.label.toLowerCase()} or keep extending this directory
301
+ 3. Point your app at \`${apiUrl}\`
212
302
 
213
303
  ## Generated Files
214
304
 
215
- - \`spaps.app.json\`: local-first app contract and blueprint hints
216
- - \`.env.local\`: SPAPS API URL wiring
305
+ - \`spaps.app.json\`: local app contract and provisioning metadata
306
+ - \`.env.local\`: SPAPS API URL wiring and, when available, a working key for this template
217
307
  - \`package.json\`: minimal dependency declaration for \`spaps-sdk\`
218
308
  `;
219
309
  }
220
310
 
221
- function createProjectStarter({
311
+ function buildNextSteps({ targetDir, provisioning, port, name, template }) {
312
+ const steps = [
313
+ `cd ${targetDir}`,
314
+ 'npm install',
315
+ ];
316
+
317
+ if (provisioning.status === 'provisioned') {
318
+ steps.push('Review .env.local and start your app');
319
+ return steps;
320
+ }
321
+
322
+ if (provisioning.status === 'local_mode') {
323
+ steps.push(`Use the starter against http://localhost:${port} while local mode stays enabled`);
324
+ return steps;
325
+ }
326
+
327
+ if (provisioning.reason === 'server_unreachable') {
328
+ steps.push(`npx spaps local --port ${port}`);
329
+ } else {
330
+ steps.push(
331
+ `SELF_SERVICE_PASSWORD=your-password npx spaps create ${name} --template ${template} --dir ${targetDir} --force`
332
+ );
333
+ }
334
+
335
+ return steps;
336
+ }
337
+
338
+ async function createProjectStarter({
222
339
  name,
223
340
  template,
224
341
  dir = null,
@@ -241,9 +358,15 @@ function createProjectStarter({
241
358
  files_created: [],
242
359
  files_overwritten: [],
243
360
  };
244
-
245
- ensureWritableTarget(targetDir, force);
246
- fs.mkdirSync(targetDir, { recursive: true });
361
+ const templateDef = SUPPORTED_TEMPLATES[template];
362
+ prepareWritableTarget(targetDir, force);
363
+ const provisioning = await provisionStarterApplication({
364
+ port,
365
+ name: normalizedName,
366
+ slug,
367
+ blueprintKey: templateDef.blueprintKey,
368
+ allowedOrigins: templateDef.allowedOrigins,
369
+ });
247
370
 
248
371
  writeManagedFile(
249
372
  targetDir,
@@ -255,11 +378,17 @@ function createProjectStarter({
255
378
  version,
256
379
  apiUrl,
257
380
  docsUrl,
381
+ provisioning,
258
382
  }),
259
383
  bookkeeping
260
384
  );
261
- writeManagedFile(targetDir, '.env.local', buildEnvFile(template, apiUrl), bookkeeping);
262
- writeManagedFile(targetDir, 'README.md', buildReadme({ name: normalizedName, template, apiUrl }), bookkeeping);
385
+ writeManagedFile(targetDir, '.env.local', buildEnvFile(template, apiUrl, provisioning), bookkeeping);
386
+ writeManagedFile(
387
+ targetDir,
388
+ 'README.md',
389
+ buildReadme({ name: normalizedName, template, apiUrl, targetDir, provisioning }),
390
+ bookkeeping
391
+ );
263
392
  writeManagedFile(targetDir, 'package.json', buildPackageJson(normalizedName, template), bookkeeping);
264
393
  writeManagedFile(
265
394
  targetDir,
@@ -268,7 +397,7 @@ function createProjectStarter({
268
397
  bookkeeping
269
398
  );
270
399
 
271
- const templateFiles = SUPPORTED_TEMPLATES[template].files({
400
+ const templateFiles = templateDef.files({
272
401
  name: normalizedName,
273
402
  slug,
274
403
  apiUrl,
@@ -287,11 +416,22 @@ function createProjectStarter({
287
416
  contract_path: path.join(targetDir, 'spaps.app.json'),
288
417
  files_created: bookkeeping.files_created,
289
418
  files_overwritten: bookkeeping.files_overwritten,
290
- next_steps: [
291
- `cd ${targetDir}`,
292
- 'npm install',
293
- 'npx spaps local',
294
- ],
419
+ provisioning: {
420
+ status: provisioning.status,
421
+ reason: provisioning.reason || null,
422
+ application_id: provisioning.application?.id || null,
423
+ application_slug: provisioning.application?.slug || slug,
424
+ local_mode_active: provisioning.runtime?.local_mode?.active ?? null,
425
+ server_url: provisioning.runtime?.url || apiUrl,
426
+ },
427
+ warnings: provisioning.warnings || [],
428
+ next_steps: buildNextSteps({
429
+ targetDir,
430
+ provisioning,
431
+ port,
432
+ name: normalizedName,
433
+ template,
434
+ }),
295
435
  };
296
436
  }
297
437