@topolo/cli 0.2.1 → 0.2.3

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 (2) hide show
  1. package/dist/index.js +500 -67
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -2582,6 +2582,7 @@ var TopoloClient = class _TopoloClient {
2582
2582
  const kind = payload.credentialType === "api_key" ? "api_key" : "access_token";
2583
2583
  return {
2584
2584
  kind,
2585
+ authorizationMode: normalizeCredentialAuthorizationMode(payload.authorizationMode, kind),
2585
2586
  user: {
2586
2587
  id: user.id,
2587
2588
  email: user.email,
@@ -2844,6 +2845,11 @@ function objectToQuery(value) {
2844
2845
  }
2845
2846
  return Object.keys(query).length > 0 ? query : void 0;
2846
2847
  }
2848
+ function normalizeCredentialAuthorizationMode(value, kind) {
2849
+ if (kind === "api_key") return "api_key";
2850
+ if (value === "context_rbac" || value === "oauth_scopes" || value === "session_rbac") return value;
2851
+ return "unknown";
2852
+ }
2847
2853
  function describeError(body, fallback) {
2848
2854
  if (body && typeof body === "object") {
2849
2855
  const maybe = body;
@@ -3312,16 +3318,6 @@ function writeContextCredential(config, environment, ctx, makeActive = true) {
3312
3318
  }
3313
3319
  return writeProfileConfig(config, environment, next);
3314
3320
  }
3315
- function activateContext(config, environment, key) {
3316
- const profile = profileForEnvironment(config, environment);
3317
- const ctx = profile.contexts?.[key];
3318
- if (!ctx) return config;
3319
- return writeProfileConfig(config, environment, {
3320
- activeContext: key,
3321
- credential: ctx.credential,
3322
- oauthClientId: ctx.oauthClientId ?? profile.oauthClientId
3323
- });
3324
- }
3325
3321
  function clearProfileCredential(config, environment) {
3326
3322
  const next = { ...config };
3327
3323
  if (environment === "production") {
@@ -3456,7 +3452,7 @@ function resolveOAuthClientId(config, environment = selectedEnvironment()) {
3456
3452
  const envSpecific = process.env[`${envVarPrefix(environment)}_CLI_CLIENT_ID`];
3457
3453
  if (envSpecific) return envSpecific;
3458
3454
  if (process.env.TOPOLO_CLI_CLIENT_ID) return process.env.TOPOLO_CLI_CLIENT_ID;
3459
- return profileForEnvironment(config, environment).oauthClientId ?? DEFAULT_CLI_CLIENT_IDS[environment];
3455
+ return DEFAULT_CLI_CLIENT_IDS[environment] ?? profileForEnvironment(config, environment).oauthClientId;
3460
3456
  }
3461
3457
  var NON_PROD_URL_MARKERS = {
3462
3458
  staging: [".stg.topolo.us", "topolo-staging"],
@@ -3842,7 +3838,7 @@ function stderrDebugLogger(event) {
3842
3838
 
3843
3839
  // src/commands/whoami.ts
3844
3840
  function registerWhoamiCommand(program2) {
3845
- program2.command("whoami").description("Show the user, organization, and scopes attached to the current credential").option("--json", "Output JSON").option("--no-json", "Output human-readable text").action(async (opts) => {
3841
+ program2.command("whoami").description("Show the current user, organization, and authorization mode").option("--json", "Output JSON").option("--no-json", "Output human-readable text").action(async (opts) => {
3846
3842
  const mode = resolveOutputMode(opts.json);
3847
3843
  try {
3848
3844
  const environment = selectedEnvironment();
@@ -3867,7 +3863,10 @@ function formatPretty(info) {
3867
3863
  `Organization: ${info.organization ? `${info.organization.name} (${info.organization.slug})` : "(none)"}`
3868
3864
  );
3869
3865
  lines.push(`Credential: ${info.kind}`);
3870
- lines.push(`Scopes: ${info.user.permissions.length === 0 ? "(none)" : ""}`);
3866
+ const dynamicRbac = info.authorizationMode === "context_rbac";
3867
+ const authorization = dynamicRbac ? "Dynamic RBAC (checked per application action)" : info.authorizationMode === "oauth_scopes" ? "Delegated OAuth scopes" : info.authorizationMode === "api_key" ? "API key scopes" : info.authorizationMode === "session_rbac" ? "Session RBAC" : "Credential scopes";
3868
+ lines.push(`Authorization: ${authorization}`);
3869
+ lines.push(`${dynamicRbac ? "Identity scopes" : "Scopes"}: ${info.user.permissions.length === 0 ? "(none)" : ""}`);
3871
3870
  for (const p of info.user.permissions) lines.push(` - ${p}`);
3872
3871
  return lines.join("\n");
3873
3872
  }
@@ -5179,7 +5178,6 @@ function isFile(filePath) {
5179
5178
  }
5180
5179
 
5181
5180
  // src/commands/app-scaffold.ts
5182
- import { randomBytes } from "crypto";
5183
5181
  import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
5184
5182
  import { dirname as dirname3, resolve as resolve2 } from "path";
5185
5183
 
@@ -5323,6 +5321,12 @@ function firstPartyAppPath(appDir, _audience) {
5323
5321
  return `apps/${appDir}`;
5324
5322
  }
5325
5323
 
5324
+ // src/commands/app-scaffold-cloudflare-constants.ts
5325
+ var CLOUDFLARE_ACCOUNT_ID = "49ef1ba682ad8cfd720c86699ae17521";
5326
+ var STAGING_CLOUDFLARE_ACCOUNT_ID = "4f4e1c69a3830946f9fea7b1eb7531ac";
5327
+ var DEVELOPMENT_CLOUDFLARE_ACCOUNT_ID = "0ebdeec262a245481a721fc4f9d6d798";
5328
+ var CLOUDFLARE_COMPATIBILITY_DATE = "2026-05-05";
5329
+
5326
5330
  // src/commands/app-scaffold-package-template.ts
5327
5331
  function packageJson(application, target, options) {
5328
5332
  const dependencies = {};
@@ -5361,17 +5365,44 @@ function packageJson(application, target, options) {
5361
5365
  devDependencies["@cloudflare/workers-types"] = "^4.20260413.1";
5362
5366
  if (target === "platform") {
5363
5367
  dependencies["@topolo-io/worker-runtime"] = topoloPackageVersion("@topolo-io/worker-runtime", target);
5368
+ dependencies["@topolo-io/auth-middleware"] = topoloPackageVersion("@topolo-io/auth-middleware", target);
5369
+ dependencies["@topolo-io/error-middleware"] = topoloPackageVersion("@topolo-io/error-middleware", target);
5370
+ dependencies["@topolo-io/security-headers"] = topoloPackageVersion("@topolo-io/security-headers", target);
5371
+ dependencies["@topolo-io/resource-context"] = topoloPackageVersion("@topolo-io/resource-context", target);
5372
+ dependencies["@topolo-io/data"] = topoloPackageVersion("@topolo-io/data", target);
5364
5373
  dependencies["@topolo/sdk"] = topoloPackageVersion("@topolo/sdk", target);
5365
5374
  dependencies.hono = "^4";
5375
+ dependencies.zod = "^4.4.3";
5366
5376
  scripts.deploy = `node -e 'console.error("\\nX Bare deploy is disabled. Use deploy:staging or deploy:production.\\n"); process.exit(1)'`;
5367
- scripts["deploy:staging"] = "DEPLOY_ENVIRONMENT=staging VITE_TOPOLO_AUTH_URL=https://auth.stg.topolo.us npm run build && wrangler deploy --config apps/api/wrangler.toml --env staging && wrangler deploy --config apps/web/wrangler.toml --env staging";
5368
- scripts["deploy:production"] = "DEPLOY_ENVIRONMENT=production VITE_TOPOLO_AUTH_URL=https://auth.topolo.app npm run build && wrangler deploy --config apps/api/wrangler.toml --env production && wrangler deploy --config apps/web/wrangler.toml --env production";
5377
+ scripts["deploy:staging"] = `DEPLOY_ENVIRONMENT=staging CLOUDFLARE_ACCOUNT_ID=${STAGING_CLOUDFLARE_ACCOUNT_ID} python3 .github/scripts/standard_deploy.py local-deploy`;
5378
+ scripts["deploy:production"] = `DEPLOY_ENVIRONMENT=production CLOUDFLARE_ACCOUNT_ID=${CLOUDFLARE_ACCOUNT_ID} python3 .github/scripts/standard_deploy.py local-deploy`;
5369
5379
  } else {
5370
5380
  dependencies["@topolo/sdk"] = topoloPackageVersion("@topolo/sdk", target);
5371
5381
  scripts.deploy = "npm run build && wrangler deploy";
5372
5382
  }
5373
5383
  devDependencies.wrangler = "^4.91.0";
5374
5384
  }
5385
+ if (target === "platform") {
5386
+ const testDirs = [];
5387
+ if (options.browser) testDirs.push("apps/web");
5388
+ if (options.api) testDirs.push("apps/api");
5389
+ if (testDirs.length > 0) {
5390
+ const chain = (withCoverage) => testDirs.map((dir, index) => {
5391
+ const enter = index === 0 ? `cd ${dir}` : `cd ../${dir.split("/").pop()}`;
5392
+ return `${enter} && vitest run --config vitest.config.ts --passWithNoTests${withCoverage ? " --coverage" : ""}`;
5393
+ }).join(" && ");
5394
+ scripts.test = chain(false);
5395
+ scripts.coverage = chain(true);
5396
+ scripts.typecheck = testDirs.map((dir) => `tsc --noEmit -p ${dir}/tsconfig.json`).join(" && ");
5397
+ devDependencies.vitest = "^3.2.4";
5398
+ devDependencies["@vitest/coverage-v8"] = "^3.2.4";
5399
+ if (options.browser) {
5400
+ devDependencies.jsdom = "^29.1.1";
5401
+ devDependencies["@testing-library/react"] = "^16.3.2";
5402
+ devDependencies["@testing-library/user-event"] = "^14.6.1";
5403
+ }
5404
+ }
5405
+ }
5375
5406
  if (target === "platform" && hasScaffoldResources(options.resources)) {
5376
5407
  scripts["check:deploy-config"] = "wrangler deploy --config apps/api/wrangler.toml --env staging --dry-run --outdir .wrangler/api-build && wrangler deploy --config apps/web/wrangler.toml --env staging --dry-run --outdir .wrangler/web-build";
5377
5408
  }
@@ -5400,7 +5431,11 @@ function topoloPackageVersion(packageName, target) {
5400
5431
  "@topolo-io/app-launcher": "^0.1.15",
5401
5432
  "@topolo-io/app-shell": "^0.5.54",
5402
5433
  "@topolo-io/auth-client": "^1.2.1",
5403
- "@topolo-io/auth-middleware": "^0.3.4",
5434
+ "@topolo-io/auth-middleware": "^0.4.0",
5435
+ "@topolo-io/error-middleware": "^0.6.0",
5436
+ "@topolo-io/security-headers": "^0.2.2",
5437
+ "@topolo-io/resource-context": "^0.3.0",
5438
+ "@topolo-io/data": "^0.1.2",
5404
5439
  "@topolo-io/worker-runtime": "^0.1.11",
5405
5440
  "@topolo-io/design-tokens": "^0.1.8",
5406
5441
  "@topolo-io/onboarding": "^0.9.5",
@@ -5436,7 +5471,12 @@ function resourceConfig(application, profile) {
5436
5471
  workflowProductionName: `topolo-${slug}-import`,
5437
5472
  workflowStagingName: `topolo-${slug}-import-staging`,
5438
5473
  containerProductionName: `topolo-${slug}-processor`,
5439
- containerStagingName: `topolo-${slug}-processor-staging`
5474
+ containerStagingName: `topolo-${slug}-processor-staging`,
5475
+ d1DevelopmentName: `topolo-${slug}-core-development`,
5476
+ r2DevelopmentName: `topolo-${slug}-assets-development`,
5477
+ queueDevelopmentName: `topolo-${slug}-processing-development`,
5478
+ workflowDevelopmentName: `topolo-${slug}-import-development`,
5479
+ containerDevelopmentName: `topolo-${slug}-processor-development`
5440
5480
  };
5441
5481
  }
5442
5482
  return {
@@ -5454,7 +5494,12 @@ function resourceConfig(application, profile) {
5454
5494
  workflowProductionName: `topolo-${slug}-workflow`,
5455
5495
  workflowStagingName: `topolo-${slug}-workflow-staging`,
5456
5496
  containerProductionName: `topolo-${slug}-processor`,
5457
- containerStagingName: `topolo-${slug}-processor-staging`
5497
+ containerStagingName: `topolo-${slug}-processor-staging`,
5498
+ d1DevelopmentName: `topolo-${slug}-core-development`,
5499
+ r2DevelopmentName: `topolo-${slug}-assets-development`,
5500
+ queueDevelopmentName: `topolo-${slug}-queue-development`,
5501
+ workflowDevelopmentName: `topolo-${slug}-workflow-development`,
5502
+ containerDevelopmentName: `topolo-${slug}-processor-development`
5458
5503
  };
5459
5504
  }
5460
5505
 
@@ -5627,6 +5672,64 @@ function tsconfigJson(api, target) {
5627
5672
  })}
5628
5673
  `;
5629
5674
  }
5675
+ var TSCONFIG_TEST_EXCLUDE = [
5676
+ "src/**/*.test.ts",
5677
+ "src/**/*.test.tsx",
5678
+ "src/**/*.test.mts",
5679
+ "src/**/*.spec.ts",
5680
+ "src/**/*.spec.tsx",
5681
+ "test"
5682
+ ];
5683
+ function webTsconfigJson() {
5684
+ return `${json({
5685
+ compilerOptions: {
5686
+ target: "ES2022",
5687
+ useDefineForClassFields: true,
5688
+ lib: ["ES2022", "DOM", "DOM.Iterable"],
5689
+ allowJs: false,
5690
+ skipLibCheck: true,
5691
+ esModuleInterop: true,
5692
+ allowSyntheticDefaultImports: true,
5693
+ strict: true,
5694
+ forceConsistentCasingInFileNames: true,
5695
+ module: "ESNext",
5696
+ moduleResolution: "Bundler",
5697
+ resolveJsonModule: true,
5698
+ isolatedModules: true,
5699
+ noEmit: true,
5700
+ jsx: "react-jsx",
5701
+ types: ["vite/client"]
5702
+ },
5703
+ include: ["src"],
5704
+ references: [],
5705
+ exclude: TSCONFIG_TEST_EXCLUDE
5706
+ })}
5707
+ `;
5708
+ }
5709
+ function apiTsconfigJson() {
5710
+ return `${json({
5711
+ compilerOptions: {
5712
+ target: "ES2022",
5713
+ lib: ["ES2022", "DOM", "DOM.Iterable", "WebWorker"],
5714
+ allowJs: false,
5715
+ skipLibCheck: true,
5716
+ esModuleInterop: true,
5717
+ allowSyntheticDefaultImports: true,
5718
+ strict: true,
5719
+ forceConsistentCasingInFileNames: true,
5720
+ module: "ESNext",
5721
+ moduleResolution: "Bundler",
5722
+ resolveJsonModule: true,
5723
+ isolatedModules: true,
5724
+ noEmit: true,
5725
+ types: ["@cloudflare/workers-types", "node"]
5726
+ },
5727
+ include: ["src"],
5728
+ references: [],
5729
+ exclude: TSCONFIG_TEST_EXCLUDE
5730
+ })}
5731
+ `;
5732
+ }
5630
5733
  function gitignore() {
5631
5734
  return [
5632
5735
  "node_modules/",
@@ -6057,12 +6160,15 @@ function platformAppTsx(application, options) {
6057
6160
  " }",
6058
6161
  "",
6059
6162
  " if (!auth) {",
6163
+ " // Pre-AppRoot: appId is still resolving, so auth can't be built yet.",
6164
+ " // Render the SAME scaffold layout AppRoot uses below so this phase and",
6165
+ " // the auth-gate fallback read as one continuous loading state (the",
6166
+ " // scaffold resolves theme + rail width on its own to match the shell).",
6060
6167
  " return (",
6061
6168
  " <TopoloAppLoadingScaffold",
6062
6169
  " appId={appId}",
6063
6170
  " appName={APP_NAME}",
6064
6171
  " layout={{ shell: 'topbar', content: 'workspace' }}",
6065
- " subtitle={`Loading ${APP_NAME}...`}",
6066
6172
  " />",
6067
6173
  " );",
6068
6174
  " }",
@@ -6103,8 +6209,6 @@ function platformAppTsx(application, options) {
6103
6209
  " appId={appId}",
6104
6210
  " appName={APP_NAME}",
6105
6211
  " layout={{ shell: loading?.shell || 'topbar', content: loading?.content || 'workspace' }}",
6106
- " title={loading?.title}",
6107
- " subtitle={`Loading ${APP_NAME}...`}",
6108
6212
  " />",
6109
6213
  " )}",
6110
6214
  " shell={{",
@@ -6368,11 +6472,6 @@ function developerAppTsx(application) {
6368
6472
  ].join("\n");
6369
6473
  }
6370
6474
 
6371
- // src/commands/app-scaffold-cloudflare-constants.ts
6372
- var CLOUDFLARE_ACCOUNT_ID = "49ef1ba682ad8cfd720c86699ae17521";
6373
- var STAGING_CLOUDFLARE_ACCOUNT_ID = "4f4e1c69a3830946f9fea7b1eb7531ac";
6374
- var CLOUDFLARE_COMPATIBILITY_DATE = "2026-05-05";
6375
-
6376
6475
  // src/commands/app-scaffold-worker-template.ts
6377
6476
  function webWorkerTs(application) {
6378
6477
  const serviceName = toTitle(application.name);
@@ -6427,6 +6526,29 @@ function platformWebWranglerToml(application) {
6427
6526
  'binding = "API"',
6428
6527
  `service = "topolo-${slug}-api"`,
6429
6528
  "",
6529
+ "[env.development]",
6530
+ `name = "topolo-${slug}-development"`,
6531
+ `account_id = "${DEVELOPMENT_CLOUDFLARE_ACCOUNT_ID}"`,
6532
+ "workers_dev = true",
6533
+ "",
6534
+ "[env.development.vars]",
6535
+ `APP_ID = "${application.appId ?? ""}"`,
6536
+ `VITE_TOPOLO_APP_ID = "${application.appId ?? ""}"`,
6537
+ "",
6538
+ "[[env.development.routes]]",
6539
+ `pattern = "${slug}.topolo.dev"`,
6540
+ "custom_domain = true",
6541
+ "",
6542
+ "[env.development.assets]",
6543
+ 'directory = "./dist"',
6544
+ 'binding = "ASSETS"',
6545
+ 'not_found_handling = "single-page-application"',
6546
+ 'run_worker_first = ["/api/*"]',
6547
+ "",
6548
+ "[[env.development.services]]",
6549
+ 'binding = "API"',
6550
+ `service = "topolo-${slug}-api-development"`,
6551
+ "",
6430
6552
  "[env.staging]",
6431
6553
  `name = "topolo-${slug}-staging"`,
6432
6554
  `account_id = "${STAGING_CLOUDFLARE_ACCOUNT_ID}"`,
@@ -6498,6 +6620,22 @@ function platformApiWranglerToml(application, resources, profile) {
6498
6620
  ""
6499
6621
  ];
6500
6622
  appendResourceWranglerBlocks(lines, resources, resource, "production");
6623
+ lines.push(
6624
+ "[env.development]",
6625
+ `name = "topolo-${slug}-api-development"`,
6626
+ `account_id = "${DEVELOPMENT_CLOUDFLARE_ACCOUNT_ID}"`,
6627
+ "workers_dev = true",
6628
+ "",
6629
+ "[env.development.vars]",
6630
+ 'AUTH_BASE_URL = "https://auth.topolo.dev"',
6631
+ 'NEXUS_GATEWAY_URL = "https://nexus.topolo.dev"',
6632
+ `NEXUS_APP_ID = "${slug}"`,
6633
+ `APP_ID = "${application.appId ?? ""}"`,
6634
+ `VITE_TOPOLO_APP_ID = "${application.appId ?? ""}"`,
6635
+ 'ENVIRONMENT = "development"',
6636
+ ""
6637
+ );
6638
+ appendResourceWranglerBlocks(lines, resources, resource, "development");
6501
6639
  lines.push(
6502
6640
  "[env.staging]",
6503
6641
  `name = "topolo-${slug}-api-staging"`,
@@ -6531,13 +6669,14 @@ function platformApiWranglerToml(application, resources, profile) {
6531
6669
  return lines.join("\n");
6532
6670
  }
6533
6671
  function appendResourceWranglerBlocks(lines, resources, resource, environment) {
6534
- const prefix = environment === "staging" ? "env.staging." : environment === "production-env" ? "env.production." : "";
6535
- const envName = environment === "staging" ? "Staging" : "Production";
6672
+ const prefix = environment === "staging" ? "env.staging." : environment === "development" ? "env.development." : environment === "production-env" ? "env.production." : "";
6673
+ const envName = environment === "staging" ? "Staging" : environment === "development" ? "Development" : "Production";
6674
+ const pick = (dev, staging, prod) => environment === "development" ? dev : environment === "staging" ? staging : prod;
6536
6675
  if (resources.d1) {
6537
6676
  lines.push(
6538
6677
  `[[${prefix}d1_databases]]`,
6539
6678
  `binding = "${resource.d1Binding}"`,
6540
- `database_name = "${environment === "staging" ? resource.d1StagingName : resource.d1ProductionName}"`,
6679
+ `database_name = "${pick(resource.d1DevelopmentName, resource.d1StagingName, resource.d1ProductionName)}"`,
6541
6680
  'database_id = "provision-after-scaffold"',
6542
6681
  'migrations_dir = "migrations"',
6543
6682
  ""
@@ -6547,7 +6686,7 @@ function appendResourceWranglerBlocks(lines, resources, resource, environment) {
6547
6686
  lines.push(
6548
6687
  `[[${prefix}r2_buckets]]`,
6549
6688
  `binding = "${resource.r2Binding}"`,
6550
- `bucket_name = "${environment === "staging" ? resource.r2StagingName : resource.r2ProductionName}"`,
6689
+ `bucket_name = "${pick(resource.r2DevelopmentName, resource.r2StagingName, resource.r2ProductionName)}"`,
6551
6690
  ""
6552
6691
  );
6553
6692
  }
@@ -6555,10 +6694,10 @@ function appendResourceWranglerBlocks(lines, resources, resource, environment) {
6555
6694
  lines.push(
6556
6695
  `[[${prefix}queues.producers]]`,
6557
6696
  `binding = "${resource.queueBinding}"`,
6558
- `queue = "${environment === "staging" ? resource.queueStagingName : resource.queueProductionName}"`,
6697
+ `queue = "${pick(resource.queueDevelopmentName, resource.queueStagingName, resource.queueProductionName)}"`,
6559
6698
  "",
6560
6699
  `[[${prefix}queues.consumers]]`,
6561
- `queue = "${environment === "staging" ? resource.queueStagingName : resource.queueProductionName}"`,
6700
+ `queue = "${pick(resource.queueDevelopmentName, resource.queueStagingName, resource.queueProductionName)}"`,
6562
6701
  ""
6563
6702
  );
6564
6703
  }
@@ -6566,7 +6705,7 @@ function appendResourceWranglerBlocks(lines, resources, resource, environment) {
6566
6705
  lines.push(
6567
6706
  `[[${prefix}workflows]]`,
6568
6707
  `binding = "${resource.workflowBinding}"`,
6569
- `name = "${environment === "staging" ? resource.workflowStagingName : resource.workflowProductionName}"`,
6708
+ `name = "${pick(resource.workflowDevelopmentName, resource.workflowStagingName, resource.workflowProductionName)}"`,
6570
6709
  `class_name = "${resource.workflowClassName}"`,
6571
6710
  ""
6572
6711
  );
@@ -6764,7 +6903,9 @@ function platformWorkerTs(application, options) {
6764
6903
  "import { Hono } from 'hono';",
6765
6904
  "import { createTopoloWidgetResponse, type TopoloWidgetDraft } from '@topolo/sdk';",
6766
6905
  "import { createTopoloWorkerRuntime, type TopoloRuntimeContext } from '@topolo-io/worker-runtime';",
6767
- "import { SCAFFOLD_APP_ID, SCAFFOLD_APP_NAME, SCAFFOLD_SERVICE_SLUG, resolveRuntimeAppId } from './app-identity';",
6906
+ "import { withSecurityHeaders } from '@topolo-io/security-headers';",
6907
+ "import { withErrorReporting } from '@topolo-io/error-middleware';",
6908
+ "import { SCAFFOLD_APP_NAME, SCAFFOLD_SERVICE_SLUG, resolveRuntimeAppId } from './app-identity';",
6768
6909
  ...options.resources.workflow ? ["import { WorkflowEntrypoint, type WorkflowEvent, type WorkflowStep } from 'cloudflare:workers';"] : [],
6769
6910
  "",
6770
6911
  "export interface Env {",
@@ -6789,12 +6930,10 @@ function platformWorkerTs(application, options) {
6789
6930
  "const runtime = createTopoloWorkerRuntime<Env>({",
6790
6931
  " appName: SCAFFOLD_APP_NAME,",
6791
6932
  " serviceSlug: SCAFFOLD_SERVICE_SLUG,",
6792
- " resolveAppId: (env) => resolveRuntimeAppId(env),",
6933
+ " // Auth verification base URL is environment-driven (staging/prod/dev),",
6934
+ " // so rt.verifyAuth() targets the correct Auth service per deploy.",
6793
6935
  " authBaseUrl: (env) => env.AUTH_BASE_URL,",
6794
- " usageMonitoring: {",
6795
- " appId: SCAFFOLD_APP_ID,",
6796
- " nexusGatewayUrl: (env) => env.NEXUS_GATEWAY_URL,",
6797
- " },",
6936
+ " authGateway: { enabled: false },",
6798
6937
  "});",
6799
6938
  "",
6800
6939
  "async function handleRequest(",
@@ -6854,11 +6993,20 @@ function platformWorkerTs(application, options) {
6854
6993
  " return handleRequest(c.req.raw, c.env, rt);",
6855
6994
  "});",
6856
6995
  "",
6996
+ "// Platform security headers + structured error reporting wrap the whole app",
6997
+ "// (outermost), so headers are stamped even on error-middleware 5xx responses.",
6998
+ "const securedFetch = withSecurityHeaders(",
6999
+ " withErrorReporting(",
7000
+ " (request: Request, env: Env, ctx: ExecutionContext) => api.fetch(request, env, ctx),",
7001
+ " { appName: SCAFFOLD_APP_NAME, appId: SCAFFOLD_SERVICE_SLUG },",
7002
+ " ),",
7003
+ ") as (request: Request, env: Env, ctx: ExecutionContext) => Response | Promise<Response>;",
7004
+ "",
6857
7005
  "export default {",
6858
7006
  " fetch: runtime.worker((request, env, ctx, rt) => {",
6859
7007
  " const bindings = env as Env & { __rt?: TopoloRuntimeContext<Env> };",
6860
7008
  " bindings.__rt = rt;",
6861
- " return api.fetch(request, bindings, ctx as ExecutionContext);",
7009
+ " return securedFetch(request, bindings, ctx as ExecutionContext);",
6862
7010
  " }),",
6863
7011
  "};",
6864
7012
  "",
@@ -7207,13 +7355,17 @@ function topoloAppManifestJson(application, options) {
7207
7355
  }
7208
7356
  } : {}
7209
7357
  };
7358
+ const actions = options.api ? baselineApiActions() : [];
7210
7359
  return `${json({
7211
7360
  $schema: "./../../topolo-platform/tooling/app-manifest-schema.json",
7212
7361
  name: `topolo-${slug}`,
7213
7362
  appId: application.appId,
7214
- primaryTarget: "web",
7215
7363
  compatibilityDate: CLOUDFLARE_COMPATIBILITY_DATE,
7216
7364
  environments: {
7365
+ development: {
7366
+ accountId: DEVELOPMENT_CLOUDFLARE_ACCOUNT_ID,
7367
+ domain: "topolo.dev"
7368
+ },
7217
7369
  staging: {
7218
7370
  accountId: STAGING_CLOUDFLARE_ACCOUNT_ID,
7219
7371
  domain: "stg.topolo.us"
@@ -7223,10 +7375,87 @@ function topoloAppManifestJson(application, options) {
7223
7375
  domain: "topolo.app"
7224
7376
  }
7225
7377
  },
7226
- targets
7378
+ targets,
7379
+ ...actions.length ? { actions } : {}
7227
7380
  })}
7228
7381
  `;
7229
7382
  }
7383
+ function topoloActionContractsMjs() {
7384
+ return [
7385
+ "// Canonical action contracts \u2014 the source of truth for the agent/SDK/MCP",
7386
+ "// surface. Each entry maps 1:1 to a worker route (method + path) and validates",
7387
+ "// its input with zod. Add product actions here and mirror them into",
7388
+ "// topolo.app.json actions[]. Mirrors the normalized fleet convention.",
7389
+ "",
7390
+ "import { z } from 'zod';",
7391
+ "",
7392
+ "const emptyInput = z.object({}).strict();",
7393
+ "",
7394
+ "export const topoloActionContracts = {",
7395
+ " 'widget.get': {",
7396
+ " name: 'widget.get',",
7397
+ " method: 'GET',",
7398
+ " path: '/api/widget',",
7399
+ " input: emptyInput,",
7400
+ " },",
7401
+ " 'workspace.bootstrap': {",
7402
+ " name: 'workspace.bootstrap',",
7403
+ " method: 'GET',",
7404
+ " path: '/api/bootstrap',",
7405
+ " input: emptyInput,",
7406
+ " },",
7407
+ "};",
7408
+ "",
7409
+ "export const topoloActionInputSchemas = Object.fromEntries(",
7410
+ " Object.entries(topoloActionContracts).map(([name, contract]) => [",
7411
+ " name,",
7412
+ " z.toJSONSchema(contract.input),",
7413
+ " ]),",
7414
+ ");",
7415
+ ""
7416
+ ].join("\n");
7417
+ }
7418
+ function baselineApiActions() {
7419
+ const emptyInputSchema = {
7420
+ $schema: "https://json-schema.org/draft/2020-12/schema",
7421
+ type: "object",
7422
+ properties: {},
7423
+ additionalProperties: false
7424
+ };
7425
+ const openOutputSchema = { type: "object", additionalProperties: true };
7426
+ return [
7427
+ {
7428
+ name: "widget.get",
7429
+ title: "Get widget summary",
7430
+ description: "Get the TopoloOne widget summary for this service.",
7431
+ method: "GET",
7432
+ path: "/api/widget",
7433
+ permission: "workspace:read",
7434
+ readOnly: true,
7435
+ inputSchema: emptyInputSchema,
7436
+ outputSchema: openOutputSchema,
7437
+ target: "api",
7438
+ destructive: false,
7439
+ requiresConfirmation: false,
7440
+ agentAccess: "auto"
7441
+ },
7442
+ {
7443
+ name: "workspace.bootstrap",
7444
+ title: "Get workspace bootstrap",
7445
+ description: "Get the signed workspace bootstrap payload for this service.",
7446
+ method: "GET",
7447
+ path: "/api/bootstrap",
7448
+ permission: "workspace:read",
7449
+ readOnly: true,
7450
+ inputSchema: emptyInputSchema,
7451
+ outputSchema: openOutputSchema,
7452
+ target: "api",
7453
+ destructive: false,
7454
+ requiresConfirmation: false,
7455
+ agentAccess: "auto"
7456
+ }
7457
+ ];
7458
+ }
7230
7459
  function cloudcontrolJson(application, options) {
7231
7460
  const slug = toSlug(application.id);
7232
7461
  const appPath = firstPartyAppPath(application.appDir, options.audience);
@@ -7254,6 +7483,15 @@ function cloudcontrolJson(application, options) {
7254
7483
  resource_name: resources.d1StagingName,
7255
7484
  resource_id: "provision-after-scaffold",
7256
7485
  migrations_dir: "migrations"
7486
+ },
7487
+ {
7488
+ resource_type: "d1",
7489
+ binding: resources.d1Binding,
7490
+ environment: "development",
7491
+ account_id: DEVELOPMENT_CLOUDFLARE_ACCOUNT_ID,
7492
+ resource_name: resources.d1DevelopmentName,
7493
+ resource_id: "provision-after-scaffold",
7494
+ migrations_dir: "migrations"
7257
7495
  }
7258
7496
  );
7259
7497
  }
@@ -7871,8 +8109,73 @@ function envExample(application) {
7871
8109
  }
7872
8110
 
7873
8111
  // src/commands/app-scaffold.ts
7874
- function generateOpaqueAppId() {
7875
- return `app_${randomBytes(9).toString("base64url")}`;
8112
+ function standardScriptStub() {
8113
+ return `#!/usr/bin/env python3
8114
+ """Canonical-fetching stub for .github/scripts/standard_*.py.
8115
+
8116
+ Replaces a vendored copy. At runtime, finds the canonical sibling in
8117
+ \`<repo-parent>/topolo-platform/tooling/<name>.py\` and execs it, passing argv
8118
+ through, so every consumer runs the current platform script on every
8119
+ invocation (no drifted per-app copies).
8120
+
8121
+ Override: set TOPOLO_PLATFORM_DIR to the absolute path of the topolo-platform
8122
+ repo when the canonical is in a non-default checkout layout (e.g. CI). If no
8123
+ canonical can be found, exits 2 with a clear message rather than silently
8124
+ falling back.
8125
+ """
8126
+ from __future__ import annotations
8127
+
8128
+ import os
8129
+ import sys
8130
+
8131
+ STUB_NAME = os.path.basename(__file__) # standard_ci.py OR standard_deploy.py
8132
+ HERE = os.path.dirname(os.path.abspath(__file__))
8133
+
8134
+
8135
+ def find_canonical() -> str | None:
8136
+ # 1. Explicit override (CI runners with non-standard layouts).
8137
+ override = os.environ.get('TOPOLO_PLATFORM_DIR')
8138
+ if override:
8139
+ path = os.path.join(override, 'tooling', STUB_NAME)
8140
+ if os.path.exists(path):
8141
+ return path
8142
+ # 2. Walk up looking for a sibling topolo-platform/tooling/<name>.py.
8143
+ cursor = HERE
8144
+ for _ in range(10):
8145
+ candidate = os.path.normpath(os.path.join(cursor, '..', 'topolo-platform', 'tooling', STUB_NAME))
8146
+ if os.path.exists(candidate):
8147
+ return candidate
8148
+ parent = os.path.dirname(cursor)
8149
+ if parent == cursor:
8150
+ break
8151
+ cursor = parent
8152
+ # 3. node_modules-based delivery (future): @topolo-io/standard-ci.
8153
+ cursor = HERE
8154
+ for _ in range(10):
8155
+ candidate = os.path.normpath(os.path.join(cursor, 'node_modules', '@topolo-io', 'standard-ci', 'bin', STUB_NAME))
8156
+ if os.path.exists(candidate):
8157
+ return candidate
8158
+ parent = os.path.dirname(cursor)
8159
+ if parent == cursor:
8160
+ break
8161
+ cursor = parent
8162
+ return None
8163
+
8164
+
8165
+ def main() -> None:
8166
+ canonical = find_canonical()
8167
+ if not canonical:
8168
+ sys.stderr.write(
8169
+ f'[topolo-canonical-stub] could not locate canonical {STUB_NAME}.\\n'
8170
+ f' Set TOPOLO_PLATFORM_DIR or checkout topolo-platform as a sibling of this app.\\n'
8171
+ )
8172
+ sys.exit(2)
8173
+ os.execv(sys.executable, [sys.executable, canonical, *sys.argv[1:]])
8174
+
8175
+
8176
+ if __name__ == '__main__':
8177
+ main()
8178
+ `;
7876
8179
  }
7877
8180
  function applicationScaffoldPayload(application, options = {}) {
7878
8181
  const target = normalizeTarget(options.target);
@@ -7892,13 +8195,16 @@ function applicationScaffoldPayload(application, options = {}) {
7892
8195
  const containerAppId = normalizeContainerAppId(options.containerAppId, surfaceModel);
7893
8196
  const profile = normalizeScaffoldProfile(options.profile);
7894
8197
  const resources = resolveScaffoldResources(profile, options);
8198
+ if (target === "platform" && api && !profile) {
8199
+ resources.d1 = true;
8200
+ }
7895
8201
  const ownerType = target === "platform" ? "topolo_first_party" : "third_party";
7896
8202
  const distributionModel = target === "platform" ? "marketplace" : "organization_internal";
7897
8203
  const appDir = options.appDir ?? catalogApplication?.appDir ?? (target === "platform" ? `Topolo${toPascalCase(id)}` : `topolo-${slug}`);
7898
8204
  const name = options.name ?? catalogApplication?.name ?? `topolo-${slug}`;
7899
8205
  const packageName = options.packageName ?? catalogApplication?.packageName ?? `topolo-${slug}`;
7900
8206
  const serviceSlug = api ? target === "platform" ? `topolo-${slug}` : id : null;
7901
- const defaultAppId = target === "platform" ? generateOpaqueAppId() : catalogApplication?.services[0] ?? id;
8207
+ const defaultAppId = target === "platform" ? `app_topolo_${id}` : catalogApplication?.services[0] ?? id;
7902
8208
  const appId = api ? options.appId ?? defaultAppId : null;
7903
8209
  const productionUrl = browser ? options.productionUrl ?? catalogApplication?.productionUrl ?? `https://${slug}.topolo.app` : null;
7904
8210
  const scopes = scaffoldScopes({ browser, api });
@@ -8038,6 +8344,28 @@ function scaffoldCapabilities(options) {
8038
8344
  function scaffoldFiles(options) {
8039
8345
  const { application, target } = options;
8040
8346
  const slug = toSlug(application.id);
8347
+ const tablePrefix = slug.replace(/-/g, "_");
8348
+ const tenantColumn = options.payload.tenancy === "household" ? "household_id" : "organization_id";
8349
+ const workspacesMigrationSql = [
8350
+ `CREATE TABLE IF NOT EXISTS ${tablePrefix}_workspaces (`,
8351
+ " id TEXT PRIMARY KEY,",
8352
+ ` ${tenantColumn} TEXT NOT NULL,`,
8353
+ " name TEXT NOT NULL,",
8354
+ " slug TEXT NOT NULL,",
8355
+ " created_by_user_id TEXT,",
8356
+ " created_by_email TEXT,",
8357
+ " created_at TEXT NOT NULL,",
8358
+ " updated_at TEXT NOT NULL,",
8359
+ ` UNIQUE(${tenantColumn}, slug)`,
8360
+ ");",
8361
+ "",
8362
+ `CREATE INDEX IF NOT EXISTS idx_${tablePrefix}_workspaces_${tenantColumn}`,
8363
+ ` ON ${tablePrefix}_workspaces(${tenantColumn});`,
8364
+ "",
8365
+ `CREATE INDEX IF NOT EXISTS idx_${tablePrefix}_workspaces_slug`,
8366
+ ` ON ${tablePrefix}_workspaces(${tenantColumn}, slug);`,
8367
+ ""
8368
+ ].join("\n");
8041
8369
  const base = target === "platform" ? firstPartyAppPath(application.appDir, options.payload.audience) : application.appDir;
8042
8370
  const browserBase = target === "platform" ? `${base}/apps/web` : base;
8043
8371
  const apiBase = target === "platform" ? `${base}/apps/api` : base;
@@ -8095,15 +8423,6 @@ function scaffoldFiles(options) {
8095
8423
  resources: options.payload.resources
8096
8424
  })
8097
8425
  },
8098
- {
8099
- path: `${base}/tsconfig.json`,
8100
- purpose: "TypeScript configuration for the generated app.",
8101
- requiredSignals: [
8102
- "uses strict mode",
8103
- "includes browser DOM types for React and Worker types for API-backed apps"
8104
- ],
8105
- content: tsconfigJson(options.api, target)
8106
- },
8107
8426
  {
8108
8427
  path: `${browserBase}/vite.config.ts`,
8109
8428
  purpose: "Vite browser build configuration.",
@@ -8161,6 +8480,31 @@ function scaffoldFiles(options) {
8161
8480
  content: stylesCss()
8162
8481
  }
8163
8482
  ];
8483
+ if (target === "platform") {
8484
+ if (options.browser) {
8485
+ files.push({
8486
+ path: `${browserBase}/tsconfig.json`,
8487
+ purpose: "TypeScript configuration for the browser surface (DOM + vite types).",
8488
+ requiredSignals: ["uses strict mode", "includes browser DOM types for React"],
8489
+ content: webTsconfigJson()
8490
+ });
8491
+ }
8492
+ if (options.api) {
8493
+ files.push({
8494
+ path: `${apiBase}/tsconfig.json`,
8495
+ purpose: "TypeScript configuration for the API worker surface (Workers types).",
8496
+ requiredSignals: ["uses strict mode", "includes Cloudflare Workers types"],
8497
+ content: apiTsconfigJson()
8498
+ });
8499
+ }
8500
+ } else {
8501
+ files.push({
8502
+ path: `${base}/tsconfig.json`,
8503
+ purpose: "TypeScript configuration for the generated app.",
8504
+ requiredSignals: ["uses strict mode", "includes browser DOM types for React and Worker types for API-backed apps"],
8505
+ content: tsconfigJson(options.api, target)
8506
+ });
8507
+ }
8164
8508
  if (target === "platform") {
8165
8509
  files.push(
8166
8510
  {
@@ -8176,6 +8520,17 @@ function scaffoldFiles(options) {
8176
8520
  profile: options.payload.profile
8177
8521
  })
8178
8522
  },
8523
+ ...options.api ? [
8524
+ {
8525
+ path: `${base}/topolo.action-contracts.mjs`,
8526
+ purpose: "Zod source of truth for the agent/SDK/MCP action catalog, mirrored into topolo.app.json actions[].",
8527
+ requiredSignals: [
8528
+ "declares topoloActionContracts with name/method/path per action",
8529
+ "each contract has a zod input validator, keeping the manifest actions[] honest"
8530
+ ],
8531
+ content: topoloActionContractsMjs()
8532
+ }
8533
+ ] : [],
8179
8534
  {
8180
8535
  path: `${base}/topolo.cloudcontrol.json`,
8181
8536
  purpose: "CloudControl source of truth for deploy targets, routes, bindings, and CI metadata.",
@@ -8201,6 +8556,33 @@ function scaffoldFiles(options) {
8201
8556
  ],
8202
8557
  content: options.ciWorkflow ?? ""
8203
8558
  },
8559
+ {
8560
+ path: `${base}/.github/scripts/standard_deploy.py`,
8561
+ purpose: "Canonical-fetching deploy stub (execs topolo-platform/tooling/standard_deploy.py).",
8562
+ requiredSignals: [
8563
+ "never a vendored copy of the canonical script",
8564
+ "runs the current platform standard_deploy.py on every invocation"
8565
+ ],
8566
+ content: standardScriptStub()
8567
+ },
8568
+ {
8569
+ path: `${base}/.github/scripts/standard_ci.py`,
8570
+ purpose: "Canonical-fetching CI stub (execs topolo-platform/tooling/standard_ci.py).",
8571
+ requiredSignals: [
8572
+ "never a vendored copy of the canonical script",
8573
+ "runs the current platform standard_ci.py on every invocation"
8574
+ ],
8575
+ content: standardScriptStub()
8576
+ },
8577
+ {
8578
+ path: `${base}/.github/scripts/standard_ops.py`,
8579
+ purpose: "Canonical-fetching ops stub (execs topolo-platform/tooling/standard_ops.py).",
8580
+ requiredSignals: [
8581
+ "never a vendored copy of the canonical script",
8582
+ "runs the current platform standard_ops.py on every invocation"
8583
+ ],
8584
+ content: standardScriptStub()
8585
+ },
8204
8586
  {
8205
8587
  path: `system-apps/TopoloDocs/apps/web/src/content/systems/topolo-${slug}.json`,
8206
8588
  purpose: "Canonical system registry entry.",
@@ -8324,6 +8706,25 @@ function scaffoldFiles(options) {
8324
8706
  portfolio: options.payload.portfolio
8325
8707
  })
8326
8708
  });
8709
+ if (target === "platform") {
8710
+ files.push({
8711
+ path: `${browserBase}/vitest.config.ts`,
8712
+ purpose: "Vitest config for the browser surface.",
8713
+ requiredSignals: [
8714
+ "runs component/unit tests under jsdom",
8715
+ "keeps the generated browser surface testable from day one"
8716
+ ],
8717
+ content: `import { defineConfig } from 'vitest/config';
8718
+
8719
+ export default defineConfig({
8720
+ test: {
8721
+ include: ['test/**/*.test.{ts,tsx}'],
8722
+ environment: 'jsdom',
8723
+ },
8724
+ });
8725
+ `
8726
+ });
8727
+ }
8327
8728
  }
8328
8729
  if (options.api) {
8329
8730
  files.push(
@@ -8380,8 +8781,45 @@ function scaffoldFiles(options) {
8380
8781
  "binds the frontend Worker to the API Worker for /api/* requests"
8381
8782
  ],
8382
8783
  content: platformWebWranglerToml(application)
8784
+ },
8785
+ {
8786
+ path: `${apiBase}/vitest.config.ts`,
8787
+ purpose: "Vitest config for the API worker surface.",
8788
+ requiredSignals: [
8789
+ "runs worker/unit tests under the node environment",
8790
+ "keeps the generated service testable from day one"
8791
+ ],
8792
+ content: `import { defineConfig } from 'vitest/config';
8793
+
8794
+ export default defineConfig({
8795
+ test: {
8796
+ include: ['test/**/*.test.{js,mjs,mts,ts,tsx}'],
8797
+ environment: 'node',
8798
+ },
8799
+ });
8800
+ `
8801
+ },
8802
+ {
8803
+ path: `${apiBase}/test/health.test.ts`,
8804
+ purpose: "Smoke test so the API test suite is non-empty from scaffold.",
8805
+ requiredSignals: [
8806
+ "asserts the scaffold boots",
8807
+ "is replaced by real route/repository tests as the surface grows"
8808
+ ],
8809
+ content: "import { describe, expect, it } from 'vitest';\n\n// Smoke test so the API test suite is non-empty from day one. Replace with\n// real route/repository tests as the surface grows.\ndescribe('api scaffold', () => {\n it('boots', () => {\n expect(true).toBe(true);\n });\n});\n"
8383
8810
  }
8384
8811
  );
8812
+ if (options.payload.resources.d1) {
8813
+ files.push({
8814
+ path: `${apiBase}/migrations/0001_initial.sql`,
8815
+ purpose: "Canonical multi-tenant baseline: the app workspaces table.",
8816
+ requiredSignals: [
8817
+ `${tablePrefix}_workspaces carries ${tenantColumn} on every row`,
8818
+ "every domain table added later carries the tenant + workspace columns"
8819
+ ],
8820
+ content: workspacesMigrationSql
8821
+ });
8822
+ }
8385
8823
  }
8386
8824
  }
8387
8825
  return files;
@@ -10299,18 +10737,13 @@ function registerContextCommand(program2) {
10299
10737
  }
10300
10738
  const key = contextKey(resolved.contextType, resolved.organizationId);
10301
10739
  const existing = profile.contexts?.[key];
10302
- let updated = config;
10303
- if (existing?.credential) {
10304
- updated = activateContext(config, environment, key);
10305
- } else {
10306
- const newCredential = await exchangeForContext(cred, serviceUrls, {
10307
- type: resolved.contextType,
10308
- id: resolved.organizationId
10309
- });
10310
- resolved.credential = newCredential;
10311
- resolved.oauthClientId = existing?.oauthClientId ?? profile.oauthClientId;
10312
- updated = writeContextCredential(config, environment, resolved, true);
10313
- }
10740
+ const newCredential = await exchangeForContext(cred, serviceUrls, {
10741
+ type: resolved.contextType,
10742
+ id: resolved.organizationId
10743
+ });
10744
+ resolved.credential = newCredential;
10745
+ resolved.oauthClientId = existing?.oauthClientId ?? profile.oauthClientId;
10746
+ const updated = writeContextCredential(config, environment, resolved, true);
10314
10747
  await writeConfig(updated);
10315
10748
  emit(
10316
10749
  { environment, active: key, context: { type: resolved.contextType, label: labelFor(resolved) } },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@topolo/cli",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Command-line interface for Topolo developers, operators, and agents, with internal first-party scaffolding available under the topolo platform namespace.",
5
5
  "type": "module",
6
6
  "bin": {