@topolo/sdk 0.2.12 → 0.4.0

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/README.md CHANGED
@@ -103,7 +103,7 @@ their own `GET /api/widget` endpoint. The SDK owns the shared payload contract:
103
103
  import { createTopoloWidgetResponse } from '@topolo/sdk';
104
104
 
105
105
  return Response.json(createTopoloWidgetResponse({
106
- serviceId: 'srv_topolo_example',
106
+ serviceId: '<runtime-service-id>',
107
107
  serviceName: 'Example',
108
108
  widgets: [
109
109
  {
package/dist/index.cjs CHANGED
@@ -27,7 +27,6 @@ __export(index_exports, {
27
27
  DEFAULT_SERVICE_URLS: () => DEFAULT_SERVICE_URLS,
28
28
  IdentityModule: () => IdentityModule,
29
29
  P2PModule: () => P2PModule,
30
- PLATFORM_SERVICE_IDS: () => PLATFORM_SERVICE_IDS,
31
30
  TOPOLO_WIDGET_ENDPOINT: () => TOPOLO_WIDGET_ENDPOINT,
32
31
  TOPOLO_WIDGET_REFRESH_INTERVAL_MS: () => TOPOLO_WIDGET_REFRESH_INTERVAL_MS,
33
32
  TopoloAuthError: () => TopoloAuthError,
@@ -146,41 +145,20 @@ var SERVICES = {
146
145
  p2p: "https://p2p.topolo.app",
147
146
  pay: "https://pay.topolo.app",
148
147
  people: "https://people.topolo.app",
148
+ pulse: "https://pulse.topolo.app",
149
149
  "quro.api": "https://api.ol0.me",
150
150
  "quro.redirect": "https://ol0.me",
151
151
  roadmapper: "https://roadmapper-api.topolo.app",
152
152
  sign: "https://sign.topolo.app",
153
153
  social_studio: "https://studio-api.topolo.app",
154
154
  socialize: "https://socialize-api.topolo.app",
155
+ spaces: "https://spaces.topolo.app",
155
156
  status: "https://status.topolo.app",
156
157
  support: "https://support.topolo.app",
157
158
  survey: "https://survey.topolo.app",
158
159
  voice: "https://voice.topolo.app",
159
160
  web: "https://web-api.topolo.app"
160
161
  };
161
- var PLATFORM_SERVICE_IDS = {
162
- books: "srv_0g260hbN8oqv",
163
- bugfix: "srv_kQ1t8qUSDgNs",
164
- compose: "srv_aR3ETYmgygmd",
165
- consent: "srv_8FIGR7Q7zP9r",
166
- crm: "srv_iCwM4jGXcwlj",
167
- customer_success: "srv_RWpOxkgZdZbH",
168
- developers: "srv_NjDq9G9qyyjr",
169
- director: "srv_wRCnLLbtvmVT",
170
- flow: "srv_GBCuKkHhnRXv",
171
- forms: "srv_UE7IcGe80J5o",
172
- home: "srv_hE6HBldVzjth",
173
- insights: "srv_DgbBMwWSIhmC",
174
- inventory: "srv_hmvjbv3ZxDnh",
175
- localize: "srv_Ze1g9OmXuZjF",
176
- notify: "srv_9pRdkIjYyKX0",
177
- p2p: "srv_4BzeLqH0wCve",
178
- people: "srv_VDMBAonA5fKb",
179
- sign: "srv_Bd8pRw5ZqRBT",
180
- status: "srv_topolo_status",
181
- survey: "srv_RCOi7CJkwf7R",
182
- voice: "srv_BZANHkshGAK5"
183
- };
184
162
 
185
163
  // src/applications.generated.ts
186
164
  var APPLICATIONS = {
@@ -260,6 +238,24 @@ var APPLICATIONS = {
260
238
  }
261
239
  ]
262
240
  },
241
+ "backup": {
242
+ "id": "backup",
243
+ "appDir": "TopoloBackup",
244
+ "name": "topolo-backup",
245
+ "packageName": "topolo-backup",
246
+ "productionUrl": null,
247
+ "services": [],
248
+ "deployTargets": [
249
+ {
250
+ "name": "topolo-backup",
251
+ "kind": "worker",
252
+ "apiUrl": null,
253
+ "domains": [],
254
+ "routes": [],
255
+ "serviceId": null
256
+ }
257
+ ]
258
+ },
263
259
  "books": {
264
260
  "id": "books",
265
261
  "appDir": "TopoloBooks",
@@ -410,15 +406,6 @@ var APPLICATIONS = {
410
406
  }
411
407
  ]
412
408
  },
413
- "cli": {
414
- "id": "cli",
415
- "appDir": "TopoloCli",
416
- "name": "@topolo/cli",
417
- "packageName": "@topolo/cli",
418
- "productionUrl": null,
419
- "services": [],
420
- "deployTargets": []
421
- },
422
409
  "cloudcontrol": {
423
410
  "id": "cloudcontrol",
424
411
  "appDir": "TopoloCloudControl",
@@ -584,24 +571,6 @@ var APPLICATIONS = {
584
571
  }
585
572
  ]
586
573
  },
587
- "d1_backup": {
588
- "id": "d1_backup",
589
- "appDir": "TopoloD1Backup",
590
- "name": "topolo-d1-backup",
591
- "packageName": "topolo-d1-backup",
592
- "productionUrl": null,
593
- "services": [],
594
- "deployTargets": [
595
- {
596
- "name": "topolo-d1-backup",
597
- "kind": "worker",
598
- "apiUrl": null,
599
- "domains": [],
600
- "routes": [],
601
- "serviceId": null
602
- }
603
- ]
604
- },
605
574
  "developers": {
606
575
  "id": "developers",
607
576
  "appDir": "TopoloDevelopers",
@@ -944,15 +913,6 @@ var APPLICATIONS = {
944
913
  }
945
914
  ]
946
915
  },
947
- "mcp": {
948
- "id": "mcp",
949
- "appDir": "TopoloMCP",
950
- "name": "@topolo/mcp",
951
- "packageName": "@topolo/mcp",
952
- "productionUrl": null,
953
- "services": [],
954
- "deployTargets": []
955
- },
956
916
  "mdm": {
957
917
  "id": "mdm",
958
918
  "appDir": "TopoloMDM",
@@ -1087,7 +1047,7 @@ var APPLICATIONS = {
1087
1047
  "one": {
1088
1048
  "id": "one",
1089
1049
  "appDir": "TopoloOne",
1090
- "name": "topolo-oneclick-sso",
1050
+ "name": "TopoloOne",
1091
1051
  "packageName": "topolo-oneclick-sso",
1092
1052
  "productionUrl": "https://one.topolo.app",
1093
1053
  "services": [
@@ -1208,9 +1168,24 @@ var APPLICATIONS = {
1208
1168
  "appDir": "TopoloPulse",
1209
1169
  "name": "topolo-pulse",
1210
1170
  "packageName": "topolo-pulse",
1211
- "productionUrl": null,
1212
- "services": [],
1213
- "deployTargets": []
1171
+ "productionUrl": "https://pulse.topolo.app",
1172
+ "services": [
1173
+ "pulse"
1174
+ ],
1175
+ "deployTargets": [
1176
+ {
1177
+ "name": "topolo-pulse",
1178
+ "kind": "worker",
1179
+ "apiUrl": "https://pulse.topolo.app",
1180
+ "domains": [
1181
+ "pulse.topolo.app"
1182
+ ],
1183
+ "routes": [
1184
+ "pulse.topolo.app"
1185
+ ],
1186
+ "serviceId": "pulse"
1187
+ }
1188
+ ]
1214
1189
  },
1215
1190
  "quro": {
1216
1191
  "id": "quro",
@@ -1393,6 +1368,30 @@ var APPLICATIONS = {
1393
1368
  }
1394
1369
  ]
1395
1370
  },
1371
+ "spaces": {
1372
+ "id": "spaces",
1373
+ "appDir": "TopoloSpaces",
1374
+ "name": "topolo-spaces",
1375
+ "packageName": "@topolo/spaces",
1376
+ "productionUrl": "https://spaces.topolo.app",
1377
+ "services": [
1378
+ "spaces"
1379
+ ],
1380
+ "deployTargets": [
1381
+ {
1382
+ "name": "topolo-spaces",
1383
+ "kind": "worker",
1384
+ "apiUrl": "https://spaces.topolo.app",
1385
+ "domains": [
1386
+ "spaces.topolo.app"
1387
+ ],
1388
+ "routes": [
1389
+ "spaces.topolo.app"
1390
+ ],
1391
+ "serviceId": "spaces"
1392
+ }
1393
+ ]
1394
+ },
1396
1395
  "status": {
1397
1396
  "id": "status",
1398
1397
  "appDir": "TopoloStatus",
@@ -1574,12 +1573,12 @@ var APPLICATION_REQUIREMENTS = [
1574
1573
  summary: "Applications must derive identity, organization, scopes, and service entitlement from TopoloAuth rather than inventing parallel auth state.",
1575
1574
  appliesTo: ["browser", "api", "tooling", "agent_surface"],
1576
1575
  evidence: [
1577
- "@topolo/auth-client or @topolo/auth-middleware is used where applicable",
1576
+ "@topolo/auth-client or @topolo-io/worker-runtime is used where applicable",
1578
1577
  "No public command, tool, or SDK method accepts orgId",
1579
1578
  "Service slug and Auth service registration are documented for protected services"
1580
1579
  ],
1581
1580
  implementation: [
1582
- "Use shared auth packages instead of copying token parsing or browser storage flows",
1581
+ "Use shared auth/runtime packages instead of copying token parsing, gateway, or browser storage flows",
1583
1582
  "Derive organization from the credential on every request",
1584
1583
  "Route OAuth, refresh, logout, and SSO paths according to the shared Auth contract"
1585
1584
  ]
@@ -1719,14 +1718,15 @@ var APPLICATION_REQUIREMENTS = [
1719
1718
  ];
1720
1719
  function applicationRequirementScopes(application) {
1721
1720
  const scopes = /* @__PURE__ */ new Set(["all"]);
1721
+ const appId = application.id;
1722
1722
  const hasBrowserSurface = Boolean(application.productionUrl) || application.deployTargets.some((target) => target.kind === "pages");
1723
1723
  const hasApiSurface = application.services.length > 0 || application.deployTargets.some((target) => Boolean(target.apiUrl));
1724
1724
  if (hasBrowserSurface) scopes.add("browser");
1725
1725
  if (hasApiSurface) scopes.add("api");
1726
- if (application.id === "cli" || application.id === "mcp" || application.id === "cloudcontrol") {
1726
+ if (appId === "cli" || appId === "mcp" || appId === "cloudcontrol") {
1727
1727
  scopes.add("tooling");
1728
1728
  }
1729
- if (application.id === "cli" || application.id === "mcp" || hasApiSurface) {
1729
+ if (appId === "cli" || appId === "mcp" || hasApiSurface) {
1730
1730
  scopes.add("agent_surface");
1731
1731
  }
1732
1732
  return Array.from(scopes);
@@ -1845,12 +1845,13 @@ function evaluateRequirement(application, requirement) {
1845
1845
  return finding(requirement, "met", "CloudControl deploy target metadata is present.", evidence);
1846
1846
  }
1847
1847
  case "agent-and-operator-surfaces": {
1848
+ const appId = application.id;
1848
1849
  if (application.services.length > 0) {
1849
1850
  evidence.push(`services=${application.services.join(",")}`);
1850
1851
  return finding(requirement, "partial", "SDK service discovery exists; typed SDK/CLI/MCP workflow coverage should be checked as APIs stabilize.", evidence);
1851
1852
  }
1852
- if (application.id === "cli" || application.id === "mcp" || application.id === "cloudcontrol") {
1853
- evidence.push(`tooling=${application.id}`);
1853
+ if (appId === "cli" || appId === "mcp" || appId === "cloudcontrol") {
1854
+ evidence.push(`tooling=${appId}`);
1854
1855
  return finding(requirement, "met", "This application is itself an agent/operator surface.", evidence);
1855
1856
  }
1856
1857
  return finding(requirement, "missing", "No SDK service or tooling surface is registered.", evidence);
@@ -1925,20 +1926,38 @@ function resolveServiceUrl(service, overrides) {
1925
1926
 
1926
1927
  // src/client.ts
1927
1928
  var WRITE_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
1928
- var TopoloClient = class {
1929
+ var POSITIVE_CACHE_TTL_MS = 5 * 60 * 1e3;
1930
+ var NEGATIVE_CACHE_TTL_MS = 60 * 1e3;
1931
+ var TopoloClient = class _TopoloClient {
1929
1932
  credential;
1930
1933
  agent;
1931
1934
  serviceUrls;
1935
+ serviceIds;
1932
1936
  requireConfirmForWrites;
1933
1937
  timeoutMs;
1934
1938
  fetchImpl;
1935
1939
  debug;
1940
+ /**
1941
+ * Process-wide cache for slug → service-id lookups. Keyed by
1942
+ * `${authBaseUrl}|${slug}` so multiple `TopoloClient` instances pointing at
1943
+ * the same Auth share, but staging vs prod don't poison each other.
1944
+ * Cleared via `TopoloClient.clearServiceIdCache()` if a test needs it.
1945
+ */
1946
+ static serviceIdCache = /* @__PURE__ */ new Map();
1947
+ /**
1948
+ * Drop the slug→service-id cache. Test-only — production callers should
1949
+ * let the 5-minute TTL expire naturally.
1950
+ */
1951
+ static clearServiceIdCache() {
1952
+ _TopoloClient.serviceIdCache.clear();
1953
+ }
1936
1954
  constructor(options) {
1937
1955
  if (!options.credential) throw new TopoloAuthError("credential is required");
1938
1956
  if (!options.agent?.clientName) throw new TopoloAuthError("agent.clientName is required");
1939
1957
  this.credential = options.credential;
1940
1958
  this.agent = options.agent;
1941
1959
  this.serviceUrls = options.serviceUrls;
1960
+ this.serviceIds = options.serviceIds;
1942
1961
  this.requireConfirmForWrites = options.requireConfirmForWrites !== false;
1943
1962
  this.timeoutMs = options.timeoutMs ?? 3e4;
1944
1963
  this.fetchImpl = options.fetch ?? fetch;
@@ -1973,7 +1992,7 @@ var TopoloClient = class {
1973
1992
  const headers = new Headers();
1974
1993
  headers.set("Accept", "application/json");
1975
1994
  if (opts.body !== void 0) headers.set("Content-Type", "application/json");
1976
- const platformServiceId = PLATFORM_SERVICE_IDS[opts.service];
1995
+ const platformServiceId = resolveConfiguredServiceId(opts.service, this.serviceIds);
1977
1996
  if (platformServiceId) headers.set("X-Service-ID", platformServiceId);
1978
1997
  applyAuthHeaders(headers, this.credential);
1979
1998
  const requestId = generateRequestId();
@@ -2047,6 +2066,115 @@ var TopoloClient = class {
2047
2066
  });
2048
2067
  return parsed;
2049
2068
  }
2069
+ /**
2070
+ * Resolve a service-slug → opaque service-id via caller config or TopoloAuth.
2071
+ *
2072
+ * Cache: 5 minutes per process per `(slug, auth_base_url)`. Negative
2073
+ * results (404) are cached for 60s so a typo doesn't hot-loop the
2074
+ * resolver. Returns null when the slug isn't registered — callers decide
2075
+ * whether to throw.
2076
+ *
2077
+ * No credential is sent: the endpoint is public (slugs aren't secret).
2078
+ * This keeps the resolver usable during early bootstrap before a caller
2079
+ * has acquired an access token.
2080
+ */
2081
+ async getServiceIdBySlug(slug) {
2082
+ if (typeof slug !== "string") return null;
2083
+ const normalized = slug.trim().toLowerCase();
2084
+ if (!normalized) return null;
2085
+ const configuredServiceId = resolveConfiguredServiceId(normalized, this.serviceIds);
2086
+ if (configuredServiceId) return configuredServiceId;
2087
+ const authBase = resolveServiceUrl("auth", this.serviceUrls);
2088
+ const cacheKey = `${authBase}|${normalized}`;
2089
+ const now = Date.now();
2090
+ const cached = _TopoloClient.serviceIdCache.get(cacheKey);
2091
+ if (cached && cached.expiresAt > now) {
2092
+ return cached.value;
2093
+ }
2094
+ const url = new URL(
2095
+ `/api/services/by-slug/${encodeURIComponent(normalized)}`,
2096
+ authBase.endsWith("/") ? authBase : `${authBase}/`
2097
+ ).toString();
2098
+ let res;
2099
+ try {
2100
+ res = await this.fetchImpl(url, {
2101
+ method: "GET",
2102
+ headers: { Accept: "application/json" }
2103
+ });
2104
+ } catch {
2105
+ return null;
2106
+ }
2107
+ if (res.status === 404) {
2108
+ _TopoloClient.serviceIdCache.set(cacheKey, {
2109
+ value: null,
2110
+ expiresAt: now + NEGATIVE_CACHE_TTL_MS
2111
+ });
2112
+ return null;
2113
+ }
2114
+ if (!res.ok) {
2115
+ return null;
2116
+ }
2117
+ const body = await res.json().catch(() => null);
2118
+ const serviceId = body?.data?.service_id;
2119
+ const value = typeof serviceId === "string" && serviceId ? serviceId : null;
2120
+ _TopoloClient.serviceIdCache.set(cacheKey, {
2121
+ value,
2122
+ expiresAt: now + POSITIVE_CACHE_TTL_MS
2123
+ });
2124
+ return value;
2125
+ }
2126
+ /**
2127
+ * List the platform service catalog from the live registry
2128
+ * (`GET /api/services/catalog` on TopoloAuth). Credential-aware: the
2129
+ * caller's credential is attached, so an authenticated caller in an
2130
+ * entitled org also receives the `developer_only` services its role
2131
+ * permits; an unauthenticated/agent caller receives `active` only.
2132
+ *
2133
+ * Resilient: on any network/HTTP failure this falls back to the
2134
+ * build-time generated `APPLICATIONS` snapshot so offline/degraded
2135
+ * callers still get a usable (if static) catalog. The live result is
2136
+ * authoritative; the snapshot is a floor, not a cap.
2137
+ */
2138
+ async listServices() {
2139
+ try {
2140
+ const res = await this.request({ service: "auth", method: "GET", path: "/api/services/catalog" });
2141
+ const raw = res.data?.services ?? res.services;
2142
+ if (Array.isArray(raw) && raw.length > 0) {
2143
+ const entries = [];
2144
+ for (const item of raw) {
2145
+ const s = item;
2146
+ const serviceId = typeof s.service_id === "string" ? s.service_id : typeof s.id === "string" ? s.id : null;
2147
+ if (!serviceId) continue;
2148
+ entries.push({
2149
+ serviceId,
2150
+ slug: typeof s.app_slug === "string" ? s.app_slug : null,
2151
+ name: typeof s.name === "string" ? s.name : null,
2152
+ description: typeof s.description === "string" ? s.description : null,
2153
+ baseUrl: typeof s.base_url === "string" ? s.base_url : null,
2154
+ status: typeof s.status === "string" ? s.status : null,
2155
+ surfaceKind: typeof s.surface_kind === "string" ? s.surface_kind : null,
2156
+ launchable: s.launchable === true
2157
+ });
2158
+ }
2159
+ if (entries.length > 0) return entries;
2160
+ }
2161
+ } catch {
2162
+ }
2163
+ return this.generatedCatalogFallback();
2164
+ }
2165
+ generatedCatalogFallback() {
2166
+ const apps = Object.values(APPLICATIONS);
2167
+ return apps.map((app) => ({
2168
+ serviceId: app.id,
2169
+ slug: app.id,
2170
+ name: app.name ?? app.id,
2171
+ description: null,
2172
+ baseUrl: app.productionUrl ?? null,
2173
+ status: "active",
2174
+ surfaceKind: "application",
2175
+ launchable: Boolean(app.productionUrl)
2176
+ }));
2177
+ }
2050
2178
  /**
2051
2179
  * Introspect the current credential. Returns the caller's resolved identity,
2052
2180
  * organization, and permission set. Used by CLI `whoami` and by MCP to gate
@@ -2083,6 +2211,26 @@ var TopoloClient = class {
2083
2211
  };
2084
2212
  }
2085
2213
  };
2214
+ function resolveConfiguredServiceId(slug, serviceIds) {
2215
+ for (const serviceKey of serviceKeyCandidates(slug)) {
2216
+ const configured = serviceIds?.[serviceKey];
2217
+ if (typeof configured === "string" && configured.trim()) return configured.trim();
2218
+ const envKey = `TOPOLO_SERVICE_ID_${serviceKey.replace(/\./g, "_").toUpperCase()}`;
2219
+ const envValue = typeof process !== "undefined" ? process.env?.[envKey] : void 0;
2220
+ if (typeof envValue === "string" && envValue.trim()) return envValue.trim();
2221
+ }
2222
+ return null;
2223
+ }
2224
+ function serviceKeyCandidates(slug) {
2225
+ const normalized = slug.trim().toLowerCase();
2226
+ const withoutTopolo = normalized.startsWith("topolo-") ? normalized.slice("topolo-".length) : normalized;
2227
+ return [.../* @__PURE__ */ new Set([
2228
+ normalized,
2229
+ normalized.replace(/-/g, "_"),
2230
+ withoutTopolo,
2231
+ withoutTopolo.replace(/-/g, "_")
2232
+ ])];
2233
+ }
2086
2234
  function describeError(body, fallback) {
2087
2235
  if (body && typeof body === "object") {
2088
2236
  const maybe = body;
@@ -2649,7 +2797,6 @@ function createTopolo(options) {
2649
2797
  DEFAULT_SERVICE_URLS,
2650
2798
  IdentityModule,
2651
2799
  P2PModule,
2652
- PLATFORM_SERVICE_IDS,
2653
2800
  TOPOLO_WIDGET_ENDPOINT,
2654
2801
  TOPOLO_WIDGET_REFRESH_INTERVAL_MS,
2655
2802
  TopoloAuthError,