@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.cjs CHANGED
@@ -28,6 +28,8 @@ __export(index_exports, {
28
28
  DEFAULT_APP_URLS: () => DEFAULT_APP_URLS,
29
29
  IdentityModule: () => IdentityModule,
30
30
  P2PModule: () => P2PModule,
31
+ TOPOLO_ENVIRONMENT_SERVICE_URLS: () => TOPOLO_ENVIRONMENT_SERVICE_URLS,
32
+ TOPOLO_RUNTIME_ENVIRONMENTS: () => TOPOLO_RUNTIME_ENVIRONMENTS,
31
33
  TOPOLO_WIDGET_ENDPOINT: () => TOPOLO_WIDGET_ENDPOINT,
32
34
  TOPOLO_WIDGET_REFRESH_INTERVAL_MS: () => TOPOLO_WIDGET_REFRESH_INTERVAL_MS,
33
35
  TopoloAuthError: () => TopoloAuthError,
@@ -41,14 +43,21 @@ __export(index_exports, {
41
43
  actionContractCompleteness: () => actionContractCompleteness,
42
44
  applicationEntryFromCatalog: () => applicationEntryFromCatalog,
43
45
  applicationRequirementScopes: () => applicationRequirementScopes,
46
+ assertTopoloServiceUrlEnvironment: () => assertTopoloServiceUrlEnvironment,
44
47
  auditAllApplicationRequirements: () => auditAllApplicationRequirements,
45
48
  auditApplicationEntries: () => auditApplicationEntries,
46
49
  auditApplicationRequirements: () => auditApplicationRequirements,
50
+ canonicalTopoloErrorCode: () => canonicalTopoloErrorCode,
51
+ canonicalTopoloRequestId: () => canonicalTopoloRequestId,
52
+ canonicalTopoloRequired: () => canonicalTopoloRequired,
53
+ canonicalTopoloRetryAfter: () => canonicalTopoloRetryAfter,
47
54
  compactActionCatalogEntry: () => compactActionCatalogEntry,
48
55
  compactActionPlan: () => compactActionPlan,
56
+ compactActionPreparation: () => compactActionPreparation,
49
57
  compactActionValidation: () => compactActionValidation,
50
58
  compactApplicationAudit: () => compactApplicationAudit,
51
59
  createTopolo: () => createTopolo,
60
+ createTopoloAgentExecutionContext: () => createTopoloAgentExecutionContext,
52
61
  createTopoloWidgetError: () => createTopoloWidgetError,
53
62
  createTopoloWidgetResponse: () => createTopoloWidgetResponse,
54
63
  defineTopoloChartWidget: () => defineTopoloChartWidget,
@@ -58,17 +67,22 @@ __export(index_exports, {
58
67
  defineTopoloStatsWidget: () => defineTopoloStatsWidget,
59
68
  digestTopoloActionCatalog: () => digestTopoloActionCatalog,
60
69
  enrichActionContract: () => enrichActionContract,
70
+ extractTopoloServiceErrorCode: () => extractTopoloServiceErrorCode,
71
+ extractTopoloServiceErrorDetails: () => extractTopoloServiceErrorDetails,
61
72
  formatDirectoryApplicationIds: () => formatDirectoryApplicationIds,
62
73
  friendlyAppId: () => friendlyAppId,
63
74
  listApplicationDirectory: () => listApplicationDirectory,
64
75
  parseTopoloActionUploadContract: () => parseTopoloActionUploadContract,
65
76
  prepareTopoloActionBase64UploadInput: () => prepareTopoloActionBase64UploadInput,
66
77
  prepareTopoloActionUploadInput: () => prepareTopoloActionUploadInput,
78
+ publicTopoloErrorMessage: () => publicTopoloErrorMessage,
67
79
  requirementsForApplication: () => requirementsForApplication,
68
80
  resolveApplicationDirectoryEntry: () => resolveApplicationDirectoryEntry,
69
81
  resolveCatalogServiceUrl: () => resolveCatalogServiceUrl,
70
82
  resolveServiceUrl: () => resolveServiceUrl,
71
83
  staticApplicationDirectory: () => staticApplicationDirectory,
84
+ topoloErrorHint: () => topoloErrorHint,
85
+ topoloServiceOrigins: () => topoloServiceOrigins,
72
86
  validateTopoloActionInput: () => validateTopoloActionInput,
73
87
  validateTopoloWidgetResponse: () => validateTopoloWidgetResponse
74
88
  });
@@ -103,6 +117,90 @@ var TopoloSdkError = class extends Error {
103
117
  this.code = code;
104
118
  }
105
119
  };
120
+ var PUBLIC_ERROR_CODES = /* @__PURE__ */ new Set([
121
+ "action_app_identity_unavailable",
122
+ "action_authentication_required",
123
+ "action_catalog_summary_unavailable",
124
+ "action_catalog_unavailable",
125
+ "action_contract_mismatch",
126
+ "action_permission_required",
127
+ "action_runtime_authorization_required",
128
+ "action_service_not_allowed",
129
+ "auth_error",
130
+ "authentication_required",
131
+ "authorization_pending",
132
+ "bad_sig",
133
+ "confirmation_required",
134
+ "dependency_unavailable",
135
+ "environment_mismatch",
136
+ "environment_service_url_required",
137
+ "expired_token",
138
+ "forbidden",
139
+ "http_error",
140
+ "immutable_drift",
141
+ "integration_dependency_unavailable",
142
+ "integration_misconfigured",
143
+ "invalid_action",
144
+ "invalid_action_input",
145
+ "invalid_grant",
146
+ "invalid_input",
147
+ "invalid_token",
148
+ "mcp_error",
149
+ "membership_required",
150
+ "network_error",
151
+ "organization_context_required",
152
+ "organization_enrollment_required",
153
+ "permission_denied",
154
+ "rate_limited",
155
+ "request_failed",
156
+ "service_catalog_unavailable",
157
+ "service_path_origin_mismatch",
158
+ "service_required",
159
+ "slow_down",
160
+ "state_conflict",
161
+ "unauthorized",
162
+ "unknown_action",
163
+ "unknown_service"
164
+ ]);
165
+ var ERROR_CODE_PATTERN = /^[a-z][a-z0-9_]{0,63}$/;
166
+ 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;
167
+ var CLOUDFLARE_RAY_ID_PATTERN = /^[0-9a-f]{16}-[A-Z]{3}$/;
168
+ var PERMISSION_PATTERN = /^[a-z][a-z0-9_-]{0,63}(?:\.[a-z][a-z0-9_-]{0,63})*:(?:[a-z][a-z0-9_-]{0,63}|\*)$/;
169
+ 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$/;
170
+ var PUBLIC_DETAIL_SOURCES = /* @__PURE__ */ new Set([
171
+ "api_key_introspection",
172
+ "api_key_validation",
173
+ "auth_context",
174
+ "auth_validate",
175
+ "catalog",
176
+ "credential",
177
+ "runtime",
178
+ "service_token_provider",
179
+ "service_verify_key",
180
+ "token_validation",
181
+ "upstream"
182
+ ]);
183
+ function canonicalTopoloErrorCode(value) {
184
+ if (typeof value !== "string") return null;
185
+ const candidate = value.trim().toLowerCase();
186
+ if (!ERROR_CODE_PATTERN.test(candidate) || !PUBLIC_ERROR_CODES.has(candidate)) return null;
187
+ return candidate;
188
+ }
189
+ function canonicalTopoloRequestId(value) {
190
+ if (typeof value !== "string") return void 0;
191
+ const candidate = value.trim();
192
+ return TOPOLO_REQUEST_ID_PATTERN.test(candidate) || CLOUDFLARE_RAY_ID_PATTERN.test(candidate) ? candidate : void 0;
193
+ }
194
+ function canonicalTopoloRetryAfter(value) {
195
+ if (typeof value !== "string") return void 0;
196
+ const candidate = value.trim();
197
+ if (/^\d{1,10}$/.test(candidate) || RETRY_AFTER_DATE_PATTERN.test(candidate)) return candidate;
198
+ return void 0;
199
+ }
200
+ function canonicalTopoloRequired(value) {
201
+ if (!Array.isArray(value)) return [];
202
+ return [...new Set(value.filter((item) => typeof item === "string" && PERMISSION_PATTERN.test(item)))].slice(0, 32);
203
+ }
106
204
  var TopoloAuthError = class extends TopoloSdkError {
107
205
  status;
108
206
  body;
@@ -110,15 +208,19 @@ var TopoloAuthError = class extends TopoloSdkError {
110
208
  path;
111
209
  requestId;
112
210
  retryAfter;
211
+ details;
113
212
  constructor(message, code = "auth_error", options = {}) {
114
- super(code, message);
213
+ super(canonicalTopoloErrorCode(code) ?? "auth_error", message);
115
214
  this.name = "TopoloAuthError";
116
215
  this.status = options.status ?? 401;
117
216
  this.body = options.body;
118
217
  this.service = options.service;
119
218
  this.path = options.path;
120
- this.requestId = options.requestId;
121
- this.retryAfter = options.retryAfter;
219
+ this.requestId = canonicalTopoloRequestId(options.requestId);
220
+ this.retryAfter = canonicalTopoloRetryAfter(options.retryAfter);
221
+ this.details = extractTopoloServiceErrorDetails(
222
+ options.details === void 0 ? options.body : { details: options.details }
223
+ );
122
224
  }
123
225
  };
124
226
  var TopoloPermissionError = class extends TopoloSdkError {
@@ -129,28 +231,140 @@ var TopoloPermissionError = class extends TopoloSdkError {
129
231
  path;
130
232
  requestId;
131
233
  retryAfter;
234
+ details;
132
235
  constructor(message, required, options = {}) {
133
- super("permission_denied", message);
236
+ super(
237
+ canonicalTopoloErrorCode(options.code) ?? serviceErrorCode(options.body, "permission_denied"),
238
+ message
239
+ );
134
240
  this.name = "TopoloPermissionError";
135
- this.required = required;
241
+ this.required = canonicalTopoloRequired(required);
136
242
  this.status = options.status ?? 403;
137
243
  this.body = options.body;
138
244
  this.service = options.service;
139
245
  this.path = options.path;
140
- this.requestId = options.requestId;
141
- this.retryAfter = options.retryAfter;
246
+ this.requestId = canonicalTopoloRequestId(options.requestId);
247
+ this.retryAfter = canonicalTopoloRetryAfter(options.retryAfter);
248
+ this.details = extractTopoloServiceErrorDetails(
249
+ options.details === void 0 ? options.body : { details: options.details }
250
+ );
142
251
  }
143
252
  };
144
- function extractHttpErrorCode(body) {
145
- if (!body || typeof body !== "object" || Array.isArray(body)) return "http_error";
253
+ function extractTopoloServiceErrorCode(body) {
254
+ if (!body || typeof body !== "object" || Array.isArray(body)) return null;
146
255
  const record = body;
147
- if (typeof record.code === "string" && record.code.trim()) return record.code.trim();
256
+ const topLevel = canonicalTopoloErrorCode(record.code);
257
+ if (topLevel) return topLevel;
148
258
  const nested = record.error;
259
+ const nestedString = canonicalTopoloErrorCode(nested);
260
+ if (nestedString) return nestedString;
149
261
  if (nested && typeof nested === "object" && !Array.isArray(nested)) {
150
262
  const code = nested.code;
151
- if (typeof code === "string" && code.trim()) return code.trim();
263
+ const nestedCode = canonicalTopoloErrorCode(code);
264
+ if (nestedCode) return nestedCode;
265
+ }
266
+ return null;
267
+ }
268
+ function serviceErrorCode(body, fallback) {
269
+ return extractTopoloServiceErrorCode(body) ?? fallback;
270
+ }
271
+ function extractTopoloServiceErrorDetails(body) {
272
+ if (!body || typeof body !== "object" || Array.isArray(body)) return void 0;
273
+ const record = body;
274
+ let candidate = record.details;
275
+ const nested = record.error;
276
+ if (nested && typeof nested === "object" && !Array.isArray(nested)) {
277
+ const nestedRecord = nested;
278
+ if (candidate === void 0) candidate = nestedRecord.details ?? nestedRecord;
279
+ }
280
+ if (candidate === void 0) candidate = record;
281
+ if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) return void 0;
282
+ const source = candidate;
283
+ const safe = {};
284
+ for (const [key, value] of Object.entries(source)) {
285
+ if (/present$/i.test(key) && typeof value === "boolean") {
286
+ safe[key] = value;
287
+ continue;
288
+ }
289
+ if (key === "retryable" && typeof value === "boolean") {
290
+ safe.retryable = value;
291
+ continue;
292
+ }
293
+ if (key === "code") {
294
+ const sanitized = canonicalTopoloErrorCode(value);
295
+ if (sanitized) safe.code = sanitized;
296
+ continue;
297
+ }
298
+ if (key === "source") {
299
+ const sanitized = typeof value === "string" ? value.trim().toLowerCase() : "";
300
+ if (PUBLIC_DETAIL_SOURCES.has(sanitized)) safe.source = sanitized;
301
+ continue;
302
+ }
303
+ if (key === "dependency" || key === "platform") {
304
+ safe[`${key}Present`] = value !== null && value !== void 0;
305
+ continue;
306
+ }
307
+ if (key === "error" && !safe.code) {
308
+ const sanitized = canonicalTopoloErrorCode(value);
309
+ if (sanitized) safe.code = sanitized;
310
+ continue;
311
+ }
312
+ if (key === "required" && Array.isArray(value)) {
313
+ const required = canonicalTopoloRequired(value);
314
+ if (required.length) safe.required = required;
315
+ continue;
316
+ }
317
+ if (["missingFields", "fields", "paths"].includes(key) && Array.isArray(value)) {
318
+ const paths = value.filter((item) => typeof item === "string" && /^[$.a-z0-9_[\]-]{1,160}$/i.test(item));
319
+ if (paths.length) safe[key] = paths;
320
+ }
321
+ }
322
+ return Object.keys(safe).length ? safe : void 0;
323
+ }
324
+ function publicTopoloErrorMessage(error) {
325
+ const code = canonicalTopoloErrorCode(
326
+ typeof error === "string" ? error : error.code ?? extractTopoloServiceErrorCode(error.body)
327
+ ) ?? "request_failed";
328
+ if (/(?:organization|org).*(?:enroll|membership|member|admission)/.test(code)) {
329
+ return "Organization or application enrollment is required.";
330
+ }
331
+ if (/(?:organization|org).*(?:context|required|missing|invalid|unknown|not_found)/.test(code)) {
332
+ return "Organization context is unavailable.";
333
+ }
334
+ if (["auth_error", "authentication_required", "unauthorized", "invalid_token", "expired_token"].includes(code)) {
335
+ return "Authentication failed.";
336
+ }
337
+ if (code === "permission_denied" || code === "forbidden") return "Permission denied.";
338
+ if (code === "confirmation_required") return "Confirmation is required.";
339
+ return `Topolo request failed (${code}).`;
340
+ }
341
+ function topoloErrorHint(error) {
342
+ const code = canonicalTopoloErrorCode(
343
+ typeof error === "string" ? error : error.code ?? extractTopoloServiceErrorCode(error.body)
344
+ );
345
+ if (!code) return void 0;
346
+ if (code === "auth_error" || code === "authentication_required" || code === "unauthorized" || code === "invalid_token" || code === "expired_token") {
347
+ return "Run `topolo auth login`, then verify the effective Topolo identity and organization.";
348
+ }
349
+ if (code === "confirmation_required") {
350
+ return "Review the unchanged action plan, obtain approval, and retry once with --confirm or confirm:true.";
351
+ }
352
+ if (code === "enrollment_required" || code === "membership_required" || /(?:organization|org).*(?:enroll|membership|member|admission)|(?:enroll|membership).*(?:organization|org)/.test(code)) {
353
+ return "The current identity is not enrolled for this organization or application. Use an enrolled identity or ask an organization administrator to complete enrollment.";
152
354
  }
153
- return "http_error";
355
+ if (/(?:organization|org).*(?:context|required|missing|invalid|unknown|not_found)/.test(code)) {
356
+ return "Select a signed-in organization context, then verify the effective identity and organization before retrying.";
357
+ }
358
+ if (code === "permission_denied" || code === "forbidden") {
359
+ return "The current identity or resource context is not authorized. Review the structured details and published authorization contract.";
360
+ }
361
+ if (code === "environment_mismatch") {
362
+ return "Use service origins and credentials from one explicit Topolo environment only.";
363
+ }
364
+ if (code === "invalid_action_input" || code === "invalid_input") {
365
+ return "Correct only the reported schema paths, then prepare the action again.";
366
+ }
367
+ return void 0;
154
368
  }
155
369
  var TopoloConfirmationRequiredError = class extends TopoloSdkError {
156
370
  constructor(message) {
@@ -165,18 +379,86 @@ var TopoloHttpError = class extends TopoloSdkError {
165
379
  path;
166
380
  requestId;
167
381
  retryAfter;
382
+ details;
168
383
  constructor(service, path, status, body, message, options = {}) {
169
- super(extractHttpErrorCode(body), message ?? `HTTP ${status} from ${service}${path}`);
384
+ super(serviceErrorCode(body, "http_error"), message ?? `HTTP ${status} from ${service}${path}`);
170
385
  this.name = "TopoloHttpError";
171
386
  this.status = status;
172
387
  this.body = body;
173
388
  this.service = service;
174
389
  this.path = path;
175
- this.requestId = options.requestId;
176
- this.retryAfter = options.retryAfter;
390
+ this.requestId = canonicalTopoloRequestId(options.requestId);
391
+ this.retryAfter = canonicalTopoloRetryAfter(options.retryAfter);
392
+ this.details = extractTopoloServiceErrorDetails(
393
+ options.details === void 0 ? body : { details: options.details }
394
+ );
177
395
  }
178
396
  };
179
397
 
398
+ // src/environment.ts
399
+ var TOPOLO_RUNTIME_ENVIRONMENTS = ["production", "staging", "development", "local"];
400
+ var TOPOLO_ENVIRONMENT_SERVICE_URLS = {
401
+ production: {
402
+ auth: "https://auth.topolo.app",
403
+ developers: "https://developers.topolo.app"
404
+ },
405
+ staging: {
406
+ auth: "https://auth.stg.topolo.us",
407
+ developers: "https://developers.stg.topolo.us",
408
+ seed: "https://seed.stg.topolo.us"
409
+ },
410
+ development: {
411
+ auth: "https://auth.topolo.dev",
412
+ developers: "https://developers.topolo.dev"
413
+ },
414
+ local: {}
415
+ };
416
+ function hostnameHasSuffix(hostname, suffix) {
417
+ return hostname === suffix || hostname.endsWith(`.${suffix}`);
418
+ }
419
+ function workersDevAccountMatches(hostname, account) {
420
+ if (!hostname.endsWith(".workers.dev")) return false;
421
+ const labels = hostname.slice(0, -".workers.dev".length).split(".");
422
+ return labels.length >= 2 && labels.at(-1) === account;
423
+ }
424
+ function originMatchesEnvironment(url, environment) {
425
+ const hostname = url.hostname.toLowerCase();
426
+ if (environment === "local") {
427
+ return hostname === "localhost" || hostname.endsWith(".localhost") || hostname === "127.0.0.1" || hostname === "[::1]";
428
+ }
429
+ if (url.protocol !== "https:") return false;
430
+ if (environment === "production") {
431
+ return hostnameHasSuffix(hostname, "topolo.app") || hostnameHasSuffix(hostname, "topolo.io") || workersDevAccountMatches(hostname, "topolo");
432
+ }
433
+ if (environment === "staging") {
434
+ return hostnameHasSuffix(hostname, "stg.topolo.us") || workersDevAccountMatches(hostname, "topolo-staging");
435
+ }
436
+ return hostnameHasSuffix(hostname, "topolo.dev") || workersDevAccountMatches(hostname, "topolo-development");
437
+ }
438
+ function assertTopoloServiceUrlEnvironment(service, value, environment) {
439
+ let url;
440
+ try {
441
+ url = new URL(value);
442
+ } catch {
443
+ throw new TopoloSdkError("invalid_service_url", `Invalid URL configured for Topolo service "${service}".`);
444
+ }
445
+ if (url.protocol !== "https:" && url.protocol !== "http:" || url.username || url.password || !originMatchesEnvironment(url, environment)) {
446
+ throw new TopoloSdkError(
447
+ "environment_mismatch",
448
+ `Refusing Topolo service "${service}": ${url.origin} does not belong to ${environment}.`
449
+ );
450
+ }
451
+ return url.origin;
452
+ }
453
+ function topoloServiceOrigins(serviceUrls, environment) {
454
+ return Object.fromEntries(
455
+ Object.entries(serviceUrls ?? {}).sort(([left], [right]) => left.localeCompare(right)).map(([service, value]) => [
456
+ service,
457
+ assertTopoloServiceUrlEnvironment(service, value, environment)
458
+ ])
459
+ );
460
+ }
461
+
180
462
  // src/bootstrap-services.ts
181
463
  var APP_API_URLS = {
182
464
  auth: "https://auth.topolo.app",
@@ -2945,6 +3227,7 @@ var TopoloClient = class _TopoloClient {
2945
3227
  credential;
2946
3228
  agent;
2947
3229
  serviceUrls;
3230
+ environment;
2948
3231
  appIds;
2949
3232
  requireConfirmForWrites;
2950
3233
  timeoutMs;
@@ -2973,7 +3256,8 @@ var TopoloClient = class _TopoloClient {
2973
3256
  if (!options.agent?.clientName) throw new TopoloAuthError("agent.clientName is required");
2974
3257
  this.credential = options.credential;
2975
3258
  this.agent = options.agent;
2976
- this.serviceUrls = options.serviceUrls;
3259
+ this.environment = options.environment;
3260
+ this.serviceUrls = options.environment ? topoloServiceOrigins(options.serviceUrls, options.environment) : options.serviceUrls;
2977
3261
  this.appIds = options.appIds;
2978
3262
  this.requireConfirmForWrites = options.requireConfirmForWrites !== false;
2979
3263
  this.timeoutMs = options.timeoutMs ?? 3e4;
@@ -3002,9 +3286,15 @@ var TopoloClient = class _TopoloClient {
3002
3286
  );
3003
3287
  }
3004
3288
  await this.loadCurrentCredential();
3005
- const resolvedService = opts.baseUrl ? { serviceKey: normalizeLookupKey(opts.service), baseUrl: requireHttpOrigin(opts.baseUrl) } : await this.resolveRequestService(opts.service);
3289
+ const resolvedService = opts.baseUrl ? { serviceKey: normalizeLookupKey(opts.service), baseUrl: this.validatedServiceOrigin(opts.service, opts.baseUrl) } : await this.resolveRequestService(opts.service);
3006
3290
  const baseUrl = resolvedService.baseUrl;
3007
3291
  const url = new URL(opts.path, baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`);
3292
+ if (url.origin !== new URL(baseUrl).origin) {
3293
+ throw new TopoloSdkError(
3294
+ "service_path_origin_mismatch",
3295
+ `Refusing Topolo request path because it changes the resolved ${resolvedService.serviceKey || opts.service} service origin.`
3296
+ );
3297
+ }
3008
3298
  if (opts.query) {
3009
3299
  for (const [k, v] of Object.entries(opts.query)) {
3010
3300
  if (v !== void 0) url.searchParams.set(k, String(v));
@@ -3075,7 +3365,7 @@ var TopoloClient = class _TopoloClient {
3075
3365
  url: urlStr,
3076
3366
  requestId,
3077
3367
  durationMs: Date.now() - startedAt,
3078
- error: err instanceof Error ? err.message : String(err)
3368
+ error: "network_error"
3079
3369
  });
3080
3370
  throw err;
3081
3371
  }
@@ -3084,6 +3374,7 @@ var TopoloClient = class _TopoloClient {
3084
3374
  if (!res.ok) {
3085
3375
  const durationMs = Date.now() - startedAt;
3086
3376
  const message = describeError(parsed, `HTTP ${res.status}`);
3377
+ const publicCode = extractTopoloServiceErrorCode(parsed) ?? "http_error";
3087
3378
  this.emit({
3088
3379
  phase: "error",
3089
3380
  method,
@@ -3092,7 +3383,7 @@ var TopoloClient = class _TopoloClient {
3092
3383
  url: urlStr,
3093
3384
  requestId,
3094
3385
  durationMs,
3095
- error: message,
3386
+ error: publicCode,
3096
3387
  status: res.status
3097
3388
  });
3098
3389
  const errorContext = {
@@ -3100,17 +3391,21 @@ var TopoloClient = class _TopoloClient {
3100
3391
  body: parsed,
3101
3392
  service: resolvedService.serviceKey,
3102
3393
  path: opts.path,
3103
- requestId: res.headers.get("X-Topolo-Request-Id") || requestId,
3104
- retryAfter: res.headers.get("Retry-After") || void 0
3394
+ requestId: canonicalTopoloRequestId(res.headers.get("X-Topolo-Request-Id")) ?? requestId,
3395
+ retryAfter: canonicalTopoloRetryAfter(res.headers.get("Retry-After"))
3105
3396
  };
3106
3397
  if (res.status === 401) {
3107
- throw new TopoloAuthError(describeError(parsed, "Unauthorized"), "auth_error", errorContext);
3398
+ throw new TopoloAuthError(
3399
+ describeError(parsed, "Unauthorized"),
3400
+ extractTopoloServiceErrorCode(parsed) ?? "auth_error",
3401
+ errorContext
3402
+ );
3108
3403
  }
3109
3404
  if (res.status === 403) {
3110
3405
  throw new TopoloPermissionError(
3111
3406
  describeError(parsed, "Permission denied"),
3112
3407
  extractRequired(parsed),
3113
- errorContext
3408
+ { ...errorContext, code: extractTopoloServiceErrorCode(parsed) ?? "permission_denied" }
3114
3409
  );
3115
3410
  }
3116
3411
  throw new TopoloHttpError(
@@ -3160,6 +3455,17 @@ var TopoloClient = class _TopoloClient {
3160
3455
  });
3161
3456
  return this.refreshInFlight;
3162
3457
  }
3458
+ validatedServiceOrigin(service, value) {
3459
+ const origin = requireHttpOrigin(value);
3460
+ return this.environment ? assertTopoloServiceUrlEnvironment(service, origin, this.environment) : origin;
3461
+ }
3462
+ validatedActionEntry(entry) {
3463
+ if (!entry.apiBaseUrl) return entry;
3464
+ return {
3465
+ ...entry,
3466
+ apiBaseUrl: this.validatedServiceOrigin(entry.appSlug || entry.appId, entry.apiBaseUrl)
3467
+ };
3468
+ }
3163
3469
  async resolveRequestService(service) {
3164
3470
  const serviceKey = normalizeLookupKey(service);
3165
3471
  if (!serviceKey) {
@@ -3167,17 +3473,20 @@ var TopoloClient = class _TopoloClient {
3167
3473
  }
3168
3474
  if (serviceKey === "auth" || serviceKey === "developers") {
3169
3475
  const overrideUrl = resolveConfiguredServiceUrl(serviceKey, this.serviceUrls);
3170
- if (overrideUrl) return { serviceKey, baseUrl: overrideUrl };
3476
+ if (overrideUrl) return { serviceKey, baseUrl: this.validatedServiceOrigin(serviceKey, overrideUrl) };
3171
3477
  if (serviceKey === "developers") {
3172
3478
  const derived = deriveDevelopersFromAuth(resolveConfiguredServiceUrl("auth", this.serviceUrls));
3173
- if (derived) return { serviceKey, baseUrl: derived };
3479
+ if (derived) return { serviceKey, baseUrl: this.validatedServiceOrigin(serviceKey, derived) };
3174
3480
  }
3175
- return { serviceKey, baseUrl: DEFAULT_APP_URLS[serviceKey] };
3481
+ return {
3482
+ serviceKey,
3483
+ baseUrl: this.validatedServiceOrigin(serviceKey, DEFAULT_APP_URLS[serviceKey])
3484
+ };
3176
3485
  }
3177
3486
  const live = await this.resolveLiveCallableService(serviceKey);
3178
3487
  if (live) {
3179
3488
  const overrideUrl = resolveConfiguredServiceUrl(serviceKey, this.serviceUrls);
3180
- return overrideUrl ? { ...live, baseUrl: overrideUrl } : live;
3489
+ return overrideUrl ? { ...live, baseUrl: this.validatedServiceOrigin(serviceKey, overrideUrl) } : { ...live, baseUrl: this.validatedServiceOrigin(serviceKey, live.baseUrl) };
3181
3490
  }
3182
3491
  throw new TopoloSdkError(
3183
3492
  "unknown_service",
@@ -3191,10 +3500,10 @@ var TopoloClient = class _TopoloClient {
3191
3500
  serviceKey,
3192
3501
  overrides: this.serviceUrls
3193
3502
  });
3194
- if (baseUrl) return { serviceKey, baseUrl };
3503
+ if (baseUrl) return { serviceKey, baseUrl: this.validatedServiceOrigin(serviceKey, baseUrl) };
3195
3504
  const actionPage = await this.searchActions({ service: match.appId, limit: 1 }).catch(() => null);
3196
3505
  const actionBaseUrl = actionPage?.actions.find((action) => action.apiBaseUrl)?.apiBaseUrl ?? null;
3197
- return actionBaseUrl ? { serviceKey, baseUrl: requireHttpOrigin(actionBaseUrl) } : null;
3506
+ return actionBaseUrl ? { serviceKey, baseUrl: this.validatedServiceOrigin(serviceKey, actionBaseUrl) } : null;
3198
3507
  }
3199
3508
  /**
3200
3509
  * Resolve a service-slug → opaque app-id via caller config or TopoloAuth.
@@ -3214,7 +3523,7 @@ var TopoloClient = class _TopoloClient {
3214
3523
  if (!normalized) return null;
3215
3524
  const configuredAppId = resolveConfiguredAppId(normalized, this.appIds);
3216
3525
  if (configuredAppId) return configuredAppId;
3217
- const authBase = resolveServiceUrl("developers", this.serviceUrls);
3526
+ const authBase = this.validatedServiceOrigin("developers", resolveServiceUrl("developers", this.serviceUrls));
3218
3527
  const cacheKey = `${authBase}|${normalized}`;
3219
3528
  const now = Date.now();
3220
3529
  const cached = _TopoloClient.appIdCache.get(cacheKey);
@@ -3390,7 +3699,7 @@ var TopoloClient = class _TopoloClient {
3390
3699
  "TopoloAuth did not return an action catalog for the current credential."
3391
3700
  );
3392
3701
  }
3393
- const entries = raw.map((item) => normalizeActionCatalogEntry(item)).filter((entry) => Boolean(entry));
3702
+ const entries = raw.map((item) => normalizeActionCatalogEntry(item)).filter((entry) => Boolean(entry)).map((entry) => this.validatedActionEntry(entry));
3394
3703
  return {
3395
3704
  actions: entries,
3396
3705
  page: normalizeCatalogPage(
@@ -3455,7 +3764,7 @@ var TopoloClient = class _TopoloClient {
3455
3764
  const raw = res.data?.action ?? res.action;
3456
3765
  const match = normalizeActionCatalogEntry(raw);
3457
3766
  if (!match) throw new TopoloSdkError("unknown_action", `Unknown or unavailable Topolo action "${action}".`);
3458
- return match;
3767
+ return this.validatedActionEntry(match);
3459
3768
  }
3460
3769
  async callAction(action, input = {}, options = {}) {
3461
3770
  const entry = await this.getAction(action);
@@ -3532,6 +3841,25 @@ var TopoloClient = class _TopoloClient {
3532
3841
  }
3533
3842
  async planAction(action, input = {}, options = {}) {
3534
3843
  const entry = await this.getAction(action);
3844
+ return this.planResolvedAction(entry, input, options);
3845
+ }
3846
+ async prepareAction(action, input, options = {}) {
3847
+ const entry = await this.getAction(action);
3848
+ 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;
3849
+ const selectedInput = input ?? example?.input ?? {};
3850
+ const plan = this.planResolvedAction(entry, selectedInput, options);
3851
+ return {
3852
+ action: entry,
3853
+ example,
3854
+ input: selectedInput,
3855
+ inputSource: input ? "provided" : example ? "example" : "empty",
3856
+ missingFields: [...new Set(
3857
+ plan.validation.errors.filter((issue) => issue.code === "required").map((issue) => issue.path)
3858
+ )],
3859
+ plan
3860
+ };
3861
+ }
3862
+ planResolvedAction(entry, input, options) {
3535
3863
  const validation = validateTopoloActionInput(entry.inputSchema, input);
3536
3864
  const agentAccess = entry.agentAccess ?? (entry.destructive ? "off" : entry.readOnly && !entry.requiresConfirmation ? "auto" : "confirm");
3537
3865
  const confirmationRequired = agentAccess === "confirm" || entry.requiresConfirmation || entry.destructive;
@@ -3989,7 +4317,16 @@ function pickString(value) {
3989
4317
  function extractRequired(body) {
3990
4318
  if (body && typeof body === "object") {
3991
4319
  const maybe = body;
3992
- if (Array.isArray(maybe.required)) return maybe.required.map(String);
4320
+ if (Array.isArray(maybe.required)) return canonicalTopoloRequired(maybe.required);
4321
+ for (const nested of [maybe.details, maybe.error]) {
4322
+ if (!nested || typeof nested !== "object" || Array.isArray(nested)) continue;
4323
+ const record = nested;
4324
+ if (Array.isArray(record.required)) return canonicalTopoloRequired(record.required);
4325
+ if (record.details && typeof record.details === "object" && !Array.isArray(record.details)) {
4326
+ const required = record.details.required;
4327
+ if (Array.isArray(required)) return canonicalTopoloRequired(required);
4328
+ }
4329
+ }
3993
4330
  }
3994
4331
  return [];
3995
4332
  }
@@ -4039,6 +4376,72 @@ function compactActionPlan(plan) {
4039
4376
  nextActionIds: plan.nextActionIds
4040
4377
  };
4041
4378
  }
4379
+ function requiredContextText(value, field) {
4380
+ if (typeof value !== "string" || !value.trim() || value.trim().toLowerCase() === "unknown") {
4381
+ throw new TopoloSdkError("invalid_identity_context", `Effective Topolo ${field} is incomplete.`);
4382
+ }
4383
+ return value.trim();
4384
+ }
4385
+ function createTopoloAgentExecutionContext(input) {
4386
+ if (input.contextKey !== null && input.contextKey !== "personal" && !/^org:[^:]+$/.test(input.contextKey)) {
4387
+ throw new TopoloSdkError("invalid_identity_context", "Effective Topolo context key is invalid.");
4388
+ }
4389
+ const user = Object.freeze({
4390
+ id: requiredContextText(input.identity.user.id, "user id"),
4391
+ email: requiredContextText(input.identity.user.email, "user email"),
4392
+ name: input.identity.user.name?.trim() || null,
4393
+ role: requiredContextText(input.identity.user.role, "user role")
4394
+ });
4395
+ const organization = input.identity.organization ? Object.freeze({
4396
+ id: requiredContextText(input.identity.organization.id, "organization id"),
4397
+ slug: requiredContextText(input.identity.organization.slug, "organization slug"),
4398
+ name: requiredContextText(input.identity.organization.name, "organization name")
4399
+ }) : null;
4400
+ if (input.contextKey === "personal" && organization) {
4401
+ throw new TopoloSdkError("identity_context_mismatch", "Personal Topolo context resolved an organization.");
4402
+ }
4403
+ if (input.contextKey?.startsWith("org:")) {
4404
+ const expectedOrganizationId = input.contextKey.slice("org:".length);
4405
+ if (!organization || organization.id !== expectedOrganizationId) {
4406
+ throw new TopoloSdkError("identity_context_mismatch", "Pinned Topolo organization does not match the effective identity.");
4407
+ }
4408
+ }
4409
+ return Object.freeze({
4410
+ environment: input.environment,
4411
+ contextKey: input.contextKey,
4412
+ user,
4413
+ organization,
4414
+ serviceOrigins: Object.freeze(topoloServiceOrigins(input.serviceUrls, input.environment))
4415
+ });
4416
+ }
4417
+ function compactActionPreparationCore(preparation) {
4418
+ const example = preparation.example ? { title: preparation.example.title ?? null, input: preparation.example.input } : null;
4419
+ const effectiveInput = preparation.inputSource === "example" ? { source: "example" } : { source: preparation.inputSource, value: preparation.input };
4420
+ return {
4421
+ action: {
4422
+ id: preparation.action.actionId,
4423
+ title: preparation.action.title,
4424
+ docsUrl: preparation.action.agentContract?.docsUrl ?? null
4425
+ },
4426
+ example,
4427
+ input: effectiveInput,
4428
+ missingFields: preparation.missingFields,
4429
+ valid: preparation.plan.validation.valid,
4430
+ errors: preparation.plan.validation.errors,
4431
+ request: preparation.plan.request,
4432
+ confirmation: {
4433
+ required: preparation.plan.confirmationRequired,
4434
+ provided: preparation.plan.confirmationProvided
4435
+ },
4436
+ executable: preparation.plan.executable,
4437
+ verification: preparation.plan.verification,
4438
+ rollbackActionId: preparation.plan.rollbackActionId
4439
+ };
4440
+ }
4441
+ function compactActionPreparation(preparation, context) {
4442
+ const compact = compactActionPreparationCore(preparation);
4443
+ return context ? { context, ...compact } : compact;
4444
+ }
4042
4445
  function compactApplicationAudit(report) {
4043
4446
  return {
4044
4447
  version: report.version,
@@ -4299,7 +4702,7 @@ function sampleString(schema, hint) {
4299
4702
  if (schema.format === "uri" || schema.format === "url") return "https://topolo.app";
4300
4703
  if (schema.format === "date-time") return patternedString(schema.pattern || "", hint) || "2026-01-15T12:00:00Z";
4301
4704
  const words = String(hint || "value").replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").toLowerCase();
4302
- 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}`;
4705
+ 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}`;
4303
4706
  const minimum = Number.isInteger(schema.minLength) ? schema.minLength : 0;
4304
4707
  const maximum = Number.isInteger(schema.maxLength) ? schema.maxLength : null;
4305
4708
  if (value.length < minimum) value = value.padEnd(minimum, "x");
@@ -5059,6 +5462,8 @@ function createTopolo(options) {
5059
5462
  DEFAULT_APP_URLS,
5060
5463
  IdentityModule,
5061
5464
  P2PModule,
5465
+ TOPOLO_ENVIRONMENT_SERVICE_URLS,
5466
+ TOPOLO_RUNTIME_ENVIRONMENTS,
5062
5467
  TOPOLO_WIDGET_ENDPOINT,
5063
5468
  TOPOLO_WIDGET_REFRESH_INTERVAL_MS,
5064
5469
  TopoloAuthError,
@@ -5072,14 +5477,21 @@ function createTopolo(options) {
5072
5477
  actionContractCompleteness,
5073
5478
  applicationEntryFromCatalog,
5074
5479
  applicationRequirementScopes,
5480
+ assertTopoloServiceUrlEnvironment,
5075
5481
  auditAllApplicationRequirements,
5076
5482
  auditApplicationEntries,
5077
5483
  auditApplicationRequirements,
5484
+ canonicalTopoloErrorCode,
5485
+ canonicalTopoloRequestId,
5486
+ canonicalTopoloRequired,
5487
+ canonicalTopoloRetryAfter,
5078
5488
  compactActionCatalogEntry,
5079
5489
  compactActionPlan,
5490
+ compactActionPreparation,
5080
5491
  compactActionValidation,
5081
5492
  compactApplicationAudit,
5082
5493
  createTopolo,
5494
+ createTopoloAgentExecutionContext,
5083
5495
  createTopoloWidgetError,
5084
5496
  createTopoloWidgetResponse,
5085
5497
  defineTopoloChartWidget,
@@ -5089,17 +5501,22 @@ function createTopolo(options) {
5089
5501
  defineTopoloStatsWidget,
5090
5502
  digestTopoloActionCatalog,
5091
5503
  enrichActionContract,
5504
+ extractTopoloServiceErrorCode,
5505
+ extractTopoloServiceErrorDetails,
5092
5506
  formatDirectoryApplicationIds,
5093
5507
  friendlyAppId,
5094
5508
  listApplicationDirectory,
5095
5509
  parseTopoloActionUploadContract,
5096
5510
  prepareTopoloActionBase64UploadInput,
5097
5511
  prepareTopoloActionUploadInput,
5512
+ publicTopoloErrorMessage,
5098
5513
  requirementsForApplication,
5099
5514
  resolveApplicationDirectoryEntry,
5100
5515
  resolveCatalogServiceUrl,
5101
5516
  resolveServiceUrl,
5102
5517
  staticApplicationDirectory,
5518
+ topoloErrorHint,
5519
+ topoloServiceOrigins,
5103
5520
  validateTopoloActionInput,
5104
5521
  validateTopoloWidgetResponse
5105
5522
  });