@topolo/cli 0.7.3 → 0.7.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +726 -90
- package/package.json +3 -8
package/dist/index.js
CHANGED
|
@@ -9,7 +9,7 @@ import { platform as platform2 } from "os";
|
|
|
9
9
|
import { createInterface } from "readline/promises";
|
|
10
10
|
import { stdin as input } from "process";
|
|
11
11
|
|
|
12
|
-
//
|
|
12
|
+
// ../../../development-workflow-acceleration/node_modules/.pnpm/@topolo+sdk@0.10.4/node_modules/@topolo/sdk/dist/index.js
|
|
13
13
|
function applyAuthHeaders(headers, credential) {
|
|
14
14
|
if (credential.kind === "api_key") {
|
|
15
15
|
headers.set("X-Api-Key", credential.apiKey);
|
|
@@ -36,6 +36,90 @@ var TopoloSdkError = class extends Error {
|
|
|
36
36
|
this.code = code;
|
|
37
37
|
}
|
|
38
38
|
};
|
|
39
|
+
var PUBLIC_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
40
|
+
"action_app_identity_unavailable",
|
|
41
|
+
"action_authentication_required",
|
|
42
|
+
"action_catalog_summary_unavailable",
|
|
43
|
+
"action_catalog_unavailable",
|
|
44
|
+
"action_contract_mismatch",
|
|
45
|
+
"action_permission_required",
|
|
46
|
+
"action_runtime_authorization_required",
|
|
47
|
+
"action_service_not_allowed",
|
|
48
|
+
"auth_error",
|
|
49
|
+
"authentication_required",
|
|
50
|
+
"authorization_pending",
|
|
51
|
+
"bad_sig",
|
|
52
|
+
"confirmation_required",
|
|
53
|
+
"dependency_unavailable",
|
|
54
|
+
"environment_mismatch",
|
|
55
|
+
"environment_service_url_required",
|
|
56
|
+
"expired_token",
|
|
57
|
+
"forbidden",
|
|
58
|
+
"http_error",
|
|
59
|
+
"immutable_drift",
|
|
60
|
+
"integration_dependency_unavailable",
|
|
61
|
+
"integration_misconfigured",
|
|
62
|
+
"invalid_action",
|
|
63
|
+
"invalid_action_input",
|
|
64
|
+
"invalid_grant",
|
|
65
|
+
"invalid_input",
|
|
66
|
+
"invalid_token",
|
|
67
|
+
"mcp_error",
|
|
68
|
+
"membership_required",
|
|
69
|
+
"network_error",
|
|
70
|
+
"organization_context_required",
|
|
71
|
+
"organization_enrollment_required",
|
|
72
|
+
"permission_denied",
|
|
73
|
+
"rate_limited",
|
|
74
|
+
"request_failed",
|
|
75
|
+
"service_catalog_unavailable",
|
|
76
|
+
"service_path_origin_mismatch",
|
|
77
|
+
"service_required",
|
|
78
|
+
"slow_down",
|
|
79
|
+
"state_conflict",
|
|
80
|
+
"unauthorized",
|
|
81
|
+
"unknown_action",
|
|
82
|
+
"unknown_service"
|
|
83
|
+
]);
|
|
84
|
+
var ERROR_CODE_PATTERN = /^[a-z][a-z0-9_]{0,63}$/;
|
|
85
|
+
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;
|
|
86
|
+
var CLOUDFLARE_RAY_ID_PATTERN = /^[0-9a-f]{16}-[A-Z]{3}$/;
|
|
87
|
+
var PERMISSION_PATTERN = /^[a-z][a-z0-9_-]{0,63}(?:\.[a-z][a-z0-9_-]{0,63})*:(?:[a-z][a-z0-9_-]{0,63}|\*)$/;
|
|
88
|
+
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$/;
|
|
89
|
+
var PUBLIC_DETAIL_SOURCES = /* @__PURE__ */ new Set([
|
|
90
|
+
"api_key_introspection",
|
|
91
|
+
"api_key_validation",
|
|
92
|
+
"auth_context",
|
|
93
|
+
"auth_validate",
|
|
94
|
+
"catalog",
|
|
95
|
+
"credential",
|
|
96
|
+
"runtime",
|
|
97
|
+
"service_token_provider",
|
|
98
|
+
"service_verify_key",
|
|
99
|
+
"token_validation",
|
|
100
|
+
"upstream"
|
|
101
|
+
]);
|
|
102
|
+
function canonicalTopoloErrorCode(value) {
|
|
103
|
+
if (typeof value !== "string") return null;
|
|
104
|
+
const candidate = value.trim().toLowerCase();
|
|
105
|
+
if (!ERROR_CODE_PATTERN.test(candidate) || !PUBLIC_ERROR_CODES.has(candidate)) return null;
|
|
106
|
+
return candidate;
|
|
107
|
+
}
|
|
108
|
+
function canonicalTopoloRequestId(value) {
|
|
109
|
+
if (typeof value !== "string") return void 0;
|
|
110
|
+
const candidate = value.trim();
|
|
111
|
+
return TOPOLO_REQUEST_ID_PATTERN.test(candidate) || CLOUDFLARE_RAY_ID_PATTERN.test(candidate) ? candidate : void 0;
|
|
112
|
+
}
|
|
113
|
+
function canonicalTopoloRetryAfter(value) {
|
|
114
|
+
if (typeof value !== "string") return void 0;
|
|
115
|
+
const candidate = value.trim();
|
|
116
|
+
if (/^\d{1,10}$/.test(candidate) || RETRY_AFTER_DATE_PATTERN.test(candidate)) return candidate;
|
|
117
|
+
return void 0;
|
|
118
|
+
}
|
|
119
|
+
function canonicalTopoloRequired(value) {
|
|
120
|
+
if (!Array.isArray(value)) return [];
|
|
121
|
+
return [...new Set(value.filter((item) => typeof item === "string" && PERMISSION_PATTERN.test(item)))].slice(0, 32);
|
|
122
|
+
}
|
|
39
123
|
var TopoloAuthError = class extends TopoloSdkError {
|
|
40
124
|
status;
|
|
41
125
|
body;
|
|
@@ -43,15 +127,19 @@ var TopoloAuthError = class extends TopoloSdkError {
|
|
|
43
127
|
path;
|
|
44
128
|
requestId;
|
|
45
129
|
retryAfter;
|
|
130
|
+
details;
|
|
46
131
|
constructor(message, code = "auth_error", options = {}) {
|
|
47
|
-
super(code, message);
|
|
132
|
+
super(canonicalTopoloErrorCode(code) ?? "auth_error", message);
|
|
48
133
|
this.name = "TopoloAuthError";
|
|
49
134
|
this.status = options.status ?? 401;
|
|
50
135
|
this.body = options.body;
|
|
51
136
|
this.service = options.service;
|
|
52
137
|
this.path = options.path;
|
|
53
|
-
this.requestId = options.requestId;
|
|
54
|
-
this.retryAfter = options.retryAfter;
|
|
138
|
+
this.requestId = canonicalTopoloRequestId(options.requestId);
|
|
139
|
+
this.retryAfter = canonicalTopoloRetryAfter(options.retryAfter);
|
|
140
|
+
this.details = extractTopoloServiceErrorDetails(
|
|
141
|
+
options.details === void 0 ? options.body : { details: options.details }
|
|
142
|
+
);
|
|
55
143
|
}
|
|
56
144
|
};
|
|
57
145
|
var TopoloPermissionError = class extends TopoloSdkError {
|
|
@@ -62,28 +150,140 @@ var TopoloPermissionError = class extends TopoloSdkError {
|
|
|
62
150
|
path;
|
|
63
151
|
requestId;
|
|
64
152
|
retryAfter;
|
|
153
|
+
details;
|
|
65
154
|
constructor(message, required, options = {}) {
|
|
66
|
-
super(
|
|
155
|
+
super(
|
|
156
|
+
canonicalTopoloErrorCode(options.code) ?? serviceErrorCode(options.body, "permission_denied"),
|
|
157
|
+
message
|
|
158
|
+
);
|
|
67
159
|
this.name = "TopoloPermissionError";
|
|
68
|
-
this.required = required;
|
|
160
|
+
this.required = canonicalTopoloRequired(required);
|
|
69
161
|
this.status = options.status ?? 403;
|
|
70
162
|
this.body = options.body;
|
|
71
163
|
this.service = options.service;
|
|
72
164
|
this.path = options.path;
|
|
73
|
-
this.requestId = options.requestId;
|
|
74
|
-
this.retryAfter = options.retryAfter;
|
|
165
|
+
this.requestId = canonicalTopoloRequestId(options.requestId);
|
|
166
|
+
this.retryAfter = canonicalTopoloRetryAfter(options.retryAfter);
|
|
167
|
+
this.details = extractTopoloServiceErrorDetails(
|
|
168
|
+
options.details === void 0 ? options.body : { details: options.details }
|
|
169
|
+
);
|
|
75
170
|
}
|
|
76
171
|
};
|
|
77
|
-
function
|
|
78
|
-
if (!body || typeof body !== "object" || Array.isArray(body)) return
|
|
172
|
+
function extractTopoloServiceErrorCode(body) {
|
|
173
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) return null;
|
|
79
174
|
const record4 = body;
|
|
80
|
-
|
|
175
|
+
const topLevel = canonicalTopoloErrorCode(record4.code);
|
|
176
|
+
if (topLevel) return topLevel;
|
|
81
177
|
const nested = record4.error;
|
|
178
|
+
const nestedString = canonicalTopoloErrorCode(nested);
|
|
179
|
+
if (nestedString) return nestedString;
|
|
82
180
|
if (nested && typeof nested === "object" && !Array.isArray(nested)) {
|
|
83
181
|
const code = nested.code;
|
|
84
|
-
|
|
182
|
+
const nestedCode = canonicalTopoloErrorCode(code);
|
|
183
|
+
if (nestedCode) return nestedCode;
|
|
184
|
+
}
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
function serviceErrorCode(body, fallback) {
|
|
188
|
+
return extractTopoloServiceErrorCode(body) ?? fallback;
|
|
189
|
+
}
|
|
190
|
+
function extractTopoloServiceErrorDetails(body) {
|
|
191
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) return void 0;
|
|
192
|
+
const record4 = body;
|
|
193
|
+
let candidate = record4.details;
|
|
194
|
+
const nested = record4.error;
|
|
195
|
+
if (nested && typeof nested === "object" && !Array.isArray(nested)) {
|
|
196
|
+
const nestedRecord = nested;
|
|
197
|
+
if (candidate === void 0) candidate = nestedRecord.details ?? nestedRecord;
|
|
198
|
+
}
|
|
199
|
+
if (candidate === void 0) candidate = record4;
|
|
200
|
+
if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) return void 0;
|
|
201
|
+
const source = candidate;
|
|
202
|
+
const safe = {};
|
|
203
|
+
for (const [key, value] of Object.entries(source)) {
|
|
204
|
+
if (/present$/i.test(key) && typeof value === "boolean") {
|
|
205
|
+
safe[key] = value;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
if (key === "retryable" && typeof value === "boolean") {
|
|
209
|
+
safe.retryable = value;
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
if (key === "code") {
|
|
213
|
+
const sanitized = canonicalTopoloErrorCode(value);
|
|
214
|
+
if (sanitized) safe.code = sanitized;
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
if (key === "source") {
|
|
218
|
+
const sanitized = typeof value === "string" ? value.trim().toLowerCase() : "";
|
|
219
|
+
if (PUBLIC_DETAIL_SOURCES.has(sanitized)) safe.source = sanitized;
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
if (key === "dependency" || key === "platform") {
|
|
223
|
+
safe[`${key}Present`] = value !== null && value !== void 0;
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
if (key === "error" && !safe.code) {
|
|
227
|
+
const sanitized = canonicalTopoloErrorCode(value);
|
|
228
|
+
if (sanitized) safe.code = sanitized;
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
if (key === "required" && Array.isArray(value)) {
|
|
232
|
+
const required = canonicalTopoloRequired(value);
|
|
233
|
+
if (required.length) safe.required = required;
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (["missingFields", "fields", "paths"].includes(key) && Array.isArray(value)) {
|
|
237
|
+
const paths = value.filter((item) => typeof item === "string" && /^[$.a-z0-9_[\]-]{1,160}$/i.test(item));
|
|
238
|
+
if (paths.length) safe[key] = paths;
|
|
239
|
+
}
|
|
85
240
|
}
|
|
86
|
-
return
|
|
241
|
+
return Object.keys(safe).length ? safe : void 0;
|
|
242
|
+
}
|
|
243
|
+
function publicTopoloErrorMessage(error) {
|
|
244
|
+
const code = canonicalTopoloErrorCode(
|
|
245
|
+
typeof error === "string" ? error : error.code ?? extractTopoloServiceErrorCode(error.body)
|
|
246
|
+
) ?? "request_failed";
|
|
247
|
+
if (/(?:organization|org).*(?:enroll|membership|member|admission)/.test(code)) {
|
|
248
|
+
return "Organization or application enrollment is required.";
|
|
249
|
+
}
|
|
250
|
+
if (/(?:organization|org).*(?:context|required|missing|invalid|unknown|not_found)/.test(code)) {
|
|
251
|
+
return "Organization context is unavailable.";
|
|
252
|
+
}
|
|
253
|
+
if (["auth_error", "authentication_required", "unauthorized", "invalid_token", "expired_token"].includes(code)) {
|
|
254
|
+
return "Authentication failed.";
|
|
255
|
+
}
|
|
256
|
+
if (code === "permission_denied" || code === "forbidden") return "Permission denied.";
|
|
257
|
+
if (code === "confirmation_required") return "Confirmation is required.";
|
|
258
|
+
return `Topolo request failed (${code}).`;
|
|
259
|
+
}
|
|
260
|
+
function topoloErrorHint(error) {
|
|
261
|
+
const code = canonicalTopoloErrorCode(
|
|
262
|
+
typeof error === "string" ? error : error.code ?? extractTopoloServiceErrorCode(error.body)
|
|
263
|
+
);
|
|
264
|
+
if (!code) return void 0;
|
|
265
|
+
if (code === "auth_error" || code === "authentication_required" || code === "unauthorized" || code === "invalid_token" || code === "expired_token") {
|
|
266
|
+
return "Run `topolo auth login`, then verify the effective Topolo identity and organization.";
|
|
267
|
+
}
|
|
268
|
+
if (code === "confirmation_required") {
|
|
269
|
+
return "Review the unchanged action plan, obtain approval, and retry once with --confirm or confirm:true.";
|
|
270
|
+
}
|
|
271
|
+
if (code === "enrollment_required" || code === "membership_required" || /(?:organization|org).*(?:enroll|membership|member|admission)|(?:enroll|membership).*(?:organization|org)/.test(code)) {
|
|
272
|
+
return "The current identity is not enrolled for this organization or application. Use an enrolled identity or ask an organization administrator to complete enrollment.";
|
|
273
|
+
}
|
|
274
|
+
if (/(?:organization|org).*(?:context|required|missing|invalid|unknown|not_found)/.test(code)) {
|
|
275
|
+
return "Select a signed-in organization context, then verify the effective identity and organization before retrying.";
|
|
276
|
+
}
|
|
277
|
+
if (code === "permission_denied" || code === "forbidden") {
|
|
278
|
+
return "The current identity or resource context is not authorized. Review the structured details and published authorization contract.";
|
|
279
|
+
}
|
|
280
|
+
if (code === "environment_mismatch") {
|
|
281
|
+
return "Use service origins and credentials from one explicit Topolo environment only.";
|
|
282
|
+
}
|
|
283
|
+
if (code === "invalid_action_input" || code === "invalid_input") {
|
|
284
|
+
return "Correct only the reported schema paths, then prepare the action again.";
|
|
285
|
+
}
|
|
286
|
+
return void 0;
|
|
87
287
|
}
|
|
88
288
|
var TopoloConfirmationRequiredError = class extends TopoloSdkError {
|
|
89
289
|
constructor(message) {
|
|
@@ -98,17 +298,66 @@ var TopoloHttpError = class extends TopoloSdkError {
|
|
|
98
298
|
path;
|
|
99
299
|
requestId;
|
|
100
300
|
retryAfter;
|
|
301
|
+
details;
|
|
101
302
|
constructor(service, path9, status, body, message, options = {}) {
|
|
102
|
-
super(
|
|
303
|
+
super(serviceErrorCode(body, "http_error"), message ?? `HTTP ${status} from ${service}${path9}`);
|
|
103
304
|
this.name = "TopoloHttpError";
|
|
104
305
|
this.status = status;
|
|
105
306
|
this.body = body;
|
|
106
307
|
this.service = service;
|
|
107
308
|
this.path = path9;
|
|
108
|
-
this.requestId = options.requestId;
|
|
109
|
-
this.retryAfter = options.retryAfter;
|
|
309
|
+
this.requestId = canonicalTopoloRequestId(options.requestId);
|
|
310
|
+
this.retryAfter = canonicalTopoloRetryAfter(options.retryAfter);
|
|
311
|
+
this.details = extractTopoloServiceErrorDetails(
|
|
312
|
+
options.details === void 0 ? body : { details: options.details }
|
|
313
|
+
);
|
|
110
314
|
}
|
|
111
315
|
};
|
|
316
|
+
function hostnameHasSuffix(hostname, suffix) {
|
|
317
|
+
return hostname === suffix || hostname.endsWith(`.${suffix}`);
|
|
318
|
+
}
|
|
319
|
+
function workersDevAccountMatches(hostname, account) {
|
|
320
|
+
if (!hostname.endsWith(".workers.dev")) return false;
|
|
321
|
+
const labels = hostname.slice(0, -".workers.dev".length).split(".");
|
|
322
|
+
return labels.length >= 2 && labels.at(-1) === account;
|
|
323
|
+
}
|
|
324
|
+
function originMatchesEnvironment(url, environment) {
|
|
325
|
+
const hostname = url.hostname.toLowerCase();
|
|
326
|
+
if (environment === "local") {
|
|
327
|
+
return hostname === "localhost" || hostname.endsWith(".localhost") || hostname === "127.0.0.1" || hostname === "[::1]";
|
|
328
|
+
}
|
|
329
|
+
if (url.protocol !== "https:") return false;
|
|
330
|
+
if (environment === "production") {
|
|
331
|
+
return hostnameHasSuffix(hostname, "topolo.app") || hostnameHasSuffix(hostname, "topolo.io") || workersDevAccountMatches(hostname, "topolo");
|
|
332
|
+
}
|
|
333
|
+
if (environment === "staging") {
|
|
334
|
+
return hostnameHasSuffix(hostname, "stg.topolo.us") || workersDevAccountMatches(hostname, "topolo-staging");
|
|
335
|
+
}
|
|
336
|
+
return hostnameHasSuffix(hostname, "topolo.dev") || workersDevAccountMatches(hostname, "topolo-development");
|
|
337
|
+
}
|
|
338
|
+
function assertTopoloServiceUrlEnvironment(service, value, environment) {
|
|
339
|
+
let url;
|
|
340
|
+
try {
|
|
341
|
+
url = new URL(value);
|
|
342
|
+
} catch {
|
|
343
|
+
throw new TopoloSdkError("invalid_service_url", `Invalid URL configured for Topolo service "${service}".`);
|
|
344
|
+
}
|
|
345
|
+
if (url.protocol !== "https:" && url.protocol !== "http:" || url.username || url.password || !originMatchesEnvironment(url, environment)) {
|
|
346
|
+
throw new TopoloSdkError(
|
|
347
|
+
"environment_mismatch",
|
|
348
|
+
`Refusing Topolo service "${service}": ${url.origin} does not belong to ${environment}.`
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
return url.origin;
|
|
352
|
+
}
|
|
353
|
+
function topoloServiceOrigins(serviceUrls, environment) {
|
|
354
|
+
return Object.fromEntries(
|
|
355
|
+
Object.entries(serviceUrls ?? {}).sort(([left], [right]) => left.localeCompare(right)).map(([service, value]) => [
|
|
356
|
+
service,
|
|
357
|
+
assertTopoloServiceUrlEnvironment(service, value, environment)
|
|
358
|
+
])
|
|
359
|
+
);
|
|
360
|
+
}
|
|
112
361
|
var APP_API_URLS = {
|
|
113
362
|
auth: "https://auth.topolo.app",
|
|
114
363
|
developers: "https://developers.topolo.app"
|
|
@@ -2857,6 +3106,7 @@ var TopoloClient = class _TopoloClient {
|
|
|
2857
3106
|
credential;
|
|
2858
3107
|
agent;
|
|
2859
3108
|
serviceUrls;
|
|
3109
|
+
environment;
|
|
2860
3110
|
appIds;
|
|
2861
3111
|
requireConfirmForWrites;
|
|
2862
3112
|
timeoutMs;
|
|
@@ -2885,7 +3135,8 @@ var TopoloClient = class _TopoloClient {
|
|
|
2885
3135
|
if (!options.agent?.clientName) throw new TopoloAuthError("agent.clientName is required");
|
|
2886
3136
|
this.credential = options.credential;
|
|
2887
3137
|
this.agent = options.agent;
|
|
2888
|
-
this.
|
|
3138
|
+
this.environment = options.environment;
|
|
3139
|
+
this.serviceUrls = options.environment ? topoloServiceOrigins(options.serviceUrls, options.environment) : options.serviceUrls;
|
|
2889
3140
|
this.appIds = options.appIds;
|
|
2890
3141
|
this.requireConfirmForWrites = options.requireConfirmForWrites !== false;
|
|
2891
3142
|
this.timeoutMs = options.timeoutMs ?? 3e4;
|
|
@@ -2914,9 +3165,15 @@ var TopoloClient = class _TopoloClient {
|
|
|
2914
3165
|
);
|
|
2915
3166
|
}
|
|
2916
3167
|
await this.loadCurrentCredential();
|
|
2917
|
-
const resolvedService = opts.baseUrl ? { serviceKey: normalizeLookupKey(opts.service), baseUrl:
|
|
3168
|
+
const resolvedService = opts.baseUrl ? { serviceKey: normalizeLookupKey(opts.service), baseUrl: this.validatedServiceOrigin(opts.service, opts.baseUrl) } : await this.resolveRequestService(opts.service);
|
|
2918
3169
|
const baseUrl = resolvedService.baseUrl;
|
|
2919
3170
|
const url = new URL(opts.path, baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`);
|
|
3171
|
+
if (url.origin !== new URL(baseUrl).origin) {
|
|
3172
|
+
throw new TopoloSdkError(
|
|
3173
|
+
"service_path_origin_mismatch",
|
|
3174
|
+
`Refusing Topolo request path because it changes the resolved ${resolvedService.serviceKey || opts.service} service origin.`
|
|
3175
|
+
);
|
|
3176
|
+
}
|
|
2920
3177
|
if (opts.query) {
|
|
2921
3178
|
for (const [k, v] of Object.entries(opts.query)) {
|
|
2922
3179
|
if (v !== void 0) url.searchParams.set(k, String(v));
|
|
@@ -2987,7 +3244,7 @@ var TopoloClient = class _TopoloClient {
|
|
|
2987
3244
|
url: urlStr,
|
|
2988
3245
|
requestId,
|
|
2989
3246
|
durationMs: Date.now() - startedAt,
|
|
2990
|
-
error:
|
|
3247
|
+
error: "network_error"
|
|
2991
3248
|
});
|
|
2992
3249
|
throw err;
|
|
2993
3250
|
}
|
|
@@ -2996,6 +3253,7 @@ var TopoloClient = class _TopoloClient {
|
|
|
2996
3253
|
if (!res.ok) {
|
|
2997
3254
|
const durationMs = Date.now() - startedAt;
|
|
2998
3255
|
const message = describeError(parsed, `HTTP ${res.status}`);
|
|
3256
|
+
const publicCode = extractTopoloServiceErrorCode(parsed) ?? "http_error";
|
|
2999
3257
|
this.emit({
|
|
3000
3258
|
phase: "error",
|
|
3001
3259
|
method,
|
|
@@ -3004,7 +3262,7 @@ var TopoloClient = class _TopoloClient {
|
|
|
3004
3262
|
url: urlStr,
|
|
3005
3263
|
requestId,
|
|
3006
3264
|
durationMs,
|
|
3007
|
-
error:
|
|
3265
|
+
error: publicCode,
|
|
3008
3266
|
status: res.status
|
|
3009
3267
|
});
|
|
3010
3268
|
const errorContext = {
|
|
@@ -3012,17 +3270,21 @@ var TopoloClient = class _TopoloClient {
|
|
|
3012
3270
|
body: parsed,
|
|
3013
3271
|
service: resolvedService.serviceKey,
|
|
3014
3272
|
path: opts.path,
|
|
3015
|
-
requestId: res.headers.get("X-Topolo-Request-Id")
|
|
3016
|
-
retryAfter: res.headers.get("Retry-After")
|
|
3273
|
+
requestId: canonicalTopoloRequestId(res.headers.get("X-Topolo-Request-Id")) ?? requestId,
|
|
3274
|
+
retryAfter: canonicalTopoloRetryAfter(res.headers.get("Retry-After"))
|
|
3017
3275
|
};
|
|
3018
3276
|
if (res.status === 401) {
|
|
3019
|
-
throw new TopoloAuthError(
|
|
3277
|
+
throw new TopoloAuthError(
|
|
3278
|
+
describeError(parsed, "Unauthorized"),
|
|
3279
|
+
extractTopoloServiceErrorCode(parsed) ?? "auth_error",
|
|
3280
|
+
errorContext
|
|
3281
|
+
);
|
|
3020
3282
|
}
|
|
3021
3283
|
if (res.status === 403) {
|
|
3022
3284
|
throw new TopoloPermissionError(
|
|
3023
3285
|
describeError(parsed, "Permission denied"),
|
|
3024
3286
|
extractRequired(parsed),
|
|
3025
|
-
errorContext
|
|
3287
|
+
{ ...errorContext, code: extractTopoloServiceErrorCode(parsed) ?? "permission_denied" }
|
|
3026
3288
|
);
|
|
3027
3289
|
}
|
|
3028
3290
|
throw new TopoloHttpError(
|
|
@@ -3072,6 +3334,17 @@ var TopoloClient = class _TopoloClient {
|
|
|
3072
3334
|
});
|
|
3073
3335
|
return this.refreshInFlight;
|
|
3074
3336
|
}
|
|
3337
|
+
validatedServiceOrigin(service, value) {
|
|
3338
|
+
const origin = requireHttpOrigin(value);
|
|
3339
|
+
return this.environment ? assertTopoloServiceUrlEnvironment(service, origin, this.environment) : origin;
|
|
3340
|
+
}
|
|
3341
|
+
validatedActionEntry(entry) {
|
|
3342
|
+
if (!entry.apiBaseUrl) return entry;
|
|
3343
|
+
return {
|
|
3344
|
+
...entry,
|
|
3345
|
+
apiBaseUrl: this.validatedServiceOrigin(entry.appSlug || entry.appId, entry.apiBaseUrl)
|
|
3346
|
+
};
|
|
3347
|
+
}
|
|
3075
3348
|
async resolveRequestService(service) {
|
|
3076
3349
|
const serviceKey = normalizeLookupKey(service);
|
|
3077
3350
|
if (!serviceKey) {
|
|
@@ -3079,17 +3352,20 @@ var TopoloClient = class _TopoloClient {
|
|
|
3079
3352
|
}
|
|
3080
3353
|
if (serviceKey === "auth" || serviceKey === "developers") {
|
|
3081
3354
|
const overrideUrl = resolveConfiguredServiceUrl(serviceKey, this.serviceUrls);
|
|
3082
|
-
if (overrideUrl) return { serviceKey, baseUrl: overrideUrl };
|
|
3355
|
+
if (overrideUrl) return { serviceKey, baseUrl: this.validatedServiceOrigin(serviceKey, overrideUrl) };
|
|
3083
3356
|
if (serviceKey === "developers") {
|
|
3084
3357
|
const derived = deriveDevelopersFromAuth(resolveConfiguredServiceUrl("auth", this.serviceUrls));
|
|
3085
|
-
if (derived) return { serviceKey, baseUrl: derived };
|
|
3358
|
+
if (derived) return { serviceKey, baseUrl: this.validatedServiceOrigin(serviceKey, derived) };
|
|
3086
3359
|
}
|
|
3087
|
-
return {
|
|
3360
|
+
return {
|
|
3361
|
+
serviceKey,
|
|
3362
|
+
baseUrl: this.validatedServiceOrigin(serviceKey, DEFAULT_APP_URLS[serviceKey])
|
|
3363
|
+
};
|
|
3088
3364
|
}
|
|
3089
3365
|
const live = await this.resolveLiveCallableService(serviceKey);
|
|
3090
3366
|
if (live) {
|
|
3091
3367
|
const overrideUrl = resolveConfiguredServiceUrl(serviceKey, this.serviceUrls);
|
|
3092
|
-
return overrideUrl ? { ...live, baseUrl: overrideUrl } : live;
|
|
3368
|
+
return overrideUrl ? { ...live, baseUrl: this.validatedServiceOrigin(serviceKey, overrideUrl) } : { ...live, baseUrl: this.validatedServiceOrigin(serviceKey, live.baseUrl) };
|
|
3093
3369
|
}
|
|
3094
3370
|
throw new TopoloSdkError(
|
|
3095
3371
|
"unknown_service",
|
|
@@ -3103,10 +3379,10 @@ var TopoloClient = class _TopoloClient {
|
|
|
3103
3379
|
serviceKey,
|
|
3104
3380
|
overrides: this.serviceUrls
|
|
3105
3381
|
});
|
|
3106
|
-
if (baseUrl) return { serviceKey, baseUrl };
|
|
3382
|
+
if (baseUrl) return { serviceKey, baseUrl: this.validatedServiceOrigin(serviceKey, baseUrl) };
|
|
3107
3383
|
const actionPage = await this.searchActions({ service: match.appId, limit: 1 }).catch(() => null);
|
|
3108
3384
|
const actionBaseUrl = actionPage?.actions.find((action) => action.apiBaseUrl)?.apiBaseUrl ?? null;
|
|
3109
|
-
return actionBaseUrl ? { serviceKey, baseUrl:
|
|
3385
|
+
return actionBaseUrl ? { serviceKey, baseUrl: this.validatedServiceOrigin(serviceKey, actionBaseUrl) } : null;
|
|
3110
3386
|
}
|
|
3111
3387
|
/**
|
|
3112
3388
|
* Resolve a service-slug → opaque app-id via caller config or TopoloAuth.
|
|
@@ -3126,7 +3402,7 @@ var TopoloClient = class _TopoloClient {
|
|
|
3126
3402
|
if (!normalized) return null;
|
|
3127
3403
|
const configuredAppId = resolveConfiguredAppId(normalized, this.appIds);
|
|
3128
3404
|
if (configuredAppId) return configuredAppId;
|
|
3129
|
-
const authBase = resolveServiceUrl("developers", this.serviceUrls);
|
|
3405
|
+
const authBase = this.validatedServiceOrigin("developers", resolveServiceUrl("developers", this.serviceUrls));
|
|
3130
3406
|
const cacheKey = `${authBase}|${normalized}`;
|
|
3131
3407
|
const now = Date.now();
|
|
3132
3408
|
const cached = _TopoloClient.appIdCache.get(cacheKey);
|
|
@@ -3302,7 +3578,7 @@ var TopoloClient = class _TopoloClient {
|
|
|
3302
3578
|
"TopoloAuth did not return an action catalog for the current credential."
|
|
3303
3579
|
);
|
|
3304
3580
|
}
|
|
3305
|
-
const entries = raw.map((item) => normalizeActionCatalogEntry(item)).filter((entry) => Boolean(entry));
|
|
3581
|
+
const entries = raw.map((item) => normalizeActionCatalogEntry(item)).filter((entry) => Boolean(entry)).map((entry) => this.validatedActionEntry(entry));
|
|
3306
3582
|
return {
|
|
3307
3583
|
actions: entries,
|
|
3308
3584
|
page: normalizeCatalogPage(
|
|
@@ -3367,7 +3643,7 @@ var TopoloClient = class _TopoloClient {
|
|
|
3367
3643
|
const raw = res.data?.action ?? res.action;
|
|
3368
3644
|
const match = normalizeActionCatalogEntry(raw);
|
|
3369
3645
|
if (!match) throw new TopoloSdkError("unknown_action", `Unknown or unavailable Topolo action "${action}".`);
|
|
3370
|
-
return match;
|
|
3646
|
+
return this.validatedActionEntry(match);
|
|
3371
3647
|
}
|
|
3372
3648
|
async callAction(action, input3 = {}, options = {}) {
|
|
3373
3649
|
const entry = await this.getAction(action);
|
|
@@ -3444,6 +3720,25 @@ var TopoloClient = class _TopoloClient {
|
|
|
3444
3720
|
}
|
|
3445
3721
|
async planAction(action, input3 = {}, options = {}) {
|
|
3446
3722
|
const entry = await this.getAction(action);
|
|
3723
|
+
return this.planResolvedAction(entry, input3, options);
|
|
3724
|
+
}
|
|
3725
|
+
async prepareAction(action, input3, options = {}) {
|
|
3726
|
+
const entry = await this.getAction(action);
|
|
3727
|
+
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;
|
|
3728
|
+
const selectedInput = input3 ?? example?.input ?? {};
|
|
3729
|
+
const plan = this.planResolvedAction(entry, selectedInput, options);
|
|
3730
|
+
return {
|
|
3731
|
+
action: entry,
|
|
3732
|
+
example,
|
|
3733
|
+
input: selectedInput,
|
|
3734
|
+
inputSource: input3 ? "provided" : example ? "example" : "empty",
|
|
3735
|
+
missingFields: [...new Set(
|
|
3736
|
+
plan.validation.errors.filter((issue) => issue.code === "required").map((issue) => issue.path)
|
|
3737
|
+
)],
|
|
3738
|
+
plan
|
|
3739
|
+
};
|
|
3740
|
+
}
|
|
3741
|
+
planResolvedAction(entry, input3, options) {
|
|
3447
3742
|
const validation = validateTopoloActionInput(entry.inputSchema, input3);
|
|
3448
3743
|
const agentAccess = entry.agentAccess ?? (entry.destructive ? "off" : entry.readOnly && !entry.requiresConfirmation ? "auto" : "confirm");
|
|
3449
3744
|
const confirmationRequired = agentAccess === "confirm" || entry.requiresConfirmation || entry.destructive;
|
|
@@ -3901,7 +4196,16 @@ function pickString(value) {
|
|
|
3901
4196
|
function extractRequired(body) {
|
|
3902
4197
|
if (body && typeof body === "object") {
|
|
3903
4198
|
const maybe = body;
|
|
3904
|
-
if (Array.isArray(maybe.required)) return maybe.required
|
|
4199
|
+
if (Array.isArray(maybe.required)) return canonicalTopoloRequired(maybe.required);
|
|
4200
|
+
for (const nested of [maybe.details, maybe.error]) {
|
|
4201
|
+
if (!nested || typeof nested !== "object" || Array.isArray(nested)) continue;
|
|
4202
|
+
const record4 = nested;
|
|
4203
|
+
if (Array.isArray(record4.required)) return canonicalTopoloRequired(record4.required);
|
|
4204
|
+
if (record4.details && typeof record4.details === "object" && !Array.isArray(record4.details)) {
|
|
4205
|
+
const required = record4.details.required;
|
|
4206
|
+
if (Array.isArray(required)) return canonicalTopoloRequired(required);
|
|
4207
|
+
}
|
|
4208
|
+
}
|
|
3905
4209
|
}
|
|
3906
4210
|
return [];
|
|
3907
4211
|
}
|
|
@@ -3949,6 +4253,72 @@ function compactActionPlan(plan) {
|
|
|
3949
4253
|
nextActionIds: plan.nextActionIds
|
|
3950
4254
|
};
|
|
3951
4255
|
}
|
|
4256
|
+
function requiredContextText(value, field) {
|
|
4257
|
+
if (typeof value !== "string" || !value.trim() || value.trim().toLowerCase() === "unknown") {
|
|
4258
|
+
throw new TopoloSdkError("invalid_identity_context", `Effective Topolo ${field} is incomplete.`);
|
|
4259
|
+
}
|
|
4260
|
+
return value.trim();
|
|
4261
|
+
}
|
|
4262
|
+
function createTopoloAgentExecutionContext(input3) {
|
|
4263
|
+
if (input3.contextKey !== null && input3.contextKey !== "personal" && !/^org:[^:]+$/.test(input3.contextKey)) {
|
|
4264
|
+
throw new TopoloSdkError("invalid_identity_context", "Effective Topolo context key is invalid.");
|
|
4265
|
+
}
|
|
4266
|
+
const user = Object.freeze({
|
|
4267
|
+
id: requiredContextText(input3.identity.user.id, "user id"),
|
|
4268
|
+
email: requiredContextText(input3.identity.user.email, "user email"),
|
|
4269
|
+
name: input3.identity.user.name?.trim() || null,
|
|
4270
|
+
role: requiredContextText(input3.identity.user.role, "user role")
|
|
4271
|
+
});
|
|
4272
|
+
const organization = input3.identity.organization ? Object.freeze({
|
|
4273
|
+
id: requiredContextText(input3.identity.organization.id, "organization id"),
|
|
4274
|
+
slug: requiredContextText(input3.identity.organization.slug, "organization slug"),
|
|
4275
|
+
name: requiredContextText(input3.identity.organization.name, "organization name")
|
|
4276
|
+
}) : null;
|
|
4277
|
+
if (input3.contextKey === "personal" && organization) {
|
|
4278
|
+
throw new TopoloSdkError("identity_context_mismatch", "Personal Topolo context resolved an organization.");
|
|
4279
|
+
}
|
|
4280
|
+
if (input3.contextKey?.startsWith("org:")) {
|
|
4281
|
+
const expectedOrganizationId = input3.contextKey.slice("org:".length);
|
|
4282
|
+
if (!organization || organization.id !== expectedOrganizationId) {
|
|
4283
|
+
throw new TopoloSdkError("identity_context_mismatch", "Pinned Topolo organization does not match the effective identity.");
|
|
4284
|
+
}
|
|
4285
|
+
}
|
|
4286
|
+
return Object.freeze({
|
|
4287
|
+
environment: input3.environment,
|
|
4288
|
+
contextKey: input3.contextKey,
|
|
4289
|
+
user,
|
|
4290
|
+
organization,
|
|
4291
|
+
serviceOrigins: Object.freeze(topoloServiceOrigins(input3.serviceUrls, input3.environment))
|
|
4292
|
+
});
|
|
4293
|
+
}
|
|
4294
|
+
function compactActionPreparationCore(preparation) {
|
|
4295
|
+
const example = preparation.example ? { title: preparation.example.title ?? null, input: preparation.example.input } : null;
|
|
4296
|
+
const effectiveInput = preparation.inputSource === "example" ? { source: "example" } : { source: preparation.inputSource, value: preparation.input };
|
|
4297
|
+
return {
|
|
4298
|
+
action: {
|
|
4299
|
+
id: preparation.action.actionId,
|
|
4300
|
+
title: preparation.action.title,
|
|
4301
|
+
docsUrl: preparation.action.agentContract?.docsUrl ?? null
|
|
4302
|
+
},
|
|
4303
|
+
example,
|
|
4304
|
+
input: effectiveInput,
|
|
4305
|
+
missingFields: preparation.missingFields,
|
|
4306
|
+
valid: preparation.plan.validation.valid,
|
|
4307
|
+
errors: preparation.plan.validation.errors,
|
|
4308
|
+
request: preparation.plan.request,
|
|
4309
|
+
confirmation: {
|
|
4310
|
+
required: preparation.plan.confirmationRequired,
|
|
4311
|
+
provided: preparation.plan.confirmationProvided
|
|
4312
|
+
},
|
|
4313
|
+
executable: preparation.plan.executable,
|
|
4314
|
+
verification: preparation.plan.verification,
|
|
4315
|
+
rollbackActionId: preparation.plan.rollbackActionId
|
|
4316
|
+
};
|
|
4317
|
+
}
|
|
4318
|
+
function compactActionPreparation(preparation, context) {
|
|
4319
|
+
const compact = compactActionPreparationCore(preparation);
|
|
4320
|
+
return context ? { context, ...compact } : compact;
|
|
4321
|
+
}
|
|
3952
4322
|
function compactApplicationAudit(report) {
|
|
3953
4323
|
return {
|
|
3954
4324
|
version: report.version,
|
|
@@ -4165,7 +4535,7 @@ function createTopolo(options) {
|
|
|
4165
4535
|
};
|
|
4166
4536
|
}
|
|
4167
4537
|
|
|
4168
|
-
//
|
|
4538
|
+
// ../../../development-workflow-acceleration/node_modules/.pnpm/@topolo+sdk@0.10.4/node_modules/@topolo/sdk/dist/node/credential-store.js
|
|
4169
4539
|
import { mkdir, readFile, writeFile, chmod } from "fs/promises";
|
|
4170
4540
|
import { existsSync } from "fs";
|
|
4171
4541
|
import { homedir, platform } from "os";
|
|
@@ -4178,6 +4548,90 @@ var TopoloSdkError2 = class extends Error {
|
|
|
4178
4548
|
this.code = code;
|
|
4179
4549
|
}
|
|
4180
4550
|
};
|
|
4551
|
+
var PUBLIC_ERROR_CODES2 = /* @__PURE__ */ new Set([
|
|
4552
|
+
"action_app_identity_unavailable",
|
|
4553
|
+
"action_authentication_required",
|
|
4554
|
+
"action_catalog_summary_unavailable",
|
|
4555
|
+
"action_catalog_unavailable",
|
|
4556
|
+
"action_contract_mismatch",
|
|
4557
|
+
"action_permission_required",
|
|
4558
|
+
"action_runtime_authorization_required",
|
|
4559
|
+
"action_service_not_allowed",
|
|
4560
|
+
"auth_error",
|
|
4561
|
+
"authentication_required",
|
|
4562
|
+
"authorization_pending",
|
|
4563
|
+
"bad_sig",
|
|
4564
|
+
"confirmation_required",
|
|
4565
|
+
"dependency_unavailable",
|
|
4566
|
+
"environment_mismatch",
|
|
4567
|
+
"environment_service_url_required",
|
|
4568
|
+
"expired_token",
|
|
4569
|
+
"forbidden",
|
|
4570
|
+
"http_error",
|
|
4571
|
+
"immutable_drift",
|
|
4572
|
+
"integration_dependency_unavailable",
|
|
4573
|
+
"integration_misconfigured",
|
|
4574
|
+
"invalid_action",
|
|
4575
|
+
"invalid_action_input",
|
|
4576
|
+
"invalid_grant",
|
|
4577
|
+
"invalid_input",
|
|
4578
|
+
"invalid_token",
|
|
4579
|
+
"mcp_error",
|
|
4580
|
+
"membership_required",
|
|
4581
|
+
"network_error",
|
|
4582
|
+
"organization_context_required",
|
|
4583
|
+
"organization_enrollment_required",
|
|
4584
|
+
"permission_denied",
|
|
4585
|
+
"rate_limited",
|
|
4586
|
+
"request_failed",
|
|
4587
|
+
"service_catalog_unavailable",
|
|
4588
|
+
"service_path_origin_mismatch",
|
|
4589
|
+
"service_required",
|
|
4590
|
+
"slow_down",
|
|
4591
|
+
"state_conflict",
|
|
4592
|
+
"unauthorized",
|
|
4593
|
+
"unknown_action",
|
|
4594
|
+
"unknown_service"
|
|
4595
|
+
]);
|
|
4596
|
+
var ERROR_CODE_PATTERN2 = /^[a-z][a-z0-9_]{0,63}$/;
|
|
4597
|
+
var TOPOLO_REQUEST_ID_PATTERN2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
4598
|
+
var CLOUDFLARE_RAY_ID_PATTERN2 = /^[0-9a-f]{16}-[A-Z]{3}$/;
|
|
4599
|
+
var PERMISSION_PATTERN2 = /^[a-z][a-z0-9_-]{0,63}(?:\.[a-z][a-z0-9_-]{0,63})*:(?:[a-z][a-z0-9_-]{0,63}|\*)$/;
|
|
4600
|
+
var RETRY_AFTER_DATE_PATTERN2 = /^(?: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$/;
|
|
4601
|
+
var PUBLIC_DETAIL_SOURCES2 = /* @__PURE__ */ new Set([
|
|
4602
|
+
"api_key_introspection",
|
|
4603
|
+
"api_key_validation",
|
|
4604
|
+
"auth_context",
|
|
4605
|
+
"auth_validate",
|
|
4606
|
+
"catalog",
|
|
4607
|
+
"credential",
|
|
4608
|
+
"runtime",
|
|
4609
|
+
"service_token_provider",
|
|
4610
|
+
"service_verify_key",
|
|
4611
|
+
"token_validation",
|
|
4612
|
+
"upstream"
|
|
4613
|
+
]);
|
|
4614
|
+
function canonicalTopoloErrorCode2(value) {
|
|
4615
|
+
if (typeof value !== "string") return null;
|
|
4616
|
+
const candidate = value.trim().toLowerCase();
|
|
4617
|
+
if (!ERROR_CODE_PATTERN2.test(candidate) || !PUBLIC_ERROR_CODES2.has(candidate)) return null;
|
|
4618
|
+
return candidate;
|
|
4619
|
+
}
|
|
4620
|
+
function canonicalTopoloRequestId2(value) {
|
|
4621
|
+
if (typeof value !== "string") return void 0;
|
|
4622
|
+
const candidate = value.trim();
|
|
4623
|
+
return TOPOLO_REQUEST_ID_PATTERN2.test(candidate) || CLOUDFLARE_RAY_ID_PATTERN2.test(candidate) ? candidate : void 0;
|
|
4624
|
+
}
|
|
4625
|
+
function canonicalTopoloRetryAfter2(value) {
|
|
4626
|
+
if (typeof value !== "string") return void 0;
|
|
4627
|
+
const candidate = value.trim();
|
|
4628
|
+
if (/^\d{1,10}$/.test(candidate) || RETRY_AFTER_DATE_PATTERN2.test(candidate)) return candidate;
|
|
4629
|
+
return void 0;
|
|
4630
|
+
}
|
|
4631
|
+
function canonicalTopoloRequired2(value) {
|
|
4632
|
+
if (!Array.isArray(value)) return [];
|
|
4633
|
+
return [...new Set(value.filter((item) => typeof item === "string" && PERMISSION_PATTERN2.test(item)))].slice(0, 32);
|
|
4634
|
+
}
|
|
4181
4635
|
var TopoloAuthError2 = class extends TopoloSdkError2 {
|
|
4182
4636
|
status;
|
|
4183
4637
|
body;
|
|
@@ -4185,27 +4639,91 @@ var TopoloAuthError2 = class extends TopoloSdkError2 {
|
|
|
4185
4639
|
path;
|
|
4186
4640
|
requestId;
|
|
4187
4641
|
retryAfter;
|
|
4642
|
+
details;
|
|
4188
4643
|
constructor(message, code = "auth_error", options = {}) {
|
|
4189
|
-
super(code, message);
|
|
4644
|
+
super(canonicalTopoloErrorCode2(code) ?? "auth_error", message);
|
|
4190
4645
|
this.name = "TopoloAuthError";
|
|
4191
4646
|
this.status = options.status ?? 401;
|
|
4192
4647
|
this.body = options.body;
|
|
4193
4648
|
this.service = options.service;
|
|
4194
4649
|
this.path = options.path;
|
|
4195
|
-
this.requestId = options.requestId;
|
|
4196
|
-
this.retryAfter = options.retryAfter;
|
|
4650
|
+
this.requestId = canonicalTopoloRequestId2(options.requestId);
|
|
4651
|
+
this.retryAfter = canonicalTopoloRetryAfter2(options.retryAfter);
|
|
4652
|
+
this.details = extractTopoloServiceErrorDetails2(
|
|
4653
|
+
options.details === void 0 ? options.body : { details: options.details }
|
|
4654
|
+
);
|
|
4197
4655
|
}
|
|
4198
4656
|
};
|
|
4199
|
-
function
|
|
4200
|
-
if (!body || typeof body !== "object" || Array.isArray(body)) return
|
|
4657
|
+
function extractTopoloServiceErrorCode2(body) {
|
|
4658
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) return null;
|
|
4201
4659
|
const record4 = body;
|
|
4202
|
-
|
|
4660
|
+
const topLevel = canonicalTopoloErrorCode2(record4.code);
|
|
4661
|
+
if (topLevel) return topLevel;
|
|
4203
4662
|
const nested = record4.error;
|
|
4663
|
+
const nestedString = canonicalTopoloErrorCode2(nested);
|
|
4664
|
+
if (nestedString) return nestedString;
|
|
4204
4665
|
if (nested && typeof nested === "object" && !Array.isArray(nested)) {
|
|
4205
4666
|
const code = nested.code;
|
|
4206
|
-
|
|
4667
|
+
const nestedCode = canonicalTopoloErrorCode2(code);
|
|
4668
|
+
if (nestedCode) return nestedCode;
|
|
4669
|
+
}
|
|
4670
|
+
return null;
|
|
4671
|
+
}
|
|
4672
|
+
function serviceErrorCode2(body, fallback) {
|
|
4673
|
+
return extractTopoloServiceErrorCode2(body) ?? fallback;
|
|
4674
|
+
}
|
|
4675
|
+
function extractTopoloServiceErrorDetails2(body) {
|
|
4676
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) return void 0;
|
|
4677
|
+
const record4 = body;
|
|
4678
|
+
let candidate = record4.details;
|
|
4679
|
+
const nested = record4.error;
|
|
4680
|
+
if (nested && typeof nested === "object" && !Array.isArray(nested)) {
|
|
4681
|
+
const nestedRecord = nested;
|
|
4682
|
+
if (candidate === void 0) candidate = nestedRecord.details ?? nestedRecord;
|
|
4683
|
+
}
|
|
4684
|
+
if (candidate === void 0) candidate = record4;
|
|
4685
|
+
if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) return void 0;
|
|
4686
|
+
const source = candidate;
|
|
4687
|
+
const safe = {};
|
|
4688
|
+
for (const [key, value] of Object.entries(source)) {
|
|
4689
|
+
if (/present$/i.test(key) && typeof value === "boolean") {
|
|
4690
|
+
safe[key] = value;
|
|
4691
|
+
continue;
|
|
4692
|
+
}
|
|
4693
|
+
if (key === "retryable" && typeof value === "boolean") {
|
|
4694
|
+
safe.retryable = value;
|
|
4695
|
+
continue;
|
|
4696
|
+
}
|
|
4697
|
+
if (key === "code") {
|
|
4698
|
+
const sanitized = canonicalTopoloErrorCode2(value);
|
|
4699
|
+
if (sanitized) safe.code = sanitized;
|
|
4700
|
+
continue;
|
|
4701
|
+
}
|
|
4702
|
+
if (key === "source") {
|
|
4703
|
+
const sanitized = typeof value === "string" ? value.trim().toLowerCase() : "";
|
|
4704
|
+
if (PUBLIC_DETAIL_SOURCES2.has(sanitized)) safe.source = sanitized;
|
|
4705
|
+
continue;
|
|
4706
|
+
}
|
|
4707
|
+
if (key === "dependency" || key === "platform") {
|
|
4708
|
+
safe[`${key}Present`] = value !== null && value !== void 0;
|
|
4709
|
+
continue;
|
|
4710
|
+
}
|
|
4711
|
+
if (key === "error" && !safe.code) {
|
|
4712
|
+
const sanitized = canonicalTopoloErrorCode2(value);
|
|
4713
|
+
if (sanitized) safe.code = sanitized;
|
|
4714
|
+
continue;
|
|
4715
|
+
}
|
|
4716
|
+
if (key === "required" && Array.isArray(value)) {
|
|
4717
|
+
const required = canonicalTopoloRequired2(value);
|
|
4718
|
+
if (required.length) safe.required = required;
|
|
4719
|
+
continue;
|
|
4720
|
+
}
|
|
4721
|
+
if (["missingFields", "fields", "paths"].includes(key) && Array.isArray(value)) {
|
|
4722
|
+
const paths = value.filter((item) => typeof item === "string" && /^[$.a-z0-9_[\]-]{1,160}$/i.test(item));
|
|
4723
|
+
if (paths.length) safe[key] = paths;
|
|
4724
|
+
}
|
|
4207
4725
|
}
|
|
4208
|
-
return
|
|
4726
|
+
return Object.keys(safe).length ? safe : void 0;
|
|
4209
4727
|
}
|
|
4210
4728
|
var TopoloHttpError2 = class extends TopoloSdkError2 {
|
|
4211
4729
|
status;
|
|
@@ -4214,15 +4732,19 @@ var TopoloHttpError2 = class extends TopoloSdkError2 {
|
|
|
4214
4732
|
path;
|
|
4215
4733
|
requestId;
|
|
4216
4734
|
retryAfter;
|
|
4735
|
+
details;
|
|
4217
4736
|
constructor(service, path9, status, body, message, options = {}) {
|
|
4218
|
-
super(
|
|
4737
|
+
super(serviceErrorCode2(body, "http_error"), message ?? `HTTP ${status} from ${service}${path9}`);
|
|
4219
4738
|
this.name = "TopoloHttpError";
|
|
4220
4739
|
this.status = status;
|
|
4221
4740
|
this.body = body;
|
|
4222
4741
|
this.service = service;
|
|
4223
4742
|
this.path = path9;
|
|
4224
|
-
this.requestId = options.requestId;
|
|
4225
|
-
this.retryAfter = options.retryAfter;
|
|
4743
|
+
this.requestId = canonicalTopoloRequestId2(options.requestId);
|
|
4744
|
+
this.retryAfter = canonicalTopoloRetryAfter2(options.retryAfter);
|
|
4745
|
+
this.details = extractTopoloServiceErrorDetails2(
|
|
4746
|
+
options.details === void 0 ? body : { details: options.details }
|
|
4747
|
+
);
|
|
4226
4748
|
}
|
|
4227
4749
|
};
|
|
4228
4750
|
var APP_API_URLS2 = {
|
|
@@ -4328,11 +4850,9 @@ var TopoloOAuth2 = class {
|
|
|
4328
4850
|
return parsed;
|
|
4329
4851
|
}
|
|
4330
4852
|
};
|
|
4331
|
-
|
|
4332
|
-
return contextType === "organization" && organizationId ? `org:${organizationId}` : "personal";
|
|
4333
|
-
}
|
|
4334
|
-
var ENVIRONMENT_DEFAULT_SERVICE_URLS = {
|
|
4853
|
+
var TOPOLO_ENVIRONMENT_SERVICE_URLS = {
|
|
4335
4854
|
production: {
|
|
4855
|
+
auth: "https://auth.topolo.app",
|
|
4336
4856
|
developers: "https://developers.topolo.app"
|
|
4337
4857
|
},
|
|
4338
4858
|
staging: {
|
|
@@ -4346,6 +4866,55 @@ var ENVIRONMENT_DEFAULT_SERVICE_URLS = {
|
|
|
4346
4866
|
},
|
|
4347
4867
|
local: {}
|
|
4348
4868
|
};
|
|
4869
|
+
function hostnameHasSuffix2(hostname, suffix) {
|
|
4870
|
+
return hostname === suffix || hostname.endsWith(`.${suffix}`);
|
|
4871
|
+
}
|
|
4872
|
+
function workersDevAccountMatches2(hostname, account) {
|
|
4873
|
+
if (!hostname.endsWith(".workers.dev")) return false;
|
|
4874
|
+
const labels = hostname.slice(0, -".workers.dev".length).split(".");
|
|
4875
|
+
return labels.length >= 2 && labels.at(-1) === account;
|
|
4876
|
+
}
|
|
4877
|
+
function originMatchesEnvironment2(url, environment) {
|
|
4878
|
+
const hostname = url.hostname.toLowerCase();
|
|
4879
|
+
if (environment === "local") {
|
|
4880
|
+
return hostname === "localhost" || hostname.endsWith(".localhost") || hostname === "127.0.0.1" || hostname === "[::1]";
|
|
4881
|
+
}
|
|
4882
|
+
if (url.protocol !== "https:") return false;
|
|
4883
|
+
if (environment === "production") {
|
|
4884
|
+
return hostnameHasSuffix2(hostname, "topolo.app") || hostnameHasSuffix2(hostname, "topolo.io") || workersDevAccountMatches2(hostname, "topolo");
|
|
4885
|
+
}
|
|
4886
|
+
if (environment === "staging") {
|
|
4887
|
+
return hostnameHasSuffix2(hostname, "stg.topolo.us") || workersDevAccountMatches2(hostname, "topolo-staging");
|
|
4888
|
+
}
|
|
4889
|
+
return hostnameHasSuffix2(hostname, "topolo.dev") || workersDevAccountMatches2(hostname, "topolo-development");
|
|
4890
|
+
}
|
|
4891
|
+
function assertTopoloServiceUrlEnvironment2(service, value, environment) {
|
|
4892
|
+
let url;
|
|
4893
|
+
try {
|
|
4894
|
+
url = new URL(value);
|
|
4895
|
+
} catch {
|
|
4896
|
+
throw new TopoloSdkError2("invalid_service_url", `Invalid URL configured for Topolo service "${service}".`);
|
|
4897
|
+
}
|
|
4898
|
+
if (url.protocol !== "https:" && url.protocol !== "http:" || url.username || url.password || !originMatchesEnvironment2(url, environment)) {
|
|
4899
|
+
throw new TopoloSdkError2(
|
|
4900
|
+
"environment_mismatch",
|
|
4901
|
+
`Refusing Topolo service "${service}": ${url.origin} does not belong to ${environment}.`
|
|
4902
|
+
);
|
|
4903
|
+
}
|
|
4904
|
+
return url.origin;
|
|
4905
|
+
}
|
|
4906
|
+
function topoloServiceOrigins2(serviceUrls, environment) {
|
|
4907
|
+
return Object.fromEntries(
|
|
4908
|
+
Object.entries(serviceUrls ?? {}).sort(([left], [right]) => left.localeCompare(right)).map(([service, value]) => [
|
|
4909
|
+
service,
|
|
4910
|
+
assertTopoloServiceUrlEnvironment2(service, value, environment)
|
|
4911
|
+
])
|
|
4912
|
+
);
|
|
4913
|
+
}
|
|
4914
|
+
function contextKey(contextType, organizationId) {
|
|
4915
|
+
return contextType === "organization" && organizationId ? `org:${organizationId}` : "personal";
|
|
4916
|
+
}
|
|
4917
|
+
var ENVIRONMENT_DEFAULT_SERVICE_URLS = TOPOLO_ENVIRONMENT_SERVICE_URLS;
|
|
4349
4918
|
function configPath() {
|
|
4350
4919
|
const override = process.env.TOPOLO_CONFIG_PATH;
|
|
4351
4920
|
if (override) return override;
|
|
@@ -4453,15 +5022,17 @@ function credentialSourceFromConfig(config, environment = selectedEnvironment())
|
|
|
4453
5022
|
const prefix = envVarPrefix(environment);
|
|
4454
5023
|
if (process.env[`${prefix}_API_KEY`]) return `env:${prefix}_API_KEY`;
|
|
4455
5024
|
if (process.env[`${prefix}_ACCESS_TOKEN`]) return `env:${prefix}_ACCESS_TOKEN`;
|
|
4456
|
-
if (process.env.TOPOLO_API_KEY) return "env:TOPOLO_API_KEY";
|
|
4457
|
-
if (process.env.TOPOLO_ACCESS_TOKEN)
|
|
5025
|
+
if (environment === "production" && process.env.TOPOLO_API_KEY) return "env:TOPOLO_API_KEY";
|
|
5026
|
+
if (environment === "production" && process.env.TOPOLO_ACCESS_TOKEN) {
|
|
5027
|
+
return "env:TOPOLO_ACCESS_TOKEN";
|
|
5028
|
+
}
|
|
4458
5029
|
return profileForEnvironment(config, environment).credential ? "config-file" : "none";
|
|
4459
5030
|
}
|
|
4460
5031
|
async function resolveCredential(environment = selectedEnvironment()) {
|
|
4461
5032
|
const prefix = envVarPrefix(environment);
|
|
4462
|
-
const apiKey = process.env[`${prefix}_API_KEY`] ?? process.env.TOPOLO_API_KEY;
|
|
5033
|
+
const apiKey = process.env[`${prefix}_API_KEY`] ?? (environment === "production" ? process.env.TOPOLO_API_KEY : void 0);
|
|
4463
5034
|
if (apiKey) return { kind: "api_key", apiKey };
|
|
4464
|
-
const accessToken = process.env[`${prefix}_ACCESS_TOKEN`] ?? process.env.TOPOLO_ACCESS_TOKEN;
|
|
5035
|
+
const accessToken = process.env[`${prefix}_ACCESS_TOKEN`] ?? (environment === "production" ? process.env.TOPOLO_ACCESS_TOKEN : void 0);
|
|
4465
5036
|
if (accessToken) return { kind: "access_token", accessToken };
|
|
4466
5037
|
const config = await readConfig();
|
|
4467
5038
|
return profileForEnvironment(config, environment).credential ?? null;
|
|
@@ -4469,7 +5040,7 @@ async function resolveCredential(environment = selectedEnvironment()) {
|
|
|
4469
5040
|
var REFRESH_SKEW_SECONDS = 60;
|
|
4470
5041
|
async function refreshCredentialIfNeeded(environment = selectedEnvironment(), options = {}) {
|
|
4471
5042
|
const prefix = envVarPrefix(environment);
|
|
4472
|
-
if (process.env[`${prefix}_API_KEY`] || process.env[`${prefix}_ACCESS_TOKEN`] || process.env.TOPOLO_API_KEY || process.env.TOPOLO_ACCESS_TOKEN) {
|
|
5043
|
+
if (process.env[`${prefix}_API_KEY`] || process.env[`${prefix}_ACCESS_TOKEN`] || environment === "production" && process.env.TOPOLO_API_KEY || environment === "production" && process.env.TOPOLO_ACCESS_TOKEN) {
|
|
4473
5044
|
return resolveCredential(environment);
|
|
4474
5045
|
}
|
|
4475
5046
|
const config = await readConfig();
|
|
@@ -4479,9 +5050,16 @@ async function refreshCredentialIfNeeded(environment = selectedEnvironment(), op
|
|
|
4479
5050
|
if (!cred.refreshToken || !profile.oauthClientId) return cred;
|
|
4480
5051
|
const now = Math.floor(Date.now() / 1e3);
|
|
4481
5052
|
if (!options.force && cred.expiresAt && cred.expiresAt - REFRESH_SKEW_SECONDS > now) return cred;
|
|
5053
|
+
const serviceUrls = resolveServiceUrls(profile.serviceUrls, environment);
|
|
5054
|
+
if (environment === "local" && !serviceUrls?.auth) {
|
|
5055
|
+
throw new TopoloSdkError2(
|
|
5056
|
+
"environment_service_url_required",
|
|
5057
|
+
"Local Topolo credential refresh requires an explicit local Auth service origin."
|
|
5058
|
+
);
|
|
5059
|
+
}
|
|
4482
5060
|
try {
|
|
4483
5061
|
const oauth = new TopoloOAuth2({
|
|
4484
|
-
serviceUrls
|
|
5062
|
+
serviceUrls
|
|
4485
5063
|
});
|
|
4486
5064
|
const tokens = await oauth.refreshToken({
|
|
4487
5065
|
clientId: profile.oauthClientId,
|
|
@@ -4522,9 +5100,16 @@ async function refreshContextCredentialIfNeeded(environment, key, options = {})
|
|
|
4522
5100
|
if (!credential.refreshToken || !oauthClientId) return credential;
|
|
4523
5101
|
const now = Math.floor(Date.now() / 1e3);
|
|
4524
5102
|
if (!options.force && credential.expiresAt && credential.expiresAt - REFRESH_SKEW_SECONDS > now) return credential;
|
|
5103
|
+
const serviceUrls = resolveServiceUrls(profile.serviceUrls, environment);
|
|
5104
|
+
if (environment === "local" && !serviceUrls?.auth) {
|
|
5105
|
+
throw new TopoloSdkError2(
|
|
5106
|
+
"environment_service_url_required",
|
|
5107
|
+
"Local Topolo context refresh requires an explicit local Auth service origin."
|
|
5108
|
+
);
|
|
5109
|
+
}
|
|
4525
5110
|
try {
|
|
4526
5111
|
const oauth = new TopoloOAuth2({
|
|
4527
|
-
serviceUrls
|
|
5112
|
+
serviceUrls
|
|
4528
5113
|
});
|
|
4529
5114
|
const tokens = await oauth.refreshToken({
|
|
4530
5115
|
clientId: oauthClientId,
|
|
@@ -4571,7 +5156,12 @@ function resolveServiceUrls(configUrls, environment = selectedEnvironment()) {
|
|
|
4571
5156
|
merged[k.slice("TOPOLO_SERVICE_URL_".length).toLowerCase()] = v;
|
|
4572
5157
|
}
|
|
4573
5158
|
}
|
|
4574
|
-
|
|
5159
|
+
const origins = topoloServiceOrigins2(merged, environment);
|
|
5160
|
+
return Object.keys(origins).length > 0 ? origins : void 0;
|
|
5161
|
+
}
|
|
5162
|
+
function assertServiceUrlForEnvironment(service, serviceUrl, environment = selectedEnvironment()) {
|
|
5163
|
+
if (!serviceUrl) return;
|
|
5164
|
+
assertTopoloServiceUrlEnvironment2(service, serviceUrl, environment);
|
|
4575
5165
|
}
|
|
4576
5166
|
function resolvedProfileConfig(config, environment = selectedEnvironment()) {
|
|
4577
5167
|
const profile = profileForEnvironment(config, environment);
|
|
@@ -4612,24 +5202,6 @@ function resolveOAuthClientId(config, environment = selectedEnvironment()) {
|
|
|
4612
5202
|
if (process.env.TOPOLO_CLI_CLIENT_ID) return process.env.TOPOLO_CLI_CLIENT_ID;
|
|
4613
5203
|
return DEFAULT_CLI_CLIENT_IDS[environment] ?? profileForEnvironment(config, environment).oauthClientId;
|
|
4614
5204
|
}
|
|
4615
|
-
var NON_PROD_URL_MARKERS = {
|
|
4616
|
-
staging: [".stg.topolo.us", "topolo-staging"],
|
|
4617
|
-
development: [".topolo.dev", "topolo-development"]
|
|
4618
|
-
};
|
|
4619
|
-
function assertServiceUrlForEnvironment(service, serviceUrl, environment = selectedEnvironment()) {
|
|
4620
|
-
const markers = NON_PROD_URL_MARKERS[environment];
|
|
4621
|
-
if (!markers) return;
|
|
4622
|
-
if (!serviceUrl) {
|
|
4623
|
-
throw new Error(`No ${environment} URL is configured for Topolo service "${service}".`);
|
|
4624
|
-
}
|
|
4625
|
-
const url = serviceUrl.toLowerCase();
|
|
4626
|
-
if (markers.some((marker) => url.includes(marker)) || url.includes("localhost") || url.includes("127.0.0.1")) {
|
|
4627
|
-
return;
|
|
4628
|
-
}
|
|
4629
|
-
throw new Error(
|
|
4630
|
-
`Refusing to call ${service} in ${environment} because its effective URL is not a ${environment} endpoint: ${serviceUrl}`
|
|
4631
|
-
);
|
|
4632
|
-
}
|
|
4633
5205
|
|
|
4634
5206
|
// src/output.ts
|
|
4635
5207
|
function resolveOutputMode(override) {
|
|
@@ -4677,23 +5249,25 @@ function fail(err, mode) {
|
|
|
4677
5249
|
function normalizeError(err) {
|
|
4678
5250
|
if (err instanceof Error) {
|
|
4679
5251
|
const code = err.code;
|
|
4680
|
-
const
|
|
4681
|
-
auth_error: "Run `topolo auth login` or set TOPOLO_API_KEY.",
|
|
4682
|
-
confirmation_required: "Review the action and rerun with --confirm to allow it.",
|
|
4683
|
-
permission_denied: "Check your API key scopes or role assignments in TopoloDevelopers.",
|
|
5252
|
+
const fallbackHintMap = {
|
|
4684
5253
|
immutable_drift: "Restore the manifest identity fields to the registered values; immutable ids cannot be updated."
|
|
4685
5254
|
};
|
|
5255
|
+
const body = err.body;
|
|
5256
|
+
const status = err.status;
|
|
5257
|
+
const suppliedDetails = err.details;
|
|
4686
5258
|
return {
|
|
4687
|
-
message: err.message,
|
|
5259
|
+
message: status !== void 0 || body !== void 0 ? publicTopoloErrorMessage({ code, body, status, message: err.message }) : err.message,
|
|
4688
5260
|
code,
|
|
4689
|
-
status
|
|
5261
|
+
status,
|
|
4690
5262
|
service: err.service,
|
|
4691
5263
|
path: err.path,
|
|
4692
5264
|
requestId: err.requestId,
|
|
4693
5265
|
retryAfter: err.retryAfter,
|
|
4694
|
-
details:
|
|
4695
|
-
|
|
4696
|
-
|
|
5266
|
+
details: extractTopoloServiceErrorDetails(
|
|
5267
|
+
suppliedDetails === void 0 ? body : { details: suppliedDetails }
|
|
5268
|
+
),
|
|
5269
|
+
hint: topoloErrorHint({ code, body }) ?? (code ? fallbackHintMap[code] : void 0),
|
|
5270
|
+
exitCode: status === 401 || code === "auth_error" || code === "confirmation_required" ? 2 : status === 403 || code === "permission_denied" ? 3 : code === "immutable_drift" ? 4 : 1
|
|
4697
5271
|
};
|
|
4698
5272
|
}
|
|
4699
5273
|
return { message: String(err), exitCode: 1 };
|
|
@@ -4872,7 +5446,12 @@ async function runDeviceFlow(options) {
|
|
|
4872
5446
|
...token.refresh_token ? { refreshToken: token.refresh_token } : {},
|
|
4873
5447
|
expiresAt: Math.floor(Date.now() / 1e3) + token.expires_in
|
|
4874
5448
|
};
|
|
4875
|
-
const context = await resolveLoginContext(
|
|
5449
|
+
const context = await resolveLoginContext(
|
|
5450
|
+
credential,
|
|
5451
|
+
resolved.serviceUrls,
|
|
5452
|
+
options.clientId,
|
|
5453
|
+
options.environment
|
|
5454
|
+
);
|
|
4876
5455
|
const updated = await readConfig();
|
|
4877
5456
|
await writeConfig(writeContextCredential(updated, options.environment, context));
|
|
4878
5457
|
emit(
|
|
@@ -4889,10 +5468,11 @@ async function runDeviceFlow(options) {
|
|
|
4889
5468
|
() => "\n \u2713 Signed in. Run `topolo whoami` to see your identity."
|
|
4890
5469
|
);
|
|
4891
5470
|
}
|
|
4892
|
-
async function resolveLoginContext(credential, serviceUrls, clientId) {
|
|
5471
|
+
async function resolveLoginContext(credential, serviceUrls, clientId, environment) {
|
|
4893
5472
|
try {
|
|
4894
5473
|
const topolo = createTopolo({
|
|
4895
5474
|
credential,
|
|
5475
|
+
environment,
|
|
4896
5476
|
agent: { clientName: "topolo-cli", clientVersion: CLI_VERSION },
|
|
4897
5477
|
serviceUrls
|
|
4898
5478
|
});
|
|
@@ -4985,6 +5565,7 @@ async function buildClient(options) {
|
|
|
4985
5565
|
const profile = resolvedProfileConfig(config, environment);
|
|
4986
5566
|
return createTopolo({
|
|
4987
5567
|
credential,
|
|
5568
|
+
environment,
|
|
4988
5569
|
agent: {
|
|
4989
5570
|
clientName: "topolo-cli",
|
|
4990
5571
|
clientVersion: CLI_VERSION,
|
|
@@ -5002,23 +5583,25 @@ function isDebugEnabled() {
|
|
|
5002
5583
|
return v !== "0" && v.toLowerCase() !== "false";
|
|
5003
5584
|
}
|
|
5004
5585
|
function stderrDebugLogger(event) {
|
|
5586
|
+
const requestId = canonicalTopoloRequestId(event.requestId) ?? "unavailable";
|
|
5005
5587
|
if (event.phase === "request") {
|
|
5006
5588
|
process.stderr.write(
|
|
5007
|
-
`[topolo] \u2192 ${event.method} ${event.service} ${event.path} (req=${
|
|
5589
|
+
`[topolo] \u2192 ${event.method} ${event.service} ${event.path} (req=${requestId})
|
|
5008
5590
|
`
|
|
5009
5591
|
);
|
|
5010
5592
|
return;
|
|
5011
5593
|
}
|
|
5012
5594
|
if (event.phase === "response") {
|
|
5013
5595
|
process.stderr.write(
|
|
5014
|
-
`[topolo] \u2190 ${event.status} ${event.method} ${event.service} ${event.path} (${event.durationMs}ms, req=${
|
|
5596
|
+
`[topolo] \u2190 ${event.status} ${event.method} ${event.service} ${event.path} (${event.durationMs}ms, req=${requestId})
|
|
5015
5597
|
`
|
|
5016
5598
|
);
|
|
5017
5599
|
return;
|
|
5018
5600
|
}
|
|
5019
5601
|
const statusPart = event.status ? ` ${event.status}` : "";
|
|
5602
|
+
const errorCode = canonicalTopoloErrorCode(event.error) ?? "request_failed";
|
|
5020
5603
|
process.stderr.write(
|
|
5021
|
-
`[topolo] \u2717${statusPart} ${event.method} ${event.service} ${event.path} (${event.durationMs}ms, req=${
|
|
5604
|
+
`[topolo] \u2717${statusPart} ${event.method} ${event.service} ${event.path} (${event.durationMs}ms, req=${requestId}, code=${errorCode})
|
|
5022
5605
|
`
|
|
5023
5606
|
);
|
|
5024
5607
|
}
|
|
@@ -5251,6 +5834,22 @@ function registerActionsCommand(program2) {
|
|
|
5251
5834
|
fail(err, mode);
|
|
5252
5835
|
}
|
|
5253
5836
|
});
|
|
5837
|
+
actions.command("prepare <action>").description("Prepare one action with identity, example, validation, exact request, verification, and rollback").option("-d, --data <json-or-@file>", "JSON action input or @path; defaults to one valid published example").option("--resource-type <type>", "App-local resource alias, such as workspace").option("--resource-id <id>", "Resource id for the selected resource type").option("--confirm", "Include confirmation in the plan without executing it").option("--json", "Output compact JSON").option("--no-json", "Output human-readable text").action(async (action, opts, command) => {
|
|
5838
|
+
const mode = resolveOutputMode(opts.json ?? command.parent?.opts().json);
|
|
5839
|
+
try {
|
|
5840
|
+
const topolo = await requiredClient();
|
|
5841
|
+
const resource = parseResourceOption(opts.resourceType, opts.resourceId);
|
|
5842
|
+
const preparation = await topolo.client.prepareAction(
|
|
5843
|
+
action,
|
|
5844
|
+
opts.data === void 0 ? void 0 : parseJsonObject(opts.data),
|
|
5845
|
+
{ confirm: opts.confirm === true, ...resource ? { resource } : {} }
|
|
5846
|
+
);
|
|
5847
|
+
const output2 = compactActionPreparation(preparation, await actionExecutionContext(topolo));
|
|
5848
|
+
emit(output2, mode, () => formatActionPreparation(output2));
|
|
5849
|
+
} catch (err) {
|
|
5850
|
+
fail(err, mode);
|
|
5851
|
+
}
|
|
5852
|
+
});
|
|
5254
5853
|
actions.command("plan <action>").description("Validate and preview the exact request, confirmation, verification, and rollback plan").option("-d, --data <json-or-@file>", "JSON action input or @path to a JSON file", "{}").option("--resource-type <type>", "App-local resource alias, such as workspace").option("--resource-id <id>", "Resource id for the selected resource type").option("--confirm", "Include confirmation in the plan without executing it").option("--detail", "Include the full action contract").option("--json", "Output JSON").option("--no-json", "Output human-readable text").action(async (action, opts, command) => {
|
|
5255
5854
|
const mode = resolveOutputMode(opts.json ?? command.parent?.opts().json);
|
|
5256
5855
|
try {
|
|
@@ -5316,6 +5915,43 @@ function registerActionsCommand(program2) {
|
|
|
5316
5915
|
}
|
|
5317
5916
|
});
|
|
5318
5917
|
}
|
|
5918
|
+
async function actionExecutionContext(topolo) {
|
|
5919
|
+
const environment = selectedEnvironment();
|
|
5920
|
+
const config = await readConfig();
|
|
5921
|
+
const profile = profileForEnvironment(config, environment);
|
|
5922
|
+
const source = credentialSourceFromConfig(config, environment);
|
|
5923
|
+
return createTopoloAgentExecutionContext({
|
|
5924
|
+
environment,
|
|
5925
|
+
contextKey: source.startsWith("env:") ? null : profile.activeContext ?? null,
|
|
5926
|
+
identity: await topolo.identity.whoami(),
|
|
5927
|
+
serviceUrls: resolvedProfileConfig(config, environment).serviceUrls
|
|
5928
|
+
});
|
|
5929
|
+
}
|
|
5930
|
+
function formatActionPreparation(preparation) {
|
|
5931
|
+
const context = preparation.context;
|
|
5932
|
+
const lines = [
|
|
5933
|
+
`Environment: ${context.environment}`,
|
|
5934
|
+
`User: ${context.user.email} (${context.user.role})`,
|
|
5935
|
+
`Organization: ${context.organization ? `${context.organization.name} (${context.organization.slug})` : "(personal)"}`,
|
|
5936
|
+
`Action: ${preparation.action.id}`,
|
|
5937
|
+
`Input source: ${preparation.input.source}`,
|
|
5938
|
+
`Valid: ${preparation.valid ? "yes" : "no"}`,
|
|
5939
|
+
`Confirmation: ${preparation.confirmation.required ? preparation.confirmation.provided ? "provided" : "required" : "not required"}`
|
|
5940
|
+
];
|
|
5941
|
+
if (preparation.example) {
|
|
5942
|
+
lines.push(`Example: ${JSON.stringify(preparation.example.input)}`);
|
|
5943
|
+
}
|
|
5944
|
+
if ("value" in preparation.input) {
|
|
5945
|
+
lines.push(`Input: ${JSON.stringify(preparation.input.value)}`);
|
|
5946
|
+
}
|
|
5947
|
+
if (preparation.request) {
|
|
5948
|
+
lines.push(`Request: ${preparation.request.method} ${preparation.request.apiBaseUrl || preparation.request.service}${preparation.request.path}`);
|
|
5949
|
+
}
|
|
5950
|
+
if (preparation.missingFields.length) lines.push(`Missing: ${preparation.missingFields.join(", ")}`);
|
|
5951
|
+
if (preparation.verification.length) lines.push(`Verify: ${preparation.verification.join(" | ")}`);
|
|
5952
|
+
lines.push(`Rollback: ${preparation.rollbackActionId || "(not published)"}`);
|
|
5953
|
+
return lines.join("\n");
|
|
5954
|
+
}
|
|
5319
5955
|
function summarizeActionCapabilities(service, actions) {
|
|
5320
5956
|
return {
|
|
5321
5957
|
service,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@topolo/cli",
|
|
3
|
-
"version": "0.7.
|
|
4
|
-
"topoloPublishedVersion": "0.7.
|
|
3
|
+
"version": "0.7.4",
|
|
4
|
+
"topoloPublishedVersion": "0.7.4",
|
|
5
5
|
"description": "Command-line interface for Topolo developers, operators, and agents, with internal first-party scaffolding available under the topolo platform namespace.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
@@ -12,24 +12,19 @@
|
|
|
12
12
|
"skills"
|
|
13
13
|
],
|
|
14
14
|
"scripts": {
|
|
15
|
-
"build:sdk": "python3 ../../tooling/with-resource-lock.py ../topolo-sdk -- pnpm --dir ../topolo-sdk build",
|
|
16
|
-
"prebuild": "pnpm run build:sdk",
|
|
17
15
|
"build": "tsup && node scripts/vendor-generator.mjs",
|
|
18
16
|
"dev": "tsup --watch",
|
|
19
|
-
"pretypecheck": "pnpm run build:sdk",
|
|
20
17
|
"typecheck": "tsc --noEmit",
|
|
21
18
|
"start": "node dist/index.js",
|
|
22
|
-
"pretest": "pnpm run build:sdk",
|
|
23
19
|
"test": "vitest run",
|
|
24
20
|
"test:watch": "vitest",
|
|
25
|
-
"precoverage": "pnpm run build:sdk",
|
|
26
21
|
"coverage": "vitest run --coverage"
|
|
27
22
|
},
|
|
28
23
|
"dependencies": {
|
|
29
24
|
"commander": "^12.1.0"
|
|
30
25
|
},
|
|
31
26
|
"devDependencies": {
|
|
32
|
-
"@topolo/sdk": "
|
|
27
|
+
"@topolo/sdk": "0.10.4",
|
|
33
28
|
"@types/node": "^20.12.0",
|
|
34
29
|
"@vitest/coverage-v8": "3.2.6",
|
|
35
30
|
"tsup": "^8.0.0",
|