@topolo/sdk 0.10.2 → 0.10.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -13,6 +13,90 @@ var TopoloSdkError = class extends Error {
13
13
  this.code = code;
14
14
  }
15
15
  };
16
+ var PUBLIC_ERROR_CODES = /* @__PURE__ */ new Set([
17
+ "action_app_identity_unavailable",
18
+ "action_authentication_required",
19
+ "action_catalog_summary_unavailable",
20
+ "action_catalog_unavailable",
21
+ "action_contract_mismatch",
22
+ "action_permission_required",
23
+ "action_runtime_authorization_required",
24
+ "action_service_not_allowed",
25
+ "auth_error",
26
+ "authentication_required",
27
+ "authorization_pending",
28
+ "bad_sig",
29
+ "confirmation_required",
30
+ "dependency_unavailable",
31
+ "environment_mismatch",
32
+ "environment_service_url_required",
33
+ "expired_token",
34
+ "forbidden",
35
+ "http_error",
36
+ "immutable_drift",
37
+ "integration_dependency_unavailable",
38
+ "integration_misconfigured",
39
+ "invalid_action",
40
+ "invalid_action_input",
41
+ "invalid_grant",
42
+ "invalid_input",
43
+ "invalid_token",
44
+ "mcp_error",
45
+ "membership_required",
46
+ "network_error",
47
+ "organization_context_required",
48
+ "organization_enrollment_required",
49
+ "permission_denied",
50
+ "rate_limited",
51
+ "request_failed",
52
+ "service_catalog_unavailable",
53
+ "service_path_origin_mismatch",
54
+ "service_required",
55
+ "slow_down",
56
+ "state_conflict",
57
+ "unauthorized",
58
+ "unknown_action",
59
+ "unknown_service"
60
+ ]);
61
+ var ERROR_CODE_PATTERN = /^[a-z][a-z0-9_]{0,63}$/;
62
+ 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;
63
+ var CLOUDFLARE_RAY_ID_PATTERN = /^[0-9a-f]{16}-[A-Z]{3}$/;
64
+ var PERMISSION_PATTERN = /^[a-z][a-z0-9_-]{0,63}(?:\.[a-z][a-z0-9_-]{0,63})*:(?:[a-z][a-z0-9_-]{0,63}|\*)$/;
65
+ 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$/;
66
+ var PUBLIC_DETAIL_SOURCES = /* @__PURE__ */ new Set([
67
+ "api_key_introspection",
68
+ "api_key_validation",
69
+ "auth_context",
70
+ "auth_validate",
71
+ "catalog",
72
+ "credential",
73
+ "runtime",
74
+ "service_token_provider",
75
+ "service_verify_key",
76
+ "token_validation",
77
+ "upstream"
78
+ ]);
79
+ function canonicalTopoloErrorCode(value) {
80
+ if (typeof value !== "string") return null;
81
+ const candidate = value.trim().toLowerCase();
82
+ if (!ERROR_CODE_PATTERN.test(candidate) || !PUBLIC_ERROR_CODES.has(candidate)) return null;
83
+ return candidate;
84
+ }
85
+ function canonicalTopoloRequestId(value) {
86
+ if (typeof value !== "string") return void 0;
87
+ const candidate = value.trim();
88
+ return TOPOLO_REQUEST_ID_PATTERN.test(candidate) || CLOUDFLARE_RAY_ID_PATTERN.test(candidate) ? candidate : void 0;
89
+ }
90
+ function canonicalTopoloRetryAfter(value) {
91
+ if (typeof value !== "string") return void 0;
92
+ const candidate = value.trim();
93
+ if (/^\d{1,10}$/.test(candidate) || RETRY_AFTER_DATE_PATTERN.test(candidate)) return candidate;
94
+ return void 0;
95
+ }
96
+ function canonicalTopoloRequired(value) {
97
+ if (!Array.isArray(value)) return [];
98
+ return [...new Set(value.filter((item) => typeof item === "string" && PERMISSION_PATTERN.test(item)))].slice(0, 32);
99
+ }
16
100
  var TopoloAuthError = class extends TopoloSdkError {
17
101
  status;
18
102
  body;
@@ -20,27 +104,91 @@ var TopoloAuthError = class extends TopoloSdkError {
20
104
  path;
21
105
  requestId;
22
106
  retryAfter;
107
+ details;
23
108
  constructor(message, code = "auth_error", options = {}) {
24
- super(code, message);
109
+ super(canonicalTopoloErrorCode(code) ?? "auth_error", message);
25
110
  this.name = "TopoloAuthError";
26
111
  this.status = options.status ?? 401;
27
112
  this.body = options.body;
28
113
  this.service = options.service;
29
114
  this.path = options.path;
30
- this.requestId = options.requestId;
31
- this.retryAfter = options.retryAfter;
115
+ this.requestId = canonicalTopoloRequestId(options.requestId);
116
+ this.retryAfter = canonicalTopoloRetryAfter(options.retryAfter);
117
+ this.details = extractTopoloServiceErrorDetails(
118
+ options.details === void 0 ? options.body : { details: options.details }
119
+ );
32
120
  }
33
121
  };
34
- function extractHttpErrorCode(body) {
35
- if (!body || typeof body !== "object" || Array.isArray(body)) return "http_error";
122
+ function extractTopoloServiceErrorCode(body) {
123
+ if (!body || typeof body !== "object" || Array.isArray(body)) return null;
36
124
  const record = body;
37
- if (typeof record.code === "string" && record.code.trim()) return record.code.trim();
125
+ const topLevel = canonicalTopoloErrorCode(record.code);
126
+ if (topLevel) return topLevel;
38
127
  const nested = record.error;
128
+ const nestedString = canonicalTopoloErrorCode(nested);
129
+ if (nestedString) return nestedString;
39
130
  if (nested && typeof nested === "object" && !Array.isArray(nested)) {
40
131
  const code = nested.code;
41
- if (typeof code === "string" && code.trim()) return code.trim();
132
+ const nestedCode = canonicalTopoloErrorCode(code);
133
+ if (nestedCode) return nestedCode;
134
+ }
135
+ return null;
136
+ }
137
+ function serviceErrorCode(body, fallback) {
138
+ return extractTopoloServiceErrorCode(body) ?? fallback;
139
+ }
140
+ function extractTopoloServiceErrorDetails(body) {
141
+ if (!body || typeof body !== "object" || Array.isArray(body)) return void 0;
142
+ const record = body;
143
+ let candidate = record.details;
144
+ const nested = record.error;
145
+ if (nested && typeof nested === "object" && !Array.isArray(nested)) {
146
+ const nestedRecord = nested;
147
+ if (candidate === void 0) candidate = nestedRecord.details ?? nestedRecord;
148
+ }
149
+ if (candidate === void 0) candidate = record;
150
+ if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) return void 0;
151
+ const source = candidate;
152
+ const safe = {};
153
+ for (const [key, value] of Object.entries(source)) {
154
+ if (/present$/i.test(key) && typeof value === "boolean") {
155
+ safe[key] = value;
156
+ continue;
157
+ }
158
+ if (key === "retryable" && typeof value === "boolean") {
159
+ safe.retryable = value;
160
+ continue;
161
+ }
162
+ if (key === "code") {
163
+ const sanitized = canonicalTopoloErrorCode(value);
164
+ if (sanitized) safe.code = sanitized;
165
+ continue;
166
+ }
167
+ if (key === "source") {
168
+ const sanitized = typeof value === "string" ? value.trim().toLowerCase() : "";
169
+ if (PUBLIC_DETAIL_SOURCES.has(sanitized)) safe.source = sanitized;
170
+ continue;
171
+ }
172
+ if (key === "dependency" || key === "platform") {
173
+ safe[`${key}Present`] = value !== null && value !== void 0;
174
+ continue;
175
+ }
176
+ if (key === "error" && !safe.code) {
177
+ const sanitized = canonicalTopoloErrorCode(value);
178
+ if (sanitized) safe.code = sanitized;
179
+ continue;
180
+ }
181
+ if (key === "required" && Array.isArray(value)) {
182
+ const required = canonicalTopoloRequired(value);
183
+ if (required.length) safe.required = required;
184
+ continue;
185
+ }
186
+ if (["missingFields", "fields", "paths"].includes(key) && Array.isArray(value)) {
187
+ const paths = value.filter((item) => typeof item === "string" && /^[$.a-z0-9_[\]-]{1,160}$/i.test(item));
188
+ if (paths.length) safe[key] = paths;
189
+ }
42
190
  }
43
- return "http_error";
191
+ return Object.keys(safe).length ? safe : void 0;
44
192
  }
45
193
  var TopoloHttpError = class extends TopoloSdkError {
46
194
  status;
@@ -49,15 +197,19 @@ var TopoloHttpError = class extends TopoloSdkError {
49
197
  path;
50
198
  requestId;
51
199
  retryAfter;
200
+ details;
52
201
  constructor(service, path, status, body, message, options = {}) {
53
- super(extractHttpErrorCode(body), message ?? `HTTP ${status} from ${service}${path}`);
202
+ super(serviceErrorCode(body, "http_error"), message ?? `HTTP ${status} from ${service}${path}`);
54
203
  this.name = "TopoloHttpError";
55
204
  this.status = status;
56
205
  this.body = body;
57
206
  this.service = service;
58
207
  this.path = path;
59
- this.requestId = options.requestId;
60
- this.retryAfter = options.retryAfter;
208
+ this.requestId = canonicalTopoloRequestId(options.requestId);
209
+ this.retryAfter = canonicalTopoloRetryAfter(options.retryAfter);
210
+ this.details = extractTopoloServiceErrorDetails(
211
+ options.details === void 0 ? body : { details: options.details }
212
+ );
61
213
  }
62
214
  };
63
215
 
@@ -170,13 +322,11 @@ var TopoloOAuth = class {
170
322
  }
171
323
  };
172
324
 
173
- // src/node/credential-store.ts
174
- var TOPOLO_ENVIRONMENTS = ["production", "staging", "development", "local"];
175
- function contextKey(contextType, organizationId) {
176
- return contextType === "organization" && organizationId ? `org:${organizationId}` : "personal";
177
- }
178
- var ENVIRONMENT_DEFAULT_SERVICE_URLS = {
325
+ // src/environment.ts
326
+ var TOPOLO_RUNTIME_ENVIRONMENTS = ["production", "staging", "development", "local"];
327
+ var TOPOLO_ENVIRONMENT_SERVICE_URLS = {
179
328
  production: {
329
+ auth: "https://auth.topolo.app",
180
330
  developers: "https://developers.topolo.app"
181
331
  },
182
332
  staging: {
@@ -190,6 +340,58 @@ var ENVIRONMENT_DEFAULT_SERVICE_URLS = {
190
340
  },
191
341
  local: {}
192
342
  };
343
+ function hostnameHasSuffix(hostname, suffix) {
344
+ return hostname === suffix || hostname.endsWith(`.${suffix}`);
345
+ }
346
+ function workersDevAccountMatches(hostname, account) {
347
+ if (!hostname.endsWith(".workers.dev")) return false;
348
+ const labels = hostname.slice(0, -".workers.dev".length).split(".");
349
+ return labels.length >= 2 && labels.at(-1) === account;
350
+ }
351
+ function originMatchesEnvironment(url, environment) {
352
+ const hostname = url.hostname.toLowerCase();
353
+ if (environment === "local") {
354
+ return hostname === "localhost" || hostname.endsWith(".localhost") || hostname === "127.0.0.1" || hostname === "[::1]";
355
+ }
356
+ if (url.protocol !== "https:") return false;
357
+ if (environment === "production") {
358
+ return hostnameHasSuffix(hostname, "topolo.app") || hostnameHasSuffix(hostname, "topolo.io") || workersDevAccountMatches(hostname, "topolo");
359
+ }
360
+ if (environment === "staging") {
361
+ return hostnameHasSuffix(hostname, "stg.topolo.us") || workersDevAccountMatches(hostname, "topolo-staging");
362
+ }
363
+ return hostnameHasSuffix(hostname, "topolo.dev") || workersDevAccountMatches(hostname, "topolo-development");
364
+ }
365
+ function assertTopoloServiceUrlEnvironment(service, value, environment) {
366
+ let url;
367
+ try {
368
+ url = new URL(value);
369
+ } catch {
370
+ throw new TopoloSdkError("invalid_service_url", `Invalid URL configured for Topolo service "${service}".`);
371
+ }
372
+ if (url.protocol !== "https:" && url.protocol !== "http:" || url.username || url.password || !originMatchesEnvironment(url, environment)) {
373
+ throw new TopoloSdkError(
374
+ "environment_mismatch",
375
+ `Refusing Topolo service "${service}": ${url.origin} does not belong to ${environment}.`
376
+ );
377
+ }
378
+ return url.origin;
379
+ }
380
+ function topoloServiceOrigins(serviceUrls, environment) {
381
+ return Object.fromEntries(
382
+ Object.entries(serviceUrls ?? {}).sort(([left], [right]) => left.localeCompare(right)).map(([service, value]) => [
383
+ service,
384
+ assertTopoloServiceUrlEnvironment(service, value, environment)
385
+ ])
386
+ );
387
+ }
388
+
389
+ // src/node/credential-store.ts
390
+ var TOPOLO_ENVIRONMENTS = TOPOLO_RUNTIME_ENVIRONMENTS;
391
+ function contextKey(contextType, organizationId) {
392
+ return contextType === "organization" && organizationId ? `org:${organizationId}` : "personal";
393
+ }
394
+ var ENVIRONMENT_DEFAULT_SERVICE_URLS = TOPOLO_ENVIRONMENT_SERVICE_URLS;
193
395
  function configPath() {
194
396
  const override = process.env.TOPOLO_CONFIG_PATH;
195
397
  if (override) return override;
@@ -307,15 +509,17 @@ function credentialSourceFromConfig(config, environment = selectedEnvironment())
307
509
  const prefix = envVarPrefix(environment);
308
510
  if (process.env[`${prefix}_API_KEY`]) return `env:${prefix}_API_KEY`;
309
511
  if (process.env[`${prefix}_ACCESS_TOKEN`]) return `env:${prefix}_ACCESS_TOKEN`;
310
- if (process.env.TOPOLO_API_KEY) return "env:TOPOLO_API_KEY";
311
- if (process.env.TOPOLO_ACCESS_TOKEN) return "env:TOPOLO_ACCESS_TOKEN";
512
+ if (environment === "production" && process.env.TOPOLO_API_KEY) return "env:TOPOLO_API_KEY";
513
+ if (environment === "production" && process.env.TOPOLO_ACCESS_TOKEN) {
514
+ return "env:TOPOLO_ACCESS_TOKEN";
515
+ }
312
516
  return profileForEnvironment(config, environment).credential ? "config-file" : "none";
313
517
  }
314
518
  async function resolveCredential(environment = selectedEnvironment()) {
315
519
  const prefix = envVarPrefix(environment);
316
- const apiKey = process.env[`${prefix}_API_KEY`] ?? process.env.TOPOLO_API_KEY;
520
+ const apiKey = process.env[`${prefix}_API_KEY`] ?? (environment === "production" ? process.env.TOPOLO_API_KEY : void 0);
317
521
  if (apiKey) return { kind: "api_key", apiKey };
318
- const accessToken = process.env[`${prefix}_ACCESS_TOKEN`] ?? process.env.TOPOLO_ACCESS_TOKEN;
522
+ const accessToken = process.env[`${prefix}_ACCESS_TOKEN`] ?? (environment === "production" ? process.env.TOPOLO_ACCESS_TOKEN : void 0);
319
523
  if (accessToken) return { kind: "access_token", accessToken };
320
524
  const config = await readConfig();
321
525
  return profileForEnvironment(config, environment).credential ?? null;
@@ -323,7 +527,7 @@ async function resolveCredential(environment = selectedEnvironment()) {
323
527
  var REFRESH_SKEW_SECONDS = 60;
324
528
  async function refreshCredentialIfNeeded(environment = selectedEnvironment(), options = {}) {
325
529
  const prefix = envVarPrefix(environment);
326
- if (process.env[`${prefix}_API_KEY`] || process.env[`${prefix}_ACCESS_TOKEN`] || process.env.TOPOLO_API_KEY || process.env.TOPOLO_ACCESS_TOKEN) {
530
+ 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) {
327
531
  return resolveCredential(environment);
328
532
  }
329
533
  const config = await readConfig();
@@ -333,9 +537,16 @@ async function refreshCredentialIfNeeded(environment = selectedEnvironment(), op
333
537
  if (!cred.refreshToken || !profile.oauthClientId) return cred;
334
538
  const now = Math.floor(Date.now() / 1e3);
335
539
  if (!options.force && cred.expiresAt && cred.expiresAt - REFRESH_SKEW_SECONDS > now) return cred;
540
+ const serviceUrls = resolveServiceUrls(profile.serviceUrls, environment);
541
+ if (environment === "local" && !serviceUrls?.auth) {
542
+ throw new TopoloSdkError(
543
+ "environment_service_url_required",
544
+ "Local Topolo credential refresh requires an explicit local Auth service origin."
545
+ );
546
+ }
336
547
  try {
337
548
  const oauth = new TopoloOAuth({
338
- serviceUrls: resolveServiceUrls(profile.serviceUrls, environment)
549
+ serviceUrls
339
550
  });
340
551
  const tokens = await oauth.refreshToken({
341
552
  clientId: profile.oauthClientId,
@@ -376,9 +587,16 @@ async function refreshContextCredentialIfNeeded(environment, key, options = {})
376
587
  if (!credential.refreshToken || !oauthClientId) return credential;
377
588
  const now = Math.floor(Date.now() / 1e3);
378
589
  if (!options.force && credential.expiresAt && credential.expiresAt - REFRESH_SKEW_SECONDS > now) return credential;
590
+ const serviceUrls = resolveServiceUrls(profile.serviceUrls, environment);
591
+ if (environment === "local" && !serviceUrls?.auth) {
592
+ throw new TopoloSdkError(
593
+ "environment_service_url_required",
594
+ "Local Topolo context refresh requires an explicit local Auth service origin."
595
+ );
596
+ }
379
597
  try {
380
598
  const oauth = new TopoloOAuth({
381
- serviceUrls: resolveServiceUrls(profile.serviceUrls, environment)
599
+ serviceUrls
382
600
  });
383
601
  const tokens = await oauth.refreshToken({
384
602
  clientId: oauthClientId,
@@ -425,7 +643,12 @@ function resolveServiceUrls(configUrls, environment = selectedEnvironment()) {
425
643
  merged[k.slice("TOPOLO_SERVICE_URL_".length).toLowerCase()] = v;
426
644
  }
427
645
  }
428
- return Object.keys(merged).length > 0 ? merged : void 0;
646
+ const origins = topoloServiceOrigins(merged, environment);
647
+ return Object.keys(origins).length > 0 ? origins : void 0;
648
+ }
649
+ function assertServiceUrlForEnvironment(service, serviceUrl, environment = selectedEnvironment()) {
650
+ if (!serviceUrl) return;
651
+ assertTopoloServiceUrlEnvironment(service, serviceUrl, environment);
429
652
  }
430
653
  function resolvedProfileConfig(config, environment = selectedEnvironment()) {
431
654
  const profile = profileForEnvironment(config, environment);
@@ -440,6 +663,8 @@ export {
440
663
  ENVIRONMENT_DEFAULT_SERVICE_URLS,
441
664
  TOPOLO_ENVIRONMENTS,
442
665
  activateContext,
666
+ assertServiceUrlForEnvironment,
667
+ assertTopoloServiceUrlEnvironment,
443
668
  clearProfileCredential,
444
669
  configPath,
445
670
  contextKey,
@@ -455,6 +680,7 @@ export {
455
680
  resolveServiceUrls,
456
681
  resolvedProfileConfig,
457
682
  selectedEnvironment,
683
+ topoloServiceOrigins,
458
684
  writeConfig,
459
685
  writeContextCredential,
460
686
  writeProfileConfig
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@topolo/sdk",
3
- "version": "0.10.2",
4
- "topoloPublishedVersion": "0.10.2",
3
+ "version": "0.10.4",
4
+ "topoloPublishedVersion": "0.10.4",
5
5
  "description": "Typed client SDK for the Topolo platform. Used by TopoloCli, TopoloMCP, and third-party agents.",
6
6
  "type": "module",
7
7
  "main": "./dist/index.js",