@topolo/cli 0.2.2 → 0.2.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 (2) hide show
  1. package/dist/index.js +217 -22
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -78,6 +78,7 @@ var APP_API_URLS = {
78
78
  bugfix: "https://bugfix.topolo.app",
79
79
  bytes: "https://bytes.topolo.app",
80
80
  calendar: "https://calendar.topolo.app",
81
+ campaigns: "https://campaigns.topolo.app",
81
82
  capacity: "https://capacity.topolo.app",
82
83
  chat: "https://chat.topolo.app",
83
84
  commerce: "https://commerce.topolo.app",
@@ -336,6 +337,38 @@ var APPLICATIONS = {
336
337
  }
337
338
  ]
338
339
  },
340
+ "campaigns": {
341
+ "id": "campaigns",
342
+ "appDir": "TopoloCampaigns",
343
+ "name": "topolo-campaigns",
344
+ "packageName": "topolo-campaigns",
345
+ "productionUrl": "https://campaigns.topolo.app",
346
+ "services": [
347
+ "campaigns"
348
+ ],
349
+ "deployTargets": [
350
+ {
351
+ "name": "topolo-campaigns",
352
+ "kind": "worker",
353
+ "apiUrl": "https://campaigns.topolo.app",
354
+ "domains": [
355
+ "campaigns.topolo.app"
356
+ ],
357
+ "routes": [
358
+ "campaigns.topolo.app"
359
+ ],
360
+ "appId": "campaigns"
361
+ },
362
+ {
363
+ "name": "topolo-campaigns-api",
364
+ "kind": "worker",
365
+ "apiUrl": null,
366
+ "domains": [],
367
+ "routes": [],
368
+ "appId": null
369
+ }
370
+ ]
371
+ },
339
372
  "capacity": {
340
373
  "id": "capacity",
341
374
  "appDir": "TopoloCapacity",
@@ -2510,7 +2543,21 @@ var TopoloClient = class _TopoloClient {
2510
2543
  return match;
2511
2544
  }
2512
2545
  async callAction(action, input2 = {}, options = {}) {
2546
+ const entry = await this.getAction(action);
2547
+ return this.executeResolvedAction(entry, input2, options);
2548
+ }
2549
+ async readAction(action, input2 = {}, options = {}) {
2513
2550
  const entry = await this.getAction(action);
2551
+ const agentAccess = entry.agentAccess ?? (entry.destructive ? "off" : entry.readOnly && !entry.requiresConfirmation ? "auto" : "confirm");
2552
+ if (entry.method !== "GET" || !entry.readOnly || entry.destructive || entry.requiresConfirmation || agentAccess !== "auto") {
2553
+ throw new TopoloPermissionError(
2554
+ `Topolo action "${entry.actionId}" is not an automatic read-only action.`,
2555
+ entry.requiredPermission ? [entry.requiredPermission] : []
2556
+ );
2557
+ }
2558
+ return this.executeResolvedAction(entry, input2, options);
2559
+ }
2560
+ async executeResolvedAction(entry, input2, options) {
2514
2561
  const agentAccess = entry.agentAccess ?? (entry.destructive ? "off" : entry.readOnly && !entry.requiresConfirmation ? "auto" : "confirm");
2515
2562
  if (agentAccess === "off") {
2516
2563
  throw new TopoloPermissionError(
@@ -2582,6 +2629,7 @@ var TopoloClient = class _TopoloClient {
2582
2629
  const kind = payload.credentialType === "api_key" ? "api_key" : "access_token";
2583
2630
  return {
2584
2631
  kind,
2632
+ authorizationMode: normalizeCredentialAuthorizationMode(payload.authorizationMode, kind),
2585
2633
  user: {
2586
2634
  id: user.id,
2587
2635
  email: user.email,
@@ -2844,6 +2892,11 @@ function objectToQuery(value) {
2844
2892
  }
2845
2893
  return Object.keys(query).length > 0 ? query : void 0;
2846
2894
  }
2895
+ function normalizeCredentialAuthorizationMode(value, kind) {
2896
+ if (kind === "api_key") return "api_key";
2897
+ if (value === "context_rbac" || value === "oauth_scopes" || value === "session_rbac") return value;
2898
+ return "unknown";
2899
+ }
2847
2900
  function describeError(body, fallback) {
2848
2901
  if (body && typeof body === "object") {
2849
2902
  const maybe = body;
@@ -3064,6 +3117,7 @@ var APP_API_URLS2 = {
3064
3117
  bugfix: "https://bugfix.topolo.app",
3065
3118
  bytes: "https://bytes.topolo.app",
3066
3119
  calendar: "https://calendar.topolo.app",
3120
+ campaigns: "https://campaigns.topolo.app",
3067
3121
  capacity: "https://capacity.topolo.app",
3068
3122
  chat: "https://chat.topolo.app",
3069
3123
  commerce: "https://commerce.topolo.app",
@@ -3388,6 +3442,46 @@ async function refreshCredentialIfNeeded(environment = selectedEnvironment(), op
3388
3442
  return cred;
3389
3443
  }
3390
3444
  }
3445
+ async function refreshContextCredentialIfNeeded(environment, key, options = {}) {
3446
+ const config = await readConfig();
3447
+ const profile = profileForEnvironment(config, environment);
3448
+ const context = profile.contexts?.[key];
3449
+ const credential = context?.credential;
3450
+ if (!credential || credential.kind !== "access_token") return credential ?? null;
3451
+ const oauthClientId = context.oauthClientId ?? profile.oauthClientId;
3452
+ if (!credential.refreshToken || !credential.expiresAt || !oauthClientId) return credential;
3453
+ const now = Math.floor(Date.now() / 1e3);
3454
+ if (!options.force && credential.expiresAt - REFRESH_SKEW_SECONDS > now) return credential;
3455
+ try {
3456
+ const oauth = new TopoloOAuth2({
3457
+ serviceUrls: resolveServiceUrls(profile.serviceUrls, environment)
3458
+ });
3459
+ const tokens = await oauth.refreshToken({
3460
+ clientId: oauthClientId,
3461
+ refreshToken: credential.refreshToken
3462
+ });
3463
+ const refreshed = {
3464
+ kind: "access_token",
3465
+ accessToken: tokens.access_token,
3466
+ ...tokens.refresh_token ? { refreshToken: tokens.refresh_token } : { refreshToken: credential.refreshToken },
3467
+ expiresAt: Math.floor(Date.now() / 1e3) + tokens.expires_in
3468
+ };
3469
+ await writeConfig(writeProfileConfig(config, environment, {
3470
+ ...profile.activeContext === key ? { credential: refreshed } : {},
3471
+ contexts: {
3472
+ ...profile.contexts ?? {},
3473
+ [key]: {
3474
+ ...context,
3475
+ credential: refreshed,
3476
+ oauthClientId
3477
+ }
3478
+ }
3479
+ }));
3480
+ return refreshed;
3481
+ } catch {
3482
+ return credential;
3483
+ }
3484
+ }
3391
3485
  function resolveAgentName(configAgentName) {
3392
3486
  return process.env.TOPOLO_AGENT_NAME ?? configAgentName;
3393
3487
  }
@@ -3446,7 +3540,7 @@ function resolveOAuthClientId(config, environment = selectedEnvironment()) {
3446
3540
  const envSpecific = process.env[`${envVarPrefix(environment)}_CLI_CLIENT_ID`];
3447
3541
  if (envSpecific) return envSpecific;
3448
3542
  if (process.env.TOPOLO_CLI_CLIENT_ID) return process.env.TOPOLO_CLI_CLIENT_ID;
3449
- return profileForEnvironment(config, environment).oauthClientId ?? DEFAULT_CLI_CLIENT_IDS[environment];
3543
+ return DEFAULT_CLI_CLIENT_IDS[environment] ?? profileForEnvironment(config, environment).oauthClientId;
3450
3544
  }
3451
3545
  var NON_PROD_URL_MARKERS = {
3452
3546
  staging: [".stg.topolo.us", "topolo-staging"],
@@ -3832,7 +3926,7 @@ function stderrDebugLogger(event) {
3832
3926
 
3833
3927
  // src/commands/whoami.ts
3834
3928
  function registerWhoamiCommand(program2) {
3835
- 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) => {
3929
+ 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) => {
3836
3930
  const mode = resolveOutputMode(opts.json);
3837
3931
  try {
3838
3932
  const environment = selectedEnvironment();
@@ -3857,7 +3951,10 @@ function formatPretty(info) {
3857
3951
  `Organization: ${info.organization ? `${info.organization.name} (${info.organization.slug})` : "(none)"}`
3858
3952
  );
3859
3953
  lines.push(`Credential: ${info.kind}`);
3860
- lines.push(`Scopes: ${info.user.permissions.length === 0 ? "(none)" : ""}`);
3954
+ const dynamicRbac = info.authorizationMode === "context_rbac";
3955
+ 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";
3956
+ lines.push(`Authorization: ${authorization}`);
3957
+ lines.push(`${dynamicRbac ? "Identity scopes" : "Scopes"}: ${info.user.permissions.length === 0 ? "(none)" : ""}`);
3861
3958
  for (const p of info.user.permissions) lines.push(` - ${p}`);
3862
3959
  return lines.join("\n");
3863
3960
  }
@@ -5169,7 +5266,6 @@ function isFile(filePath) {
5169
5266
  }
5170
5267
 
5171
5268
  // src/commands/app-scaffold.ts
5172
- import { randomBytes } from "crypto";
5173
5269
  import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
5174
5270
  import { dirname as dirname3, resolve as resolve2 } from "path";
5175
5271
 
@@ -6152,12 +6248,15 @@ function platformAppTsx(application, options) {
6152
6248
  " }",
6153
6249
  "",
6154
6250
  " if (!auth) {",
6251
+ " // Pre-AppRoot: appId is still resolving, so auth can't be built yet.",
6252
+ " // Render the SAME scaffold layout AppRoot uses below so this phase and",
6253
+ " // the auth-gate fallback read as one continuous loading state (the",
6254
+ " // scaffold resolves theme + rail width on its own to match the shell).",
6155
6255
  " return (",
6156
6256
  " <TopoloAppLoadingScaffold",
6157
6257
  " appId={appId}",
6158
6258
  " appName={APP_NAME}",
6159
6259
  " layout={{ shell: 'topbar', content: 'workspace' }}",
6160
- " subtitle={`Loading ${APP_NAME}...`}",
6161
6260
  " />",
6162
6261
  " );",
6163
6262
  " }",
@@ -6198,8 +6297,6 @@ function platformAppTsx(application, options) {
6198
6297
  " appId={appId}",
6199
6298
  " appName={APP_NAME}",
6200
6299
  " layout={{ shell: loading?.shell || 'topbar', content: loading?.content || 'workspace' }}",
6201
- " title={loading?.title}",
6202
- " subtitle={`Loading ${APP_NAME}...`}",
6203
6300
  " />",
6204
6301
  " )}",
6205
6302
  " shell={{",
@@ -6896,7 +6993,7 @@ function platformWorkerTs(application, options) {
6896
6993
  "import { createTopoloWorkerRuntime, type TopoloRuntimeContext } from '@topolo-io/worker-runtime';",
6897
6994
  "import { withSecurityHeaders } from '@topolo-io/security-headers';",
6898
6995
  "import { withErrorReporting } from '@topolo-io/error-middleware';",
6899
- "import { SCAFFOLD_APP_ID, SCAFFOLD_APP_NAME, SCAFFOLD_SERVICE_SLUG, resolveRuntimeAppId } from './app-identity';",
6996
+ "import { SCAFFOLD_APP_NAME, SCAFFOLD_SERVICE_SLUG, resolveRuntimeAppId } from './app-identity';",
6900
6997
  ...options.resources.workflow ? ["import { WorkflowEntrypoint, type WorkflowEvent, type WorkflowStep } from 'cloudflare:workers';"] : [],
6901
6998
  "",
6902
6999
  "export interface Env {",
@@ -6921,12 +7018,10 @@ function platformWorkerTs(application, options) {
6921
7018
  "const runtime = createTopoloWorkerRuntime<Env>({",
6922
7019
  " appName: SCAFFOLD_APP_NAME,",
6923
7020
  " serviceSlug: SCAFFOLD_SERVICE_SLUG,",
6924
- " resolveAppId: (env) => resolveRuntimeAppId(env),",
7021
+ " // Auth verification base URL is environment-driven (staging/prod/dev),",
7022
+ " // so rt.verifyAuth() targets the correct Auth service per deploy.",
6925
7023
  " authBaseUrl: (env) => env.AUTH_BASE_URL,",
6926
- " usageMonitoring: {",
6927
- " appId: SCAFFOLD_APP_ID,",
6928
- " nexusGatewayUrl: (env) => env.NEXUS_GATEWAY_URL,",
6929
- " },",
7024
+ " authGateway: { enabled: false },",
6930
7025
  "});",
6931
7026
  "",
6932
7027
  "async function handleRequest(",
@@ -7348,6 +7443,7 @@ function topoloAppManifestJson(application, options) {
7348
7443
  }
7349
7444
  } : {}
7350
7445
  };
7446
+ const actions = options.api ? baselineApiActions() : [];
7351
7447
  return `${json({
7352
7448
  $schema: "./../../topolo-platform/tooling/app-manifest-schema.json",
7353
7449
  name: `topolo-${slug}`,
@@ -7367,10 +7463,87 @@ function topoloAppManifestJson(application, options) {
7367
7463
  domain: "topolo.app"
7368
7464
  }
7369
7465
  },
7370
- targets
7466
+ targets,
7467
+ ...actions.length ? { actions } : {}
7371
7468
  })}
7372
7469
  `;
7373
7470
  }
7471
+ function topoloActionContractsMjs() {
7472
+ return [
7473
+ "// Canonical action contracts \u2014 the source of truth for the agent/SDK/MCP",
7474
+ "// surface. Each entry maps 1:1 to a worker route (method + path) and validates",
7475
+ "// its input with zod. Add product actions here and mirror them into",
7476
+ "// topolo.app.json actions[]. Mirrors the normalized fleet convention.",
7477
+ "",
7478
+ "import { z } from 'zod';",
7479
+ "",
7480
+ "const emptyInput = z.object({}).strict();",
7481
+ "",
7482
+ "export const topoloActionContracts = {",
7483
+ " 'widget.get': {",
7484
+ " name: 'widget.get',",
7485
+ " method: 'GET',",
7486
+ " path: '/api/widget',",
7487
+ " input: emptyInput,",
7488
+ " },",
7489
+ " 'workspace.bootstrap': {",
7490
+ " name: 'workspace.bootstrap',",
7491
+ " method: 'GET',",
7492
+ " path: '/api/bootstrap',",
7493
+ " input: emptyInput,",
7494
+ " },",
7495
+ "};",
7496
+ "",
7497
+ "export const topoloActionInputSchemas = Object.fromEntries(",
7498
+ " Object.entries(topoloActionContracts).map(([name, contract]) => [",
7499
+ " name,",
7500
+ " z.toJSONSchema(contract.input),",
7501
+ " ]),",
7502
+ ");",
7503
+ ""
7504
+ ].join("\n");
7505
+ }
7506
+ function baselineApiActions() {
7507
+ const emptyInputSchema = {
7508
+ $schema: "https://json-schema.org/draft/2020-12/schema",
7509
+ type: "object",
7510
+ properties: {},
7511
+ additionalProperties: false
7512
+ };
7513
+ const openOutputSchema = { type: "object", additionalProperties: true };
7514
+ return [
7515
+ {
7516
+ name: "widget.get",
7517
+ title: "Get widget summary",
7518
+ description: "Get the TopoloOne widget summary for this service.",
7519
+ method: "GET",
7520
+ path: "/api/widget",
7521
+ permission: "workspace:read",
7522
+ readOnly: true,
7523
+ inputSchema: emptyInputSchema,
7524
+ outputSchema: openOutputSchema,
7525
+ target: "api",
7526
+ destructive: false,
7527
+ requiresConfirmation: false,
7528
+ agentAccess: "auto"
7529
+ },
7530
+ {
7531
+ name: "workspace.bootstrap",
7532
+ title: "Get workspace bootstrap",
7533
+ description: "Get the signed workspace bootstrap payload for this service.",
7534
+ method: "GET",
7535
+ path: "/api/bootstrap",
7536
+ permission: "workspace:read",
7537
+ readOnly: true,
7538
+ inputSchema: emptyInputSchema,
7539
+ outputSchema: openOutputSchema,
7540
+ target: "api",
7541
+ destructive: false,
7542
+ requiresConfirmation: false,
7543
+ agentAccess: "auto"
7544
+ }
7545
+ ];
7546
+ }
7374
7547
  function cloudcontrolJson(application, options) {
7375
7548
  const slug = toSlug(application.id);
7376
7549
  const appPath = firstPartyAppPath(application.appDir, options.audience);
@@ -8024,9 +8197,6 @@ function envExample(application) {
8024
8197
  }
8025
8198
 
8026
8199
  // src/commands/app-scaffold.ts
8027
- function generateOpaqueAppId() {
8028
- return `app_${randomBytes(9).toString("base64url")}`;
8029
- }
8030
8200
  function standardScriptStub() {
8031
8201
  return `#!/usr/bin/env python3
8032
8202
  """Canonical-fetching stub for .github/scripts/standard_*.py.
@@ -8122,7 +8292,7 @@ function applicationScaffoldPayload(application, options = {}) {
8122
8292
  const name = options.name ?? catalogApplication?.name ?? `topolo-${slug}`;
8123
8293
  const packageName = options.packageName ?? catalogApplication?.packageName ?? `topolo-${slug}`;
8124
8294
  const serviceSlug = api ? target === "platform" ? `topolo-${slug}` : id : null;
8125
- const defaultAppId = target === "platform" ? generateOpaqueAppId() : catalogApplication?.services[0] ?? id;
8295
+ const defaultAppId = target === "platform" ? `app_topolo_${id}` : catalogApplication?.services[0] ?? id;
8126
8296
  const appId = api ? options.appId ?? defaultAppId : null;
8127
8297
  const productionUrl = browser ? options.productionUrl ?? catalogApplication?.productionUrl ?? `https://${slug}.topolo.app` : null;
8128
8298
  const scopes = scaffoldScopes({ browser, api });
@@ -8438,6 +8608,17 @@ function scaffoldFiles(options) {
8438
8608
  profile: options.payload.profile
8439
8609
  })
8440
8610
  },
8611
+ ...options.api ? [
8612
+ {
8613
+ path: `${base}/topolo.action-contracts.mjs`,
8614
+ purpose: "Zod source of truth for the agent/SDK/MCP action catalog, mirrored into topolo.app.json actions[].",
8615
+ requiredSignals: [
8616
+ "declares topoloActionContracts with name/method/path per action",
8617
+ "each contract has a zod input validator, keeping the manifest actions[] honest"
8618
+ ],
8619
+ content: topoloActionContractsMjs()
8620
+ }
8621
+ ] : [],
8441
8622
  {
8442
8623
  path: `${base}/topolo.cloudcontrol.json`,
8443
8624
  purpose: "CloudControl source of truth for deploy targets, routes, bindings, and CI metadata.",
@@ -10621,11 +10802,25 @@ function registerContextCommand(program2) {
10621
10802
  }
10622
10803
  const wantPersonal = /^(personal|me)$/i.test(target.trim());
10623
10804
  let resolved;
10624
- if (wantPersonal) {
10805
+ let exchangeCredential = cred;
10806
+ const needle = target.trim().toLowerCase();
10807
+ const storedEntry = Object.entries(profile.contexts ?? {}).find(([, candidate]) => {
10808
+ if (wantPersonal) return candidate.contextType === "personal";
10809
+ return candidate.contextType === "organization" && (candidate.organizationId === target || candidate.organizationSlug?.toLowerCase() === needle || candidate.organizationName?.toLowerCase() === needle);
10810
+ });
10811
+ const storedContext = storedEntry?.[1];
10812
+ if (storedEntry && storedContext?.credential) {
10813
+ exchangeCredential = await refreshContextCredentialIfNeeded(environment, storedEntry[0]) ?? storedContext.credential;
10814
+ resolved = {
10815
+ contextType: storedContext.contextType,
10816
+ organizationId: storedContext.organizationId,
10817
+ organizationSlug: storedContext.organizationSlug,
10818
+ organizationName: storedContext.organizationName
10819
+ };
10820
+ } else if (wantPersonal) {
10625
10821
  resolved = { contextType: "personal" };
10626
10822
  } else {
10627
10823
  const me = await fetchMe(cred, serviceUrls);
10628
- const needle = target.trim().toLowerCase();
10629
10824
  const match = me.memberships.find(
10630
10825
  (m) => m.id === target || m.slug?.toLowerCase() === needle || m.name?.toLowerCase() === needle
10631
10826
  );
@@ -10644,13 +10839,13 @@ function registerContextCommand(program2) {
10644
10839
  }
10645
10840
  const key = contextKey(resolved.contextType, resolved.organizationId);
10646
10841
  const existing = profile.contexts?.[key];
10647
- const newCredential = await exchangeForContext(cred, serviceUrls, {
10842
+ const newCredential = await exchangeForContext(exchangeCredential, serviceUrls, {
10648
10843
  type: resolved.contextType,
10649
10844
  id: resolved.organizationId
10650
10845
  });
10651
10846
  resolved.credential = newCredential;
10652
10847
  resolved.oauthClientId = existing?.oauthClientId ?? profile.oauthClientId;
10653
- const updated = writeContextCredential(config, environment, resolved, true);
10848
+ const updated = writeContextCredential(await readConfig(), environment, resolved, true);
10654
10849
  await writeConfig(updated);
10655
10850
  emit(
10656
10851
  { 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.2",
3
+ "version": "0.2.4",
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": {