@topolo/cli 0.1.24 → 0.1.26

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 (3) hide show
  1. package/README.md +13 -13
  2. package/dist/index.js +172 -102
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -108,26 +108,26 @@ topolo crm contacts:get contact_abc123
108
108
 
109
109
  ### `topolo services`
110
110
 
111
- Lists every service id and effective URL known to the generated SDK registry.
112
- In staging, `environmentSafe: false` means that service still resolves to a
113
- production SDK default and will be refused by `topolo api` until a staging URL
114
- override is configured.
111
+ Lists the services the current credential can use in its organization, with the
112
+ effective URL and granted permissions. This command requires a credential and
113
+ does not expose the global platform catalog. In staging, `environmentSafe:
114
+ false` means that service still resolves to a production URL and will be
115
+ refused by `topolo api` until a staging URL override is configured.
115
116
 
116
117
  ```bash
117
118
  topolo services --json
118
119
  topolo services --env staging --json
119
120
  ```
120
121
 
121
- `topolo api` also accepts live Auth-catalog service IDs/slugs for newly
122
- registered API/technical service surfaces, so agents can address future services
123
- before the next CLI package release. Use `topolo services --json` as the stable
124
- offline discovery floor, then rely on the SDK's live resolver for newly
125
- registered services.
122
+ `topolo api` accepts the IDs/slugs returned by this credential-scoped live
123
+ catalog. New services become usable without a CLI release once Auth grants the
124
+ organization and credential the relevant permissions.
126
125
 
127
126
  ### `topolo apps`
128
127
 
129
- Lists every Topolo application known to the generated SDK catalog, including
130
- browser-only Worker applications that do not expose a callable API service.
128
+ Lists launchable Topolo applications available to the current credential. This
129
+ uses the same live Auth catalog as `topolo services`; it is not the global
130
+ generated application list.
131
131
 
132
132
  ```bash
133
133
  topolo apps --json
@@ -209,8 +209,8 @@ topolo api auth GET /api/me
209
209
  topolo api crm POST /api/contacts --data '{"first":"Jane"}' --confirm
210
210
  ```
211
211
 
212
- Use `topolo services --json` to discover valid service ids before calling a
213
- service that does not have a typed command yet.
212
+ Use `topolo services --json` to discover service ids permitted for the current
213
+ credential before calling a service that does not have a typed command yet.
214
214
 
215
215
  ## Output modes
216
216
 
package/dist/index.js CHANGED
@@ -2203,12 +2203,16 @@ var TopoloClient = class _TopoloClient {
2203
2203
  if (!serviceKey) {
2204
2204
  throw new TopoloSdkError("unknown_service", "service is required");
2205
2205
  }
2206
- const overrideUrl = resolveConfiguredServiceUrl(serviceKey, this.serviceUrls);
2207
- if (overrideUrl) return { serviceKey, baseUrl: overrideUrl };
2208
- const staticUrl = DEFAULT_SERVICE_URLS[serviceKey];
2209
- if (staticUrl) return { serviceKey, baseUrl: staticUrl };
2206
+ if (serviceKey === "auth") {
2207
+ const overrideUrl = resolveConfiguredServiceUrl(serviceKey, this.serviceUrls);
2208
+ if (overrideUrl) return { serviceKey, baseUrl: overrideUrl };
2209
+ return { serviceKey, baseUrl: DEFAULT_SERVICE_URLS.auth };
2210
+ }
2210
2211
  const live = await this.resolveLiveCallableService(serviceKey);
2211
- if (live) return live;
2212
+ if (live) {
2213
+ const overrideUrl = resolveConfiguredServiceUrl(serviceKey, this.serviceUrls);
2214
+ return overrideUrl ? { ...live, baseUrl: overrideUrl } : live;
2215
+ }
2212
2216
  throw new TopoloSdkError(
2213
2217
  "unknown_service",
2214
2218
  `Unknown Topolo service "${service}". Run service discovery first and pass a callable service id or slug.`
@@ -2218,10 +2222,7 @@ var TopoloClient = class _TopoloClient {
2218
2222
  const catalog = await this.listServices();
2219
2223
  const match = catalog.find((entry) => catalogEntryMatches(entry, serviceKey));
2220
2224
  if (!match?.baseUrl || !isCallableCatalogEntry(match)) return null;
2221
- return {
2222
- serviceKey: normalizeLookupKey(match.slug || match.name || match.serviceId) || serviceKey,
2223
- baseUrl: match.baseUrl
2224
- };
2225
+ return { serviceKey, baseUrl: match.baseUrl };
2225
2226
  }
2226
2227
  /**
2227
2228
  * Resolve a service-slug → opaque service-id via caller config or TopoloAuth.
@@ -2283,53 +2284,55 @@ var TopoloClient = class _TopoloClient {
2283
2284
  /**
2284
2285
  * List the platform service catalog from the live registry
2285
2286
  * (`GET /api/services/catalog` on TopoloAuth). Credential-aware: the
2286
- * caller's credential is attached, so an authenticated caller in an
2287
- * entitled org also receives the `developer_only` services its role
2288
- * permits; an unauthenticated/agent caller receives `active` only.
2287
+ * caller's credential is attached, so the result only includes services the
2288
+ * credential's organization and effective permissions can use.
2289
2289
  *
2290
- * Resilient: on any network/HTTP failure this falls back to the
2291
- * build-time generated `APPLICATIONS` snapshot so offline/degraded
2292
- * callers still get a usable (if static) catalog. The live result is
2293
- * authoritative; the snapshot is a floor, not a cap.
2290
+ * Security boundary: this never falls back to the generated global snapshot.
2291
+ * The live Auth catalog is the authority because it is scoped to the caller's
2292
+ * organization and effective permissions.
2294
2293
  */
2295
2294
  async listServices() {
2296
2295
  const now = Date.now();
2297
2296
  if (this.serviceCatalogCache && this.serviceCatalogCache.expiresAt > now) {
2298
2297
  return this.serviceCatalogCache.entries;
2299
2298
  }
2300
- try {
2301
- const res = await this.request({ service: "auth", method: "GET", path: "/api/services/catalog" });
2302
- const raw = res.data?.services ?? res.services;
2303
- if (Array.isArray(raw) && raw.length > 0) {
2304
- const entries = [];
2305
- for (const item of raw) {
2306
- const s = item;
2307
- const serviceId = typeof s.service_id === "string" ? s.service_id : typeof s.id === "string" ? s.id : null;
2308
- if (!serviceId) continue;
2309
- entries.push({
2310
- serviceId,
2311
- slug: typeof s.app_slug === "string" ? s.app_slug : null,
2312
- name: typeof s.name === "string" ? s.name : null,
2313
- description: typeof s.description === "string" ? s.description : null,
2314
- baseUrl: typeof s.base_url === "string" ? s.base_url : null,
2315
- status: typeof s.status === "string" ? s.status : null,
2316
- surfaceKind: typeof s.surface_kind === "string" ? s.surface_kind : null,
2317
- launchable: s.launchable === true
2318
- });
2319
- }
2320
- if (entries.length > 0) {
2321
- this.serviceCatalogCache = {
2322
- entries,
2323
- expiresAt: now + CATALOG_CACHE_TTL_MS
2324
- };
2325
- return entries;
2326
- }
2327
- }
2328
- } catch {
2299
+ const res = await this.request({ service: "auth", method: "GET", path: "/api/services/catalog" });
2300
+ const raw = res.data?.services ?? res.services;
2301
+ if (!Array.isArray(raw)) {
2302
+ throw new TopoloSdkError(
2303
+ "service_catalog_unavailable",
2304
+ "TopoloAuth did not return a service catalog for the current credential."
2305
+ );
2306
+ }
2307
+ const entries = [];
2308
+ for (const item of raw) {
2309
+ const s = item;
2310
+ const serviceId = typeof s.service_id === "string" ? s.service_id : typeof s.id === "string" ? s.id : null;
2311
+ if (!serviceId) continue;
2312
+ const permissions = Array.isArray(s.granted_permissions) ? s.granted_permissions.filter((permission) => typeof permission === "string" && permission.trim().length > 0) : [];
2313
+ entries.push({
2314
+ serviceId,
2315
+ slug: typeof s.app_slug === "string" ? s.app_slug : null,
2316
+ name: typeof s.name === "string" ? s.name : null,
2317
+ description: typeof s.description === "string" ? s.description : null,
2318
+ baseUrl: typeof s.base_url === "string" ? s.base_url : null,
2319
+ status: typeof s.status === "string" ? s.status : null,
2320
+ surfaceKind: typeof s.surface_kind === "string" ? s.surface_kind : null,
2321
+ launchable: s.launchable === true,
2322
+ permissions
2323
+ });
2329
2324
  }
2330
- return this.generatedCatalogFallback();
2325
+ this.serviceCatalogCache = {
2326
+ entries,
2327
+ expiresAt: now + CATALOG_CACHE_TTL_MS
2328
+ };
2329
+ return entries;
2331
2330
  }
2332
- generatedCatalogFallback() {
2331
+ /**
2332
+ * Bundled generated registry for tooling that audits platform packaging, not
2333
+ * for deciding what the current credential may use.
2334
+ */
2335
+ listBundledServices() {
2333
2336
  const apps = Object.values(APPLICATIONS);
2334
2337
  return apps.map((app) => ({
2335
2338
  serviceId: app.id,
@@ -2337,9 +2340,10 @@ var TopoloClient = class _TopoloClient {
2337
2340
  name: app.name ?? app.id,
2338
2341
  description: null,
2339
2342
  baseUrl: app.productionUrl ?? null,
2340
- status: "active",
2343
+ status: "bundled",
2341
2344
  surfaceKind: "application",
2342
- launchable: Boolean(app.productionUrl)
2345
+ launchable: Boolean(app.productionUrl),
2346
+ permissions: []
2343
2347
  }));
2344
2348
  }
2345
2349
  /**
@@ -2409,7 +2413,7 @@ function serviceKeyCandidates(slug) {
2409
2413
  ])];
2410
2414
  }
2411
2415
  function normalizeLookupKey(value) {
2412
- return String(value || "").trim().toLowerCase();
2416
+ return String(value || "").trim().toLowerCase().replace(/\s+/g, "-");
2413
2417
  }
2414
2418
  function catalogEntryMatches(entry, needle) {
2415
2419
  return catalogEntryLookupKeys(entry).includes(needle);
@@ -2420,12 +2424,8 @@ function catalogEntryLookupKeys(entry) {
2420
2424
  for (const value of values) {
2421
2425
  const normalized = normalizeLookupKey(value);
2422
2426
  if (!normalized) continue;
2423
- keys.add(normalized);
2424
- keys.add(normalized.replace(/-/g, "_"));
2425
- if (normalized.startsWith("topolo-")) {
2426
- const withoutTopolo = normalized.slice("topolo-".length);
2427
- keys.add(withoutTopolo);
2428
- keys.add(withoutTopolo.replace(/-/g, "_"));
2427
+ for (const candidate of serviceKeyCandidates(normalized)) {
2428
+ keys.add(candidate);
2429
2429
  }
2430
2430
  }
2431
2431
  return [...keys];
@@ -2433,7 +2433,9 @@ function catalogEntryLookupKeys(entry) {
2433
2433
  function isCallableCatalogEntry(entry) {
2434
2434
  const kind = normalizeLookupKey(entry.surfaceKind || "");
2435
2435
  if (kind === "api" || kind === "technical_service" || kind === "service") return true;
2436
- return Boolean(entry.slug && Object.prototype.hasOwnProperty.call(DEFAULT_SERVICE_URLS, catalogEntryLookupKeys(entry)[0] ?? ""));
2436
+ return Boolean(
2437
+ entry.baseUrl && catalogEntryLookupKeys(entry).some((key) => Object.prototype.hasOwnProperty.call(DEFAULT_SERVICE_URLS, key))
2438
+ );
2437
2439
  }
2438
2440
  function describeError(body, fallback) {
2439
2441
  if (body && typeof body === "object") {
@@ -6797,8 +6799,14 @@ function topoloAppManifestJson(application, options) {
6797
6799
  AUTH_BASE_URL: {
6798
6800
  staging: "https://auth.stg.topolo.us",
6799
6801
  production: "https://auth.topolo.app"
6800
- }
6802
+ },
6803
+ NEXUS_GATEWAY_URL: {
6804
+ staging: "https://nexus-api.stg.topolo.us",
6805
+ production: "https://nexus-api.topolo.app"
6806
+ },
6807
+ NEXUS_APP_ID: slug
6801
6808
  },
6809
+ secrets: ["TOPOLO_SERVICE_CLIENT_SECRET"],
6802
6810
  ...Object.keys(bindings).length ? { bindings } : {}
6803
6811
  };
6804
6812
  }
@@ -7862,20 +7870,28 @@ var CLOUDCONTROL_LOCAL_APPLICATION = {
7862
7870
  deployTargets: []
7863
7871
  };
7864
7872
  function registerAppsCommand(program2) {
7865
- const apps = program2.command("apps").description("List every Topolo platform application known to the SDK catalog").option("--json", "Output JSON", true).option("--no-json", "Output human-readable table").action((opts) => {
7873
+ const apps = program2.command("apps").description("List Topolo platform applications available to the current credential").option("--json", "Output JSON", true).option("--no-json", "Output human-readable table").action(async (opts) => {
7866
7874
  const mode = resolveOutputMode(opts.json);
7867
7875
  try {
7868
- const rows = applicationRows();
7876
+ const topolo = await buildClient();
7877
+ if (!topolo) {
7878
+ throw new Error("No credential configured. Run `topolo auth login` or set TOPOLO_API_KEY.");
7879
+ }
7880
+ const rows = applicationRowsFromCatalog(await topolo.client.listServices());
7869
7881
  emit({ applications: rows }, mode, () => formatRows(rows));
7870
7882
  } catch (err) {
7871
7883
  fail(err, mode);
7872
7884
  }
7873
7885
  });
7874
- apps.command("get <application>").description("Show one Topolo application. Run `topolo apps --json` to discover application IDs.").option("--json", "Output JSON", true).option("--no-json", "Output human-readable details").action((application, opts) => {
7886
+ apps.command("get <application>").description("Show one Topolo application available to the current credential.").option("--json", "Output JSON", true).option("--no-json", "Output human-readable details").action(async (application, opts) => {
7875
7887
  const mode = resolveOutputMode(opts.json);
7876
7888
  try {
7877
- const app = resolveApplication(application);
7878
- emit({ application: app }, mode, () => formatApplication(app));
7889
+ const topolo = await buildClient();
7890
+ if (!topolo) {
7891
+ throw new Error("No credential configured. Run `topolo auth login` or set TOPOLO_API_KEY.");
7892
+ }
7893
+ const app = resolveCatalogApplication(await topolo.client.listServices(), application);
7894
+ emit({ application: app }, mode, () => formatApplicationRow(app));
7879
7895
  } catch (err) {
7880
7896
  fail(err, mode);
7881
7897
  }
@@ -7983,19 +7999,27 @@ function registerAppsCommand(program2) {
7983
7999
  }
7984
8000
  });
7985
8001
  }
7986
- function applicationRows() {
7987
- return APPLICATION_IDS.map((id) => {
7988
- const app = APPLICATIONS[id];
7989
- return {
7990
- id,
7991
- name: app.name,
7992
- appDir: app.appDir,
7993
- packageName: app.packageName,
7994
- productionUrl: app.productionUrl,
7995
- services: [...app.services],
7996
- deployTargets: app.deployTargets.length
7997
- };
7998
- });
8002
+ function applicationRowsFromCatalog(catalog) {
8003
+ return catalog.filter((entry) => entry.launchable || entry.surfaceKind === "application").map((entry) => ({
8004
+ id: displayCatalogId(entry),
8005
+ name: entry.name ?? entry.slug ?? entry.serviceId,
8006
+ appDir: "-",
8007
+ packageName: null,
8008
+ productionUrl: entry.baseUrl,
8009
+ services: [displayCatalogId(entry)],
8010
+ deployTargets: 0
8011
+ })).sort((left, right) => left.id.localeCompare(right.id));
8012
+ }
8013
+ function resolveCatalogApplication(catalog, application) {
8014
+ const needle = application.trim().toLowerCase();
8015
+ const rows = applicationRowsFromCatalog(catalog);
8016
+ const match = rows.find(
8017
+ (row) => row.id.toLowerCase() === needle || row.name.toLowerCase() === needle || row.services.some((service) => service.toLowerCase() === needle)
8018
+ );
8019
+ if (!match) {
8020
+ throw new Error(`Unknown application "${application}". Run \`topolo apps --json\` to discover applications available to this credential.`);
8021
+ }
8022
+ return match;
7999
8023
  }
8000
8024
  function resolveApplication(application) {
8001
8025
  const id = application.trim().toLowerCase();
@@ -8150,28 +8174,31 @@ function formatRows(rows) {
8150
8174
  }
8151
8175
  return lines.join("\n");
8152
8176
  }
8153
- function formatApplication(application) {
8154
- const deployTargets = application.deployTargets;
8155
- const lines = [
8177
+ function formatApplicationRow(application) {
8178
+ return [
8156
8179
  `ID: ${application.id}`,
8157
8180
  `Name: ${application.name}`,
8158
- `Directory: ${application.appDir}`,
8159
- `Package: ${application.packageName ?? "-"}`,
8160
8181
  `URL: ${application.productionUrl ?? "-"}`,
8161
- `Services: ${application.services.length ? application.services.join(", ") : "-"}`,
8162
- "Targets:"
8163
- ];
8164
- if (deployTargets.length === 0) {
8165
- lines.push(" - none");
8166
- } else {
8167
- for (const target of deployTargets) {
8168
- const parts = [`${target.name} (${target.kind})`];
8169
- if (target.serviceId) parts.push(`service=${target.serviceId}`);
8170
- if (target.apiUrl) parts.push(`api=${target.apiUrl}`);
8171
- lines.push(` - ${parts.join(" ")}`);
8172
- }
8182
+ `Services: ${application.services.length ? application.services.join(", ") : "-"}`
8183
+ ].join("\n");
8184
+ }
8185
+ function displayCatalogId(entry) {
8186
+ for (const value of [entry.slug, entry.name, entry.serviceId]) {
8187
+ if (typeof value !== "string" || !value.trim()) continue;
8188
+ const candidates = catalogIdCandidates(value);
8189
+ const withoutTopolo = candidates.find((candidate) => !candidate.startsWith("topolo-"));
8190
+ if (withoutTopolo) return withoutTopolo;
8191
+ if (candidates[0]) return candidates[0];
8173
8192
  }
8174
- return lines.join("\n");
8193
+ return entry.serviceId;
8194
+ }
8195
+ function catalogIdCandidates(value) {
8196
+ const normalized = value.trim().toLowerCase().replace(/\s+/g, "-");
8197
+ const withoutTopolo = normalized.startsWith("topolo-") ? normalized.slice("topolo-".length) : normalized;
8198
+ return [.../* @__PURE__ */ new Set([
8199
+ normalized,
8200
+ withoutTopolo
8201
+ ])].filter(Boolean);
8175
8202
  }
8176
8203
  function formatRequirements(payload) {
8177
8204
  const lines = [
@@ -8550,26 +8577,35 @@ function registerServicesCommand(program2) {
8550
8577
  try {
8551
8578
  const environment = selectedEnvironment();
8552
8579
  const config = await readConfig();
8553
- const rows = buildRows(config.serviceUrls, environment);
8580
+ const topolo = await buildClient();
8581
+ if (!topolo) {
8582
+ throw new Error("No credential configured. Run `topolo auth login` or set TOPOLO_API_KEY.");
8583
+ }
8584
+ const rows = buildRows(await topolo.client.listServices(), config.serviceUrls, environment);
8554
8585
  emit({ environment, services: rows }, mode, () => formatRows2(rows, environment));
8555
8586
  } catch (err) {
8556
8587
  fail(err, mode);
8557
8588
  }
8558
8589
  });
8559
8590
  }
8560
- function buildRows(overrides, environment = selectedEnvironment()) {
8591
+ function buildRows(catalog, overrides, environment = selectedEnvironment()) {
8561
8592
  const resolvedOverrides = resolveServiceUrls(overrides, environment);
8562
- return Object.keys(DEFAULT_SERVICE_URLS).sort().map((id) => {
8563
- const defaultUrl = DEFAULT_SERVICE_URLS[id];
8564
- const effectiveUrl = resolveServiceUrl(id, resolvedOverrides);
8593
+ return catalog.map((entry) => {
8594
+ const id = displayServiceId(entry);
8595
+ const defaultUrl = defaultServiceUrl(id, entry.baseUrl);
8596
+ const configuredUrl = configuredServiceUrl(id, resolvedOverrides);
8597
+ const effectiveUrl = configuredUrl || defaultUrl;
8565
8598
  return {
8566
8599
  id,
8600
+ serviceId: entry.serviceId,
8601
+ slug: entry.slug,
8567
8602
  defaultUrl,
8568
8603
  effectiveUrl,
8569
- overridden: defaultUrl !== effectiveUrl,
8570
- environmentSafe: environment !== "staging" || isStagingUrl(effectiveUrl)
8604
+ overridden: Boolean(configuredUrl && configuredUrl !== defaultUrl),
8605
+ environmentSafe: environment !== "staging" || isStagingUrl(effectiveUrl),
8606
+ permissions: [...entry.permissions]
8571
8607
  };
8572
- });
8608
+ }).sort((left, right) => left.id.localeCompare(right.id));
8573
8609
  }
8574
8610
  function formatRows2(rows, environment = selectedEnvironment()) {
8575
8611
  const idWidth = Math.max(3, ...rows.map((r) => r.id.length));
@@ -8590,6 +8626,40 @@ function isStagingUrl(value) {
8590
8626
  const url = value.toLowerCase();
8591
8627
  return url.includes(".stg.topolo.us") || url.includes("localhost") || url.includes("127.0.0.1") || url.includes("topolo-staging");
8592
8628
  }
8629
+ function displayServiceId(entry) {
8630
+ for (const value of [entry.slug, entry.name, entry.serviceId]) {
8631
+ if (typeof value !== "string" || !value.trim()) continue;
8632
+ for (const candidate of serviceIdCandidates(value)) {
8633
+ if (Object.prototype.hasOwnProperty.call(DEFAULT_SERVICE_URLS, candidate)) {
8634
+ return candidate;
8635
+ }
8636
+ }
8637
+ }
8638
+ return serviceIdCandidates(entry.slug || entry.serviceId)[0] || entry.serviceId;
8639
+ }
8640
+ function defaultServiceUrl(id, fallback) {
8641
+ return fallback ?? DEFAULT_SERVICE_URLS[id] ?? "";
8642
+ }
8643
+ function configuredServiceUrl(id, overrides) {
8644
+ if (!overrides) return null;
8645
+ for (const candidate of serviceIdCandidates(id)) {
8646
+ const direct = overrides[candidate];
8647
+ if (direct) return direct;
8648
+ const envStyle = overrides[candidate.replace(/\./g, "_")];
8649
+ if (envStyle) return envStyle;
8650
+ }
8651
+ return null;
8652
+ }
8653
+ function serviceIdCandidates(value) {
8654
+ const normalized = value.trim().toLowerCase().replace(/\s+/g, "-");
8655
+ const withoutTopolo = normalized.startsWith("topolo-") ? normalized.slice("topolo-".length) : normalized;
8656
+ return [.../* @__PURE__ */ new Set([
8657
+ normalized,
8658
+ normalized.replace(/-/g, "_"),
8659
+ withoutTopolo,
8660
+ withoutTopolo.replace(/-/g, "_")
8661
+ ])].filter(Boolean);
8662
+ }
8593
8663
 
8594
8664
  // src/commands/seed.ts
8595
8665
  var DEFAULT_SEED_BASE_URL = "https://seed.stg.topolo.us";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@topolo/cli",
3
- "version": "0.1.24",
3
+ "version": "0.1.26",
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": {