@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 +455 -33
- package/dist/index.d.cts +121 -1
- package/dist/index.d.ts +121 -1
- package/dist/index.js +441 -33
- 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;
|
|
@@ -3804,6 +4118,7 @@ function normalizeActionCatalogEntry(item) {
|
|
|
3804
4118
|
path,
|
|
3805
4119
|
permissionName: pickActionString(a.permission_name) ?? pickActionString(a.permissionName) ?? "",
|
|
3806
4120
|
requiredPermission: pickActionString(a.required_permission) ?? pickActionString(a.requiredPermission) ?? "",
|
|
4121
|
+
principalAccess: pickPrincipalAccess(a.principal_access ?? a.principalAccess),
|
|
3807
4122
|
authorizationMode: pickAuthorizationMode(a.authorization_mode ?? a.authorizationMode),
|
|
3808
4123
|
allowedServices: stringArray(a.allowed_services ?? a.allowedServices),
|
|
3809
4124
|
inputSchema: plainObject(a.input_schema ?? a.inputSchema),
|
|
@@ -3820,6 +4135,9 @@ function normalizeActionCatalogEntry(item) {
|
|
|
3820
4135
|
function isHttpMethod(value) {
|
|
3821
4136
|
return value === "GET" || value === "POST" || value === "PUT" || value === "PATCH" || value === "DELETE";
|
|
3822
4137
|
}
|
|
4138
|
+
function pickPrincipalAccess(value) {
|
|
4139
|
+
return value === "platform_admin" || value === "platform_super_admin" ? value : null;
|
|
4140
|
+
}
|
|
3823
4141
|
function pickActionString(value) {
|
|
3824
4142
|
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
3825
4143
|
}
|
|
@@ -3913,7 +4231,16 @@ function pickString(value) {
|
|
|
3913
4231
|
function extractRequired(body) {
|
|
3914
4232
|
if (body && typeof body === "object") {
|
|
3915
4233
|
const maybe = body;
|
|
3916
|
-
if (Array.isArray(maybe.required)) return maybe.required
|
|
4234
|
+
if (Array.isArray(maybe.required)) return canonicalTopoloRequired(maybe.required);
|
|
4235
|
+
for (const nested of [maybe.details, maybe.error]) {
|
|
4236
|
+
if (!nested || typeof nested !== "object" || Array.isArray(nested)) continue;
|
|
4237
|
+
const record = nested;
|
|
4238
|
+
if (Array.isArray(record.required)) return canonicalTopoloRequired(record.required);
|
|
4239
|
+
if (record.details && typeof record.details === "object" && !Array.isArray(record.details)) {
|
|
4240
|
+
const required = record.details.required;
|
|
4241
|
+
if (Array.isArray(required)) return canonicalTopoloRequired(required);
|
|
4242
|
+
}
|
|
4243
|
+
}
|
|
3917
4244
|
}
|
|
3918
4245
|
return [];
|
|
3919
4246
|
}
|
|
@@ -3963,6 +4290,72 @@ function compactActionPlan(plan) {
|
|
|
3963
4290
|
nextActionIds: plan.nextActionIds
|
|
3964
4291
|
};
|
|
3965
4292
|
}
|
|
4293
|
+
function requiredContextText(value, field) {
|
|
4294
|
+
if (typeof value !== "string" || !value.trim() || value.trim().toLowerCase() === "unknown") {
|
|
4295
|
+
throw new TopoloSdkError("invalid_identity_context", `Effective Topolo ${field} is incomplete.`);
|
|
4296
|
+
}
|
|
4297
|
+
return value.trim();
|
|
4298
|
+
}
|
|
4299
|
+
function createTopoloAgentExecutionContext(input) {
|
|
4300
|
+
if (input.contextKey !== null && input.contextKey !== "personal" && !/^org:[^:]+$/.test(input.contextKey)) {
|
|
4301
|
+
throw new TopoloSdkError("invalid_identity_context", "Effective Topolo context key is invalid.");
|
|
4302
|
+
}
|
|
4303
|
+
const user = Object.freeze({
|
|
4304
|
+
id: requiredContextText(input.identity.user.id, "user id"),
|
|
4305
|
+
email: requiredContextText(input.identity.user.email, "user email"),
|
|
4306
|
+
name: input.identity.user.name?.trim() || null,
|
|
4307
|
+
role: requiredContextText(input.identity.user.role, "user role")
|
|
4308
|
+
});
|
|
4309
|
+
const organization = input.identity.organization ? Object.freeze({
|
|
4310
|
+
id: requiredContextText(input.identity.organization.id, "organization id"),
|
|
4311
|
+
slug: requiredContextText(input.identity.organization.slug, "organization slug"),
|
|
4312
|
+
name: requiredContextText(input.identity.organization.name, "organization name")
|
|
4313
|
+
}) : null;
|
|
4314
|
+
if (input.contextKey === "personal" && organization) {
|
|
4315
|
+
throw new TopoloSdkError("identity_context_mismatch", "Personal Topolo context resolved an organization.");
|
|
4316
|
+
}
|
|
4317
|
+
if (input.contextKey?.startsWith("org:")) {
|
|
4318
|
+
const expectedOrganizationId = input.contextKey.slice("org:".length);
|
|
4319
|
+
if (!organization || organization.id !== expectedOrganizationId) {
|
|
4320
|
+
throw new TopoloSdkError("identity_context_mismatch", "Pinned Topolo organization does not match the effective identity.");
|
|
4321
|
+
}
|
|
4322
|
+
}
|
|
4323
|
+
return Object.freeze({
|
|
4324
|
+
environment: input.environment,
|
|
4325
|
+
contextKey: input.contextKey,
|
|
4326
|
+
user,
|
|
4327
|
+
organization,
|
|
4328
|
+
serviceOrigins: Object.freeze(topoloServiceOrigins(input.serviceUrls, input.environment))
|
|
4329
|
+
});
|
|
4330
|
+
}
|
|
4331
|
+
function compactActionPreparationCore(preparation) {
|
|
4332
|
+
const example = preparation.example ? { title: preparation.example.title ?? null, input: preparation.example.input } : null;
|
|
4333
|
+
const effectiveInput = preparation.inputSource === "example" ? { source: "example" } : { source: preparation.inputSource, value: preparation.input };
|
|
4334
|
+
return {
|
|
4335
|
+
action: {
|
|
4336
|
+
id: preparation.action.actionId,
|
|
4337
|
+
title: preparation.action.title,
|
|
4338
|
+
docsUrl: preparation.action.agentContract?.docsUrl ?? null
|
|
4339
|
+
},
|
|
4340
|
+
example,
|
|
4341
|
+
input: effectiveInput,
|
|
4342
|
+
missingFields: preparation.missingFields,
|
|
4343
|
+
valid: preparation.plan.validation.valid,
|
|
4344
|
+
errors: preparation.plan.validation.errors,
|
|
4345
|
+
request: preparation.plan.request,
|
|
4346
|
+
confirmation: {
|
|
4347
|
+
required: preparation.plan.confirmationRequired,
|
|
4348
|
+
provided: preparation.plan.confirmationProvided
|
|
4349
|
+
},
|
|
4350
|
+
executable: preparation.plan.executable,
|
|
4351
|
+
verification: preparation.plan.verification,
|
|
4352
|
+
rollbackActionId: preparation.plan.rollbackActionId
|
|
4353
|
+
};
|
|
4354
|
+
}
|
|
4355
|
+
function compactActionPreparation(preparation, context) {
|
|
4356
|
+
const compact = compactActionPreparationCore(preparation);
|
|
4357
|
+
return context ? { context, ...compact } : compact;
|
|
4358
|
+
}
|
|
3966
4359
|
function compactApplicationAudit(report) {
|
|
3967
4360
|
return {
|
|
3968
4361
|
version: report.version,
|
|
@@ -3997,6 +4390,7 @@ async function digestTopoloActionCatalog(actions, appId) {
|
|
|
3997
4390
|
const allowedServices = [...action.allowedServices || []].map((service) => service.trim()).filter(Boolean).sort();
|
|
3998
4391
|
return {
|
|
3999
4392
|
actionId: action.actionId || `${appId}.${action.name}`,
|
|
4393
|
+
principalAccess: action.principalAccess ?? null,
|
|
4000
4394
|
authorizationMode,
|
|
4001
4395
|
allowedServices: authorizationMode === "service" || authorizationMode === "runtime" ? allowedServices : [],
|
|
4002
4396
|
inputSchema: action.inputSchema,
|
|
@@ -4982,6 +5376,8 @@ export {
|
|
|
4982
5376
|
DEFAULT_APP_URLS,
|
|
4983
5377
|
IdentityModule,
|
|
4984
5378
|
P2PModule,
|
|
5379
|
+
TOPOLO_ENVIRONMENT_SERVICE_URLS,
|
|
5380
|
+
TOPOLO_RUNTIME_ENVIRONMENTS,
|
|
4985
5381
|
TOPOLO_WIDGET_ENDPOINT,
|
|
4986
5382
|
TOPOLO_WIDGET_REFRESH_INTERVAL_MS,
|
|
4987
5383
|
TopoloAuthError,
|
|
@@ -4995,14 +5391,21 @@ export {
|
|
|
4995
5391
|
actionContractCompleteness,
|
|
4996
5392
|
applicationEntryFromCatalog,
|
|
4997
5393
|
applicationRequirementScopes,
|
|
5394
|
+
assertTopoloServiceUrlEnvironment,
|
|
4998
5395
|
auditAllApplicationRequirements,
|
|
4999
5396
|
auditApplicationEntries,
|
|
5000
5397
|
auditApplicationRequirements,
|
|
5398
|
+
canonicalTopoloErrorCode,
|
|
5399
|
+
canonicalTopoloRequestId,
|
|
5400
|
+
canonicalTopoloRequired,
|
|
5401
|
+
canonicalTopoloRetryAfter,
|
|
5001
5402
|
compactActionCatalogEntry,
|
|
5002
5403
|
compactActionPlan,
|
|
5404
|
+
compactActionPreparation,
|
|
5003
5405
|
compactActionValidation,
|
|
5004
5406
|
compactApplicationAudit,
|
|
5005
5407
|
createTopolo,
|
|
5408
|
+
createTopoloAgentExecutionContext,
|
|
5006
5409
|
createTopoloWidgetError,
|
|
5007
5410
|
createTopoloWidgetResponse,
|
|
5008
5411
|
defineTopoloChartWidget,
|
|
@@ -5012,17 +5415,22 @@ export {
|
|
|
5012
5415
|
defineTopoloStatsWidget,
|
|
5013
5416
|
digestTopoloActionCatalog,
|
|
5014
5417
|
enrichActionContract,
|
|
5418
|
+
extractTopoloServiceErrorCode,
|
|
5419
|
+
extractTopoloServiceErrorDetails,
|
|
5015
5420
|
formatDirectoryApplicationIds,
|
|
5016
5421
|
friendlyAppId,
|
|
5017
5422
|
listApplicationDirectory,
|
|
5018
5423
|
parseTopoloActionUploadContract,
|
|
5019
5424
|
prepareTopoloActionBase64UploadInput,
|
|
5020
5425
|
prepareTopoloActionUploadInput,
|
|
5426
|
+
publicTopoloErrorMessage,
|
|
5021
5427
|
requirementsForApplication,
|
|
5022
5428
|
resolveApplicationDirectoryEntry,
|
|
5023
5429
|
resolveCatalogServiceUrl,
|
|
5024
5430
|
resolveServiceUrl,
|
|
5025
5431
|
staticApplicationDirectory,
|
|
5432
|
+
topoloErrorHint,
|
|
5433
|
+
topoloServiceOrigins,
|
|
5026
5434
|
validateTopoloActionInput,
|
|
5027
5435
|
validateTopoloWidgetResponse
|
|
5028
5436
|
};
|