@topolo/cli 0.7.3 → 0.7.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 +726 -90
  2. package/package.json +3 -8
package/dist/index.js CHANGED
@@ -9,7 +9,7 @@ import { platform as platform2 } from "os";
9
9
  import { createInterface } from "readline/promises";
10
10
  import { stdin as input } from "process";
11
11
 
12
- // ../topolo-sdk/dist/index.js
12
+ // ../../../development-workflow-acceleration/node_modules/.pnpm/@topolo+sdk@0.10.4/node_modules/@topolo/sdk/dist/index.js
13
13
  function applyAuthHeaders(headers, credential) {
14
14
  if (credential.kind === "api_key") {
15
15
  headers.set("X-Api-Key", credential.apiKey);
@@ -36,6 +36,90 @@ var TopoloSdkError = class extends Error {
36
36
  this.code = code;
37
37
  }
38
38
  };
39
+ var PUBLIC_ERROR_CODES = /* @__PURE__ */ new Set([
40
+ "action_app_identity_unavailable",
41
+ "action_authentication_required",
42
+ "action_catalog_summary_unavailable",
43
+ "action_catalog_unavailable",
44
+ "action_contract_mismatch",
45
+ "action_permission_required",
46
+ "action_runtime_authorization_required",
47
+ "action_service_not_allowed",
48
+ "auth_error",
49
+ "authentication_required",
50
+ "authorization_pending",
51
+ "bad_sig",
52
+ "confirmation_required",
53
+ "dependency_unavailable",
54
+ "environment_mismatch",
55
+ "environment_service_url_required",
56
+ "expired_token",
57
+ "forbidden",
58
+ "http_error",
59
+ "immutable_drift",
60
+ "integration_dependency_unavailable",
61
+ "integration_misconfigured",
62
+ "invalid_action",
63
+ "invalid_action_input",
64
+ "invalid_grant",
65
+ "invalid_input",
66
+ "invalid_token",
67
+ "mcp_error",
68
+ "membership_required",
69
+ "network_error",
70
+ "organization_context_required",
71
+ "organization_enrollment_required",
72
+ "permission_denied",
73
+ "rate_limited",
74
+ "request_failed",
75
+ "service_catalog_unavailable",
76
+ "service_path_origin_mismatch",
77
+ "service_required",
78
+ "slow_down",
79
+ "state_conflict",
80
+ "unauthorized",
81
+ "unknown_action",
82
+ "unknown_service"
83
+ ]);
84
+ var ERROR_CODE_PATTERN = /^[a-z][a-z0-9_]{0,63}$/;
85
+ 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;
86
+ var CLOUDFLARE_RAY_ID_PATTERN = /^[0-9a-f]{16}-[A-Z]{3}$/;
87
+ var PERMISSION_PATTERN = /^[a-z][a-z0-9_-]{0,63}(?:\.[a-z][a-z0-9_-]{0,63})*:(?:[a-z][a-z0-9_-]{0,63}|\*)$/;
88
+ 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$/;
89
+ var PUBLIC_DETAIL_SOURCES = /* @__PURE__ */ new Set([
90
+ "api_key_introspection",
91
+ "api_key_validation",
92
+ "auth_context",
93
+ "auth_validate",
94
+ "catalog",
95
+ "credential",
96
+ "runtime",
97
+ "service_token_provider",
98
+ "service_verify_key",
99
+ "token_validation",
100
+ "upstream"
101
+ ]);
102
+ function canonicalTopoloErrorCode(value) {
103
+ if (typeof value !== "string") return null;
104
+ const candidate = value.trim().toLowerCase();
105
+ if (!ERROR_CODE_PATTERN.test(candidate) || !PUBLIC_ERROR_CODES.has(candidate)) return null;
106
+ return candidate;
107
+ }
108
+ function canonicalTopoloRequestId(value) {
109
+ if (typeof value !== "string") return void 0;
110
+ const candidate = value.trim();
111
+ return TOPOLO_REQUEST_ID_PATTERN.test(candidate) || CLOUDFLARE_RAY_ID_PATTERN.test(candidate) ? candidate : void 0;
112
+ }
113
+ function canonicalTopoloRetryAfter(value) {
114
+ if (typeof value !== "string") return void 0;
115
+ const candidate = value.trim();
116
+ if (/^\d{1,10}$/.test(candidate) || RETRY_AFTER_DATE_PATTERN.test(candidate)) return candidate;
117
+ return void 0;
118
+ }
119
+ function canonicalTopoloRequired(value) {
120
+ if (!Array.isArray(value)) return [];
121
+ return [...new Set(value.filter((item) => typeof item === "string" && PERMISSION_PATTERN.test(item)))].slice(0, 32);
122
+ }
39
123
  var TopoloAuthError = class extends TopoloSdkError {
40
124
  status;
41
125
  body;
@@ -43,15 +127,19 @@ var TopoloAuthError = class extends TopoloSdkError {
43
127
  path;
44
128
  requestId;
45
129
  retryAfter;
130
+ details;
46
131
  constructor(message, code = "auth_error", options = {}) {
47
- super(code, message);
132
+ super(canonicalTopoloErrorCode(code) ?? "auth_error", message);
48
133
  this.name = "TopoloAuthError";
49
134
  this.status = options.status ?? 401;
50
135
  this.body = options.body;
51
136
  this.service = options.service;
52
137
  this.path = options.path;
53
- this.requestId = options.requestId;
54
- this.retryAfter = options.retryAfter;
138
+ this.requestId = canonicalTopoloRequestId(options.requestId);
139
+ this.retryAfter = canonicalTopoloRetryAfter(options.retryAfter);
140
+ this.details = extractTopoloServiceErrorDetails(
141
+ options.details === void 0 ? options.body : { details: options.details }
142
+ );
55
143
  }
56
144
  };
57
145
  var TopoloPermissionError = class extends TopoloSdkError {
@@ -62,28 +150,140 @@ var TopoloPermissionError = class extends TopoloSdkError {
62
150
  path;
63
151
  requestId;
64
152
  retryAfter;
153
+ details;
65
154
  constructor(message, required, options = {}) {
66
- super("permission_denied", message);
155
+ super(
156
+ canonicalTopoloErrorCode(options.code) ?? serviceErrorCode(options.body, "permission_denied"),
157
+ message
158
+ );
67
159
  this.name = "TopoloPermissionError";
68
- this.required = required;
160
+ this.required = canonicalTopoloRequired(required);
69
161
  this.status = options.status ?? 403;
70
162
  this.body = options.body;
71
163
  this.service = options.service;
72
164
  this.path = options.path;
73
- this.requestId = options.requestId;
74
- this.retryAfter = options.retryAfter;
165
+ this.requestId = canonicalTopoloRequestId(options.requestId);
166
+ this.retryAfter = canonicalTopoloRetryAfter(options.retryAfter);
167
+ this.details = extractTopoloServiceErrorDetails(
168
+ options.details === void 0 ? options.body : { details: options.details }
169
+ );
75
170
  }
76
171
  };
77
- function extractHttpErrorCode(body) {
78
- if (!body || typeof body !== "object" || Array.isArray(body)) return "http_error";
172
+ function extractTopoloServiceErrorCode(body) {
173
+ if (!body || typeof body !== "object" || Array.isArray(body)) return null;
79
174
  const record4 = body;
80
- if (typeof record4.code === "string" && record4.code.trim()) return record4.code.trim();
175
+ const topLevel = canonicalTopoloErrorCode(record4.code);
176
+ if (topLevel) return topLevel;
81
177
  const nested = record4.error;
178
+ const nestedString = canonicalTopoloErrorCode(nested);
179
+ if (nestedString) return nestedString;
82
180
  if (nested && typeof nested === "object" && !Array.isArray(nested)) {
83
181
  const code = nested.code;
84
- if (typeof code === "string" && code.trim()) return code.trim();
182
+ const nestedCode = canonicalTopoloErrorCode(code);
183
+ if (nestedCode) return nestedCode;
184
+ }
185
+ return null;
186
+ }
187
+ function serviceErrorCode(body, fallback) {
188
+ return extractTopoloServiceErrorCode(body) ?? fallback;
189
+ }
190
+ function extractTopoloServiceErrorDetails(body) {
191
+ if (!body || typeof body !== "object" || Array.isArray(body)) return void 0;
192
+ const record4 = body;
193
+ let candidate = record4.details;
194
+ const nested = record4.error;
195
+ if (nested && typeof nested === "object" && !Array.isArray(nested)) {
196
+ const nestedRecord = nested;
197
+ if (candidate === void 0) candidate = nestedRecord.details ?? nestedRecord;
198
+ }
199
+ if (candidate === void 0) candidate = record4;
200
+ if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) return void 0;
201
+ const source = candidate;
202
+ const safe = {};
203
+ for (const [key, value] of Object.entries(source)) {
204
+ if (/present$/i.test(key) && typeof value === "boolean") {
205
+ safe[key] = value;
206
+ continue;
207
+ }
208
+ if (key === "retryable" && typeof value === "boolean") {
209
+ safe.retryable = value;
210
+ continue;
211
+ }
212
+ if (key === "code") {
213
+ const sanitized = canonicalTopoloErrorCode(value);
214
+ if (sanitized) safe.code = sanitized;
215
+ continue;
216
+ }
217
+ if (key === "source") {
218
+ const sanitized = typeof value === "string" ? value.trim().toLowerCase() : "";
219
+ if (PUBLIC_DETAIL_SOURCES.has(sanitized)) safe.source = sanitized;
220
+ continue;
221
+ }
222
+ if (key === "dependency" || key === "platform") {
223
+ safe[`${key}Present`] = value !== null && value !== void 0;
224
+ continue;
225
+ }
226
+ if (key === "error" && !safe.code) {
227
+ const sanitized = canonicalTopoloErrorCode(value);
228
+ if (sanitized) safe.code = sanitized;
229
+ continue;
230
+ }
231
+ if (key === "required" && Array.isArray(value)) {
232
+ const required = canonicalTopoloRequired(value);
233
+ if (required.length) safe.required = required;
234
+ continue;
235
+ }
236
+ if (["missingFields", "fields", "paths"].includes(key) && Array.isArray(value)) {
237
+ const paths = value.filter((item) => typeof item === "string" && /^[$.a-z0-9_[\]-]{1,160}$/i.test(item));
238
+ if (paths.length) safe[key] = paths;
239
+ }
85
240
  }
86
- return "http_error";
241
+ return Object.keys(safe).length ? safe : void 0;
242
+ }
243
+ function publicTopoloErrorMessage(error) {
244
+ const code = canonicalTopoloErrorCode(
245
+ typeof error === "string" ? error : error.code ?? extractTopoloServiceErrorCode(error.body)
246
+ ) ?? "request_failed";
247
+ if (/(?:organization|org).*(?:enroll|membership|member|admission)/.test(code)) {
248
+ return "Organization or application enrollment is required.";
249
+ }
250
+ if (/(?:organization|org).*(?:context|required|missing|invalid|unknown|not_found)/.test(code)) {
251
+ return "Organization context is unavailable.";
252
+ }
253
+ if (["auth_error", "authentication_required", "unauthorized", "invalid_token", "expired_token"].includes(code)) {
254
+ return "Authentication failed.";
255
+ }
256
+ if (code === "permission_denied" || code === "forbidden") return "Permission denied.";
257
+ if (code === "confirmation_required") return "Confirmation is required.";
258
+ return `Topolo request failed (${code}).`;
259
+ }
260
+ function topoloErrorHint(error) {
261
+ const code = canonicalTopoloErrorCode(
262
+ typeof error === "string" ? error : error.code ?? extractTopoloServiceErrorCode(error.body)
263
+ );
264
+ if (!code) return void 0;
265
+ if (code === "auth_error" || code === "authentication_required" || code === "unauthorized" || code === "invalid_token" || code === "expired_token") {
266
+ return "Run `topolo auth login`, then verify the effective Topolo identity and organization.";
267
+ }
268
+ if (code === "confirmation_required") {
269
+ return "Review the unchanged action plan, obtain approval, and retry once with --confirm or confirm:true.";
270
+ }
271
+ if (code === "enrollment_required" || code === "membership_required" || /(?:organization|org).*(?:enroll|membership|member|admission)|(?:enroll|membership).*(?:organization|org)/.test(code)) {
272
+ return "The current identity is not enrolled for this organization or application. Use an enrolled identity or ask an organization administrator to complete enrollment.";
273
+ }
274
+ if (/(?:organization|org).*(?:context|required|missing|invalid|unknown|not_found)/.test(code)) {
275
+ return "Select a signed-in organization context, then verify the effective identity and organization before retrying.";
276
+ }
277
+ if (code === "permission_denied" || code === "forbidden") {
278
+ return "The current identity or resource context is not authorized. Review the structured details and published authorization contract.";
279
+ }
280
+ if (code === "environment_mismatch") {
281
+ return "Use service origins and credentials from one explicit Topolo environment only.";
282
+ }
283
+ if (code === "invalid_action_input" || code === "invalid_input") {
284
+ return "Correct only the reported schema paths, then prepare the action again.";
285
+ }
286
+ return void 0;
87
287
  }
88
288
  var TopoloConfirmationRequiredError = class extends TopoloSdkError {
89
289
  constructor(message) {
@@ -98,17 +298,66 @@ var TopoloHttpError = class extends TopoloSdkError {
98
298
  path;
99
299
  requestId;
100
300
  retryAfter;
301
+ details;
101
302
  constructor(service, path9, status, body, message, options = {}) {
102
- super(extractHttpErrorCode(body), message ?? `HTTP ${status} from ${service}${path9}`);
303
+ super(serviceErrorCode(body, "http_error"), message ?? `HTTP ${status} from ${service}${path9}`);
103
304
  this.name = "TopoloHttpError";
104
305
  this.status = status;
105
306
  this.body = body;
106
307
  this.service = service;
107
308
  this.path = path9;
108
- this.requestId = options.requestId;
109
- this.retryAfter = options.retryAfter;
309
+ this.requestId = canonicalTopoloRequestId(options.requestId);
310
+ this.retryAfter = canonicalTopoloRetryAfter(options.retryAfter);
311
+ this.details = extractTopoloServiceErrorDetails(
312
+ options.details === void 0 ? body : { details: options.details }
313
+ );
110
314
  }
111
315
  };
316
+ function hostnameHasSuffix(hostname, suffix) {
317
+ return hostname === suffix || hostname.endsWith(`.${suffix}`);
318
+ }
319
+ function workersDevAccountMatches(hostname, account) {
320
+ if (!hostname.endsWith(".workers.dev")) return false;
321
+ const labels = hostname.slice(0, -".workers.dev".length).split(".");
322
+ return labels.length >= 2 && labels.at(-1) === account;
323
+ }
324
+ function originMatchesEnvironment(url, environment) {
325
+ const hostname = url.hostname.toLowerCase();
326
+ if (environment === "local") {
327
+ return hostname === "localhost" || hostname.endsWith(".localhost") || hostname === "127.0.0.1" || hostname === "[::1]";
328
+ }
329
+ if (url.protocol !== "https:") return false;
330
+ if (environment === "production") {
331
+ return hostnameHasSuffix(hostname, "topolo.app") || hostnameHasSuffix(hostname, "topolo.io") || workersDevAccountMatches(hostname, "topolo");
332
+ }
333
+ if (environment === "staging") {
334
+ return hostnameHasSuffix(hostname, "stg.topolo.us") || workersDevAccountMatches(hostname, "topolo-staging");
335
+ }
336
+ return hostnameHasSuffix(hostname, "topolo.dev") || workersDevAccountMatches(hostname, "topolo-development");
337
+ }
338
+ function assertTopoloServiceUrlEnvironment(service, value, environment) {
339
+ let url;
340
+ try {
341
+ url = new URL(value);
342
+ } catch {
343
+ throw new TopoloSdkError("invalid_service_url", `Invalid URL configured for Topolo service "${service}".`);
344
+ }
345
+ if (url.protocol !== "https:" && url.protocol !== "http:" || url.username || url.password || !originMatchesEnvironment(url, environment)) {
346
+ throw new TopoloSdkError(
347
+ "environment_mismatch",
348
+ `Refusing Topolo service "${service}": ${url.origin} does not belong to ${environment}.`
349
+ );
350
+ }
351
+ return url.origin;
352
+ }
353
+ function topoloServiceOrigins(serviceUrls, environment) {
354
+ return Object.fromEntries(
355
+ Object.entries(serviceUrls ?? {}).sort(([left], [right]) => left.localeCompare(right)).map(([service, value]) => [
356
+ service,
357
+ assertTopoloServiceUrlEnvironment(service, value, environment)
358
+ ])
359
+ );
360
+ }
112
361
  var APP_API_URLS = {
113
362
  auth: "https://auth.topolo.app",
114
363
  developers: "https://developers.topolo.app"
@@ -2857,6 +3106,7 @@ var TopoloClient = class _TopoloClient {
2857
3106
  credential;
2858
3107
  agent;
2859
3108
  serviceUrls;
3109
+ environment;
2860
3110
  appIds;
2861
3111
  requireConfirmForWrites;
2862
3112
  timeoutMs;
@@ -2885,7 +3135,8 @@ var TopoloClient = class _TopoloClient {
2885
3135
  if (!options.agent?.clientName) throw new TopoloAuthError("agent.clientName is required");
2886
3136
  this.credential = options.credential;
2887
3137
  this.agent = options.agent;
2888
- this.serviceUrls = options.serviceUrls;
3138
+ this.environment = options.environment;
3139
+ this.serviceUrls = options.environment ? topoloServiceOrigins(options.serviceUrls, options.environment) : options.serviceUrls;
2889
3140
  this.appIds = options.appIds;
2890
3141
  this.requireConfirmForWrites = options.requireConfirmForWrites !== false;
2891
3142
  this.timeoutMs = options.timeoutMs ?? 3e4;
@@ -2914,9 +3165,15 @@ var TopoloClient = class _TopoloClient {
2914
3165
  );
2915
3166
  }
2916
3167
  await this.loadCurrentCredential();
2917
- const resolvedService = opts.baseUrl ? { serviceKey: normalizeLookupKey(opts.service), baseUrl: requireHttpOrigin(opts.baseUrl) } : await this.resolveRequestService(opts.service);
3168
+ const resolvedService = opts.baseUrl ? { serviceKey: normalizeLookupKey(opts.service), baseUrl: this.validatedServiceOrigin(opts.service, opts.baseUrl) } : await this.resolveRequestService(opts.service);
2918
3169
  const baseUrl = resolvedService.baseUrl;
2919
3170
  const url = new URL(opts.path, baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`);
3171
+ if (url.origin !== new URL(baseUrl).origin) {
3172
+ throw new TopoloSdkError(
3173
+ "service_path_origin_mismatch",
3174
+ `Refusing Topolo request path because it changes the resolved ${resolvedService.serviceKey || opts.service} service origin.`
3175
+ );
3176
+ }
2920
3177
  if (opts.query) {
2921
3178
  for (const [k, v] of Object.entries(opts.query)) {
2922
3179
  if (v !== void 0) url.searchParams.set(k, String(v));
@@ -2987,7 +3244,7 @@ var TopoloClient = class _TopoloClient {
2987
3244
  url: urlStr,
2988
3245
  requestId,
2989
3246
  durationMs: Date.now() - startedAt,
2990
- error: err instanceof Error ? err.message : String(err)
3247
+ error: "network_error"
2991
3248
  });
2992
3249
  throw err;
2993
3250
  }
@@ -2996,6 +3253,7 @@ var TopoloClient = class _TopoloClient {
2996
3253
  if (!res.ok) {
2997
3254
  const durationMs = Date.now() - startedAt;
2998
3255
  const message = describeError(parsed, `HTTP ${res.status}`);
3256
+ const publicCode = extractTopoloServiceErrorCode(parsed) ?? "http_error";
2999
3257
  this.emit({
3000
3258
  phase: "error",
3001
3259
  method,
@@ -3004,7 +3262,7 @@ var TopoloClient = class _TopoloClient {
3004
3262
  url: urlStr,
3005
3263
  requestId,
3006
3264
  durationMs,
3007
- error: message,
3265
+ error: publicCode,
3008
3266
  status: res.status
3009
3267
  });
3010
3268
  const errorContext = {
@@ -3012,17 +3270,21 @@ var TopoloClient = class _TopoloClient {
3012
3270
  body: parsed,
3013
3271
  service: resolvedService.serviceKey,
3014
3272
  path: opts.path,
3015
- requestId: res.headers.get("X-Topolo-Request-Id") || requestId,
3016
- retryAfter: res.headers.get("Retry-After") || void 0
3273
+ requestId: canonicalTopoloRequestId(res.headers.get("X-Topolo-Request-Id")) ?? requestId,
3274
+ retryAfter: canonicalTopoloRetryAfter(res.headers.get("Retry-After"))
3017
3275
  };
3018
3276
  if (res.status === 401) {
3019
- throw new TopoloAuthError(describeError(parsed, "Unauthorized"), "auth_error", errorContext);
3277
+ throw new TopoloAuthError(
3278
+ describeError(parsed, "Unauthorized"),
3279
+ extractTopoloServiceErrorCode(parsed) ?? "auth_error",
3280
+ errorContext
3281
+ );
3020
3282
  }
3021
3283
  if (res.status === 403) {
3022
3284
  throw new TopoloPermissionError(
3023
3285
  describeError(parsed, "Permission denied"),
3024
3286
  extractRequired(parsed),
3025
- errorContext
3287
+ { ...errorContext, code: extractTopoloServiceErrorCode(parsed) ?? "permission_denied" }
3026
3288
  );
3027
3289
  }
3028
3290
  throw new TopoloHttpError(
@@ -3072,6 +3334,17 @@ var TopoloClient = class _TopoloClient {
3072
3334
  });
3073
3335
  return this.refreshInFlight;
3074
3336
  }
3337
+ validatedServiceOrigin(service, value) {
3338
+ const origin = requireHttpOrigin(value);
3339
+ return this.environment ? assertTopoloServiceUrlEnvironment(service, origin, this.environment) : origin;
3340
+ }
3341
+ validatedActionEntry(entry) {
3342
+ if (!entry.apiBaseUrl) return entry;
3343
+ return {
3344
+ ...entry,
3345
+ apiBaseUrl: this.validatedServiceOrigin(entry.appSlug || entry.appId, entry.apiBaseUrl)
3346
+ };
3347
+ }
3075
3348
  async resolveRequestService(service) {
3076
3349
  const serviceKey = normalizeLookupKey(service);
3077
3350
  if (!serviceKey) {
@@ -3079,17 +3352,20 @@ var TopoloClient = class _TopoloClient {
3079
3352
  }
3080
3353
  if (serviceKey === "auth" || serviceKey === "developers") {
3081
3354
  const overrideUrl = resolveConfiguredServiceUrl(serviceKey, this.serviceUrls);
3082
- if (overrideUrl) return { serviceKey, baseUrl: overrideUrl };
3355
+ if (overrideUrl) return { serviceKey, baseUrl: this.validatedServiceOrigin(serviceKey, overrideUrl) };
3083
3356
  if (serviceKey === "developers") {
3084
3357
  const derived = deriveDevelopersFromAuth(resolveConfiguredServiceUrl("auth", this.serviceUrls));
3085
- if (derived) return { serviceKey, baseUrl: derived };
3358
+ if (derived) return { serviceKey, baseUrl: this.validatedServiceOrigin(serviceKey, derived) };
3086
3359
  }
3087
- return { serviceKey, baseUrl: DEFAULT_APP_URLS[serviceKey] };
3360
+ return {
3361
+ serviceKey,
3362
+ baseUrl: this.validatedServiceOrigin(serviceKey, DEFAULT_APP_URLS[serviceKey])
3363
+ };
3088
3364
  }
3089
3365
  const live = await this.resolveLiveCallableService(serviceKey);
3090
3366
  if (live) {
3091
3367
  const overrideUrl = resolveConfiguredServiceUrl(serviceKey, this.serviceUrls);
3092
- return overrideUrl ? { ...live, baseUrl: overrideUrl } : live;
3368
+ return overrideUrl ? { ...live, baseUrl: this.validatedServiceOrigin(serviceKey, overrideUrl) } : { ...live, baseUrl: this.validatedServiceOrigin(serviceKey, live.baseUrl) };
3093
3369
  }
3094
3370
  throw new TopoloSdkError(
3095
3371
  "unknown_service",
@@ -3103,10 +3379,10 @@ var TopoloClient = class _TopoloClient {
3103
3379
  serviceKey,
3104
3380
  overrides: this.serviceUrls
3105
3381
  });
3106
- if (baseUrl) return { serviceKey, baseUrl };
3382
+ if (baseUrl) return { serviceKey, baseUrl: this.validatedServiceOrigin(serviceKey, baseUrl) };
3107
3383
  const actionPage = await this.searchActions({ service: match.appId, limit: 1 }).catch(() => null);
3108
3384
  const actionBaseUrl = actionPage?.actions.find((action) => action.apiBaseUrl)?.apiBaseUrl ?? null;
3109
- return actionBaseUrl ? { serviceKey, baseUrl: requireHttpOrigin(actionBaseUrl) } : null;
3385
+ return actionBaseUrl ? { serviceKey, baseUrl: this.validatedServiceOrigin(serviceKey, actionBaseUrl) } : null;
3110
3386
  }
3111
3387
  /**
3112
3388
  * Resolve a service-slug → opaque app-id via caller config or TopoloAuth.
@@ -3126,7 +3402,7 @@ var TopoloClient = class _TopoloClient {
3126
3402
  if (!normalized) return null;
3127
3403
  const configuredAppId = resolveConfiguredAppId(normalized, this.appIds);
3128
3404
  if (configuredAppId) return configuredAppId;
3129
- const authBase = resolveServiceUrl("developers", this.serviceUrls);
3405
+ const authBase = this.validatedServiceOrigin("developers", resolveServiceUrl("developers", this.serviceUrls));
3130
3406
  const cacheKey = `${authBase}|${normalized}`;
3131
3407
  const now = Date.now();
3132
3408
  const cached = _TopoloClient.appIdCache.get(cacheKey);
@@ -3302,7 +3578,7 @@ var TopoloClient = class _TopoloClient {
3302
3578
  "TopoloAuth did not return an action catalog for the current credential."
3303
3579
  );
3304
3580
  }
3305
- const entries = raw.map((item) => normalizeActionCatalogEntry(item)).filter((entry) => Boolean(entry));
3581
+ const entries = raw.map((item) => normalizeActionCatalogEntry(item)).filter((entry) => Boolean(entry)).map((entry) => this.validatedActionEntry(entry));
3306
3582
  return {
3307
3583
  actions: entries,
3308
3584
  page: normalizeCatalogPage(
@@ -3367,7 +3643,7 @@ var TopoloClient = class _TopoloClient {
3367
3643
  const raw = res.data?.action ?? res.action;
3368
3644
  const match = normalizeActionCatalogEntry(raw);
3369
3645
  if (!match) throw new TopoloSdkError("unknown_action", `Unknown or unavailable Topolo action "${action}".`);
3370
- return match;
3646
+ return this.validatedActionEntry(match);
3371
3647
  }
3372
3648
  async callAction(action, input3 = {}, options = {}) {
3373
3649
  const entry = await this.getAction(action);
@@ -3444,6 +3720,25 @@ var TopoloClient = class _TopoloClient {
3444
3720
  }
3445
3721
  async planAction(action, input3 = {}, options = {}) {
3446
3722
  const entry = await this.getAction(action);
3723
+ return this.planResolvedAction(entry, input3, options);
3724
+ }
3725
+ async prepareAction(action, input3, options = {}) {
3726
+ const entry = await this.getAction(action);
3727
+ const example = (entry.agentContract?.examples ?? []).find((candidate) => Boolean(candidate?.input) && typeof candidate.input === "object" && !Array.isArray(candidate.input) && validateTopoloActionInput(entry.inputSchema, candidate.input).valid) ?? null;
3728
+ const selectedInput = input3 ?? example?.input ?? {};
3729
+ const plan = this.planResolvedAction(entry, selectedInput, options);
3730
+ return {
3731
+ action: entry,
3732
+ example,
3733
+ input: selectedInput,
3734
+ inputSource: input3 ? "provided" : example ? "example" : "empty",
3735
+ missingFields: [...new Set(
3736
+ plan.validation.errors.filter((issue) => issue.code === "required").map((issue) => issue.path)
3737
+ )],
3738
+ plan
3739
+ };
3740
+ }
3741
+ planResolvedAction(entry, input3, options) {
3447
3742
  const validation = validateTopoloActionInput(entry.inputSchema, input3);
3448
3743
  const agentAccess = entry.agentAccess ?? (entry.destructive ? "off" : entry.readOnly && !entry.requiresConfirmation ? "auto" : "confirm");
3449
3744
  const confirmationRequired = agentAccess === "confirm" || entry.requiresConfirmation || entry.destructive;
@@ -3901,7 +4196,16 @@ function pickString(value) {
3901
4196
  function extractRequired(body) {
3902
4197
  if (body && typeof body === "object") {
3903
4198
  const maybe = body;
3904
- if (Array.isArray(maybe.required)) return maybe.required.map(String);
4199
+ if (Array.isArray(maybe.required)) return canonicalTopoloRequired(maybe.required);
4200
+ for (const nested of [maybe.details, maybe.error]) {
4201
+ if (!nested || typeof nested !== "object" || Array.isArray(nested)) continue;
4202
+ const record4 = nested;
4203
+ if (Array.isArray(record4.required)) return canonicalTopoloRequired(record4.required);
4204
+ if (record4.details && typeof record4.details === "object" && !Array.isArray(record4.details)) {
4205
+ const required = record4.details.required;
4206
+ if (Array.isArray(required)) return canonicalTopoloRequired(required);
4207
+ }
4208
+ }
3905
4209
  }
3906
4210
  return [];
3907
4211
  }
@@ -3949,6 +4253,72 @@ function compactActionPlan(plan) {
3949
4253
  nextActionIds: plan.nextActionIds
3950
4254
  };
3951
4255
  }
4256
+ function requiredContextText(value, field) {
4257
+ if (typeof value !== "string" || !value.trim() || value.trim().toLowerCase() === "unknown") {
4258
+ throw new TopoloSdkError("invalid_identity_context", `Effective Topolo ${field} is incomplete.`);
4259
+ }
4260
+ return value.trim();
4261
+ }
4262
+ function createTopoloAgentExecutionContext(input3) {
4263
+ if (input3.contextKey !== null && input3.contextKey !== "personal" && !/^org:[^:]+$/.test(input3.contextKey)) {
4264
+ throw new TopoloSdkError("invalid_identity_context", "Effective Topolo context key is invalid.");
4265
+ }
4266
+ const user = Object.freeze({
4267
+ id: requiredContextText(input3.identity.user.id, "user id"),
4268
+ email: requiredContextText(input3.identity.user.email, "user email"),
4269
+ name: input3.identity.user.name?.trim() || null,
4270
+ role: requiredContextText(input3.identity.user.role, "user role")
4271
+ });
4272
+ const organization = input3.identity.organization ? Object.freeze({
4273
+ id: requiredContextText(input3.identity.organization.id, "organization id"),
4274
+ slug: requiredContextText(input3.identity.organization.slug, "organization slug"),
4275
+ name: requiredContextText(input3.identity.organization.name, "organization name")
4276
+ }) : null;
4277
+ if (input3.contextKey === "personal" && organization) {
4278
+ throw new TopoloSdkError("identity_context_mismatch", "Personal Topolo context resolved an organization.");
4279
+ }
4280
+ if (input3.contextKey?.startsWith("org:")) {
4281
+ const expectedOrganizationId = input3.contextKey.slice("org:".length);
4282
+ if (!organization || organization.id !== expectedOrganizationId) {
4283
+ throw new TopoloSdkError("identity_context_mismatch", "Pinned Topolo organization does not match the effective identity.");
4284
+ }
4285
+ }
4286
+ return Object.freeze({
4287
+ environment: input3.environment,
4288
+ contextKey: input3.contextKey,
4289
+ user,
4290
+ organization,
4291
+ serviceOrigins: Object.freeze(topoloServiceOrigins(input3.serviceUrls, input3.environment))
4292
+ });
4293
+ }
4294
+ function compactActionPreparationCore(preparation) {
4295
+ const example = preparation.example ? { title: preparation.example.title ?? null, input: preparation.example.input } : null;
4296
+ const effectiveInput = preparation.inputSource === "example" ? { source: "example" } : { source: preparation.inputSource, value: preparation.input };
4297
+ return {
4298
+ action: {
4299
+ id: preparation.action.actionId,
4300
+ title: preparation.action.title,
4301
+ docsUrl: preparation.action.agentContract?.docsUrl ?? null
4302
+ },
4303
+ example,
4304
+ input: effectiveInput,
4305
+ missingFields: preparation.missingFields,
4306
+ valid: preparation.plan.validation.valid,
4307
+ errors: preparation.plan.validation.errors,
4308
+ request: preparation.plan.request,
4309
+ confirmation: {
4310
+ required: preparation.plan.confirmationRequired,
4311
+ provided: preparation.plan.confirmationProvided
4312
+ },
4313
+ executable: preparation.plan.executable,
4314
+ verification: preparation.plan.verification,
4315
+ rollbackActionId: preparation.plan.rollbackActionId
4316
+ };
4317
+ }
4318
+ function compactActionPreparation(preparation, context) {
4319
+ const compact = compactActionPreparationCore(preparation);
4320
+ return context ? { context, ...compact } : compact;
4321
+ }
3952
4322
  function compactApplicationAudit(report) {
3953
4323
  return {
3954
4324
  version: report.version,
@@ -4165,7 +4535,7 @@ function createTopolo(options) {
4165
4535
  };
4166
4536
  }
4167
4537
 
4168
- // ../topolo-sdk/dist/node/credential-store.js
4538
+ // ../../../development-workflow-acceleration/node_modules/.pnpm/@topolo+sdk@0.10.4/node_modules/@topolo/sdk/dist/node/credential-store.js
4169
4539
  import { mkdir, readFile, writeFile, chmod } from "fs/promises";
4170
4540
  import { existsSync } from "fs";
4171
4541
  import { homedir, platform } from "os";
@@ -4178,6 +4548,90 @@ var TopoloSdkError2 = class extends Error {
4178
4548
  this.code = code;
4179
4549
  }
4180
4550
  };
4551
+ var PUBLIC_ERROR_CODES2 = /* @__PURE__ */ new Set([
4552
+ "action_app_identity_unavailable",
4553
+ "action_authentication_required",
4554
+ "action_catalog_summary_unavailable",
4555
+ "action_catalog_unavailable",
4556
+ "action_contract_mismatch",
4557
+ "action_permission_required",
4558
+ "action_runtime_authorization_required",
4559
+ "action_service_not_allowed",
4560
+ "auth_error",
4561
+ "authentication_required",
4562
+ "authorization_pending",
4563
+ "bad_sig",
4564
+ "confirmation_required",
4565
+ "dependency_unavailable",
4566
+ "environment_mismatch",
4567
+ "environment_service_url_required",
4568
+ "expired_token",
4569
+ "forbidden",
4570
+ "http_error",
4571
+ "immutable_drift",
4572
+ "integration_dependency_unavailable",
4573
+ "integration_misconfigured",
4574
+ "invalid_action",
4575
+ "invalid_action_input",
4576
+ "invalid_grant",
4577
+ "invalid_input",
4578
+ "invalid_token",
4579
+ "mcp_error",
4580
+ "membership_required",
4581
+ "network_error",
4582
+ "organization_context_required",
4583
+ "organization_enrollment_required",
4584
+ "permission_denied",
4585
+ "rate_limited",
4586
+ "request_failed",
4587
+ "service_catalog_unavailable",
4588
+ "service_path_origin_mismatch",
4589
+ "service_required",
4590
+ "slow_down",
4591
+ "state_conflict",
4592
+ "unauthorized",
4593
+ "unknown_action",
4594
+ "unknown_service"
4595
+ ]);
4596
+ var ERROR_CODE_PATTERN2 = /^[a-z][a-z0-9_]{0,63}$/;
4597
+ var TOPOLO_REQUEST_ID_PATTERN2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
4598
+ var CLOUDFLARE_RAY_ID_PATTERN2 = /^[0-9a-f]{16}-[A-Z]{3}$/;
4599
+ var PERMISSION_PATTERN2 = /^[a-z][a-z0-9_-]{0,63}(?:\.[a-z][a-z0-9_-]{0,63})*:(?:[a-z][a-z0-9_-]{0,63}|\*)$/;
4600
+ var RETRY_AFTER_DATE_PATTERN2 = /^(?: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$/;
4601
+ var PUBLIC_DETAIL_SOURCES2 = /* @__PURE__ */ new Set([
4602
+ "api_key_introspection",
4603
+ "api_key_validation",
4604
+ "auth_context",
4605
+ "auth_validate",
4606
+ "catalog",
4607
+ "credential",
4608
+ "runtime",
4609
+ "service_token_provider",
4610
+ "service_verify_key",
4611
+ "token_validation",
4612
+ "upstream"
4613
+ ]);
4614
+ function canonicalTopoloErrorCode2(value) {
4615
+ if (typeof value !== "string") return null;
4616
+ const candidate = value.trim().toLowerCase();
4617
+ if (!ERROR_CODE_PATTERN2.test(candidate) || !PUBLIC_ERROR_CODES2.has(candidate)) return null;
4618
+ return candidate;
4619
+ }
4620
+ function canonicalTopoloRequestId2(value) {
4621
+ if (typeof value !== "string") return void 0;
4622
+ const candidate = value.trim();
4623
+ return TOPOLO_REQUEST_ID_PATTERN2.test(candidate) || CLOUDFLARE_RAY_ID_PATTERN2.test(candidate) ? candidate : void 0;
4624
+ }
4625
+ function canonicalTopoloRetryAfter2(value) {
4626
+ if (typeof value !== "string") return void 0;
4627
+ const candidate = value.trim();
4628
+ if (/^\d{1,10}$/.test(candidate) || RETRY_AFTER_DATE_PATTERN2.test(candidate)) return candidate;
4629
+ return void 0;
4630
+ }
4631
+ function canonicalTopoloRequired2(value) {
4632
+ if (!Array.isArray(value)) return [];
4633
+ return [...new Set(value.filter((item) => typeof item === "string" && PERMISSION_PATTERN2.test(item)))].slice(0, 32);
4634
+ }
4181
4635
  var TopoloAuthError2 = class extends TopoloSdkError2 {
4182
4636
  status;
4183
4637
  body;
@@ -4185,27 +4639,91 @@ var TopoloAuthError2 = class extends TopoloSdkError2 {
4185
4639
  path;
4186
4640
  requestId;
4187
4641
  retryAfter;
4642
+ details;
4188
4643
  constructor(message, code = "auth_error", options = {}) {
4189
- super(code, message);
4644
+ super(canonicalTopoloErrorCode2(code) ?? "auth_error", message);
4190
4645
  this.name = "TopoloAuthError";
4191
4646
  this.status = options.status ?? 401;
4192
4647
  this.body = options.body;
4193
4648
  this.service = options.service;
4194
4649
  this.path = options.path;
4195
- this.requestId = options.requestId;
4196
- this.retryAfter = options.retryAfter;
4650
+ this.requestId = canonicalTopoloRequestId2(options.requestId);
4651
+ this.retryAfter = canonicalTopoloRetryAfter2(options.retryAfter);
4652
+ this.details = extractTopoloServiceErrorDetails2(
4653
+ options.details === void 0 ? options.body : { details: options.details }
4654
+ );
4197
4655
  }
4198
4656
  };
4199
- function extractHttpErrorCode2(body) {
4200
- if (!body || typeof body !== "object" || Array.isArray(body)) return "http_error";
4657
+ function extractTopoloServiceErrorCode2(body) {
4658
+ if (!body || typeof body !== "object" || Array.isArray(body)) return null;
4201
4659
  const record4 = body;
4202
- if (typeof record4.code === "string" && record4.code.trim()) return record4.code.trim();
4660
+ const topLevel = canonicalTopoloErrorCode2(record4.code);
4661
+ if (topLevel) return topLevel;
4203
4662
  const nested = record4.error;
4663
+ const nestedString = canonicalTopoloErrorCode2(nested);
4664
+ if (nestedString) return nestedString;
4204
4665
  if (nested && typeof nested === "object" && !Array.isArray(nested)) {
4205
4666
  const code = nested.code;
4206
- if (typeof code === "string" && code.trim()) return code.trim();
4667
+ const nestedCode = canonicalTopoloErrorCode2(code);
4668
+ if (nestedCode) return nestedCode;
4669
+ }
4670
+ return null;
4671
+ }
4672
+ function serviceErrorCode2(body, fallback) {
4673
+ return extractTopoloServiceErrorCode2(body) ?? fallback;
4674
+ }
4675
+ function extractTopoloServiceErrorDetails2(body) {
4676
+ if (!body || typeof body !== "object" || Array.isArray(body)) return void 0;
4677
+ const record4 = body;
4678
+ let candidate = record4.details;
4679
+ const nested = record4.error;
4680
+ if (nested && typeof nested === "object" && !Array.isArray(nested)) {
4681
+ const nestedRecord = nested;
4682
+ if (candidate === void 0) candidate = nestedRecord.details ?? nestedRecord;
4683
+ }
4684
+ if (candidate === void 0) candidate = record4;
4685
+ if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) return void 0;
4686
+ const source = candidate;
4687
+ const safe = {};
4688
+ for (const [key, value] of Object.entries(source)) {
4689
+ if (/present$/i.test(key) && typeof value === "boolean") {
4690
+ safe[key] = value;
4691
+ continue;
4692
+ }
4693
+ if (key === "retryable" && typeof value === "boolean") {
4694
+ safe.retryable = value;
4695
+ continue;
4696
+ }
4697
+ if (key === "code") {
4698
+ const sanitized = canonicalTopoloErrorCode2(value);
4699
+ if (sanitized) safe.code = sanitized;
4700
+ continue;
4701
+ }
4702
+ if (key === "source") {
4703
+ const sanitized = typeof value === "string" ? value.trim().toLowerCase() : "";
4704
+ if (PUBLIC_DETAIL_SOURCES2.has(sanitized)) safe.source = sanitized;
4705
+ continue;
4706
+ }
4707
+ if (key === "dependency" || key === "platform") {
4708
+ safe[`${key}Present`] = value !== null && value !== void 0;
4709
+ continue;
4710
+ }
4711
+ if (key === "error" && !safe.code) {
4712
+ const sanitized = canonicalTopoloErrorCode2(value);
4713
+ if (sanitized) safe.code = sanitized;
4714
+ continue;
4715
+ }
4716
+ if (key === "required" && Array.isArray(value)) {
4717
+ const required = canonicalTopoloRequired2(value);
4718
+ if (required.length) safe.required = required;
4719
+ continue;
4720
+ }
4721
+ if (["missingFields", "fields", "paths"].includes(key) && Array.isArray(value)) {
4722
+ const paths = value.filter((item) => typeof item === "string" && /^[$.a-z0-9_[\]-]{1,160}$/i.test(item));
4723
+ if (paths.length) safe[key] = paths;
4724
+ }
4207
4725
  }
4208
- return "http_error";
4726
+ return Object.keys(safe).length ? safe : void 0;
4209
4727
  }
4210
4728
  var TopoloHttpError2 = class extends TopoloSdkError2 {
4211
4729
  status;
@@ -4214,15 +4732,19 @@ var TopoloHttpError2 = class extends TopoloSdkError2 {
4214
4732
  path;
4215
4733
  requestId;
4216
4734
  retryAfter;
4735
+ details;
4217
4736
  constructor(service, path9, status, body, message, options = {}) {
4218
- super(extractHttpErrorCode2(body), message ?? `HTTP ${status} from ${service}${path9}`);
4737
+ super(serviceErrorCode2(body, "http_error"), message ?? `HTTP ${status} from ${service}${path9}`);
4219
4738
  this.name = "TopoloHttpError";
4220
4739
  this.status = status;
4221
4740
  this.body = body;
4222
4741
  this.service = service;
4223
4742
  this.path = path9;
4224
- this.requestId = options.requestId;
4225
- this.retryAfter = options.retryAfter;
4743
+ this.requestId = canonicalTopoloRequestId2(options.requestId);
4744
+ this.retryAfter = canonicalTopoloRetryAfter2(options.retryAfter);
4745
+ this.details = extractTopoloServiceErrorDetails2(
4746
+ options.details === void 0 ? body : { details: options.details }
4747
+ );
4226
4748
  }
4227
4749
  };
4228
4750
  var APP_API_URLS2 = {
@@ -4328,11 +4850,9 @@ var TopoloOAuth2 = class {
4328
4850
  return parsed;
4329
4851
  }
4330
4852
  };
4331
- function contextKey(contextType, organizationId) {
4332
- return contextType === "organization" && organizationId ? `org:${organizationId}` : "personal";
4333
- }
4334
- var ENVIRONMENT_DEFAULT_SERVICE_URLS = {
4853
+ var TOPOLO_ENVIRONMENT_SERVICE_URLS = {
4335
4854
  production: {
4855
+ auth: "https://auth.topolo.app",
4336
4856
  developers: "https://developers.topolo.app"
4337
4857
  },
4338
4858
  staging: {
@@ -4346,6 +4866,55 @@ var ENVIRONMENT_DEFAULT_SERVICE_URLS = {
4346
4866
  },
4347
4867
  local: {}
4348
4868
  };
4869
+ function hostnameHasSuffix2(hostname, suffix) {
4870
+ return hostname === suffix || hostname.endsWith(`.${suffix}`);
4871
+ }
4872
+ function workersDevAccountMatches2(hostname, account) {
4873
+ if (!hostname.endsWith(".workers.dev")) return false;
4874
+ const labels = hostname.slice(0, -".workers.dev".length).split(".");
4875
+ return labels.length >= 2 && labels.at(-1) === account;
4876
+ }
4877
+ function originMatchesEnvironment2(url, environment) {
4878
+ const hostname = url.hostname.toLowerCase();
4879
+ if (environment === "local") {
4880
+ return hostname === "localhost" || hostname.endsWith(".localhost") || hostname === "127.0.0.1" || hostname === "[::1]";
4881
+ }
4882
+ if (url.protocol !== "https:") return false;
4883
+ if (environment === "production") {
4884
+ return hostnameHasSuffix2(hostname, "topolo.app") || hostnameHasSuffix2(hostname, "topolo.io") || workersDevAccountMatches2(hostname, "topolo");
4885
+ }
4886
+ if (environment === "staging") {
4887
+ return hostnameHasSuffix2(hostname, "stg.topolo.us") || workersDevAccountMatches2(hostname, "topolo-staging");
4888
+ }
4889
+ return hostnameHasSuffix2(hostname, "topolo.dev") || workersDevAccountMatches2(hostname, "topolo-development");
4890
+ }
4891
+ function assertTopoloServiceUrlEnvironment2(service, value, environment) {
4892
+ let url;
4893
+ try {
4894
+ url = new URL(value);
4895
+ } catch {
4896
+ throw new TopoloSdkError2("invalid_service_url", `Invalid URL configured for Topolo service "${service}".`);
4897
+ }
4898
+ if (url.protocol !== "https:" && url.protocol !== "http:" || url.username || url.password || !originMatchesEnvironment2(url, environment)) {
4899
+ throw new TopoloSdkError2(
4900
+ "environment_mismatch",
4901
+ `Refusing Topolo service "${service}": ${url.origin} does not belong to ${environment}.`
4902
+ );
4903
+ }
4904
+ return url.origin;
4905
+ }
4906
+ function topoloServiceOrigins2(serviceUrls, environment) {
4907
+ return Object.fromEntries(
4908
+ Object.entries(serviceUrls ?? {}).sort(([left], [right]) => left.localeCompare(right)).map(([service, value]) => [
4909
+ service,
4910
+ assertTopoloServiceUrlEnvironment2(service, value, environment)
4911
+ ])
4912
+ );
4913
+ }
4914
+ function contextKey(contextType, organizationId) {
4915
+ return contextType === "organization" && organizationId ? `org:${organizationId}` : "personal";
4916
+ }
4917
+ var ENVIRONMENT_DEFAULT_SERVICE_URLS = TOPOLO_ENVIRONMENT_SERVICE_URLS;
4349
4918
  function configPath() {
4350
4919
  const override = process.env.TOPOLO_CONFIG_PATH;
4351
4920
  if (override) return override;
@@ -4453,15 +5022,17 @@ function credentialSourceFromConfig(config, environment = selectedEnvironment())
4453
5022
  const prefix = envVarPrefix(environment);
4454
5023
  if (process.env[`${prefix}_API_KEY`]) return `env:${prefix}_API_KEY`;
4455
5024
  if (process.env[`${prefix}_ACCESS_TOKEN`]) return `env:${prefix}_ACCESS_TOKEN`;
4456
- if (process.env.TOPOLO_API_KEY) return "env:TOPOLO_API_KEY";
4457
- if (process.env.TOPOLO_ACCESS_TOKEN) return "env:TOPOLO_ACCESS_TOKEN";
5025
+ if (environment === "production" && process.env.TOPOLO_API_KEY) return "env:TOPOLO_API_KEY";
5026
+ if (environment === "production" && process.env.TOPOLO_ACCESS_TOKEN) {
5027
+ return "env:TOPOLO_ACCESS_TOKEN";
5028
+ }
4458
5029
  return profileForEnvironment(config, environment).credential ? "config-file" : "none";
4459
5030
  }
4460
5031
  async function resolveCredential(environment = selectedEnvironment()) {
4461
5032
  const prefix = envVarPrefix(environment);
4462
- const apiKey = process.env[`${prefix}_API_KEY`] ?? process.env.TOPOLO_API_KEY;
5033
+ const apiKey = process.env[`${prefix}_API_KEY`] ?? (environment === "production" ? process.env.TOPOLO_API_KEY : void 0);
4463
5034
  if (apiKey) return { kind: "api_key", apiKey };
4464
- const accessToken = process.env[`${prefix}_ACCESS_TOKEN`] ?? process.env.TOPOLO_ACCESS_TOKEN;
5035
+ const accessToken = process.env[`${prefix}_ACCESS_TOKEN`] ?? (environment === "production" ? process.env.TOPOLO_ACCESS_TOKEN : void 0);
4465
5036
  if (accessToken) return { kind: "access_token", accessToken };
4466
5037
  const config = await readConfig();
4467
5038
  return profileForEnvironment(config, environment).credential ?? null;
@@ -4469,7 +5040,7 @@ async function resolveCredential(environment = selectedEnvironment()) {
4469
5040
  var REFRESH_SKEW_SECONDS = 60;
4470
5041
  async function refreshCredentialIfNeeded(environment = selectedEnvironment(), options = {}) {
4471
5042
  const prefix = envVarPrefix(environment);
4472
- if (process.env[`${prefix}_API_KEY`] || process.env[`${prefix}_ACCESS_TOKEN`] || process.env.TOPOLO_API_KEY || process.env.TOPOLO_ACCESS_TOKEN) {
5043
+ 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) {
4473
5044
  return resolveCredential(environment);
4474
5045
  }
4475
5046
  const config = await readConfig();
@@ -4479,9 +5050,16 @@ async function refreshCredentialIfNeeded(environment = selectedEnvironment(), op
4479
5050
  if (!cred.refreshToken || !profile.oauthClientId) return cred;
4480
5051
  const now = Math.floor(Date.now() / 1e3);
4481
5052
  if (!options.force && cred.expiresAt && cred.expiresAt - REFRESH_SKEW_SECONDS > now) return cred;
5053
+ const serviceUrls = resolveServiceUrls(profile.serviceUrls, environment);
5054
+ if (environment === "local" && !serviceUrls?.auth) {
5055
+ throw new TopoloSdkError2(
5056
+ "environment_service_url_required",
5057
+ "Local Topolo credential refresh requires an explicit local Auth service origin."
5058
+ );
5059
+ }
4482
5060
  try {
4483
5061
  const oauth = new TopoloOAuth2({
4484
- serviceUrls: resolveServiceUrls(profile.serviceUrls, environment)
5062
+ serviceUrls
4485
5063
  });
4486
5064
  const tokens = await oauth.refreshToken({
4487
5065
  clientId: profile.oauthClientId,
@@ -4522,9 +5100,16 @@ async function refreshContextCredentialIfNeeded(environment, key, options = {})
4522
5100
  if (!credential.refreshToken || !oauthClientId) return credential;
4523
5101
  const now = Math.floor(Date.now() / 1e3);
4524
5102
  if (!options.force && credential.expiresAt && credential.expiresAt - REFRESH_SKEW_SECONDS > now) return credential;
5103
+ const serviceUrls = resolveServiceUrls(profile.serviceUrls, environment);
5104
+ if (environment === "local" && !serviceUrls?.auth) {
5105
+ throw new TopoloSdkError2(
5106
+ "environment_service_url_required",
5107
+ "Local Topolo context refresh requires an explicit local Auth service origin."
5108
+ );
5109
+ }
4525
5110
  try {
4526
5111
  const oauth = new TopoloOAuth2({
4527
- serviceUrls: resolveServiceUrls(profile.serviceUrls, environment)
5112
+ serviceUrls
4528
5113
  });
4529
5114
  const tokens = await oauth.refreshToken({
4530
5115
  clientId: oauthClientId,
@@ -4571,7 +5156,12 @@ function resolveServiceUrls(configUrls, environment = selectedEnvironment()) {
4571
5156
  merged[k.slice("TOPOLO_SERVICE_URL_".length).toLowerCase()] = v;
4572
5157
  }
4573
5158
  }
4574
- return Object.keys(merged).length > 0 ? merged : void 0;
5159
+ const origins = topoloServiceOrigins2(merged, environment);
5160
+ return Object.keys(origins).length > 0 ? origins : void 0;
5161
+ }
5162
+ function assertServiceUrlForEnvironment(service, serviceUrl, environment = selectedEnvironment()) {
5163
+ if (!serviceUrl) return;
5164
+ assertTopoloServiceUrlEnvironment2(service, serviceUrl, environment);
4575
5165
  }
4576
5166
  function resolvedProfileConfig(config, environment = selectedEnvironment()) {
4577
5167
  const profile = profileForEnvironment(config, environment);
@@ -4612,24 +5202,6 @@ function resolveOAuthClientId(config, environment = selectedEnvironment()) {
4612
5202
  if (process.env.TOPOLO_CLI_CLIENT_ID) return process.env.TOPOLO_CLI_CLIENT_ID;
4613
5203
  return DEFAULT_CLI_CLIENT_IDS[environment] ?? profileForEnvironment(config, environment).oauthClientId;
4614
5204
  }
4615
- var NON_PROD_URL_MARKERS = {
4616
- staging: [".stg.topolo.us", "topolo-staging"],
4617
- development: [".topolo.dev", "topolo-development"]
4618
- };
4619
- function assertServiceUrlForEnvironment(service, serviceUrl, environment = selectedEnvironment()) {
4620
- const markers = NON_PROD_URL_MARKERS[environment];
4621
- if (!markers) return;
4622
- if (!serviceUrl) {
4623
- throw new Error(`No ${environment} URL is configured for Topolo service "${service}".`);
4624
- }
4625
- const url = serviceUrl.toLowerCase();
4626
- if (markers.some((marker) => url.includes(marker)) || url.includes("localhost") || url.includes("127.0.0.1")) {
4627
- return;
4628
- }
4629
- throw new Error(
4630
- `Refusing to call ${service} in ${environment} because its effective URL is not a ${environment} endpoint: ${serviceUrl}`
4631
- );
4632
- }
4633
5205
 
4634
5206
  // src/output.ts
4635
5207
  function resolveOutputMode(override) {
@@ -4677,23 +5249,25 @@ function fail(err, mode) {
4677
5249
  function normalizeError(err) {
4678
5250
  if (err instanceof Error) {
4679
5251
  const code = err.code;
4680
- const hintMap = {
4681
- auth_error: "Run `topolo auth login` or set TOPOLO_API_KEY.",
4682
- confirmation_required: "Review the action and rerun with --confirm to allow it.",
4683
- permission_denied: "Check your API key scopes or role assignments in TopoloDevelopers.",
5252
+ const fallbackHintMap = {
4684
5253
  immutable_drift: "Restore the manifest identity fields to the registered values; immutable ids cannot be updated."
4685
5254
  };
5255
+ const body = err.body;
5256
+ const status = err.status;
5257
+ const suppliedDetails = err.details;
4686
5258
  return {
4687
- message: err.message,
5259
+ message: status !== void 0 || body !== void 0 ? publicTopoloErrorMessage({ code, body, status, message: err.message }) : err.message,
4688
5260
  code,
4689
- status: err.status,
5261
+ status,
4690
5262
  service: err.service,
4691
5263
  path: err.path,
4692
5264
  requestId: err.requestId,
4693
5265
  retryAfter: err.retryAfter,
4694
- details: err.body,
4695
- hint: code ? hintMap[code] : void 0,
4696
- exitCode: code === "auth_error" || code === "confirmation_required" ? 2 : code === "permission_denied" ? 3 : code === "immutable_drift" ? 4 : 1
5266
+ details: extractTopoloServiceErrorDetails(
5267
+ suppliedDetails === void 0 ? body : { details: suppliedDetails }
5268
+ ),
5269
+ hint: topoloErrorHint({ code, body }) ?? (code ? fallbackHintMap[code] : void 0),
5270
+ exitCode: status === 401 || code === "auth_error" || code === "confirmation_required" ? 2 : status === 403 || code === "permission_denied" ? 3 : code === "immutable_drift" ? 4 : 1
4697
5271
  };
4698
5272
  }
4699
5273
  return { message: String(err), exitCode: 1 };
@@ -4872,7 +5446,12 @@ async function runDeviceFlow(options) {
4872
5446
  ...token.refresh_token ? { refreshToken: token.refresh_token } : {},
4873
5447
  expiresAt: Math.floor(Date.now() / 1e3) + token.expires_in
4874
5448
  };
4875
- const context = await resolveLoginContext(credential, resolved.serviceUrls, options.clientId);
5449
+ const context = await resolveLoginContext(
5450
+ credential,
5451
+ resolved.serviceUrls,
5452
+ options.clientId,
5453
+ options.environment
5454
+ );
4876
5455
  const updated = await readConfig();
4877
5456
  await writeConfig(writeContextCredential(updated, options.environment, context));
4878
5457
  emit(
@@ -4889,10 +5468,11 @@ async function runDeviceFlow(options) {
4889
5468
  () => "\n \u2713 Signed in. Run `topolo whoami` to see your identity."
4890
5469
  );
4891
5470
  }
4892
- async function resolveLoginContext(credential, serviceUrls, clientId) {
5471
+ async function resolveLoginContext(credential, serviceUrls, clientId, environment) {
4893
5472
  try {
4894
5473
  const topolo = createTopolo({
4895
5474
  credential,
5475
+ environment,
4896
5476
  agent: { clientName: "topolo-cli", clientVersion: CLI_VERSION },
4897
5477
  serviceUrls
4898
5478
  });
@@ -4985,6 +5565,7 @@ async function buildClient(options) {
4985
5565
  const profile = resolvedProfileConfig(config, environment);
4986
5566
  return createTopolo({
4987
5567
  credential,
5568
+ environment,
4988
5569
  agent: {
4989
5570
  clientName: "topolo-cli",
4990
5571
  clientVersion: CLI_VERSION,
@@ -5002,23 +5583,25 @@ function isDebugEnabled() {
5002
5583
  return v !== "0" && v.toLowerCase() !== "false";
5003
5584
  }
5004
5585
  function stderrDebugLogger(event) {
5586
+ const requestId = canonicalTopoloRequestId(event.requestId) ?? "unavailable";
5005
5587
  if (event.phase === "request") {
5006
5588
  process.stderr.write(
5007
- `[topolo] \u2192 ${event.method} ${event.service} ${event.path} (req=${event.requestId})
5589
+ `[topolo] \u2192 ${event.method} ${event.service} ${event.path} (req=${requestId})
5008
5590
  `
5009
5591
  );
5010
5592
  return;
5011
5593
  }
5012
5594
  if (event.phase === "response") {
5013
5595
  process.stderr.write(
5014
- `[topolo] \u2190 ${event.status} ${event.method} ${event.service} ${event.path} (${event.durationMs}ms, req=${event.requestId})
5596
+ `[topolo] \u2190 ${event.status} ${event.method} ${event.service} ${event.path} (${event.durationMs}ms, req=${requestId})
5015
5597
  `
5016
5598
  );
5017
5599
  return;
5018
5600
  }
5019
5601
  const statusPart = event.status ? ` ${event.status}` : "";
5602
+ const errorCode = canonicalTopoloErrorCode(event.error) ?? "request_failed";
5020
5603
  process.stderr.write(
5021
- `[topolo] \u2717${statusPart} ${event.method} ${event.service} ${event.path} (${event.durationMs}ms, req=${event.requestId}): ${event.error}
5604
+ `[topolo] \u2717${statusPart} ${event.method} ${event.service} ${event.path} (${event.durationMs}ms, req=${requestId}, code=${errorCode})
5022
5605
  `
5023
5606
  );
5024
5607
  }
@@ -5251,6 +5834,22 @@ function registerActionsCommand(program2) {
5251
5834
  fail(err, mode);
5252
5835
  }
5253
5836
  });
5837
+ actions.command("prepare <action>").description("Prepare one action with identity, example, validation, exact request, verification, and rollback").option("-d, --data <json-or-@file>", "JSON action input or @path; defaults to one valid published example").option("--resource-type <type>", "App-local resource alias, such as workspace").option("--resource-id <id>", "Resource id for the selected resource type").option("--confirm", "Include confirmation in the plan without executing it").option("--json", "Output compact JSON").option("--no-json", "Output human-readable text").action(async (action, opts, command) => {
5838
+ const mode = resolveOutputMode(opts.json ?? command.parent?.opts().json);
5839
+ try {
5840
+ const topolo = await requiredClient();
5841
+ const resource = parseResourceOption(opts.resourceType, opts.resourceId);
5842
+ const preparation = await topolo.client.prepareAction(
5843
+ action,
5844
+ opts.data === void 0 ? void 0 : parseJsonObject(opts.data),
5845
+ { confirm: opts.confirm === true, ...resource ? { resource } : {} }
5846
+ );
5847
+ const output2 = compactActionPreparation(preparation, await actionExecutionContext(topolo));
5848
+ emit(output2, mode, () => formatActionPreparation(output2));
5849
+ } catch (err) {
5850
+ fail(err, mode);
5851
+ }
5852
+ });
5254
5853
  actions.command("plan <action>").description("Validate and preview the exact request, confirmation, verification, and rollback plan").option("-d, --data <json-or-@file>", "JSON action input or @path to a JSON file", "{}").option("--resource-type <type>", "App-local resource alias, such as workspace").option("--resource-id <id>", "Resource id for the selected resource type").option("--confirm", "Include confirmation in the plan without executing it").option("--detail", "Include the full action contract").option("--json", "Output JSON").option("--no-json", "Output human-readable text").action(async (action, opts, command) => {
5255
5854
  const mode = resolveOutputMode(opts.json ?? command.parent?.opts().json);
5256
5855
  try {
@@ -5316,6 +5915,43 @@ function registerActionsCommand(program2) {
5316
5915
  }
5317
5916
  });
5318
5917
  }
5918
+ async function actionExecutionContext(topolo) {
5919
+ const environment = selectedEnvironment();
5920
+ const config = await readConfig();
5921
+ const profile = profileForEnvironment(config, environment);
5922
+ const source = credentialSourceFromConfig(config, environment);
5923
+ return createTopoloAgentExecutionContext({
5924
+ environment,
5925
+ contextKey: source.startsWith("env:") ? null : profile.activeContext ?? null,
5926
+ identity: await topolo.identity.whoami(),
5927
+ serviceUrls: resolvedProfileConfig(config, environment).serviceUrls
5928
+ });
5929
+ }
5930
+ function formatActionPreparation(preparation) {
5931
+ const context = preparation.context;
5932
+ const lines = [
5933
+ `Environment: ${context.environment}`,
5934
+ `User: ${context.user.email} (${context.user.role})`,
5935
+ `Organization: ${context.organization ? `${context.organization.name} (${context.organization.slug})` : "(personal)"}`,
5936
+ `Action: ${preparation.action.id}`,
5937
+ `Input source: ${preparation.input.source}`,
5938
+ `Valid: ${preparation.valid ? "yes" : "no"}`,
5939
+ `Confirmation: ${preparation.confirmation.required ? preparation.confirmation.provided ? "provided" : "required" : "not required"}`
5940
+ ];
5941
+ if (preparation.example) {
5942
+ lines.push(`Example: ${JSON.stringify(preparation.example.input)}`);
5943
+ }
5944
+ if ("value" in preparation.input) {
5945
+ lines.push(`Input: ${JSON.stringify(preparation.input.value)}`);
5946
+ }
5947
+ if (preparation.request) {
5948
+ lines.push(`Request: ${preparation.request.method} ${preparation.request.apiBaseUrl || preparation.request.service}${preparation.request.path}`);
5949
+ }
5950
+ if (preparation.missingFields.length) lines.push(`Missing: ${preparation.missingFields.join(", ")}`);
5951
+ if (preparation.verification.length) lines.push(`Verify: ${preparation.verification.join(" | ")}`);
5952
+ lines.push(`Rollback: ${preparation.rollbackActionId || "(not published)"}`);
5953
+ return lines.join("\n");
5954
+ }
5319
5955
  function summarizeActionCapabilities(service, actions) {
5320
5956
  return {
5321
5957
  service,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@topolo/cli",
3
- "version": "0.7.3",
4
- "topoloPublishedVersion": "0.7.3",
3
+ "version": "0.7.4",
4
+ "topoloPublishedVersion": "0.7.4",
5
5
  "description": "Command-line interface for Topolo developers, operators, and agents, with internal first-party scaffolding available under the topolo platform namespace.",
6
6
  "type": "module",
7
7
  "bin": {
@@ -12,24 +12,19 @@
12
12
  "skills"
13
13
  ],
14
14
  "scripts": {
15
- "build:sdk": "python3 ../../tooling/with-resource-lock.py ../topolo-sdk -- pnpm --dir ../topolo-sdk build",
16
- "prebuild": "pnpm run build:sdk",
17
15
  "build": "tsup && node scripts/vendor-generator.mjs",
18
16
  "dev": "tsup --watch",
19
- "pretypecheck": "pnpm run build:sdk",
20
17
  "typecheck": "tsc --noEmit",
21
18
  "start": "node dist/index.js",
22
- "pretest": "pnpm run build:sdk",
23
19
  "test": "vitest run",
24
20
  "test:watch": "vitest",
25
- "precoverage": "pnpm run build:sdk",
26
21
  "coverage": "vitest run --coverage"
27
22
  },
28
23
  "dependencies": {
29
24
  "commander": "^12.1.0"
30
25
  },
31
26
  "devDependencies": {
32
- "@topolo/sdk": "file:../topolo-sdk",
27
+ "@topolo/sdk": "0.10.4",
33
28
  "@types/node": "^20.12.0",
34
29
  "@vitest/coverage-v8": "3.2.6",
35
30
  "tsup": "^8.0.0",