@topolo/sdk 0.4.8 → 0.4.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -140,7 +140,7 @@ var SERVICES = {
140
140
  mail: "https://mail.topolo.app",
141
141
  mdm: "https://mdm.topolo.app",
142
142
  messages: "https://messages.topolo.app",
143
- nexus: "https://nexus-api.topolo.app",
143
+ nexus: "https://nexus.topolo.app",
144
144
  notify: "https://notify.topolo.app",
145
145
  observability_watch: "https://observability-watch.topolo.app",
146
146
  one: "https://www.topolo.app",
@@ -691,8 +691,10 @@ var APPLICATIONS = {
691
691
  {
692
692
  "name": "topolo-feed-api",
693
693
  "kind": "worker",
694
- "apiUrl": null,
695
- "domains": [],
694
+ "apiUrl": "https://feed-api.topolo.app",
695
+ "domains": [
696
+ "feed-api.topolo.app"
697
+ ],
696
698
  "routes": [],
697
699
  "serviceId": null
698
700
  },
@@ -1063,9 +1065,9 @@ var APPLICATIONS = {
1063
1065
  {
1064
1066
  "name": "topolo-nexus-gateway",
1065
1067
  "kind": "worker",
1066
- "apiUrl": "https://nexus-api.topolo.app",
1068
+ "apiUrl": "https://nexus.topolo.app",
1067
1069
  "domains": [
1068
- "nexus-api.topolo.app"
1070
+ "nexus.topolo.app"
1069
1071
  ],
1070
1072
  "routes": [],
1071
1073
  "serviceId": "nexus"
@@ -1425,8 +1427,10 @@ var APPLICATIONS = {
1425
1427
  {
1426
1428
  "name": "topolo-socialize-api",
1427
1429
  "kind": "worker",
1428
- "apiUrl": null,
1429
- "domains": [],
1430
+ "apiUrl": "https://socialize-api.topolo.app",
1431
+ "domains": [
1432
+ "socialize-api.topolo.app"
1433
+ ],
1430
1434
  "routes": [],
1431
1435
  "serviceId": null
1432
1436
  },
@@ -2276,10 +2280,14 @@ var TopoloClient = class _TopoloClient {
2276
2280
  if (!serviceKey) {
2277
2281
  throw new TopoloSdkError("unknown_service", "service is required");
2278
2282
  }
2279
- if (serviceKey === "auth") {
2283
+ if (serviceKey === "auth" || serviceKey === "developers") {
2280
2284
  const overrideUrl = resolveConfiguredServiceUrl(serviceKey, this.serviceUrls);
2281
2285
  if (overrideUrl) return { serviceKey, baseUrl: overrideUrl };
2282
- return { serviceKey, baseUrl: DEFAULT_SERVICE_URLS.auth };
2286
+ if (serviceKey === "developers") {
2287
+ const derived = deriveDevelopersFromAuth(resolveConfiguredServiceUrl("auth", this.serviceUrls));
2288
+ if (derived) return { serviceKey, baseUrl: derived };
2289
+ }
2290
+ return { serviceKey, baseUrl: DEFAULT_SERVICE_URLS[serviceKey] };
2283
2291
  }
2284
2292
  const live = await this.resolveLiveCallableService(serviceKey);
2285
2293
  if (live) {
@@ -2319,7 +2327,7 @@ var TopoloClient = class _TopoloClient {
2319
2327
  if (!normalized) return null;
2320
2328
  const configuredServiceId = resolveConfiguredServiceId(normalized, this.serviceIds);
2321
2329
  if (configuredServiceId) return configuredServiceId;
2322
- const authBase = resolveServiceUrl("auth", this.serviceUrls);
2330
+ const authBase = resolveServiceUrl("developers", this.serviceUrls);
2323
2331
  const cacheKey = `${authBase}|${normalized}`;
2324
2332
  const now = Date.now();
2325
2333
  const cached = _TopoloClient.serviceIdCache.get(cacheKey);
@@ -2373,7 +2381,7 @@ var TopoloClient = class _TopoloClient {
2373
2381
  if (this.serviceCatalogCache && this.serviceCatalogCache.expiresAt > now) {
2374
2382
  return this.serviceCatalogCache.entries;
2375
2383
  }
2376
- const res = await this.request({ service: "auth", method: "GET", path: "/api/services/catalog" });
2384
+ const res = await this.request({ service: "developers", method: "GET", path: "/api/services/catalog" });
2377
2385
  const raw = res.data?.services ?? res.services;
2378
2386
  if (!Array.isArray(raw)) {
2379
2387
  throw new TopoloSdkError(
@@ -2604,6 +2612,17 @@ function serviceKeyCandidates2(slug) {
2604
2612
  function normalizeLookupKey(value) {
2605
2613
  return String(value || "").trim().toLowerCase().replace(/\s+/g, "-");
2606
2614
  }
2615
+ function deriveDevelopersFromAuth(authUrl) {
2616
+ if (!authUrl) return null;
2617
+ try {
2618
+ const url = new URL(authUrl);
2619
+ if (!/^auth\./i.test(url.hostname)) return null;
2620
+ url.hostname = url.hostname.replace(/^auth\./i, "developers.");
2621
+ return url.origin;
2622
+ } catch {
2623
+ return null;
2624
+ }
2625
+ }
2607
2626
  function catalogEntryMatches(entry, needle) {
2608
2627
  return catalogEntryLookupKeys(entry).includes(needle);
2609
2628
  }
@@ -2751,7 +2770,6 @@ var CrmModule = class {
2751
2770
  constructor(client) {
2752
2771
  this.client = client;
2753
2772
  }
2754
- client;
2755
2773
  async listContacts(options = {}) {
2756
2774
  const res = await this.client.request({
2757
2775
  service: "crm",
@@ -2799,7 +2817,6 @@ var IdentityModule = class {
2799
2817
  constructor(client) {
2800
2818
  this.client = client;
2801
2819
  }
2802
- client;
2803
2820
  /** Returns the resolved user, organization, and permission set for the
2804
2821
  * current credential. Equivalent to `TopoloClient.introspect()` — exposed
2805
2822
  * on this module for symmetry with the other domain modules. */
@@ -2813,7 +2830,6 @@ var P2PModule = class {
2813
2830
  constructor(client) {
2814
2831
  this.client = client;
2815
2832
  }
2816
- client;
2817
2833
  // Directory ----------------------------------------------------------------
2818
2834
  async listDirectory(options = {}) {
2819
2835
  const res = await this.client.request({
package/dist/index.d.cts CHANGED
@@ -277,7 +277,7 @@ declare const SERVICES: {
277
277
  readonly mail: "https://mail.topolo.app";
278
278
  readonly mdm: "https://mdm.topolo.app";
279
279
  readonly messages: "https://messages.topolo.app";
280
- readonly nexus: "https://nexus-api.topolo.app";
280
+ readonly nexus: "https://nexus.topolo.app";
281
281
  readonly notify: "https://notify.topolo.app";
282
282
  readonly observability_watch: "https://observability-watch.topolo.app";
283
283
  readonly one: "https://www.topolo.app";
@@ -702,8 +702,8 @@ declare const APPLICATIONS: {
702
702
  }, {
703
703
  readonly name: "topolo-feed-api";
704
704
  readonly kind: "worker";
705
- readonly apiUrl: null;
706
- readonly domains: readonly [];
705
+ readonly apiUrl: "https://feed-api.topolo.app";
706
+ readonly domains: readonly ["feed-api.topolo.app"];
707
707
  readonly routes: readonly [];
708
708
  readonly serviceId: null;
709
709
  }, {
@@ -985,8 +985,8 @@ declare const APPLICATIONS: {
985
985
  }, {
986
986
  readonly name: "topolo-nexus-gateway";
987
987
  readonly kind: "worker";
988
- readonly apiUrl: "https://nexus-api.topolo.app";
989
- readonly domains: readonly ["nexus-api.topolo.app"];
988
+ readonly apiUrl: "https://nexus.topolo.app";
989
+ readonly domains: readonly ["nexus.topolo.app"];
990
990
  readonly routes: readonly [];
991
991
  readonly serviceId: "nexus";
992
992
  }];
@@ -1245,8 +1245,8 @@ declare const APPLICATIONS: {
1245
1245
  }, {
1246
1246
  readonly name: "topolo-socialize-api";
1247
1247
  readonly kind: "worker";
1248
- readonly apiUrl: null;
1249
- readonly domains: readonly [];
1248
+ readonly apiUrl: "https://socialize-api.topolo.app";
1249
+ readonly domains: readonly ["socialize-api.topolo.app"];
1250
1250
  readonly routes: readonly [];
1251
1251
  readonly serviceId: null;
1252
1252
  }, {
package/dist/index.d.ts CHANGED
@@ -277,7 +277,7 @@ declare const SERVICES: {
277
277
  readonly mail: "https://mail.topolo.app";
278
278
  readonly mdm: "https://mdm.topolo.app";
279
279
  readonly messages: "https://messages.topolo.app";
280
- readonly nexus: "https://nexus-api.topolo.app";
280
+ readonly nexus: "https://nexus.topolo.app";
281
281
  readonly notify: "https://notify.topolo.app";
282
282
  readonly observability_watch: "https://observability-watch.topolo.app";
283
283
  readonly one: "https://www.topolo.app";
@@ -702,8 +702,8 @@ declare const APPLICATIONS: {
702
702
  }, {
703
703
  readonly name: "topolo-feed-api";
704
704
  readonly kind: "worker";
705
- readonly apiUrl: null;
706
- readonly domains: readonly [];
705
+ readonly apiUrl: "https://feed-api.topolo.app";
706
+ readonly domains: readonly ["feed-api.topolo.app"];
707
707
  readonly routes: readonly [];
708
708
  readonly serviceId: null;
709
709
  }, {
@@ -985,8 +985,8 @@ declare const APPLICATIONS: {
985
985
  }, {
986
986
  readonly name: "topolo-nexus-gateway";
987
987
  readonly kind: "worker";
988
- readonly apiUrl: "https://nexus-api.topolo.app";
989
- readonly domains: readonly ["nexus-api.topolo.app"];
988
+ readonly apiUrl: "https://nexus.topolo.app";
989
+ readonly domains: readonly ["nexus.topolo.app"];
990
990
  readonly routes: readonly [];
991
991
  readonly serviceId: "nexus";
992
992
  }];
@@ -1245,8 +1245,8 @@ declare const APPLICATIONS: {
1245
1245
  }, {
1246
1246
  readonly name: "topolo-socialize-api";
1247
1247
  readonly kind: "worker";
1248
- readonly apiUrl: null;
1249
- readonly domains: readonly [];
1248
+ readonly apiUrl: "https://socialize-api.topolo.app";
1249
+ readonly domains: readonly ["socialize-api.topolo.app"];
1250
1250
  readonly routes: readonly [];
1251
1251
  readonly serviceId: null;
1252
1252
  }, {
package/dist/index.js CHANGED
@@ -85,7 +85,7 @@ var SERVICES = {
85
85
  mail: "https://mail.topolo.app",
86
86
  mdm: "https://mdm.topolo.app",
87
87
  messages: "https://messages.topolo.app",
88
- nexus: "https://nexus-api.topolo.app",
88
+ nexus: "https://nexus.topolo.app",
89
89
  notify: "https://notify.topolo.app",
90
90
  observability_watch: "https://observability-watch.topolo.app",
91
91
  one: "https://www.topolo.app",
@@ -636,8 +636,10 @@ var APPLICATIONS = {
636
636
  {
637
637
  "name": "topolo-feed-api",
638
638
  "kind": "worker",
639
- "apiUrl": null,
640
- "domains": [],
639
+ "apiUrl": "https://feed-api.topolo.app",
640
+ "domains": [
641
+ "feed-api.topolo.app"
642
+ ],
641
643
  "routes": [],
642
644
  "serviceId": null
643
645
  },
@@ -1008,9 +1010,9 @@ var APPLICATIONS = {
1008
1010
  {
1009
1011
  "name": "topolo-nexus-gateway",
1010
1012
  "kind": "worker",
1011
- "apiUrl": "https://nexus-api.topolo.app",
1013
+ "apiUrl": "https://nexus.topolo.app",
1012
1014
  "domains": [
1013
- "nexus-api.topolo.app"
1015
+ "nexus.topolo.app"
1014
1016
  ],
1015
1017
  "routes": [],
1016
1018
  "serviceId": "nexus"
@@ -1370,8 +1372,10 @@ var APPLICATIONS = {
1370
1372
  {
1371
1373
  "name": "topolo-socialize-api",
1372
1374
  "kind": "worker",
1373
- "apiUrl": null,
1374
- "domains": [],
1375
+ "apiUrl": "https://socialize-api.topolo.app",
1376
+ "domains": [
1377
+ "socialize-api.topolo.app"
1378
+ ],
1375
1379
  "routes": [],
1376
1380
  "serviceId": null
1377
1381
  },
@@ -2221,10 +2225,14 @@ var TopoloClient = class _TopoloClient {
2221
2225
  if (!serviceKey) {
2222
2226
  throw new TopoloSdkError("unknown_service", "service is required");
2223
2227
  }
2224
- if (serviceKey === "auth") {
2228
+ if (serviceKey === "auth" || serviceKey === "developers") {
2225
2229
  const overrideUrl = resolveConfiguredServiceUrl(serviceKey, this.serviceUrls);
2226
2230
  if (overrideUrl) return { serviceKey, baseUrl: overrideUrl };
2227
- return { serviceKey, baseUrl: DEFAULT_SERVICE_URLS.auth };
2231
+ if (serviceKey === "developers") {
2232
+ const derived = deriveDevelopersFromAuth(resolveConfiguredServiceUrl("auth", this.serviceUrls));
2233
+ if (derived) return { serviceKey, baseUrl: derived };
2234
+ }
2235
+ return { serviceKey, baseUrl: DEFAULT_SERVICE_URLS[serviceKey] };
2228
2236
  }
2229
2237
  const live = await this.resolveLiveCallableService(serviceKey);
2230
2238
  if (live) {
@@ -2264,7 +2272,7 @@ var TopoloClient = class _TopoloClient {
2264
2272
  if (!normalized) return null;
2265
2273
  const configuredServiceId = resolveConfiguredServiceId(normalized, this.serviceIds);
2266
2274
  if (configuredServiceId) return configuredServiceId;
2267
- const authBase = resolveServiceUrl("auth", this.serviceUrls);
2275
+ const authBase = resolveServiceUrl("developers", this.serviceUrls);
2268
2276
  const cacheKey = `${authBase}|${normalized}`;
2269
2277
  const now = Date.now();
2270
2278
  const cached = _TopoloClient.serviceIdCache.get(cacheKey);
@@ -2318,7 +2326,7 @@ var TopoloClient = class _TopoloClient {
2318
2326
  if (this.serviceCatalogCache && this.serviceCatalogCache.expiresAt > now) {
2319
2327
  return this.serviceCatalogCache.entries;
2320
2328
  }
2321
- const res = await this.request({ service: "auth", method: "GET", path: "/api/services/catalog" });
2329
+ const res = await this.request({ service: "developers", method: "GET", path: "/api/services/catalog" });
2322
2330
  const raw = res.data?.services ?? res.services;
2323
2331
  if (!Array.isArray(raw)) {
2324
2332
  throw new TopoloSdkError(
@@ -2549,6 +2557,17 @@ function serviceKeyCandidates2(slug) {
2549
2557
  function normalizeLookupKey(value) {
2550
2558
  return String(value || "").trim().toLowerCase().replace(/\s+/g, "-");
2551
2559
  }
2560
+ function deriveDevelopersFromAuth(authUrl) {
2561
+ if (!authUrl) return null;
2562
+ try {
2563
+ const url = new URL(authUrl);
2564
+ if (!/^auth\./i.test(url.hostname)) return null;
2565
+ url.hostname = url.hostname.replace(/^auth\./i, "developers.");
2566
+ return url.origin;
2567
+ } catch {
2568
+ return null;
2569
+ }
2570
+ }
2552
2571
  function catalogEntryMatches(entry, needle) {
2553
2572
  return catalogEntryLookupKeys(entry).includes(needle);
2554
2573
  }
@@ -2696,7 +2715,6 @@ var CrmModule = class {
2696
2715
  constructor(client) {
2697
2716
  this.client = client;
2698
2717
  }
2699
- client;
2700
2718
  async listContacts(options = {}) {
2701
2719
  const res = await this.client.request({
2702
2720
  service: "crm",
@@ -2744,7 +2762,6 @@ var IdentityModule = class {
2744
2762
  constructor(client) {
2745
2763
  this.client = client;
2746
2764
  }
2747
- client;
2748
2765
  /** Returns the resolved user, organization, and permission set for the
2749
2766
  * current credential. Equivalent to `TopoloClient.introspect()` — exposed
2750
2767
  * on this module for symmetry with the other domain modules. */
@@ -2758,7 +2775,6 @@ var P2PModule = class {
2758
2775
  constructor(client) {
2759
2776
  this.client = client;
2760
2777
  }
2761
- client;
2762
2778
  // Directory ----------------------------------------------------------------
2763
2779
  async listDirectory(options = {}) {
2764
2780
  const res = await this.client.request({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@topolo/sdk",
3
- "version": "0.4.8",
3
+ "version": "0.4.9",
4
4
  "description": "Typed client SDK for the Topolo platform. Used by TopoloCli, TopoloMCP, and third-party agents.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -530,8 +530,10 @@ export const APPLICATIONS = {
530
530
  {
531
531
  "name": "topolo-feed-api",
532
532
  "kind": "worker",
533
- "apiUrl": null,
534
- "domains": [],
533
+ "apiUrl": "https://feed-api.topolo.app",
534
+ "domains": [
535
+ "feed-api.topolo.app"
536
+ ],
535
537
  "routes": [],
536
538
  "serviceId": null
537
539
  },
@@ -902,9 +904,9 @@ export const APPLICATIONS = {
902
904
  {
903
905
  "name": "topolo-nexus-gateway",
904
906
  "kind": "worker",
905
- "apiUrl": "https://nexus-api.topolo.app",
907
+ "apiUrl": "https://nexus.topolo.app",
906
908
  "domains": [
907
- "nexus-api.topolo.app"
909
+ "nexus.topolo.app"
908
910
  ],
909
911
  "routes": [],
910
912
  "serviceId": "nexus"
@@ -1264,8 +1266,10 @@ export const APPLICATIONS = {
1264
1266
  {
1265
1267
  "name": "topolo-socialize-api",
1266
1268
  "kind": "worker",
1267
- "apiUrl": null,
1268
- "domains": [],
1269
+ "apiUrl": "https://socialize-api.topolo.app",
1270
+ "domains": [
1271
+ "socialize-api.topolo.app"
1272
+ ],
1269
1273
  "routes": [],
1270
1274
  "serviceId": null
1271
1275
  },
@@ -401,7 +401,7 @@ describe('TopoloClient.getServiceIdBySlug', () => {
401
401
  credential: { kind: 'api_key', apiKey: 'x' },
402
402
  agent: AGENT,
403
403
  fetch: impl,
404
- serviceUrls: { auth: 'https://auth.example.com' } as never,
404
+ serviceUrls: { auth: 'https://auth.example.com', developers: 'https://auth.example.com' } as never,
405
405
  serviceIds: { notify: 'service_notify' },
406
406
  });
407
407
  const id = await client.getServiceIdBySlug('topolo-notify');
@@ -413,7 +413,7 @@ describe('TopoloClient.getServiceIdBySlug', () => {
413
413
  const { impl, calls } = stubFetch([
414
414
  { body: { success: true, data: { service_id: 'service_abc123', api_base_url: 'https://x-api' } } },
415
415
  ]);
416
- const client = freshClient(impl, { auth: 'https://auth.example.com' });
416
+ const client = freshClient(impl, { auth: 'https://auth.example.com', developers: 'https://auth.example.com' });
417
417
  const id = await client.getServiceIdBySlug('external-service');
418
418
  expect(id).toBe('service_abc123');
419
419
  expect(calls[0]!.url).toBe('https://auth.example.com/api/services/by-slug/external-service');
@@ -425,7 +425,7 @@ describe('TopoloClient.getServiceIdBySlug', () => {
425
425
 
426
426
  it('returns null on 404 and caches the negative result', async () => {
427
427
  const { impl, calls } = stubFetch([{ status: 404, body: { success: false } }]);
428
- const client = freshClient(impl, { auth: 'https://auth.example.com' });
428
+ const client = freshClient(impl, { auth: 'https://auth.example.com', developers: 'https://auth.example.com' });
429
429
  expect(await client.getServiceIdBySlug('nope')).toBeNull();
430
430
  expect(await client.getServiceIdBySlug('nope')).toBeNull();
431
431
  expect(calls.length).toBe(1);
@@ -435,7 +435,7 @@ describe('TopoloClient.getServiceIdBySlug', () => {
435
435
  const { impl, calls } = stubFetch([
436
436
  { body: { success: true, data: { service_id: 'service_external' } } },
437
437
  ]);
438
- const client = freshClient(impl, { auth: 'https://auth.example.com' });
438
+ const client = freshClient(impl, { auth: 'https://auth.example.com', developers: 'https://auth.example.com' });
439
439
  expect(await client.getServiceIdBySlug('external-service')).toBe('service_external');
440
440
  expect(await client.getServiceIdBySlug('external-service')).toBe('service_external');
441
441
  expect(calls.length).toBe(1);
@@ -445,7 +445,7 @@ describe('TopoloClient.getServiceIdBySlug', () => {
445
445
  const { impl, calls } = stubFetch([
446
446
  { body: { success: true, data: { service_id: 'service_x' } } },
447
447
  ]);
448
- const client = freshClient(impl, { auth: 'https://auth.example.com' });
448
+ const client = freshClient(impl, { auth: 'https://auth.example.com', developers: 'https://auth.example.com' });
449
449
  await client.getServiceIdBySlug(' EXTERNAL-SERVICE ');
450
450
  expect(calls[0]!.url).toBe('https://auth.example.com/api/services/by-slug/external-service');
451
451
  });
@@ -467,7 +467,7 @@ describe('TopoloClient.getServiceIdBySlug', () => {
467
467
  credential: { kind: 'api_key', apiKey: 'x' },
468
468
  agent: AGENT,
469
469
  fetch: impl,
470
- serviceUrls: { auth: 'https://auth.example.com' } as never,
470
+ serviceUrls: { auth: 'https://auth.example.com', developers: 'https://auth.example.com' } as never,
471
471
  });
472
472
  expect(await client.getServiceIdBySlug('flaky')).toBeNull();
473
473
  // Second call retries — earlier null was not cached.
@@ -523,7 +523,7 @@ describe('TopoloClient.listServices', () => {
523
523
  credential: { kind: 'access_token', accessToken: 'tok_abc' },
524
524
  agent: AGENT,
525
525
  fetch: impl,
526
- serviceUrls: { auth: 'https://auth.example.com' },
526
+ serviceUrls: { auth: 'https://auth.example.com', developers: 'https://auth.example.com' },
527
527
  });
528
528
  const out = await client.listServices();
529
529
  expect(out).toEqual([
@@ -574,7 +574,7 @@ describe('TopoloClient.listServices', () => {
574
574
  credential: { kind: 'api_key', apiKey: 'dak_xyz' },
575
575
  agent: AGENT,
576
576
  fetch: impl,
577
- serviceUrls: { auth: 'https://auth.example.com' },
577
+ serviceUrls: { auth: 'https://auth.example.com', developers: 'https://auth.example.com' },
578
578
  });
579
579
  await expect(client.listServices()).rejects.toBeInstanceOf(TopoloHttpError);
580
580
  });
@@ -605,7 +605,7 @@ describe('TopoloClient.listServices', () => {
605
605
  credential: { kind: 'api_key', apiKey: 'dak_xyz' },
606
606
  agent: AGENT,
607
607
  fetch: impl,
608
- serviceUrls: { auth: 'https://auth.example.com' },
608
+ serviceUrls: { auth: 'https://auth.example.com', developers: 'https://auth.example.com' },
609
609
  });
610
610
 
611
611
  await client.request({ service: 'mail', path: '/api/health' });
@@ -640,7 +640,7 @@ describe('TopoloClient.listServices', () => {
640
640
  credential: { kind: 'api_key', apiKey: 'dak_xyz' },
641
641
  agent: AGENT,
642
642
  fetch: impl,
643
- serviceUrls: { auth: 'https://auth.example.com' },
643
+ serviceUrls: { auth: 'https://auth.example.com', developers: 'https://auth.example.com' },
644
644
  });
645
645
 
646
646
  await client.request({ service: 'crm', path: '/health' });
@@ -649,6 +649,21 @@ describe('TopoloClient.listServices', () => {
649
649
  expect(calls[1]!.url).toBe('https://crm-api.topolo.app/health');
650
650
  });
651
651
 
652
+ it('derives the catalog (developers) host from the auth url when developers is unset', async () => {
653
+ const { impl, calls } = stubFetch([{ body: { data: { services: [] } } }]);
654
+ const client = new TopoloClient({
655
+ credential: { kind: 'api_key', apiKey: 'dak_xyz' },
656
+ agent: AGENT,
657
+ fetch: impl,
658
+ // Fleet apps configure only `auth` per-env; the catalog host is derived.
659
+ serviceUrls: { auth: 'https://auth.stg.topolo.us' },
660
+ });
661
+
662
+ await client.listServices();
663
+
664
+ expect(calls[0]!.url).toBe('https://developers.stg.topolo.us/api/services/catalog');
665
+ });
666
+
652
667
  it('prefers live catalog api_base_url for dynamic services', async () => {
653
668
  const { impl, calls } = stubFetch([
654
669
  {
@@ -675,7 +690,7 @@ describe('TopoloClient.listServices', () => {
675
690
  credential: { kind: 'api_key', apiKey: 'dak_xyz' },
676
691
  agent: AGENT,
677
692
  fetch: impl,
678
- serviceUrls: { auth: 'https://auth.example.com' },
693
+ serviceUrls: { auth: 'https://auth.example.com', developers: 'https://auth.example.com' },
679
694
  });
680
695
 
681
696
  await client.request({ service: 'future', path: '/health' });
@@ -708,7 +723,7 @@ describe('TopoloClient.listServices', () => {
708
723
  credential: { kind: 'api_key', apiKey: 'dak_xyz' },
709
724
  agent: AGENT,
710
725
  fetch: impl,
711
- serviceUrls: { auth: 'https://auth.example.com' },
726
+ serviceUrls: { auth: 'https://auth.example.com', developers: 'https://auth.example.com' },
712
727
  });
713
728
 
714
729
  await expect(client.request({ service: 'mail', path: '/api/health' })).rejects.toMatchObject({
@@ -742,7 +757,7 @@ describe('TopoloClient.listServices', () => {
742
757
  credential: { kind: 'api_key', apiKey: 'dak_xyz' },
743
758
  agent: AGENT,
744
759
  fetch: impl,
745
- serviceUrls: { auth: 'https://auth.example.com' },
760
+ serviceUrls: { auth: 'https://auth.example.com', developers: 'https://auth.example.com' },
746
761
  });
747
762
 
748
763
  await expect(client.request({ service: 'future-app', path: '/api/health' })).rejects.toMatchObject({
@@ -777,7 +792,7 @@ describe('TopoloClient.listServices', () => {
777
792
  credential: { kind: 'api_key', apiKey: 'dak_xyz' },
778
793
  agent: AGENT,
779
794
  fetch: impl,
780
- serviceUrls: { auth: 'https://auth.example.com' },
795
+ serviceUrls: { auth: 'https://auth.example.com', developers: 'https://auth.example.com' },
781
796
  });
782
797
 
783
798
  await client.request({ service: 'warehouse', path: '/api/one' });
@@ -798,7 +813,7 @@ describe('TopoloClient action catalog', () => {
798
813
  credential: { kind: 'access_token', accessToken: 'tok_abc' },
799
814
  agent: AGENT,
800
815
  fetch: impl,
801
- serviceUrls: { auth: 'https://auth.example.com' },
816
+ serviceUrls: { auth: 'https://auth.example.com', developers: 'https://auth.example.com' },
802
817
  });
803
818
  const actions = await client.listActions();
804
819
  expect(actions.map((action) => action.name)).toEqual([
@@ -815,7 +830,7 @@ describe('TopoloClient action catalog', () => {
815
830
  credential: { kind: 'api_key', apiKey: 'dak_xyz' },
816
831
  agent: AGENT,
817
832
  fetch: impl,
818
- serviceUrls: { auth: 'https://auth.example.com' },
833
+ serviceUrls: { auth: 'https://auth.example.com', developers: 'https://auth.example.com' },
819
834
  });
820
835
  await client.callAction('messages.list', { mailboxId: 'primary', q: 'Ada', page: 2 });
821
836
  const call = requestCalls(calls)[0]!;
@@ -833,7 +848,7 @@ describe('TopoloClient action catalog', () => {
833
848
  credential: { kind: 'api_key', apiKey: 'dak_xyz' },
834
849
  agent: AGENT,
835
850
  fetch: impl,
836
- serviceUrls: { auth: 'https://auth.example.com' },
851
+ serviceUrls: { auth: 'https://auth.example.com', developers: 'https://auth.example.com' },
837
852
  });
838
853
  await client.callAction('topolo_mail_messages_get', {
839
854
  mailboxId: 'primary',
@@ -853,7 +868,7 @@ describe('TopoloClient action catalog', () => {
853
868
  credential: { kind: 'api_key', apiKey: 'dak_xyz' },
854
869
  agent: AGENT,
855
870
  fetch: impl,
856
- serviceUrls: { auth: 'https://auth.example.com' },
871
+ serviceUrls: { auth: 'https://auth.example.com', developers: 'https://auth.example.com' },
857
872
  });
858
873
  await expect(
859
874
  client.callAction('messages.send', { mailboxId: 'primary', subject: 'Hello' }),
@@ -866,7 +881,7 @@ describe('TopoloClient action catalog', () => {
866
881
  credential: { kind: 'api_key', apiKey: 'dak_xyz' },
867
882
  agent: AGENT,
868
883
  fetch: impl,
869
- serviceUrls: { auth: 'https://auth.example.com' },
884
+ serviceUrls: { auth: 'https://auth.example.com', developers: 'https://auth.example.com' },
870
885
  });
871
886
  await client.callAction('messages.send', { mailboxId: 'primary', subject: 'Hello' }, { confirm: true });
872
887
  const call = requestCalls(calls)[0]!;
package/src/client.ts CHANGED
@@ -337,10 +337,20 @@ export class TopoloClient {
337
337
  throw new TopoloSdkError('unknown_service', 'service is required');
338
338
  }
339
339
 
340
- if (serviceKey === 'auth') {
340
+ // auth + developers are DIRECT services resolved without a catalog lookup:
341
+ // developers serves the catalog itself, so resolving it via the catalog would
342
+ // recurse infinitely.
343
+ if (serviceKey === 'auth' || serviceKey === 'developers') {
341
344
  const overrideUrl = resolveConfiguredServiceUrl(serviceKey, this.serviceUrls);
342
345
  if (overrideUrl) return { serviceKey, baseUrl: overrideUrl };
343
- return { serviceKey, baseUrl: DEFAULT_SERVICE_URLS.auth };
346
+ // The catalog host (developers) defaults to the SAME environment as auth.
347
+ // Apps configure only `auth` per-env; derive developers from it (auth.X ->
348
+ // developers.X) so the catalog cutover needs no per-app config change.
349
+ if (serviceKey === 'developers') {
350
+ const derived = deriveDevelopersFromAuth(resolveConfiguredServiceUrl('auth', this.serviceUrls));
351
+ if (derived) return { serviceKey, baseUrl: derived };
352
+ }
353
+ return { serviceKey, baseUrl: DEFAULT_SERVICE_URLS[serviceKey as 'auth' | 'developers'] };
344
354
  }
345
355
 
346
356
  const live = await this.resolveLiveCallableService(serviceKey);
@@ -386,7 +396,8 @@ export class TopoloClient {
386
396
  const configuredServiceId = resolveConfiguredServiceId(normalized, this.serviceIds);
387
397
  if (configuredServiceId) return configuredServiceId;
388
398
 
389
- const authBase = resolveServiceUrl('auth', this.serviceUrls);
399
+ // app-catalog greenfield: by-slug resolver lives on the Catalog (developers).
400
+ const authBase = resolveServiceUrl('developers', this.serviceUrls);
390
401
  const cacheKey = `${authBase}|${normalized}`;
391
402
  const now = Date.now();
392
403
  const cached = TopoloClient.serviceIdCache.get(cacheKey);
@@ -450,10 +461,13 @@ export class TopoloClient {
450
461
  return this.serviceCatalogCache.entries;
451
462
  }
452
463
 
464
+ // app-catalog greenfield: the catalog moved to the Catalog (developers)
465
+ // service. Auth no longer owns the registry; it only provides scoping, which
466
+ // the Catalog composes server-side. Consumers pass the per-env developers URL.
453
467
  const res = await this.request<
454
468
  | { data?: { services?: unknown[] } }
455
469
  | { services?: unknown[] }
456
- >({ service: 'auth', method: 'GET', path: '/api/services/catalog' });
470
+ >({ service: 'developers', method: 'GET', path: '/api/services/catalog' });
457
471
  const raw =
458
472
  (res as { data?: { services?: unknown[] } }).data?.services ??
459
473
  (res as { services?: unknown[] }).services;
@@ -774,6 +788,24 @@ function normalizeLookupKey(value: string): string {
774
788
  return String(value || '').trim().toLowerCase().replace(/\s+/g, '-');
775
789
  }
776
790
 
791
+ /**
792
+ * Derive the Catalog (developers) base URL from a configured auth base URL by
793
+ * swapping the leading `auth` host label for `developers` (auth.stg.topolo.us ->
794
+ * developers.stg.topolo.us). Returns null if the auth URL is absent or doesn't
795
+ * start with the `auth.` label, so the caller falls back to the bundled default.
796
+ */
797
+ function deriveDevelopersFromAuth(authUrl: string | null | undefined): string | null {
798
+ if (!authUrl) return null;
799
+ try {
800
+ const url = new URL(authUrl);
801
+ if (!/^auth\./i.test(url.hostname)) return null;
802
+ url.hostname = url.hostname.replace(/^auth\./i, 'developers.');
803
+ return url.origin;
804
+ } catch {
805
+ return null;
806
+ }
807
+ }
808
+
777
809
  function catalogEntryMatches(entry: TopoloServiceCatalogEntry, needle: string): boolean {
778
810
  return catalogEntryLookupKeys(entry).includes(needle);
779
811
  }
@@ -30,7 +30,7 @@ export const SERVICES = {
30
30
  mail: "https://mail.topolo.app",
31
31
  mdm: "https://mdm.topolo.app",
32
32
  messages: "https://messages.topolo.app",
33
- nexus: "https://nexus-api.topolo.app",
33
+ nexus: "https://nexus.topolo.app",
34
34
  notify: "https://notify.topolo.app",
35
35
  observability_watch: "https://observability-watch.topolo.app",
36
36
  one: "https://www.topolo.app",