@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 +451 -34
- package/dist/index.d.cts +119 -1
- package/dist/index.d.ts +119 -1
- package/dist/index.js +437 -34
- package/dist/node/credential-store.cjs +254 -25
- package/dist/node/credential-store.d.cts +10 -3
- package/dist/node/credential-store.d.ts +10 -3
- package/dist/node/credential-store.js +251 -25
- package/package.json +2 -2
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(
|
|
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
|
|
69
|
-
if (!body || typeof body !== "object" || Array.isArray(body)) return
|
|
163
|
+
function extractTopoloServiceErrorCode(body) {
|
|
164
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) return null;
|
|
70
165
|
const record = body;
|
|
71
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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.
|
|
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:
|
|
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:
|
|
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:
|
|
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")
|
|
3028
|
-
retryAfter: res.headers.get("Retry-After")
|
|
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(
|
|
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 {
|
|
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:
|
|
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
|
|
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
|
};
|