@topolo/sdk 0.10.3 → 0.10.5

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;
@@ -3880,6 +4208,7 @@ function normalizeActionCatalogEntry(item) {
3880
4208
  path,
3881
4209
  permissionName: pickActionString(a.permission_name) ?? pickActionString(a.permissionName) ?? "",
3882
4210
  requiredPermission: pickActionString(a.required_permission) ?? pickActionString(a.requiredPermission) ?? "",
4211
+ principalAccess: pickPrincipalAccess(a.principal_access ?? a.principalAccess),
3883
4212
  authorizationMode: pickAuthorizationMode(a.authorization_mode ?? a.authorizationMode),
3884
4213
  allowedServices: stringArray(a.allowed_services ?? a.allowedServices),
3885
4214
  inputSchema: plainObject(a.input_schema ?? a.inputSchema),
@@ -3896,6 +4225,9 @@ function normalizeActionCatalogEntry(item) {
3896
4225
  function isHttpMethod(value) {
3897
4226
  return value === "GET" || value === "POST" || value === "PUT" || value === "PATCH" || value === "DELETE";
3898
4227
  }
4228
+ function pickPrincipalAccess(value) {
4229
+ return value === "platform_admin" || value === "platform_super_admin" ? value : null;
4230
+ }
3899
4231
  function pickActionString(value) {
3900
4232
  return typeof value === "string" && value.trim() ? value.trim() : null;
3901
4233
  }
@@ -3989,7 +4321,16 @@ function pickString(value) {
3989
4321
  function extractRequired(body) {
3990
4322
  if (body && typeof body === "object") {
3991
4323
  const maybe = body;
3992
- if (Array.isArray(maybe.required)) return maybe.required.map(String);
4324
+ if (Array.isArray(maybe.required)) return canonicalTopoloRequired(maybe.required);
4325
+ for (const nested of [maybe.details, maybe.error]) {
4326
+ if (!nested || typeof nested !== "object" || Array.isArray(nested)) continue;
4327
+ const record = nested;
4328
+ if (Array.isArray(record.required)) return canonicalTopoloRequired(record.required);
4329
+ if (record.details && typeof record.details === "object" && !Array.isArray(record.details)) {
4330
+ const required = record.details.required;
4331
+ if (Array.isArray(required)) return canonicalTopoloRequired(required);
4332
+ }
4333
+ }
3993
4334
  }
3994
4335
  return [];
3995
4336
  }
@@ -4039,6 +4380,72 @@ function compactActionPlan(plan) {
4039
4380
  nextActionIds: plan.nextActionIds
4040
4381
  };
4041
4382
  }
4383
+ function requiredContextText(value, field) {
4384
+ if (typeof value !== "string" || !value.trim() || value.trim().toLowerCase() === "unknown") {
4385
+ throw new TopoloSdkError("invalid_identity_context", `Effective Topolo ${field} is incomplete.`);
4386
+ }
4387
+ return value.trim();
4388
+ }
4389
+ function createTopoloAgentExecutionContext(input) {
4390
+ if (input.contextKey !== null && input.contextKey !== "personal" && !/^org:[^:]+$/.test(input.contextKey)) {
4391
+ throw new TopoloSdkError("invalid_identity_context", "Effective Topolo context key is invalid.");
4392
+ }
4393
+ const user = Object.freeze({
4394
+ id: requiredContextText(input.identity.user.id, "user id"),
4395
+ email: requiredContextText(input.identity.user.email, "user email"),
4396
+ name: input.identity.user.name?.trim() || null,
4397
+ role: requiredContextText(input.identity.user.role, "user role")
4398
+ });
4399
+ const organization = input.identity.organization ? Object.freeze({
4400
+ id: requiredContextText(input.identity.organization.id, "organization id"),
4401
+ slug: requiredContextText(input.identity.organization.slug, "organization slug"),
4402
+ name: requiredContextText(input.identity.organization.name, "organization name")
4403
+ }) : null;
4404
+ if (input.contextKey === "personal" && organization) {
4405
+ throw new TopoloSdkError("identity_context_mismatch", "Personal Topolo context resolved an organization.");
4406
+ }
4407
+ if (input.contextKey?.startsWith("org:")) {
4408
+ const expectedOrganizationId = input.contextKey.slice("org:".length);
4409
+ if (!organization || organization.id !== expectedOrganizationId) {
4410
+ throw new TopoloSdkError("identity_context_mismatch", "Pinned Topolo organization does not match the effective identity.");
4411
+ }
4412
+ }
4413
+ return Object.freeze({
4414
+ environment: input.environment,
4415
+ contextKey: input.contextKey,
4416
+ user,
4417
+ organization,
4418
+ serviceOrigins: Object.freeze(topoloServiceOrigins(input.serviceUrls, input.environment))
4419
+ });
4420
+ }
4421
+ function compactActionPreparationCore(preparation) {
4422
+ const example = preparation.example ? { title: preparation.example.title ?? null, input: preparation.example.input } : null;
4423
+ const effectiveInput = preparation.inputSource === "example" ? { source: "example" } : { source: preparation.inputSource, value: preparation.input };
4424
+ return {
4425
+ action: {
4426
+ id: preparation.action.actionId,
4427
+ title: preparation.action.title,
4428
+ docsUrl: preparation.action.agentContract?.docsUrl ?? null
4429
+ },
4430
+ example,
4431
+ input: effectiveInput,
4432
+ missingFields: preparation.missingFields,
4433
+ valid: preparation.plan.validation.valid,
4434
+ errors: preparation.plan.validation.errors,
4435
+ request: preparation.plan.request,
4436
+ confirmation: {
4437
+ required: preparation.plan.confirmationRequired,
4438
+ provided: preparation.plan.confirmationProvided
4439
+ },
4440
+ executable: preparation.plan.executable,
4441
+ verification: preparation.plan.verification,
4442
+ rollbackActionId: preparation.plan.rollbackActionId
4443
+ };
4444
+ }
4445
+ function compactActionPreparation(preparation, context) {
4446
+ const compact = compactActionPreparationCore(preparation);
4447
+ return context ? { context, ...compact } : compact;
4448
+ }
4042
4449
  function compactApplicationAudit(report) {
4043
4450
  return {
4044
4451
  version: report.version,
@@ -4073,6 +4480,7 @@ async function digestTopoloActionCatalog(actions, appId) {
4073
4480
  const allowedServices = [...action.allowedServices || []].map((service) => service.trim()).filter(Boolean).sort();
4074
4481
  return {
4075
4482
  actionId: action.actionId || `${appId}.${action.name}`,
4483
+ principalAccess: action.principalAccess ?? null,
4076
4484
  authorizationMode,
4077
4485
  allowedServices: authorizationMode === "service" || authorizationMode === "runtime" ? allowedServices : [],
4078
4486
  inputSchema: action.inputSchema,
@@ -5059,6 +5467,8 @@ function createTopolo(options) {
5059
5467
  DEFAULT_APP_URLS,
5060
5468
  IdentityModule,
5061
5469
  P2PModule,
5470
+ TOPOLO_ENVIRONMENT_SERVICE_URLS,
5471
+ TOPOLO_RUNTIME_ENVIRONMENTS,
5062
5472
  TOPOLO_WIDGET_ENDPOINT,
5063
5473
  TOPOLO_WIDGET_REFRESH_INTERVAL_MS,
5064
5474
  TopoloAuthError,
@@ -5072,14 +5482,21 @@ function createTopolo(options) {
5072
5482
  actionContractCompleteness,
5073
5483
  applicationEntryFromCatalog,
5074
5484
  applicationRequirementScopes,
5485
+ assertTopoloServiceUrlEnvironment,
5075
5486
  auditAllApplicationRequirements,
5076
5487
  auditApplicationEntries,
5077
5488
  auditApplicationRequirements,
5489
+ canonicalTopoloErrorCode,
5490
+ canonicalTopoloRequestId,
5491
+ canonicalTopoloRequired,
5492
+ canonicalTopoloRetryAfter,
5078
5493
  compactActionCatalogEntry,
5079
5494
  compactActionPlan,
5495
+ compactActionPreparation,
5080
5496
  compactActionValidation,
5081
5497
  compactApplicationAudit,
5082
5498
  createTopolo,
5499
+ createTopoloAgentExecutionContext,
5083
5500
  createTopoloWidgetError,
5084
5501
  createTopoloWidgetResponse,
5085
5502
  defineTopoloChartWidget,
@@ -5089,17 +5506,22 @@ function createTopolo(options) {
5089
5506
  defineTopoloStatsWidget,
5090
5507
  digestTopoloActionCatalog,
5091
5508
  enrichActionContract,
5509
+ extractTopoloServiceErrorCode,
5510
+ extractTopoloServiceErrorDetails,
5092
5511
  formatDirectoryApplicationIds,
5093
5512
  friendlyAppId,
5094
5513
  listApplicationDirectory,
5095
5514
  parseTopoloActionUploadContract,
5096
5515
  prepareTopoloActionBase64UploadInput,
5097
5516
  prepareTopoloActionUploadInput,
5517
+ publicTopoloErrorMessage,
5098
5518
  requirementsForApplication,
5099
5519
  resolveApplicationDirectoryEntry,
5100
5520
  resolveCatalogServiceUrl,
5101
5521
  resolveServiceUrl,
5102
5522
  staticApplicationDirectory,
5523
+ topoloErrorHint,
5524
+ topoloServiceOrigins,
5103
5525
  validateTopoloActionInput,
5104
5526
  validateTopoloWidgetResponse
5105
5527
  });