@topolo/cli 0.4.4 → 0.4.6

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 +146 -34
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -75,6 +75,7 @@ var APP_API_URLS = {
75
75
  auth: "https://auth.topolo.app",
76
76
  backup: "https://backup.topolo.app",
77
77
  books: "https://books.topolo.app",
78
+ brand: "https://brand.topolo.app",
78
79
  bugfix: "https://bugfix.topolo.app",
79
80
  bytes: "https://bytes.topolo.app",
80
81
  calendar: "https://calendar.topolo.app",
@@ -255,6 +256,38 @@ var APPLICATIONS = {
255
256
  }
256
257
  ]
257
258
  },
259
+ "brand": {
260
+ "id": "brand",
261
+ "appDir": "TopoloBrand",
262
+ "name": "topolo-brand",
263
+ "packageName": "@topolo/topolo-brand",
264
+ "productionUrl": "https://brand.topolo.app",
265
+ "services": [
266
+ "brand"
267
+ ],
268
+ "deployTargets": [
269
+ {
270
+ "name": "topolo-brand",
271
+ "kind": "worker",
272
+ "apiUrl": "https://brand.topolo.app",
273
+ "domains": [
274
+ "brand.topolo.app"
275
+ ],
276
+ "routes": [
277
+ "brand.topolo.app"
278
+ ],
279
+ "appId": "brand"
280
+ },
281
+ {
282
+ "name": "topolo-brand-api",
283
+ "kind": "worker",
284
+ "apiUrl": null,
285
+ "domains": [],
286
+ "routes": [],
287
+ "appId": null
288
+ }
289
+ ]
290
+ },
258
291
  "bugfix": {
259
292
  "id": "bugfix",
260
293
  "appDir": "TopoloBugFix",
@@ -2369,6 +2402,8 @@ var TopoloClient = class _TopoloClient {
2369
2402
  requireConfirmForWrites;
2370
2403
  timeoutMs;
2371
2404
  fetchImpl;
2405
+ credentialProvider;
2406
+ credentialLoadInFlight = null;
2372
2407
  refreshCredential;
2373
2408
  refreshInFlight = null;
2374
2409
  debug;
@@ -2396,6 +2431,7 @@ var TopoloClient = class _TopoloClient {
2396
2431
  this.requireConfirmForWrites = options.requireConfirmForWrites !== false;
2397
2432
  this.timeoutMs = options.timeoutMs ?? 3e4;
2398
2433
  this.fetchImpl = options.fetch ?? fetch;
2434
+ this.credentialProvider = options.credentialProvider;
2399
2435
  this.refreshCredential = options.refreshCredential;
2400
2436
  this.debug = options.debug;
2401
2437
  }
@@ -2418,6 +2454,7 @@ var TopoloClient = class _TopoloClient {
2418
2454
  `Mutating ${method} requests require { confirm: true }. This guardrail prevents agents from issuing writes without an explicit human-in-the-loop acknowledgement.`
2419
2455
  );
2420
2456
  }
2457
+ await this.loadCurrentCredential();
2421
2458
  const resolvedService = opts.baseUrl ? { serviceKey: normalizeLookupKey(opts.service), baseUrl: requireHttpOrigin(opts.baseUrl) } : await this.resolveRequestService(opts.service);
2422
2459
  const baseUrl = resolvedService.baseUrl;
2423
2460
  const url = new URL(opts.path, baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`);
@@ -2529,6 +2566,19 @@ var TopoloClient = class _TopoloClient {
2529
2566
  });
2530
2567
  return parsed;
2531
2568
  }
2569
+ async loadCurrentCredential() {
2570
+ if (!this.credentialProvider) return;
2571
+ this.credentialLoadInFlight ??= this.credentialProvider().then((credential) => {
2572
+ if (!credential) {
2573
+ throw new TopoloAuthError("The configured Topolo credential is no longer available.");
2574
+ }
2575
+ this.credential = credential;
2576
+ return credential;
2577
+ }).finally(() => {
2578
+ this.credentialLoadInFlight = null;
2579
+ });
2580
+ await this.credentialLoadInFlight;
2581
+ }
2532
2582
  async refreshRejectedAccessToken(attemptedAccessToken) {
2533
2583
  if (this.credential.kind === "access_token" && this.credential.accessToken !== attemptedAccessToken) {
2534
2584
  return this.credential;
@@ -3197,6 +3247,7 @@ function normalizeActionCatalogEntry(item) {
3197
3247
  permissionName: pickActionString(a.permission_name) ?? pickActionString(a.permissionName) ?? "",
3198
3248
  requiredPermission: pickActionString(a.required_permission) ?? pickActionString(a.requiredPermission) ?? "",
3199
3249
  authorizationMode: pickAuthorizationMode(a.authorization_mode ?? a.authorizationMode),
3250
+ allowedServices: stringArray(a.allowed_services ?? a.allowedServices),
3200
3251
  inputSchema: plainObject(a.input_schema ?? a.inputSchema),
3201
3252
  outputSchema: plainObject(a.output_schema ?? a.outputSchema),
3202
3253
  agentContract: plainObject(a.agent_contract ?? a.agentContract),
@@ -3218,7 +3269,7 @@ function pickAgentAccess(value) {
3218
3269
  return value === "auto" || value === "confirm" || value === "off" ? value : null;
3219
3270
  }
3220
3271
  function pickAuthorizationMode(value) {
3221
- return value === "runtime" ? "runtime" : "static";
3272
+ return value === "runtime" || value === "public" || value === "service" ? value : "static";
3222
3273
  }
3223
3274
  function plainObject(value) {
3224
3275
  return value && typeof value === "object" && !Array.isArray(value) ? value : {};
@@ -3312,6 +3363,7 @@ var CrmModule = class {
3312
3363
  constructor(client) {
3313
3364
  this.client = client;
3314
3365
  }
3366
+ client;
3315
3367
  async listContacts(options = {}) {
3316
3368
  const res = await this.client.request({
3317
3369
  service: "crm",
@@ -3357,6 +3409,7 @@ var IdentityModule = class {
3357
3409
  constructor(client) {
3358
3410
  this.client = client;
3359
3411
  }
3412
+ client;
3360
3413
  /** Returns the resolved user, organization, and permission set for the
3361
3414
  * current credential. Equivalent to `TopoloClient.introspect()` — exposed
3362
3415
  * on this module for symmetry with the other domain modules. */
@@ -3489,6 +3542,7 @@ var APP_API_URLS2 = {
3489
3542
  auth: "https://auth.topolo.app",
3490
3543
  backup: "https://backup.topolo.app",
3491
3544
  books: "https://books.topolo.app",
3545
+ brand: "https://brand.topolo.app",
3492
3546
  bugfix: "https://bugfix.topolo.app",
3493
3547
  bytes: "https://bytes.topolo.app",
3494
3548
  calendar: "https://calendar.topolo.app",
@@ -3817,6 +3871,46 @@ async function refreshCredentialIfNeeded(environment = selectedEnvironment(), op
3817
3871
  return cred;
3818
3872
  }
3819
3873
  }
3874
+ async function refreshContextCredentialIfNeeded(environment, key, options = {}) {
3875
+ const config = await readConfig();
3876
+ const profile = profileForEnvironment(config, environment);
3877
+ const context = profile.contexts?.[key];
3878
+ const credential = context?.credential;
3879
+ if (!credential || credential.kind !== "access_token") return credential ?? null;
3880
+ const oauthClientId = context.oauthClientId ?? profile.oauthClientId;
3881
+ if (!credential.refreshToken || !oauthClientId) return credential;
3882
+ const now = Math.floor(Date.now() / 1e3);
3883
+ if (!options.force && credential.expiresAt && credential.expiresAt - REFRESH_SKEW_SECONDS > now) return credential;
3884
+ try {
3885
+ const oauth = new TopoloOAuth2({
3886
+ serviceUrls: resolveServiceUrls(profile.serviceUrls, environment)
3887
+ });
3888
+ const tokens = await oauth.refreshToken({
3889
+ clientId: oauthClientId,
3890
+ refreshToken: credential.refreshToken
3891
+ });
3892
+ const refreshed = {
3893
+ kind: "access_token",
3894
+ accessToken: tokens.access_token,
3895
+ ...tokens.refresh_token ? { refreshToken: tokens.refresh_token } : { refreshToken: credential.refreshToken },
3896
+ expiresAt: Math.floor(Date.now() / 1e3) + tokens.expires_in
3897
+ };
3898
+ await writeConfig(writeProfileConfig(config, environment, {
3899
+ ...profile.activeContext === key ? { credential: refreshed } : {},
3900
+ contexts: {
3901
+ ...profile.contexts ?? {},
3902
+ [key]: {
3903
+ ...context,
3904
+ credential: refreshed,
3905
+ oauthClientId
3906
+ }
3907
+ }
3908
+ }));
3909
+ return refreshed;
3910
+ } catch {
3911
+ return credential;
3912
+ }
3913
+ }
3820
3914
  function resolveAgentName(configAgentName) {
3821
3915
  return process.env.TOPOLO_AGENT_NAME ?? configAgentName;
3822
3916
  }
@@ -5924,6 +6018,7 @@ function packageJson(application, target, options) {
5924
6018
  version: "0.1.0",
5925
6019
  private: true,
5926
6020
  type: "module",
6021
+ ...target === "platform" ? { packageManager: "pnpm@10.17.1" } : {},
5927
6022
  scripts,
5928
6023
  dependencies,
5929
6024
  devDependencies
@@ -5942,7 +6037,7 @@ function topoloPackageVersion(packageName, target) {
5942
6037
  "@topolo/login": "^0.1.0",
5943
6038
  "@topolo-io/app-funnel": "^0.1.6",
5944
6039
  "@topolo-io/app-launcher": "^0.1.15",
5945
- "@topolo-io/app-shell": "^0.5.54",
6040
+ "@topolo-io/app-shell": "^0.8.4",
5946
6041
  "@topolo-io/auth-client": "^1.2.1",
5947
6042
  "@topolo-io/auth-middleware": "^0.4.0",
5948
6043
  "@topolo-io/error-middleware": "^0.6.0",
@@ -7419,6 +7514,7 @@ function platformWorkerTs(application, options) {
7419
7514
  "import { withSecurityHeaders } from '@topolo-io/security-headers';",
7420
7515
  "import { withErrorReporting } from '@topolo-io/error-middleware';",
7421
7516
  "import { SCAFFOLD_APP_NAME, SCAFFOLD_SERVICE_SLUG, resolveRuntimeAppId } from './app-identity';",
7517
+ "import { topoloActionContracts } from '../../../topolo.action-contracts.mjs';",
7422
7518
  ...options.resources.workflow ? ["import { WorkflowEntrypoint, type WorkflowEvent, type WorkflowStep } from 'cloudflare:workers';"] : [],
7423
7519
  "",
7424
7520
  "export interface Env {",
@@ -7447,6 +7543,7 @@ function platformWorkerTs(application, options) {
7447
7543
  " // so rt.verifyAuth() targets the correct Auth service per deploy.",
7448
7544
  " authBaseUrl: (env) => env.AUTH_BASE_URL,",
7449
7545
  " authGateway: { enabled: false },",
7546
+ " actionContracts: topoloActionContracts,",
7450
7547
  "});",
7451
7548
  "",
7452
7549
  "async function handleRequest(",
@@ -7476,9 +7573,10 @@ function platformWorkerTs(application, options) {
7476
7573
  " }, { status: 200 });",
7477
7574
  " }",
7478
7575
  "",
7576
+ " const auth = await rt.verifyAuth({ appId });",
7577
+ " if (!auth.ok) return rt.authError(auth);",
7578
+ "",
7479
7579
  " if (url.pathname === '/api/widget') {",
7480
- " const auth = await rt.verifyAuth({ appId });",
7481
- " if (!auth.ok) return rt.authError(auth);",
7482
7580
  " return rt.json(createTopoloWidgetResponse({",
7483
7581
  " appId,",
7484
7582
  " serviceName: SCAFFOLD_APP_NAME,",
@@ -7486,12 +7584,6 @@ function platformWorkerTs(application, options) {
7486
7584
  " }), { status: 200, headers: { 'Cache-Control': 'public, max-age=15' } });",
7487
7585
  " }",
7488
7586
  "",
7489
- " const auth = await rt.verifyAuth({ appId });",
7490
- "",
7491
- " if (!auth.ok) {",
7492
- " return rt.authError(auth);",
7493
- " }",
7494
- "",
7495
7587
  " if (url.pathname === '/api/bootstrap') {",
7496
7588
  ...bootstrapBody,
7497
7589
  " }",
@@ -7850,8 +7942,8 @@ function topoloAppManifestJson(application, options) {
7850
7942
  },
7851
7943
  compatibilityFlags: ["nodejs_compat"],
7852
7944
  buildCommand: {
7853
- staging: "cd ../.. && DEPLOY_ENVIRONMENT=staging VITE_TOPOLO_AUTH_URL=https://auth.stg.topolo.us npm run build",
7854
- production: "cd ../.. && DEPLOY_ENVIRONMENT=production VITE_TOPOLO_AUTH_URL=https://auth.topolo.app npm run build"
7945
+ staging: "cd ../.. && DEPLOY_ENVIRONMENT=staging VITE_TOPOLO_AUTH_URL=https://auth.stg.topolo.us pnpm run build",
7946
+ production: "cd ../.. && DEPLOY_ENVIRONMENT=production VITE_TOPOLO_AUTH_URL=https://auth.topolo.app pnpm run build"
7855
7947
  },
7856
7948
  healthcheckPath: options.api ? "/api/health" : "/",
7857
7949
  ...options.api ? {
@@ -7909,12 +8001,14 @@ function topoloActionContractsMjs() {
7909
8001
  " name: 'widget.get',",
7910
8002
  " method: 'GET',",
7911
8003
  " path: '/api/widget',",
8004
+ " permission: 'workspace:read',",
7912
8005
  " input: emptyInput,",
7913
8006
  " },",
7914
8007
  " 'workspace.bootstrap': {",
7915
8008
  " name: 'workspace.bootstrap',",
7916
8009
  " method: 'GET',",
7917
8010
  " path: '/api/bootstrap',",
8011
+ " permission: 'workspace:read',",
7918
8012
  " input: emptyInput,",
7919
8013
  " },",
7920
8014
  "};",
@@ -8105,7 +8199,7 @@ function cloudcontrolJson(application, options) {
8105
8199
  cloudflare_account_id: STAGING_CLOUDFLARE_ACCOUNT_ID,
8106
8200
  config_path: "apps/web/wrangler.toml",
8107
8201
  workdir: "apps/web",
8108
- build_command: "DEPLOY_ENVIRONMENT=staging VITE_TOPOLO_AUTH_URL=https://auth.stg.topolo.us npm run build",
8202
+ build_command: "cd ../.. && DEPLOY_ENVIRONMENT=staging VITE_TOPOLO_AUTH_URL=https://auth.stg.topolo.us pnpm run build",
8109
8203
  output_dir: null,
8110
8204
  production_branch: "staging",
8111
8205
  healthcheck_path: "/api/health",
@@ -8138,7 +8232,7 @@ function cloudcontrolJson(application, options) {
8138
8232
  cloudflare_account_id: CLOUDFLARE_ACCOUNT_ID,
8139
8233
  config_path: "apps/web/wrangler.toml",
8140
8234
  workdir: "apps/web",
8141
- build_command: "DEPLOY_ENVIRONMENT=production VITE_TOPOLO_AUTH_URL=https://auth.topolo.app npm run build",
8235
+ build_command: "cd ../.. && DEPLOY_ENVIRONMENT=production VITE_TOPOLO_AUTH_URL=https://auth.topolo.app pnpm run build",
8142
8236
  output_dir: null,
8143
8237
  production_branch: "main",
8144
8238
  healthcheck_path: "/api/health",
@@ -8206,17 +8300,17 @@ function cloudcontrolJson(application, options) {
8206
8300
  ci_targets: [
8207
8301
  {
8208
8302
  name: "typecheck",
8209
- command: "npm run typecheck",
8303
+ command: "pnpm run typecheck",
8210
8304
  required: true
8211
8305
  },
8212
8306
  {
8213
8307
  name: "test",
8214
- command: "npm run test",
8308
+ command: "pnpm run test",
8215
8309
  required: true
8216
8310
  },
8217
8311
  {
8218
8312
  name: "build",
8219
- command: "npm run build",
8313
+ command: "pnpm run build",
8220
8314
  required: true
8221
8315
  }
8222
8316
  ],
@@ -11181,6 +11275,10 @@ function hasSameAccessToken(left, right) {
11181
11275
  const rightCredential = right;
11182
11276
  return leftCredential?.kind === "access_token" && rightCredential?.kind === "access_token" && leftCredential.accessToken === rightCredential.accessToken;
11183
11277
  }
11278
+ function identityMatchesContext(me, context) {
11279
+ if (context.contextType === "personal") return me.activeType === "personal";
11280
+ return me.activeType === "organization" && me.activeOrgId === context.organizationId;
11281
+ }
11184
11282
  function registerContextCommand(program2) {
11185
11283
  const context = program2.command("context").description("Show and switch the active context (personal or an organization)");
11186
11284
  context.command("ls", { isDefault: true }).alias("list").description("List the contexts you can act in and which is active").option("--json", "Output JSON", true).option("--no-json", "Output human-readable text").action(async (opts) => {
@@ -11276,24 +11374,38 @@ function registerContextCommand(program2) {
11276
11374
  const key = contextKey(resolved.contextType, resolved.organizationId);
11277
11375
  const existing = profile.contexts?.[key];
11278
11376
  let newCredential;
11279
- try {
11280
- newCredential = await exchangeForContext(cred, serviceUrls, {
11281
- type: resolved.contextType,
11282
- id: resolved.organizationId
11283
- });
11284
- } catch (error) {
11285
- if (!isAuthenticationFailure(error)) throw error;
11286
- const refreshed = await refreshCredentialIfNeeded(environment, { force: true });
11287
- if (!refreshed || hasSameAccessToken(cred, refreshed)) {
11288
- throw new Error(
11289
- `The active credential could not be refreshed. Run \`topolo --env ${environment} auth login\` to sign in again.`
11290
- );
11377
+ if (existing?.credential && profile.activeContext !== key) {
11378
+ const storedCredential = await refreshContextCredentialIfNeeded(environment, key);
11379
+ if (storedCredential) {
11380
+ try {
11381
+ const storedIdentity = await fetchMe(storedCredential, serviceUrls);
11382
+ if (identityMatchesContext(storedIdentity, resolved)) {
11383
+ newCredential = storedCredential;
11384
+ }
11385
+ } catch {
11386
+ }
11387
+ }
11388
+ }
11389
+ if (!newCredential) {
11390
+ try {
11391
+ newCredential = await exchangeForContext(cred, serviceUrls, {
11392
+ type: resolved.contextType,
11393
+ id: resolved.organizationId
11394
+ });
11395
+ } catch (error) {
11396
+ if (!isAuthenticationFailure(error)) throw error;
11397
+ const refreshed = await refreshCredentialIfNeeded(environment, { force: true });
11398
+ if (!refreshed || hasSameAccessToken(cred, refreshed)) {
11399
+ throw new Error(
11400
+ `The active credential could not be refreshed. Run \`topolo --env ${environment} auth login\` to sign in again.`
11401
+ );
11402
+ }
11403
+ cred = refreshed;
11404
+ newCredential = await exchangeForContext(cred, serviceUrls, {
11405
+ type: resolved.contextType,
11406
+ id: resolved.organizationId
11407
+ });
11291
11408
  }
11292
- cred = refreshed;
11293
- newCredential = await exchangeForContext(cred, serviceUrls, {
11294
- type: resolved.contextType,
11295
- id: resolved.organizationId
11296
- });
11297
11409
  }
11298
11410
  resolved.credential = newCredential;
11299
11411
  resolved.oauthClientId = existing?.oauthClientId ?? profile.oauthClientId;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@topolo/cli",
3
- "version": "0.4.4",
3
+ "version": "0.4.6",
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": {