@topolo/sdk 0.10.2 → 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.
package/dist/index.js CHANGED
@@ -27,6 +27,90 @@ var TopoloSdkError = class extends Error {
27
27
  this.code = code;
28
28
  }
29
29
  };
30
+ var PUBLIC_ERROR_CODES = /* @__PURE__ */ new Set([
31
+ "action_app_identity_unavailable",
32
+ "action_authentication_required",
33
+ "action_catalog_summary_unavailable",
34
+ "action_catalog_unavailable",
35
+ "action_contract_mismatch",
36
+ "action_permission_required",
37
+ "action_runtime_authorization_required",
38
+ "action_service_not_allowed",
39
+ "auth_error",
40
+ "authentication_required",
41
+ "authorization_pending",
42
+ "bad_sig",
43
+ "confirmation_required",
44
+ "dependency_unavailable",
45
+ "environment_mismatch",
46
+ "environment_service_url_required",
47
+ "expired_token",
48
+ "forbidden",
49
+ "http_error",
50
+ "immutable_drift",
51
+ "integration_dependency_unavailable",
52
+ "integration_misconfigured",
53
+ "invalid_action",
54
+ "invalid_action_input",
55
+ "invalid_grant",
56
+ "invalid_input",
57
+ "invalid_token",
58
+ "mcp_error",
59
+ "membership_required",
60
+ "network_error",
61
+ "organization_context_required",
62
+ "organization_enrollment_required",
63
+ "permission_denied",
64
+ "rate_limited",
65
+ "request_failed",
66
+ "service_catalog_unavailable",
67
+ "service_path_origin_mismatch",
68
+ "service_required",
69
+ "slow_down",
70
+ "state_conflict",
71
+ "unauthorized",
72
+ "unknown_action",
73
+ "unknown_service"
74
+ ]);
75
+ var ERROR_CODE_PATTERN = /^[a-z][a-z0-9_]{0,63}$/;
76
+ 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;
77
+ var CLOUDFLARE_RAY_ID_PATTERN = /^[0-9a-f]{16}-[A-Z]{3}$/;
78
+ var PERMISSION_PATTERN = /^[a-z][a-z0-9_-]{0,63}(?:\.[a-z][a-z0-9_-]{0,63})*:(?:[a-z][a-z0-9_-]{0,63}|\*)$/;
79
+ 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$/;
80
+ var PUBLIC_DETAIL_SOURCES = /* @__PURE__ */ new Set([
81
+ "api_key_introspection",
82
+ "api_key_validation",
83
+ "auth_context",
84
+ "auth_validate",
85
+ "catalog",
86
+ "credential",
87
+ "runtime",
88
+ "service_token_provider",
89
+ "service_verify_key",
90
+ "token_validation",
91
+ "upstream"
92
+ ]);
93
+ function canonicalTopoloErrorCode(value) {
94
+ if (typeof value !== "string") return null;
95
+ const candidate = value.trim().toLowerCase();
96
+ if (!ERROR_CODE_PATTERN.test(candidate) || !PUBLIC_ERROR_CODES.has(candidate)) return null;
97
+ return candidate;
98
+ }
99
+ function canonicalTopoloRequestId(value) {
100
+ if (typeof value !== "string") return void 0;
101
+ const candidate = value.trim();
102
+ return TOPOLO_REQUEST_ID_PATTERN.test(candidate) || CLOUDFLARE_RAY_ID_PATTERN.test(candidate) ? candidate : void 0;
103
+ }
104
+ function canonicalTopoloRetryAfter(value) {
105
+ if (typeof value !== "string") return void 0;
106
+ const candidate = value.trim();
107
+ if (/^\d{1,10}$/.test(candidate) || RETRY_AFTER_DATE_PATTERN.test(candidate)) return candidate;
108
+ return void 0;
109
+ }
110
+ function canonicalTopoloRequired(value) {
111
+ if (!Array.isArray(value)) return [];
112
+ return [...new Set(value.filter((item) => typeof item === "string" && PERMISSION_PATTERN.test(item)))].slice(0, 32);
113
+ }
30
114
  var TopoloAuthError = class extends TopoloSdkError {
31
115
  status;
32
116
  body;
@@ -34,15 +118,19 @@ var TopoloAuthError = class extends TopoloSdkError {
34
118
  path;
35
119
  requestId;
36
120
  retryAfter;
121
+ details;
37
122
  constructor(message, code = "auth_error", options = {}) {
38
- super(code, message);
123
+ super(canonicalTopoloErrorCode(code) ?? "auth_error", message);
39
124
  this.name = "TopoloAuthError";
40
125
  this.status = options.status ?? 401;
41
126
  this.body = options.body;
42
127
  this.service = options.service;
43
128
  this.path = options.path;
44
- this.requestId = options.requestId;
45
- this.retryAfter = options.retryAfter;
129
+ this.requestId = canonicalTopoloRequestId(options.requestId);
130
+ this.retryAfter = canonicalTopoloRetryAfter(options.retryAfter);
131
+ this.details = extractTopoloServiceErrorDetails(
132
+ options.details === void 0 ? options.body : { details: options.details }
133
+ );
46
134
  }
47
135
  };
48
136
  var TopoloPermissionError = class extends TopoloSdkError {
@@ -53,28 +141,140 @@ var TopoloPermissionError = class extends TopoloSdkError {
53
141
  path;
54
142
  requestId;
55
143
  retryAfter;
144
+ details;
56
145
  constructor(message, required, options = {}) {
57
- super("permission_denied", message);
146
+ super(
147
+ canonicalTopoloErrorCode(options.code) ?? serviceErrorCode(options.body, "permission_denied"),
148
+ message
149
+ );
58
150
  this.name = "TopoloPermissionError";
59
- this.required = required;
151
+ this.required = canonicalTopoloRequired(required);
60
152
  this.status = options.status ?? 403;
61
153
  this.body = options.body;
62
154
  this.service = options.service;
63
155
  this.path = options.path;
64
- this.requestId = options.requestId;
65
- this.retryAfter = options.retryAfter;
156
+ this.requestId = canonicalTopoloRequestId(options.requestId);
157
+ this.retryAfter = canonicalTopoloRetryAfter(options.retryAfter);
158
+ this.details = extractTopoloServiceErrorDetails(
159
+ options.details === void 0 ? options.body : { details: options.details }
160
+ );
66
161
  }
67
162
  };
68
- function extractHttpErrorCode(body) {
69
- if (!body || typeof body !== "object" || Array.isArray(body)) return "http_error";
163
+ function extractTopoloServiceErrorCode(body) {
164
+ if (!body || typeof body !== "object" || Array.isArray(body)) return null;
70
165
  const record = body;
71
- if (typeof record.code === "string" && record.code.trim()) return record.code.trim();
166
+ const topLevel = canonicalTopoloErrorCode(record.code);
167
+ if (topLevel) return topLevel;
72
168
  const nested = record.error;
169
+ const nestedString = canonicalTopoloErrorCode(nested);
170
+ if (nestedString) return nestedString;
73
171
  if (nested && typeof nested === "object" && !Array.isArray(nested)) {
74
172
  const code = nested.code;
75
- if (typeof code === "string" && code.trim()) return code.trim();
173
+ const nestedCode = canonicalTopoloErrorCode(code);
174
+ if (nestedCode) return nestedCode;
175
+ }
176
+ return null;
177
+ }
178
+ function serviceErrorCode(body, fallback) {
179
+ return extractTopoloServiceErrorCode(body) ?? fallback;
180
+ }
181
+ function extractTopoloServiceErrorDetails(body) {
182
+ if (!body || typeof body !== "object" || Array.isArray(body)) return void 0;
183
+ const record = body;
184
+ let candidate = record.details;
185
+ const nested = record.error;
186
+ if (nested && typeof nested === "object" && !Array.isArray(nested)) {
187
+ const nestedRecord = nested;
188
+ if (candidate === void 0) candidate = nestedRecord.details ?? nestedRecord;
189
+ }
190
+ if (candidate === void 0) candidate = record;
191
+ if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) return void 0;
192
+ const source = candidate;
193
+ const safe = {};
194
+ for (const [key, value] of Object.entries(source)) {
195
+ if (/present$/i.test(key) && typeof value === "boolean") {
196
+ safe[key] = value;
197
+ continue;
198
+ }
199
+ if (key === "retryable" && typeof value === "boolean") {
200
+ safe.retryable = value;
201
+ continue;
202
+ }
203
+ if (key === "code") {
204
+ const sanitized = canonicalTopoloErrorCode(value);
205
+ if (sanitized) safe.code = sanitized;
206
+ continue;
207
+ }
208
+ if (key === "source") {
209
+ const sanitized = typeof value === "string" ? value.trim().toLowerCase() : "";
210
+ if (PUBLIC_DETAIL_SOURCES.has(sanitized)) safe.source = sanitized;
211
+ continue;
212
+ }
213
+ if (key === "dependency" || key === "platform") {
214
+ safe[`${key}Present`] = value !== null && value !== void 0;
215
+ continue;
216
+ }
217
+ if (key === "error" && !safe.code) {
218
+ const sanitized = canonicalTopoloErrorCode(value);
219
+ if (sanitized) safe.code = sanitized;
220
+ continue;
221
+ }
222
+ if (key === "required" && Array.isArray(value)) {
223
+ const required = canonicalTopoloRequired(value);
224
+ if (required.length) safe.required = required;
225
+ continue;
226
+ }
227
+ if (["missingFields", "fields", "paths"].includes(key) && Array.isArray(value)) {
228
+ const paths = value.filter((item) => typeof item === "string" && /^[$.a-z0-9_[\]-]{1,160}$/i.test(item));
229
+ if (paths.length) safe[key] = paths;
230
+ }
231
+ }
232
+ return Object.keys(safe).length ? safe : void 0;
233
+ }
234
+ function publicTopoloErrorMessage(error) {
235
+ const code = canonicalTopoloErrorCode(
236
+ typeof error === "string" ? error : error.code ?? extractTopoloServiceErrorCode(error.body)
237
+ ) ?? "request_failed";
238
+ if (/(?:organization|org).*(?:enroll|membership|member|admission)/.test(code)) {
239
+ return "Organization or application enrollment is required.";
240
+ }
241
+ if (/(?:organization|org).*(?:context|required|missing|invalid|unknown|not_found)/.test(code)) {
242
+ return "Organization context is unavailable.";
243
+ }
244
+ if (["auth_error", "authentication_required", "unauthorized", "invalid_token", "expired_token"].includes(code)) {
245
+ return "Authentication failed.";
246
+ }
247
+ if (code === "permission_denied" || code === "forbidden") return "Permission denied.";
248
+ if (code === "confirmation_required") return "Confirmation is required.";
249
+ return `Topolo request failed (${code}).`;
250
+ }
251
+ function topoloErrorHint(error) {
252
+ const code = canonicalTopoloErrorCode(
253
+ typeof error === "string" ? error : error.code ?? extractTopoloServiceErrorCode(error.body)
254
+ );
255
+ if (!code) return void 0;
256
+ if (code === "auth_error" || code === "authentication_required" || code === "unauthorized" || code === "invalid_token" || code === "expired_token") {
257
+ return "Run `topolo auth login`, then verify the effective Topolo identity and organization.";
258
+ }
259
+ if (code === "confirmation_required") {
260
+ return "Review the unchanged action plan, obtain approval, and retry once with --confirm or confirm:true.";
261
+ }
262
+ if (code === "enrollment_required" || code === "membership_required" || /(?:organization|org).*(?:enroll|membership|member|admission)|(?:enroll|membership).*(?:organization|org)/.test(code)) {
263
+ return "The current identity is not enrolled for this organization or application. Use an enrolled identity or ask an organization administrator to complete enrollment.";
76
264
  }
77
- return "http_error";
265
+ if (/(?:organization|org).*(?:context|required|missing|invalid|unknown|not_found)/.test(code)) {
266
+ return "Select a signed-in organization context, then verify the effective identity and organization before retrying.";
267
+ }
268
+ if (code === "permission_denied" || code === "forbidden") {
269
+ return "The current identity or resource context is not authorized. Review the structured details and published authorization contract.";
270
+ }
271
+ if (code === "environment_mismatch") {
272
+ return "Use service origins and credentials from one explicit Topolo environment only.";
273
+ }
274
+ if (code === "invalid_action_input" || code === "invalid_input") {
275
+ return "Correct only the reported schema paths, then prepare the action again.";
276
+ }
277
+ return void 0;
78
278
  }
79
279
  var TopoloConfirmationRequiredError = class extends TopoloSdkError {
80
280
  constructor(message) {
@@ -89,18 +289,86 @@ var TopoloHttpError = class extends TopoloSdkError {
89
289
  path;
90
290
  requestId;
91
291
  retryAfter;
292
+ details;
92
293
  constructor(service, path, status, body, message, options = {}) {
93
- super(extractHttpErrorCode(body), message ?? `HTTP ${status} from ${service}${path}`);
294
+ super(serviceErrorCode(body, "http_error"), message ?? `HTTP ${status} from ${service}${path}`);
94
295
  this.name = "TopoloHttpError";
95
296
  this.status = status;
96
297
  this.body = body;
97
298
  this.service = service;
98
299
  this.path = path;
99
- this.requestId = options.requestId;
100
- this.retryAfter = options.retryAfter;
300
+ this.requestId = canonicalTopoloRequestId(options.requestId);
301
+ this.retryAfter = canonicalTopoloRetryAfter(options.retryAfter);
302
+ this.details = extractTopoloServiceErrorDetails(
303
+ options.details === void 0 ? body : { details: options.details }
304
+ );
101
305
  }
102
306
  };
103
307
 
308
+ // src/environment.ts
309
+ var TOPOLO_RUNTIME_ENVIRONMENTS = ["production", "staging", "development", "local"];
310
+ var TOPOLO_ENVIRONMENT_SERVICE_URLS = {
311
+ production: {
312
+ auth: "https://auth.topolo.app",
313
+ developers: "https://developers.topolo.app"
314
+ },
315
+ staging: {
316
+ auth: "https://auth.stg.topolo.us",
317
+ developers: "https://developers.stg.topolo.us",
318
+ seed: "https://seed.stg.topolo.us"
319
+ },
320
+ development: {
321
+ auth: "https://auth.topolo.dev",
322
+ developers: "https://developers.topolo.dev"
323
+ },
324
+ local: {}
325
+ };
326
+ function hostnameHasSuffix(hostname, suffix) {
327
+ return hostname === suffix || hostname.endsWith(`.${suffix}`);
328
+ }
329
+ function workersDevAccountMatches(hostname, account) {
330
+ if (!hostname.endsWith(".workers.dev")) return false;
331
+ const labels = hostname.slice(0, -".workers.dev".length).split(".");
332
+ return labels.length >= 2 && labels.at(-1) === account;
333
+ }
334
+ function originMatchesEnvironment(url, environment) {
335
+ const hostname = url.hostname.toLowerCase();
336
+ if (environment === "local") {
337
+ return hostname === "localhost" || hostname.endsWith(".localhost") || hostname === "127.0.0.1" || hostname === "[::1]";
338
+ }
339
+ if (url.protocol !== "https:") return false;
340
+ if (environment === "production") {
341
+ return hostnameHasSuffix(hostname, "topolo.app") || hostnameHasSuffix(hostname, "topolo.io") || workersDevAccountMatches(hostname, "topolo");
342
+ }
343
+ if (environment === "staging") {
344
+ return hostnameHasSuffix(hostname, "stg.topolo.us") || workersDevAccountMatches(hostname, "topolo-staging");
345
+ }
346
+ return hostnameHasSuffix(hostname, "topolo.dev") || workersDevAccountMatches(hostname, "topolo-development");
347
+ }
348
+ function assertTopoloServiceUrlEnvironment(service, value, environment) {
349
+ let url;
350
+ try {
351
+ url = new URL(value);
352
+ } catch {
353
+ throw new TopoloSdkError("invalid_service_url", `Invalid URL configured for Topolo service "${service}".`);
354
+ }
355
+ if (url.protocol !== "https:" && url.protocol !== "http:" || url.username || url.password || !originMatchesEnvironment(url, environment)) {
356
+ throw new TopoloSdkError(
357
+ "environment_mismatch",
358
+ `Refusing Topolo service "${service}": ${url.origin} does not belong to ${environment}.`
359
+ );
360
+ }
361
+ return url.origin;
362
+ }
363
+ function topoloServiceOrigins(serviceUrls, environment) {
364
+ return Object.fromEntries(
365
+ Object.entries(serviceUrls ?? {}).sort(([left], [right]) => left.localeCompare(right)).map(([service, value]) => [
366
+ service,
367
+ assertTopoloServiceUrlEnvironment(service, value, environment)
368
+ ])
369
+ );
370
+ }
371
+
104
372
  // src/bootstrap-services.ts
105
373
  var APP_API_URLS = {
106
374
  auth: "https://auth.topolo.app",
@@ -2869,6 +3137,7 @@ var TopoloClient = class _TopoloClient {
2869
3137
  credential;
2870
3138
  agent;
2871
3139
  serviceUrls;
3140
+ environment;
2872
3141
  appIds;
2873
3142
  requireConfirmForWrites;
2874
3143
  timeoutMs;
@@ -2897,7 +3166,8 @@ var TopoloClient = class _TopoloClient {
2897
3166
  if (!options.agent?.clientName) throw new TopoloAuthError("agent.clientName is required");
2898
3167
  this.credential = options.credential;
2899
3168
  this.agent = options.agent;
2900
- this.serviceUrls = options.serviceUrls;
3169
+ this.environment = options.environment;
3170
+ this.serviceUrls = options.environment ? topoloServiceOrigins(options.serviceUrls, options.environment) : options.serviceUrls;
2901
3171
  this.appIds = options.appIds;
2902
3172
  this.requireConfirmForWrites = options.requireConfirmForWrites !== false;
2903
3173
  this.timeoutMs = options.timeoutMs ?? 3e4;
@@ -2926,9 +3196,15 @@ var TopoloClient = class _TopoloClient {
2926
3196
  );
2927
3197
  }
2928
3198
  await this.loadCurrentCredential();
2929
- const resolvedService = opts.baseUrl ? { serviceKey: normalizeLookupKey(opts.service), baseUrl: requireHttpOrigin(opts.baseUrl) } : await this.resolveRequestService(opts.service);
3199
+ const resolvedService = opts.baseUrl ? { serviceKey: normalizeLookupKey(opts.service), baseUrl: this.validatedServiceOrigin(opts.service, opts.baseUrl) } : await this.resolveRequestService(opts.service);
2930
3200
  const baseUrl = resolvedService.baseUrl;
2931
3201
  const url = new URL(opts.path, baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`);
3202
+ if (url.origin !== new URL(baseUrl).origin) {
3203
+ throw new TopoloSdkError(
3204
+ "service_path_origin_mismatch",
3205
+ `Refusing Topolo request path because it changes the resolved ${resolvedService.serviceKey || opts.service} service origin.`
3206
+ );
3207
+ }
2932
3208
  if (opts.query) {
2933
3209
  for (const [k, v] of Object.entries(opts.query)) {
2934
3210
  if (v !== void 0) url.searchParams.set(k, String(v));
@@ -2999,7 +3275,7 @@ var TopoloClient = class _TopoloClient {
2999
3275
  url: urlStr,
3000
3276
  requestId,
3001
3277
  durationMs: Date.now() - startedAt,
3002
- error: err instanceof Error ? err.message : String(err)
3278
+ error: "network_error"
3003
3279
  });
3004
3280
  throw err;
3005
3281
  }
@@ -3008,6 +3284,7 @@ var TopoloClient = class _TopoloClient {
3008
3284
  if (!res.ok) {
3009
3285
  const durationMs = Date.now() - startedAt;
3010
3286
  const message = describeError(parsed, `HTTP ${res.status}`);
3287
+ const publicCode = extractTopoloServiceErrorCode(parsed) ?? "http_error";
3011
3288
  this.emit({
3012
3289
  phase: "error",
3013
3290
  method,
@@ -3016,7 +3293,7 @@ var TopoloClient = class _TopoloClient {
3016
3293
  url: urlStr,
3017
3294
  requestId,
3018
3295
  durationMs,
3019
- error: message,
3296
+ error: publicCode,
3020
3297
  status: res.status
3021
3298
  });
3022
3299
  const errorContext = {
@@ -3024,17 +3301,21 @@ var TopoloClient = class _TopoloClient {
3024
3301
  body: parsed,
3025
3302
  service: resolvedService.serviceKey,
3026
3303
  path: opts.path,
3027
- requestId: res.headers.get("X-Topolo-Request-Id") || requestId,
3028
- retryAfter: res.headers.get("Retry-After") || void 0
3304
+ requestId: canonicalTopoloRequestId(res.headers.get("X-Topolo-Request-Id")) ?? requestId,
3305
+ retryAfter: canonicalTopoloRetryAfter(res.headers.get("Retry-After"))
3029
3306
  };
3030
3307
  if (res.status === 401) {
3031
- throw new TopoloAuthError(describeError(parsed, "Unauthorized"), "auth_error", errorContext);
3308
+ throw new TopoloAuthError(
3309
+ describeError(parsed, "Unauthorized"),
3310
+ extractTopoloServiceErrorCode(parsed) ?? "auth_error",
3311
+ errorContext
3312
+ );
3032
3313
  }
3033
3314
  if (res.status === 403) {
3034
3315
  throw new TopoloPermissionError(
3035
3316
  describeError(parsed, "Permission denied"),
3036
3317
  extractRequired(parsed),
3037
- errorContext
3318
+ { ...errorContext, code: extractTopoloServiceErrorCode(parsed) ?? "permission_denied" }
3038
3319
  );
3039
3320
  }
3040
3321
  throw new TopoloHttpError(
@@ -3084,6 +3365,17 @@ var TopoloClient = class _TopoloClient {
3084
3365
  });
3085
3366
  return this.refreshInFlight;
3086
3367
  }
3368
+ validatedServiceOrigin(service, value) {
3369
+ const origin = requireHttpOrigin(value);
3370
+ return this.environment ? assertTopoloServiceUrlEnvironment(service, origin, this.environment) : origin;
3371
+ }
3372
+ validatedActionEntry(entry) {
3373
+ if (!entry.apiBaseUrl) return entry;
3374
+ return {
3375
+ ...entry,
3376
+ apiBaseUrl: this.validatedServiceOrigin(entry.appSlug || entry.appId, entry.apiBaseUrl)
3377
+ };
3378
+ }
3087
3379
  async resolveRequestService(service) {
3088
3380
  const serviceKey = normalizeLookupKey(service);
3089
3381
  if (!serviceKey) {
@@ -3091,17 +3383,20 @@ var TopoloClient = class _TopoloClient {
3091
3383
  }
3092
3384
  if (serviceKey === "auth" || serviceKey === "developers") {
3093
3385
  const overrideUrl = resolveConfiguredServiceUrl(serviceKey, this.serviceUrls);
3094
- if (overrideUrl) return { serviceKey, baseUrl: overrideUrl };
3386
+ if (overrideUrl) return { serviceKey, baseUrl: this.validatedServiceOrigin(serviceKey, overrideUrl) };
3095
3387
  if (serviceKey === "developers") {
3096
3388
  const derived = deriveDevelopersFromAuth(resolveConfiguredServiceUrl("auth", this.serviceUrls));
3097
- if (derived) return { serviceKey, baseUrl: derived };
3389
+ if (derived) return { serviceKey, baseUrl: this.validatedServiceOrigin(serviceKey, derived) };
3098
3390
  }
3099
- return { serviceKey, baseUrl: DEFAULT_APP_URLS[serviceKey] };
3391
+ return {
3392
+ serviceKey,
3393
+ baseUrl: this.validatedServiceOrigin(serviceKey, DEFAULT_APP_URLS[serviceKey])
3394
+ };
3100
3395
  }
3101
3396
  const live = await this.resolveLiveCallableService(serviceKey);
3102
3397
  if (live) {
3103
3398
  const overrideUrl = resolveConfiguredServiceUrl(serviceKey, this.serviceUrls);
3104
- return overrideUrl ? { ...live, baseUrl: overrideUrl } : live;
3399
+ return overrideUrl ? { ...live, baseUrl: this.validatedServiceOrigin(serviceKey, overrideUrl) } : { ...live, baseUrl: this.validatedServiceOrigin(serviceKey, live.baseUrl) };
3105
3400
  }
3106
3401
  throw new TopoloSdkError(
3107
3402
  "unknown_service",
@@ -3115,10 +3410,10 @@ var TopoloClient = class _TopoloClient {
3115
3410
  serviceKey,
3116
3411
  overrides: this.serviceUrls
3117
3412
  });
3118
- if (baseUrl) return { serviceKey, baseUrl };
3413
+ if (baseUrl) return { serviceKey, baseUrl: this.validatedServiceOrigin(serviceKey, baseUrl) };
3119
3414
  const actionPage = await this.searchActions({ service: match.appId, limit: 1 }).catch(() => null);
3120
3415
  const actionBaseUrl = actionPage?.actions.find((action) => action.apiBaseUrl)?.apiBaseUrl ?? null;
3121
- return actionBaseUrl ? { serviceKey, baseUrl: requireHttpOrigin(actionBaseUrl) } : null;
3416
+ return actionBaseUrl ? { serviceKey, baseUrl: this.validatedServiceOrigin(serviceKey, actionBaseUrl) } : null;
3122
3417
  }
3123
3418
  /**
3124
3419
  * Resolve a service-slug → opaque app-id via caller config or TopoloAuth.
@@ -3138,7 +3433,7 @@ var TopoloClient = class _TopoloClient {
3138
3433
  if (!normalized) return null;
3139
3434
  const configuredAppId = resolveConfiguredAppId(normalized, this.appIds);
3140
3435
  if (configuredAppId) return configuredAppId;
3141
- const authBase = resolveServiceUrl("developers", this.serviceUrls);
3436
+ const authBase = this.validatedServiceOrigin("developers", resolveServiceUrl("developers", this.serviceUrls));
3142
3437
  const cacheKey = `${authBase}|${normalized}`;
3143
3438
  const now = Date.now();
3144
3439
  const cached = _TopoloClient.appIdCache.get(cacheKey);
@@ -3314,7 +3609,7 @@ var TopoloClient = class _TopoloClient {
3314
3609
  "TopoloAuth did not return an action catalog for the current credential."
3315
3610
  );
3316
3611
  }
3317
- const entries = raw.map((item) => normalizeActionCatalogEntry(item)).filter((entry) => Boolean(entry));
3612
+ const entries = raw.map((item) => normalizeActionCatalogEntry(item)).filter((entry) => Boolean(entry)).map((entry) => this.validatedActionEntry(entry));
3318
3613
  return {
3319
3614
  actions: entries,
3320
3615
  page: normalizeCatalogPage(
@@ -3379,7 +3674,7 @@ var TopoloClient = class _TopoloClient {
3379
3674
  const raw = res.data?.action ?? res.action;
3380
3675
  const match = normalizeActionCatalogEntry(raw);
3381
3676
  if (!match) throw new TopoloSdkError("unknown_action", `Unknown or unavailable Topolo action "${action}".`);
3382
- return match;
3677
+ return this.validatedActionEntry(match);
3383
3678
  }
3384
3679
  async callAction(action, input = {}, options = {}) {
3385
3680
  const entry = await this.getAction(action);
@@ -3456,6 +3751,25 @@ var TopoloClient = class _TopoloClient {
3456
3751
  }
3457
3752
  async planAction(action, input = {}, options = {}) {
3458
3753
  const entry = await this.getAction(action);
3754
+ return this.planResolvedAction(entry, input, options);
3755
+ }
3756
+ async prepareAction(action, input, options = {}) {
3757
+ const entry = await this.getAction(action);
3758
+ 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;
3759
+ const selectedInput = input ?? example?.input ?? {};
3760
+ const plan = this.planResolvedAction(entry, selectedInput, options);
3761
+ return {
3762
+ action: entry,
3763
+ example,
3764
+ input: selectedInput,
3765
+ inputSource: input ? "provided" : example ? "example" : "empty",
3766
+ missingFields: [...new Set(
3767
+ plan.validation.errors.filter((issue) => issue.code === "required").map((issue) => issue.path)
3768
+ )],
3769
+ plan
3770
+ };
3771
+ }
3772
+ planResolvedAction(entry, input, options) {
3459
3773
  const validation = validateTopoloActionInput(entry.inputSchema, input);
3460
3774
  const agentAccess = entry.agentAccess ?? (entry.destructive ? "off" : entry.readOnly && !entry.requiresConfirmation ? "auto" : "confirm");
3461
3775
  const confirmationRequired = agentAccess === "confirm" || entry.requiresConfirmation || entry.destructive;
@@ -3913,7 +4227,16 @@ function pickString(value) {
3913
4227
  function extractRequired(body) {
3914
4228
  if (body && typeof body === "object") {
3915
4229
  const maybe = body;
3916
- if (Array.isArray(maybe.required)) return maybe.required.map(String);
4230
+ if (Array.isArray(maybe.required)) return canonicalTopoloRequired(maybe.required);
4231
+ for (const nested of [maybe.details, maybe.error]) {
4232
+ if (!nested || typeof nested !== "object" || Array.isArray(nested)) continue;
4233
+ const record = nested;
4234
+ if (Array.isArray(record.required)) return canonicalTopoloRequired(record.required);
4235
+ if (record.details && typeof record.details === "object" && !Array.isArray(record.details)) {
4236
+ const required = record.details.required;
4237
+ if (Array.isArray(required)) return canonicalTopoloRequired(required);
4238
+ }
4239
+ }
3917
4240
  }
3918
4241
  return [];
3919
4242
  }
@@ -3963,6 +4286,72 @@ function compactActionPlan(plan) {
3963
4286
  nextActionIds: plan.nextActionIds
3964
4287
  };
3965
4288
  }
4289
+ function requiredContextText(value, field) {
4290
+ if (typeof value !== "string" || !value.trim() || value.trim().toLowerCase() === "unknown") {
4291
+ throw new TopoloSdkError("invalid_identity_context", `Effective Topolo ${field} is incomplete.`);
4292
+ }
4293
+ return value.trim();
4294
+ }
4295
+ function createTopoloAgentExecutionContext(input) {
4296
+ if (input.contextKey !== null && input.contextKey !== "personal" && !/^org:[^:]+$/.test(input.contextKey)) {
4297
+ throw new TopoloSdkError("invalid_identity_context", "Effective Topolo context key is invalid.");
4298
+ }
4299
+ const user = Object.freeze({
4300
+ id: requiredContextText(input.identity.user.id, "user id"),
4301
+ email: requiredContextText(input.identity.user.email, "user email"),
4302
+ name: input.identity.user.name?.trim() || null,
4303
+ role: requiredContextText(input.identity.user.role, "user role")
4304
+ });
4305
+ const organization = input.identity.organization ? Object.freeze({
4306
+ id: requiredContextText(input.identity.organization.id, "organization id"),
4307
+ slug: requiredContextText(input.identity.organization.slug, "organization slug"),
4308
+ name: requiredContextText(input.identity.organization.name, "organization name")
4309
+ }) : null;
4310
+ if (input.contextKey === "personal" && organization) {
4311
+ throw new TopoloSdkError("identity_context_mismatch", "Personal Topolo context resolved an organization.");
4312
+ }
4313
+ if (input.contextKey?.startsWith("org:")) {
4314
+ const expectedOrganizationId = input.contextKey.slice("org:".length);
4315
+ if (!organization || organization.id !== expectedOrganizationId) {
4316
+ throw new TopoloSdkError("identity_context_mismatch", "Pinned Topolo organization does not match the effective identity.");
4317
+ }
4318
+ }
4319
+ return Object.freeze({
4320
+ environment: input.environment,
4321
+ contextKey: input.contextKey,
4322
+ user,
4323
+ organization,
4324
+ serviceOrigins: Object.freeze(topoloServiceOrigins(input.serviceUrls, input.environment))
4325
+ });
4326
+ }
4327
+ function compactActionPreparationCore(preparation) {
4328
+ const example = preparation.example ? { title: preparation.example.title ?? null, input: preparation.example.input } : null;
4329
+ const effectiveInput = preparation.inputSource === "example" ? { source: "example" } : { source: preparation.inputSource, value: preparation.input };
4330
+ return {
4331
+ action: {
4332
+ id: preparation.action.actionId,
4333
+ title: preparation.action.title,
4334
+ docsUrl: preparation.action.agentContract?.docsUrl ?? null
4335
+ },
4336
+ example,
4337
+ input: effectiveInput,
4338
+ missingFields: preparation.missingFields,
4339
+ valid: preparation.plan.validation.valid,
4340
+ errors: preparation.plan.validation.errors,
4341
+ request: preparation.plan.request,
4342
+ confirmation: {
4343
+ required: preparation.plan.confirmationRequired,
4344
+ provided: preparation.plan.confirmationProvided
4345
+ },
4346
+ executable: preparation.plan.executable,
4347
+ verification: preparation.plan.verification,
4348
+ rollbackActionId: preparation.plan.rollbackActionId
4349
+ };
4350
+ }
4351
+ function compactActionPreparation(preparation, context) {
4352
+ const compact = compactActionPreparationCore(preparation);
4353
+ return context ? { context, ...compact } : compact;
4354
+ }
3966
4355
  function compactApplicationAudit(report) {
3967
4356
  return {
3968
4357
  version: report.version,
@@ -4223,7 +4612,7 @@ function sampleString(schema, hint) {
4223
4612
  if (schema.format === "uri" || schema.format === "url") return "https://topolo.app";
4224
4613
  if (schema.format === "date-time") return patternedString(schema.pattern || "", hint) || "2026-01-15T12:00:00Z";
4225
4614
  const words = String(hint || "value").replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").toLowerCase();
4226
- let value = typeof schema.pattern === "string" ? patternedString(schema.pattern, hint) : /(^|\s)id$/.test(words) ? `${slugify(words.replace(/\sid$/, "") || "resource")}_01` : words.includes("date") ? "2026-01-15" : words.includes("period") || words.includes("month") ? "2026-01" : words.includes("year") ? "2026" : `Sample ${words}`;
4615
+ let value = typeof schema.pattern === "string" ? patternedString(schema.pattern, hint) : words === "workspace" ? "ws_example" : /(^|\s)id$/.test(words) ? `${slugify(words.replace(/\sid$/, "") || "resource")}_01` : words.includes("date") ? "2026-01-15" : words.includes("period") || words.includes("month") ? "2026-01" : words.includes("year") ? "2026" : `Sample ${words}`;
4227
4616
  const minimum = Number.isInteger(schema.minLength) ? schema.minLength : 0;
4228
4617
  const maximum = Number.isInteger(schema.maxLength) ? schema.maxLength : null;
4229
4618
  if (value.length < minimum) value = value.padEnd(minimum, "x");
@@ -4982,6 +5371,8 @@ export {
4982
5371
  DEFAULT_APP_URLS,
4983
5372
  IdentityModule,
4984
5373
  P2PModule,
5374
+ TOPOLO_ENVIRONMENT_SERVICE_URLS,
5375
+ TOPOLO_RUNTIME_ENVIRONMENTS,
4985
5376
  TOPOLO_WIDGET_ENDPOINT,
4986
5377
  TOPOLO_WIDGET_REFRESH_INTERVAL_MS,
4987
5378
  TopoloAuthError,
@@ -4995,14 +5386,21 @@ export {
4995
5386
  actionContractCompleteness,
4996
5387
  applicationEntryFromCatalog,
4997
5388
  applicationRequirementScopes,
5389
+ assertTopoloServiceUrlEnvironment,
4998
5390
  auditAllApplicationRequirements,
4999
5391
  auditApplicationEntries,
5000
5392
  auditApplicationRequirements,
5393
+ canonicalTopoloErrorCode,
5394
+ canonicalTopoloRequestId,
5395
+ canonicalTopoloRequired,
5396
+ canonicalTopoloRetryAfter,
5001
5397
  compactActionCatalogEntry,
5002
5398
  compactActionPlan,
5399
+ compactActionPreparation,
5003
5400
  compactActionValidation,
5004
5401
  compactApplicationAudit,
5005
5402
  createTopolo,
5403
+ createTopoloAgentExecutionContext,
5006
5404
  createTopoloWidgetError,
5007
5405
  createTopoloWidgetResponse,
5008
5406
  defineTopoloChartWidget,
@@ -5012,17 +5410,22 @@ export {
5012
5410
  defineTopoloStatsWidget,
5013
5411
  digestTopoloActionCatalog,
5014
5412
  enrichActionContract,
5413
+ extractTopoloServiceErrorCode,
5414
+ extractTopoloServiceErrorDetails,
5015
5415
  formatDirectoryApplicationIds,
5016
5416
  friendlyAppId,
5017
5417
  listApplicationDirectory,
5018
5418
  parseTopoloActionUploadContract,
5019
5419
  prepareTopoloActionBase64UploadInput,
5020
5420
  prepareTopoloActionUploadInput,
5421
+ publicTopoloErrorMessage,
5021
5422
  requirementsForApplication,
5022
5423
  resolveApplicationDirectoryEntry,
5023
5424
  resolveCatalogServiceUrl,
5024
5425
  resolveServiceUrl,
5025
5426
  staticApplicationDirectory,
5427
+ topoloErrorHint,
5428
+ topoloServiceOrigins,
5026
5429
  validateTopoloActionInput,
5027
5430
  validateTopoloWidgetResponse
5028
5431
  };