@topolo/sdk 0.10.3 → 0.10.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.
@@ -23,6 +23,8 @@ __export(credential_store_exports, {
23
23
  ENVIRONMENT_DEFAULT_SERVICE_URLS: () => ENVIRONMENT_DEFAULT_SERVICE_URLS,
24
24
  TOPOLO_ENVIRONMENTS: () => TOPOLO_ENVIRONMENTS,
25
25
  activateContext: () => activateContext,
26
+ assertServiceUrlForEnvironment: () => assertServiceUrlForEnvironment,
27
+ assertTopoloServiceUrlEnvironment: () => assertTopoloServiceUrlEnvironment,
26
28
  clearProfileCredential: () => clearProfileCredential,
27
29
  configPath: () => configPath,
28
30
  contextKey: () => contextKey,
@@ -38,6 +40,7 @@ __export(credential_store_exports, {
38
40
  resolveServiceUrls: () => resolveServiceUrls,
39
41
  resolvedProfileConfig: () => resolvedProfileConfig,
40
42
  selectedEnvironment: () => selectedEnvironment,
43
+ topoloServiceOrigins: () => topoloServiceOrigins,
41
44
  writeConfig: () => writeConfig,
42
45
  writeContextCredential: () => writeContextCredential,
43
46
  writeProfileConfig: () => writeProfileConfig
@@ -57,6 +60,90 @@ var TopoloSdkError = class extends Error {
57
60
  this.code = code;
58
61
  }
59
62
  };
63
+ var PUBLIC_ERROR_CODES = /* @__PURE__ */ new Set([
64
+ "action_app_identity_unavailable",
65
+ "action_authentication_required",
66
+ "action_catalog_summary_unavailable",
67
+ "action_catalog_unavailable",
68
+ "action_contract_mismatch",
69
+ "action_permission_required",
70
+ "action_runtime_authorization_required",
71
+ "action_service_not_allowed",
72
+ "auth_error",
73
+ "authentication_required",
74
+ "authorization_pending",
75
+ "bad_sig",
76
+ "confirmation_required",
77
+ "dependency_unavailable",
78
+ "environment_mismatch",
79
+ "environment_service_url_required",
80
+ "expired_token",
81
+ "forbidden",
82
+ "http_error",
83
+ "immutable_drift",
84
+ "integration_dependency_unavailable",
85
+ "integration_misconfigured",
86
+ "invalid_action",
87
+ "invalid_action_input",
88
+ "invalid_grant",
89
+ "invalid_input",
90
+ "invalid_token",
91
+ "mcp_error",
92
+ "membership_required",
93
+ "network_error",
94
+ "organization_context_required",
95
+ "organization_enrollment_required",
96
+ "permission_denied",
97
+ "rate_limited",
98
+ "request_failed",
99
+ "service_catalog_unavailable",
100
+ "service_path_origin_mismatch",
101
+ "service_required",
102
+ "slow_down",
103
+ "state_conflict",
104
+ "unauthorized",
105
+ "unknown_action",
106
+ "unknown_service"
107
+ ]);
108
+ var ERROR_CODE_PATTERN = /^[a-z][a-z0-9_]{0,63}$/;
109
+ var TOPOLO_REQUEST_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
110
+ var CLOUDFLARE_RAY_ID_PATTERN = /^[0-9a-f]{16}-[A-Z]{3}$/;
111
+ var PERMISSION_PATTERN = /^[a-z][a-z0-9_-]{0,63}(?:\.[a-z][a-z0-9_-]{0,63})*:(?:[a-z][a-z0-9_-]{0,63}|\*)$/;
112
+ var RETRY_AFTER_DATE_PATTERN = /^(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun), \d{2} (?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) \d{4} \d{2}:\d{2}:\d{2} GMT$/;
113
+ var PUBLIC_DETAIL_SOURCES = /* @__PURE__ */ new Set([
114
+ "api_key_introspection",
115
+ "api_key_validation",
116
+ "auth_context",
117
+ "auth_validate",
118
+ "catalog",
119
+ "credential",
120
+ "runtime",
121
+ "service_token_provider",
122
+ "service_verify_key",
123
+ "token_validation",
124
+ "upstream"
125
+ ]);
126
+ function canonicalTopoloErrorCode(value) {
127
+ if (typeof value !== "string") return null;
128
+ const candidate = value.trim().toLowerCase();
129
+ if (!ERROR_CODE_PATTERN.test(candidate) || !PUBLIC_ERROR_CODES.has(candidate)) return null;
130
+ return candidate;
131
+ }
132
+ function canonicalTopoloRequestId(value) {
133
+ if (typeof value !== "string") return void 0;
134
+ const candidate = value.trim();
135
+ return TOPOLO_REQUEST_ID_PATTERN.test(candidate) || CLOUDFLARE_RAY_ID_PATTERN.test(candidate) ? candidate : void 0;
136
+ }
137
+ function canonicalTopoloRetryAfter(value) {
138
+ if (typeof value !== "string") return void 0;
139
+ const candidate = value.trim();
140
+ if (/^\d{1,10}$/.test(candidate) || RETRY_AFTER_DATE_PATTERN.test(candidate)) return candidate;
141
+ return void 0;
142
+ }
143
+ function canonicalTopoloRequired(value) {
144
+ if (!Array.isArray(value)) return [];
145
+ return [...new Set(value.filter((item) => typeof item === "string" && PERMISSION_PATTERN.test(item)))].slice(0, 32);
146
+ }
60
147
  var TopoloAuthError = class extends TopoloSdkError {
61
148
  status;
62
149
  body;
@@ -64,27 +151,91 @@ var TopoloAuthError = class extends TopoloSdkError {
64
151
  path;
65
152
  requestId;
66
153
  retryAfter;
154
+ details;
67
155
  constructor(message, code = "auth_error", options = {}) {
68
- super(code, message);
156
+ super(canonicalTopoloErrorCode(code) ?? "auth_error", message);
69
157
  this.name = "TopoloAuthError";
70
158
  this.status = options.status ?? 401;
71
159
  this.body = options.body;
72
160
  this.service = options.service;
73
161
  this.path = options.path;
74
- this.requestId = options.requestId;
75
- this.retryAfter = options.retryAfter;
162
+ this.requestId = canonicalTopoloRequestId(options.requestId);
163
+ this.retryAfter = canonicalTopoloRetryAfter(options.retryAfter);
164
+ this.details = extractTopoloServiceErrorDetails(
165
+ options.details === void 0 ? options.body : { details: options.details }
166
+ );
76
167
  }
77
168
  };
78
- function extractHttpErrorCode(body) {
79
- if (!body || typeof body !== "object" || Array.isArray(body)) return "http_error";
169
+ function extractTopoloServiceErrorCode(body) {
170
+ if (!body || typeof body !== "object" || Array.isArray(body)) return null;
80
171
  const record = body;
81
- if (typeof record.code === "string" && record.code.trim()) return record.code.trim();
172
+ const topLevel = canonicalTopoloErrorCode(record.code);
173
+ if (topLevel) return topLevel;
82
174
  const nested = record.error;
175
+ const nestedString = canonicalTopoloErrorCode(nested);
176
+ if (nestedString) return nestedString;
83
177
  if (nested && typeof nested === "object" && !Array.isArray(nested)) {
84
178
  const code = nested.code;
85
- if (typeof code === "string" && code.trim()) return code.trim();
179
+ const nestedCode = canonicalTopoloErrorCode(code);
180
+ if (nestedCode) return nestedCode;
181
+ }
182
+ return null;
183
+ }
184
+ function serviceErrorCode(body, fallback) {
185
+ return extractTopoloServiceErrorCode(body) ?? fallback;
186
+ }
187
+ function extractTopoloServiceErrorDetails(body) {
188
+ if (!body || typeof body !== "object" || Array.isArray(body)) return void 0;
189
+ const record = body;
190
+ let candidate = record.details;
191
+ const nested = record.error;
192
+ if (nested && typeof nested === "object" && !Array.isArray(nested)) {
193
+ const nestedRecord = nested;
194
+ if (candidate === void 0) candidate = nestedRecord.details ?? nestedRecord;
195
+ }
196
+ if (candidate === void 0) candidate = record;
197
+ if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) return void 0;
198
+ const source = candidate;
199
+ const safe = {};
200
+ for (const [key, value] of Object.entries(source)) {
201
+ if (/present$/i.test(key) && typeof value === "boolean") {
202
+ safe[key] = value;
203
+ continue;
204
+ }
205
+ if (key === "retryable" && typeof value === "boolean") {
206
+ safe.retryable = value;
207
+ continue;
208
+ }
209
+ if (key === "code") {
210
+ const sanitized = canonicalTopoloErrorCode(value);
211
+ if (sanitized) safe.code = sanitized;
212
+ continue;
213
+ }
214
+ if (key === "source") {
215
+ const sanitized = typeof value === "string" ? value.trim().toLowerCase() : "";
216
+ if (PUBLIC_DETAIL_SOURCES.has(sanitized)) safe.source = sanitized;
217
+ continue;
218
+ }
219
+ if (key === "dependency" || key === "platform") {
220
+ safe[`${key}Present`] = value !== null && value !== void 0;
221
+ continue;
222
+ }
223
+ if (key === "error" && !safe.code) {
224
+ const sanitized = canonicalTopoloErrorCode(value);
225
+ if (sanitized) safe.code = sanitized;
226
+ continue;
227
+ }
228
+ if (key === "required" && Array.isArray(value)) {
229
+ const required = canonicalTopoloRequired(value);
230
+ if (required.length) safe.required = required;
231
+ continue;
232
+ }
233
+ if (["missingFields", "fields", "paths"].includes(key) && Array.isArray(value)) {
234
+ const paths = value.filter((item) => typeof item === "string" && /^[$.a-z0-9_[\]-]{1,160}$/i.test(item));
235
+ if (paths.length) safe[key] = paths;
236
+ }
86
237
  }
87
- return "http_error";
238
+ return Object.keys(safe).length ? safe : void 0;
88
239
  }
89
240
  var TopoloHttpError = class extends TopoloSdkError {
90
241
  status;
@@ -93,15 +244,19 @@ var TopoloHttpError = class extends TopoloSdkError {
93
244
  path;
94
245
  requestId;
95
246
  retryAfter;
247
+ details;
96
248
  constructor(service, path, status, body, message, options = {}) {
97
- super(extractHttpErrorCode(body), message ?? `HTTP ${status} from ${service}${path}`);
249
+ super(serviceErrorCode(body, "http_error"), message ?? `HTTP ${status} from ${service}${path}`);
98
250
  this.name = "TopoloHttpError";
99
251
  this.status = status;
100
252
  this.body = body;
101
253
  this.service = service;
102
254
  this.path = path;
103
- this.requestId = options.requestId;
104
- this.retryAfter = options.retryAfter;
255
+ this.requestId = canonicalTopoloRequestId(options.requestId);
256
+ this.retryAfter = canonicalTopoloRetryAfter(options.retryAfter);
257
+ this.details = extractTopoloServiceErrorDetails(
258
+ options.details === void 0 ? body : { details: options.details }
259
+ );
105
260
  }
106
261
  };
107
262
 
@@ -214,13 +369,11 @@ var TopoloOAuth = class {
214
369
  }
215
370
  };
216
371
 
217
- // src/node/credential-store.ts
218
- var TOPOLO_ENVIRONMENTS = ["production", "staging", "development", "local"];
219
- function contextKey(contextType, organizationId) {
220
- return contextType === "organization" && organizationId ? `org:${organizationId}` : "personal";
221
- }
222
- var ENVIRONMENT_DEFAULT_SERVICE_URLS = {
372
+ // src/environment.ts
373
+ var TOPOLO_RUNTIME_ENVIRONMENTS = ["production", "staging", "development", "local"];
374
+ var TOPOLO_ENVIRONMENT_SERVICE_URLS = {
223
375
  production: {
376
+ auth: "https://auth.topolo.app",
224
377
  developers: "https://developers.topolo.app"
225
378
  },
226
379
  staging: {
@@ -234,6 +387,58 @@ var ENVIRONMENT_DEFAULT_SERVICE_URLS = {
234
387
  },
235
388
  local: {}
236
389
  };
390
+ function hostnameHasSuffix(hostname, suffix) {
391
+ return hostname === suffix || hostname.endsWith(`.${suffix}`);
392
+ }
393
+ function workersDevAccountMatches(hostname, account) {
394
+ if (!hostname.endsWith(".workers.dev")) return false;
395
+ const labels = hostname.slice(0, -".workers.dev".length).split(".");
396
+ return labels.length >= 2 && labels.at(-1) === account;
397
+ }
398
+ function originMatchesEnvironment(url, environment) {
399
+ const hostname = url.hostname.toLowerCase();
400
+ if (environment === "local") {
401
+ return hostname === "localhost" || hostname.endsWith(".localhost") || hostname === "127.0.0.1" || hostname === "[::1]";
402
+ }
403
+ if (url.protocol !== "https:") return false;
404
+ if (environment === "production") {
405
+ return hostnameHasSuffix(hostname, "topolo.app") || hostnameHasSuffix(hostname, "topolo.io") || workersDevAccountMatches(hostname, "topolo");
406
+ }
407
+ if (environment === "staging") {
408
+ return hostnameHasSuffix(hostname, "stg.topolo.us") || workersDevAccountMatches(hostname, "topolo-staging");
409
+ }
410
+ return hostnameHasSuffix(hostname, "topolo.dev") || workersDevAccountMatches(hostname, "topolo-development");
411
+ }
412
+ function assertTopoloServiceUrlEnvironment(service, value, environment) {
413
+ let url;
414
+ try {
415
+ url = new URL(value);
416
+ } catch {
417
+ throw new TopoloSdkError("invalid_service_url", `Invalid URL configured for Topolo service "${service}".`);
418
+ }
419
+ if (url.protocol !== "https:" && url.protocol !== "http:" || url.username || url.password || !originMatchesEnvironment(url, environment)) {
420
+ throw new TopoloSdkError(
421
+ "environment_mismatch",
422
+ `Refusing Topolo service "${service}": ${url.origin} does not belong to ${environment}.`
423
+ );
424
+ }
425
+ return url.origin;
426
+ }
427
+ function topoloServiceOrigins(serviceUrls, environment) {
428
+ return Object.fromEntries(
429
+ Object.entries(serviceUrls ?? {}).sort(([left], [right]) => left.localeCompare(right)).map(([service, value]) => [
430
+ service,
431
+ assertTopoloServiceUrlEnvironment(service, value, environment)
432
+ ])
433
+ );
434
+ }
435
+
436
+ // src/node/credential-store.ts
437
+ var TOPOLO_ENVIRONMENTS = TOPOLO_RUNTIME_ENVIRONMENTS;
438
+ function contextKey(contextType, organizationId) {
439
+ return contextType === "organization" && organizationId ? `org:${organizationId}` : "personal";
440
+ }
441
+ var ENVIRONMENT_DEFAULT_SERVICE_URLS = TOPOLO_ENVIRONMENT_SERVICE_URLS;
237
442
  function configPath() {
238
443
  const override = process.env.TOPOLO_CONFIG_PATH;
239
444
  if (override) return override;
@@ -351,15 +556,17 @@ function credentialSourceFromConfig(config, environment = selectedEnvironment())
351
556
  const prefix = envVarPrefix(environment);
352
557
  if (process.env[`${prefix}_API_KEY`]) return `env:${prefix}_API_KEY`;
353
558
  if (process.env[`${prefix}_ACCESS_TOKEN`]) return `env:${prefix}_ACCESS_TOKEN`;
354
- if (process.env.TOPOLO_API_KEY) return "env:TOPOLO_API_KEY";
355
- if (process.env.TOPOLO_ACCESS_TOKEN) return "env:TOPOLO_ACCESS_TOKEN";
559
+ if (environment === "production" && process.env.TOPOLO_API_KEY) return "env:TOPOLO_API_KEY";
560
+ if (environment === "production" && process.env.TOPOLO_ACCESS_TOKEN) {
561
+ return "env:TOPOLO_ACCESS_TOKEN";
562
+ }
356
563
  return profileForEnvironment(config, environment).credential ? "config-file" : "none";
357
564
  }
358
565
  async function resolveCredential(environment = selectedEnvironment()) {
359
566
  const prefix = envVarPrefix(environment);
360
- const apiKey = process.env[`${prefix}_API_KEY`] ?? process.env.TOPOLO_API_KEY;
567
+ const apiKey = process.env[`${prefix}_API_KEY`] ?? (environment === "production" ? process.env.TOPOLO_API_KEY : void 0);
361
568
  if (apiKey) return { kind: "api_key", apiKey };
362
- const accessToken = process.env[`${prefix}_ACCESS_TOKEN`] ?? process.env.TOPOLO_ACCESS_TOKEN;
569
+ const accessToken = process.env[`${prefix}_ACCESS_TOKEN`] ?? (environment === "production" ? process.env.TOPOLO_ACCESS_TOKEN : void 0);
363
570
  if (accessToken) return { kind: "access_token", accessToken };
364
571
  const config = await readConfig();
365
572
  return profileForEnvironment(config, environment).credential ?? null;
@@ -367,7 +574,7 @@ async function resolveCredential(environment = selectedEnvironment()) {
367
574
  var REFRESH_SKEW_SECONDS = 60;
368
575
  async function refreshCredentialIfNeeded(environment = selectedEnvironment(), options = {}) {
369
576
  const prefix = envVarPrefix(environment);
370
- if (process.env[`${prefix}_API_KEY`] || process.env[`${prefix}_ACCESS_TOKEN`] || process.env.TOPOLO_API_KEY || process.env.TOPOLO_ACCESS_TOKEN) {
577
+ if (process.env[`${prefix}_API_KEY`] || process.env[`${prefix}_ACCESS_TOKEN`] || environment === "production" && process.env.TOPOLO_API_KEY || environment === "production" && process.env.TOPOLO_ACCESS_TOKEN) {
371
578
  return resolveCredential(environment);
372
579
  }
373
580
  const config = await readConfig();
@@ -377,9 +584,16 @@ async function refreshCredentialIfNeeded(environment = selectedEnvironment(), op
377
584
  if (!cred.refreshToken || !profile.oauthClientId) return cred;
378
585
  const now = Math.floor(Date.now() / 1e3);
379
586
  if (!options.force && cred.expiresAt && cred.expiresAt - REFRESH_SKEW_SECONDS > now) return cred;
587
+ const serviceUrls = resolveServiceUrls(profile.serviceUrls, environment);
588
+ if (environment === "local" && !serviceUrls?.auth) {
589
+ throw new TopoloSdkError(
590
+ "environment_service_url_required",
591
+ "Local Topolo credential refresh requires an explicit local Auth service origin."
592
+ );
593
+ }
380
594
  try {
381
595
  const oauth = new TopoloOAuth({
382
- serviceUrls: resolveServiceUrls(profile.serviceUrls, environment)
596
+ serviceUrls
383
597
  });
384
598
  const tokens = await oauth.refreshToken({
385
599
  clientId: profile.oauthClientId,
@@ -420,9 +634,16 @@ async function refreshContextCredentialIfNeeded(environment, key, options = {})
420
634
  if (!credential.refreshToken || !oauthClientId) return credential;
421
635
  const now = Math.floor(Date.now() / 1e3);
422
636
  if (!options.force && credential.expiresAt && credential.expiresAt - REFRESH_SKEW_SECONDS > now) return credential;
637
+ const serviceUrls = resolveServiceUrls(profile.serviceUrls, environment);
638
+ if (environment === "local" && !serviceUrls?.auth) {
639
+ throw new TopoloSdkError(
640
+ "environment_service_url_required",
641
+ "Local Topolo context refresh requires an explicit local Auth service origin."
642
+ );
643
+ }
423
644
  try {
424
645
  const oauth = new TopoloOAuth({
425
- serviceUrls: resolveServiceUrls(profile.serviceUrls, environment)
646
+ serviceUrls
426
647
  });
427
648
  const tokens = await oauth.refreshToken({
428
649
  clientId: oauthClientId,
@@ -469,7 +690,12 @@ function resolveServiceUrls(configUrls, environment = selectedEnvironment()) {
469
690
  merged[k.slice("TOPOLO_SERVICE_URL_".length).toLowerCase()] = v;
470
691
  }
471
692
  }
472
- return Object.keys(merged).length > 0 ? merged : void 0;
693
+ const origins = topoloServiceOrigins(merged, environment);
694
+ return Object.keys(origins).length > 0 ? origins : void 0;
695
+ }
696
+ function assertServiceUrlForEnvironment(service, serviceUrl, environment = selectedEnvironment()) {
697
+ if (!serviceUrl) return;
698
+ assertTopoloServiceUrlEnvironment(service, serviceUrl, environment);
473
699
  }
474
700
  function resolvedProfileConfig(config, environment = selectedEnvironment()) {
475
701
  const profile = profileForEnvironment(config, environment);
@@ -485,6 +711,8 @@ function resolvedProfileConfig(config, environment = selectedEnvironment()) {
485
711
  ENVIRONMENT_DEFAULT_SERVICE_URLS,
486
712
  TOPOLO_ENVIRONMENTS,
487
713
  activateContext,
714
+ assertServiceUrlForEnvironment,
715
+ assertTopoloServiceUrlEnvironment,
488
716
  clearProfileCredential,
489
717
  configPath,
490
718
  contextKey,
@@ -500,6 +728,7 @@ function resolvedProfileConfig(config, environment = selectedEnvironment()) {
500
728
  resolveServiceUrls,
501
729
  resolvedProfileConfig,
502
730
  selectedEnvironment,
731
+ topoloServiceOrigins,
503
732
  writeConfig,
504
733
  writeContextCredential,
505
734
  writeProfileConfig
@@ -20,6 +20,11 @@ type TopoloCredential = {
20
20
  expiresAt?: number;
21
21
  };
22
22
 
23
+ declare const TOPOLO_RUNTIME_ENVIRONMENTS: readonly ["production", "staging", "development", "local"];
24
+ type TopoloRuntimeEnvironment = (typeof TOPOLO_RUNTIME_ENVIRONMENTS)[number];
25
+ declare function assertTopoloServiceUrlEnvironment(service: string, value: string, environment: TopoloRuntimeEnvironment): string;
26
+ declare function topoloServiceOrigins(serviceUrls: Record<string, string> | undefined, environment: TopoloRuntimeEnvironment): Record<string, string>;
27
+
23
28
  /**
24
29
  * Node-only credential store shared by the `topolo` CLI and `@topolo/mcp`.
25
30
  *
@@ -33,7 +38,8 @@ type TopoloCredential = {
33
38
  * back to the on-disk credential when no `TOPOLO_*` env credential is set.
34
39
  */
35
40
  declare const TOPOLO_ENVIRONMENTS: readonly ["production", "staging", "development", "local"];
36
- type TopoloEnvironment = (typeof TOPOLO_ENVIRONMENTS)[number];
41
+ type TopoloEnvironment = TopoloRuntimeEnvironment;
42
+
37
43
  interface TopoloConfig {
38
44
  credential?: TopoloCredential;
39
45
  /** OAuth client_id used for the most recent login — needed to refresh the access token. */
@@ -75,7 +81,7 @@ declare function contextKey(contextType: 'personal' | 'organization', organizati
75
81
  * local dev loop points services at ad-hoc ports, so it is driven entirely by
76
82
  * `TOPOLO_LOCAL_SERVICE_URL_*` / `TOPOLO_SERVICE_URL_*` overrides.
77
83
  */
78
- declare const ENVIRONMENT_DEFAULT_SERVICE_URLS: Record<TopoloEnvironment, Record<string, string>>;
84
+ declare const ENVIRONMENT_DEFAULT_SERVICE_URLS: Record<"production" | "staging" | "development" | "local", Record<string, string>>;
79
85
  declare function configPath(): string;
80
86
  declare function readConfig(): Promise<TopoloConfig>;
81
87
  declare function writeConfig(config: TopoloConfig): Promise<void>;
@@ -109,6 +115,7 @@ declare function refreshContextCredentialIfNeeded(environment: TopoloEnvironment
109
115
  }): Promise<TopoloCredential | null>;
110
116
  declare function resolveAgentName(configAgentName?: string): string | undefined;
111
117
  declare function resolveServiceUrls(configUrls?: Record<string, string>, environment?: TopoloEnvironment): Record<string, string> | undefined;
118
+ declare function assertServiceUrlForEnvironment(service: string, serviceUrl: string | undefined, environment?: TopoloEnvironment): void;
112
119
  declare function resolvedProfileConfig(config: TopoloConfig, environment?: TopoloEnvironment): TopoloEnvironmentProfile;
113
120
 
114
- export { ENVIRONMENT_DEFAULT_SERVICE_URLS, TOPOLO_ENVIRONMENTS, type TopoloConfig, type TopoloContextCredential, type TopoloEnvironment, type TopoloEnvironmentProfile, activateContext, clearProfileCredential, configPath, contextKey, credentialSourceFromConfig, envVarPrefix, normalizeEnvironment, profileForEnvironment, readConfig, refreshContextCredentialIfNeeded, refreshCredentialIfNeeded, resolveAgentName, resolveCredential, resolveServiceUrls, resolvedProfileConfig, selectedEnvironment, writeConfig, writeContextCredential, writeProfileConfig };
121
+ export { ENVIRONMENT_DEFAULT_SERVICE_URLS, TOPOLO_ENVIRONMENTS, type TopoloConfig, type TopoloContextCredential, type TopoloEnvironment, type TopoloEnvironmentProfile, activateContext, assertServiceUrlForEnvironment, assertTopoloServiceUrlEnvironment, clearProfileCredential, configPath, contextKey, credentialSourceFromConfig, envVarPrefix, normalizeEnvironment, profileForEnvironment, readConfig, refreshContextCredentialIfNeeded, refreshCredentialIfNeeded, resolveAgentName, resolveCredential, resolveServiceUrls, resolvedProfileConfig, selectedEnvironment, topoloServiceOrigins, writeConfig, writeContextCredential, writeProfileConfig };
@@ -20,6 +20,11 @@ type TopoloCredential = {
20
20
  expiresAt?: number;
21
21
  };
22
22
 
23
+ declare const TOPOLO_RUNTIME_ENVIRONMENTS: readonly ["production", "staging", "development", "local"];
24
+ type TopoloRuntimeEnvironment = (typeof TOPOLO_RUNTIME_ENVIRONMENTS)[number];
25
+ declare function assertTopoloServiceUrlEnvironment(service: string, value: string, environment: TopoloRuntimeEnvironment): string;
26
+ declare function topoloServiceOrigins(serviceUrls: Record<string, string> | undefined, environment: TopoloRuntimeEnvironment): Record<string, string>;
27
+
23
28
  /**
24
29
  * Node-only credential store shared by the `topolo` CLI and `@topolo/mcp`.
25
30
  *
@@ -33,7 +38,8 @@ type TopoloCredential = {
33
38
  * back to the on-disk credential when no `TOPOLO_*` env credential is set.
34
39
  */
35
40
  declare const TOPOLO_ENVIRONMENTS: readonly ["production", "staging", "development", "local"];
36
- type TopoloEnvironment = (typeof TOPOLO_ENVIRONMENTS)[number];
41
+ type TopoloEnvironment = TopoloRuntimeEnvironment;
42
+
37
43
  interface TopoloConfig {
38
44
  credential?: TopoloCredential;
39
45
  /** OAuth client_id used for the most recent login — needed to refresh the access token. */
@@ -75,7 +81,7 @@ declare function contextKey(contextType: 'personal' | 'organization', organizati
75
81
  * local dev loop points services at ad-hoc ports, so it is driven entirely by
76
82
  * `TOPOLO_LOCAL_SERVICE_URL_*` / `TOPOLO_SERVICE_URL_*` overrides.
77
83
  */
78
- declare const ENVIRONMENT_DEFAULT_SERVICE_URLS: Record<TopoloEnvironment, Record<string, string>>;
84
+ declare const ENVIRONMENT_DEFAULT_SERVICE_URLS: Record<"production" | "staging" | "development" | "local", Record<string, string>>;
79
85
  declare function configPath(): string;
80
86
  declare function readConfig(): Promise<TopoloConfig>;
81
87
  declare function writeConfig(config: TopoloConfig): Promise<void>;
@@ -109,6 +115,7 @@ declare function refreshContextCredentialIfNeeded(environment: TopoloEnvironment
109
115
  }): Promise<TopoloCredential | null>;
110
116
  declare function resolveAgentName(configAgentName?: string): string | undefined;
111
117
  declare function resolveServiceUrls(configUrls?: Record<string, string>, environment?: TopoloEnvironment): Record<string, string> | undefined;
118
+ declare function assertServiceUrlForEnvironment(service: string, serviceUrl: string | undefined, environment?: TopoloEnvironment): void;
112
119
  declare function resolvedProfileConfig(config: TopoloConfig, environment?: TopoloEnvironment): TopoloEnvironmentProfile;
113
120
 
114
- export { ENVIRONMENT_DEFAULT_SERVICE_URLS, TOPOLO_ENVIRONMENTS, type TopoloConfig, type TopoloContextCredential, type TopoloEnvironment, type TopoloEnvironmentProfile, activateContext, clearProfileCredential, configPath, contextKey, credentialSourceFromConfig, envVarPrefix, normalizeEnvironment, profileForEnvironment, readConfig, refreshContextCredentialIfNeeded, refreshCredentialIfNeeded, resolveAgentName, resolveCredential, resolveServiceUrls, resolvedProfileConfig, selectedEnvironment, writeConfig, writeContextCredential, writeProfileConfig };
121
+ export { ENVIRONMENT_DEFAULT_SERVICE_URLS, TOPOLO_ENVIRONMENTS, type TopoloConfig, type TopoloContextCredential, type TopoloEnvironment, type TopoloEnvironmentProfile, activateContext, assertServiceUrlForEnvironment, assertTopoloServiceUrlEnvironment, clearProfileCredential, configPath, contextKey, credentialSourceFromConfig, envVarPrefix, normalizeEnvironment, profileForEnvironment, readConfig, refreshContextCredentialIfNeeded, refreshCredentialIfNeeded, resolveAgentName, resolveCredential, resolveServiceUrls, resolvedProfileConfig, selectedEnvironment, topoloServiceOrigins, writeConfig, writeContextCredential, writeProfileConfig };