spaps 0.7.2 → 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 (49) hide show
  1. package/AI_TOOLS.json +10 -11
  2. package/README.md +267 -110
  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 +5 -4
  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 +155 -24
  42. package/src/docs-system.js +7 -7
  43. package/src/error-handler.js +42 -0
  44. package/src/fixture-kernel.js +1143 -0
  45. package/src/handlers.js +252 -15
  46. package/src/help-system.js +3 -1
  47. package/src/local-runtime.js +258 -0
  48. package/src/local-server.js +597 -199
  49. package/src/project-scaffolder.js +441 -0
@@ -0,0 +1,441 @@
1
+ const fs = require('node:fs');
2
+ const path = require('node:path');
3
+
4
+ const { DEFAULT_PORT } = require('./config');
5
+ const { provisionStarterApplication } = require('./local-runtime');
6
+
7
+ const SUPPORTED_TEMPLATES = {
8
+ nextjs: {
9
+ label: 'Next.js starter',
10
+ blueprintKey: 'browser_auth',
11
+ allowedOrigins: ['http://localhost:3000'],
12
+ publicApiUrlEnv: 'NEXT_PUBLIC_SPAPS_API_URL',
13
+ publicApiKeyEnv: 'NEXT_PUBLIC_SPAPS_API_KEY',
14
+ files: ({ apiUrl }) => ({
15
+ 'lib/spaps.ts': `import { createBrowserClient, SPAPSClient } from 'spaps-sdk';
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
+ });
26
+ `,
27
+ 'app/providers.tsx': `'use client';
28
+
29
+ import { PropsWithChildren } from 'react';
30
+ import { spaps } from '../lib/spaps';
31
+
32
+ export function Providers({ children }: PropsWithChildren) {
33
+ void spaps;
34
+ return children;
35
+ }
36
+ `,
37
+ }),
38
+ },
39
+ react: {
40
+ label: 'React + Vite starter',
41
+ blueprintKey: 'browser_auth',
42
+ allowedOrigins: ['http://localhost:5173'],
43
+ publicApiUrlEnv: 'VITE_SPAPS_API_URL',
44
+ publicApiKeyEnv: 'VITE_SPAPS_API_KEY',
45
+ files: ({ apiUrl }) => ({
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;
50
+
51
+ export const spaps = apiKey && apiKey.startsWith('spaps_pub_')
52
+ ? createBrowserClient(apiKey, { apiUrl })
53
+ : new SPAPSClient({
54
+ apiUrl,
55
+ ...(apiKey ? { apiKey } : {}),
56
+ });
57
+ `,
58
+ }),
59
+ },
60
+ node: {
61
+ label: 'Node.js starter',
62
+ blueprintKey: 'default',
63
+ allowedOrigins: [],
64
+ publicApiUrlEnv: 'SPAPS_API_URL',
65
+ publicApiKeyEnv: 'SPAPS_API_KEY',
66
+ files: ({ apiUrl }) => ({
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;
71
+
72
+ const spaps = apiKey
73
+ ? createServerClient(apiKey, { apiUrl })
74
+ : new SPAPSClient({ apiUrl });
75
+
76
+ module.exports = { spaps };
77
+ `,
78
+ }),
79
+ },
80
+ vanilla: {
81
+ label: 'Vanilla JavaScript starter',
82
+ blueprintKey: 'browser_auth',
83
+ allowedOrigins: ['http://localhost:8080'],
84
+ publicApiUrlEnv: 'SPAPS_API_URL',
85
+ publicApiKeyEnv: 'SPAPS_API_KEY',
86
+ files: ({ apiUrl }) => ({
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;
91
+
92
+ export const spaps = apiKey && apiKey.startsWith('spaps_pub_')
93
+ ? createBrowserClient(apiKey, { apiUrl })
94
+ : new SPAPSClient({
95
+ apiUrl,
96
+ ...(apiKey ? { apiKey } : {}),
97
+ });
98
+ `,
99
+ }),
100
+ },
101
+ };
102
+
103
+ function createCliError(code, message) {
104
+ const error = new Error(message);
105
+ error.code = code;
106
+ return error;
107
+ }
108
+
109
+ function slugifyProjectName(name) {
110
+ return String(name)
111
+ .trim()
112
+ .toLowerCase()
113
+ .replace(/[^a-z0-9]+/g, '-')
114
+ .replace(/^-+|-+$/g, '');
115
+ }
116
+
117
+ function ensureSupportedTemplate(template) {
118
+ if (!template) {
119
+ throw createCliError(
120
+ 'EINVAL',
121
+ `The --template flag is required. Supported templates: ${Object.keys(SUPPORTED_TEMPLATES).join(', ')}.`
122
+ );
123
+ }
124
+
125
+ if (!SUPPORTED_TEMPLATES[template]) {
126
+ throw createCliError(
127
+ 'EINVAL',
128
+ `Unsupported template "${template}". Supported templates: ${Object.keys(SUPPORTED_TEMPLATES).join(', ')}.`
129
+ );
130
+ }
131
+ }
132
+
133
+ function ensureWritableTarget(targetDir, force) {
134
+ if (!fs.existsSync(targetDir)) {
135
+ return;
136
+ }
137
+
138
+ const entries = fs.readdirSync(targetDir);
139
+ if (entries.length > 0 && !force) {
140
+ throw createCliError(
141
+ 'EEXIST',
142
+ `Target directory "${targetDir}" is not empty. Re-run with --force to overwrite managed files.`
143
+ );
144
+ }
145
+ }
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
+
153
+ function writeManagedFile(targetDir, relativePath, content, bookkeeping) {
154
+ const fullPath = path.join(targetDir, relativePath);
155
+ fs.mkdirSync(path.dirname(fullPath), { recursive: true });
156
+
157
+ if (fs.existsSync(fullPath)) {
158
+ bookkeeping.files_overwritten.push(relativePath);
159
+ } else {
160
+ bookkeeping.files_created.push(relativePath);
161
+ }
162
+
163
+ fs.writeFileSync(fullPath, content);
164
+ }
165
+
166
+ function buildPackageJson(name, template) {
167
+ const pkg = {
168
+ name,
169
+ private: true,
170
+ version: '0.0.0',
171
+ description: `SPAPS ${SUPPORTED_TEMPLATES[template].label.toLowerCase()} for ${name}`,
172
+ dependencies: {
173
+ 'spaps-sdk': 'latest',
174
+ },
175
+ };
176
+
177
+ if (template === 'node') {
178
+ pkg.type = 'commonjs';
179
+ }
180
+
181
+ return `${JSON.stringify(pkg, null, 2)}\n`;
182
+ }
183
+
184
+ function buildContract({ name, slug, template, version, apiUrl, docsUrl, provisioning }) {
185
+ const templateDef = SUPPORTED_TEMPLATES[template];
186
+ const contract = {
187
+ name,
188
+ slug,
189
+ template,
190
+ created_with: `spaps@${version}`,
191
+ spaps: {
192
+ local: {
193
+ api_url: apiUrl,
194
+ docs_url: docsUrl,
195
+ local_mode_active: provisioning.runtime?.local_mode?.active ?? null,
196
+ },
197
+ application: {
198
+ id: provisioning.application?.id || null,
199
+ slug,
200
+ blueprint_key: templateDef.blueprintKey,
201
+ allowed_origins: templateDef.allowedOrigins,
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,
209
+ },
210
+ },
211
+ };
212
+
213
+ return `${JSON.stringify(contract, null, 2)}\n`;
214
+ }
215
+
216
+ function buildEnvFile(template, apiUrl, provisioning) {
217
+ const templateDef = SUPPORTED_TEMPLATES[template];
218
+ const lines = [
219
+ '# SPAPS local development',
220
+ `SPAPS_API_URL=${apiUrl}`,
221
+ ];
222
+
223
+ if (templateDef.publicApiUrlEnv !== 'SPAPS_API_URL') {
224
+ lines.push(`${templateDef.publicApiUrlEnv}=${apiUrl}`);
225
+ }
226
+
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=');
240
+ }
241
+
242
+ return `${lines.join('\n')}\n`;
243
+ }
244
+
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 }) {
283
+ const templateDef = SUPPORTED_TEMPLATES[template];
284
+
285
+ return `# ${name}
286
+
287
+ This directory is a SPAPS ${templateDef.label.toLowerCase()}.
288
+
289
+ It gives you three things immediately:
290
+
291
+ - a machine-readable SPAPS app contract in \`spaps.app.json\`
292
+ - local env wiring in \`.env.local\`
293
+ - a small template-specific integration starter you can drop into a real app
294
+
295
+ This is not a full framework generator. It does not run \`create-next-app\`, Vite, or Express setup for you.
296
+
297
+ ${buildProvisioningNote({ provisioning, targetDir, template })}## Next Steps
298
+
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}\`
302
+
303
+ ## Generated Files
304
+
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
307
+ - \`package.json\`: minimal dependency declaration for \`spaps-sdk\`
308
+ `;
309
+ }
310
+
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({
339
+ name,
340
+ template,
341
+ dir = null,
342
+ force = false,
343
+ version = '0.0.0',
344
+ port = DEFAULT_PORT,
345
+ }) {
346
+ if (!name || !String(name).trim()) {
347
+ throw createCliError('EINVAL', 'Project name is required.');
348
+ }
349
+
350
+ ensureSupportedTemplate(template);
351
+
352
+ const normalizedName = String(name).trim();
353
+ const slug = slugifyProjectName(normalizedName);
354
+ const targetDir = path.resolve(dir || path.join(process.cwd(), slug));
355
+ const apiUrl = `http://localhost:${port}`;
356
+ const docsUrl = `${apiUrl}/docs`;
357
+ const bookkeeping = {
358
+ files_created: [],
359
+ files_overwritten: [],
360
+ };
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
+ });
370
+
371
+ writeManagedFile(
372
+ targetDir,
373
+ 'spaps.app.json',
374
+ buildContract({
375
+ name: normalizedName,
376
+ slug,
377
+ template,
378
+ version,
379
+ apiUrl,
380
+ docsUrl,
381
+ provisioning,
382
+ }),
383
+ bookkeeping
384
+ );
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
+ );
392
+ writeManagedFile(targetDir, 'package.json', buildPackageJson(normalizedName, template), bookkeeping);
393
+ writeManagedFile(
394
+ targetDir,
395
+ '.gitignore',
396
+ 'node_modules\n.env\n.env.local\n',
397
+ bookkeeping
398
+ );
399
+
400
+ const templateFiles = templateDef.files({
401
+ name: normalizedName,
402
+ slug,
403
+ apiUrl,
404
+ });
405
+
406
+ for (const [relativePath, content] of Object.entries(templateFiles)) {
407
+ writeManagedFile(targetDir, relativePath, content, bookkeeping);
408
+ }
409
+
410
+ return {
411
+ success: true,
412
+ command: 'create',
413
+ project_name: normalizedName,
414
+ template,
415
+ target_dir: targetDir,
416
+ contract_path: path.join(targetDir, 'spaps.app.json'),
417
+ files_created: bookkeeping.files_created,
418
+ files_overwritten: bookkeeping.files_overwritten,
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
+ }),
435
+ };
436
+ }
437
+
438
+ module.exports = {
439
+ SUPPORTED_TEMPLATES,
440
+ createProjectStarter,
441
+ };