@topolo/sdk 0.3.0 → 0.4.1

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/src/client.ts CHANGED
@@ -5,9 +5,10 @@ import {
5
5
  type AgentIdentity,
6
6
  type TopoloCredential,
7
7
  } from './auth.js';
8
- import { TopoloAuthError, TopoloHttpError, TopoloPermissionError } from './errors.js';
8
+ import { TopoloAuthError, TopoloHttpError, TopoloPermissionError, TopoloSdkError } from './errors.js';
9
9
  import {
10
- PLATFORM_SERVICE_IDS,
10
+ DEFAULT_SERVICE_URLS,
11
+ APPLICATIONS,
11
12
  resolveServiceUrl,
12
13
  type ServiceId,
13
14
  } from './services.js';
@@ -16,7 +17,9 @@ export interface TopoloClientOptions {
16
17
  credential: TopoloCredential;
17
18
  agent: AgentIdentity;
18
19
  /** Per-service URL overrides. Useful for staging/dev. */
19
- serviceUrls?: Partial<Record<ServiceId, string>>;
20
+ serviceUrls?: Record<string, string>;
21
+ /** Per-service TopoloAuth service id overrides from the owning app/deployment. */
22
+ serviceIds?: Record<string, string>;
20
23
  /**
21
24
  * When true, mutating helpers (POST/PUT/PATCH/DELETE) require callers to pass
22
25
  * `{ confirm: true }`. Defaults to true. Set false only for trusted surfaces.
@@ -39,7 +42,7 @@ export type TopoloDebugEvent =
39
42
  | {
40
43
  phase: 'request';
41
44
  method: string;
42
- service: ServiceId;
45
+ service: string;
43
46
  path: string;
44
47
  url: string;
45
48
  requestId: string;
@@ -47,7 +50,7 @@ export type TopoloDebugEvent =
47
50
  | {
48
51
  phase: 'response';
49
52
  method: string;
50
- service: ServiceId;
53
+ service: string;
51
54
  path: string;
52
55
  url: string;
53
56
  requestId: string;
@@ -57,7 +60,7 @@ export type TopoloDebugEvent =
57
60
  | {
58
61
  phase: 'error';
59
62
  method: string;
60
- service: ServiceId;
63
+ service: string;
61
64
  path: string;
62
65
  url: string;
63
66
  requestId: string;
@@ -67,7 +70,7 @@ export type TopoloDebugEvent =
67
70
  };
68
71
 
69
72
  export interface RequestOptions {
70
- service: ServiceId;
73
+ service: string;
71
74
  path: string;
72
75
  method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
73
76
  query?: Record<string, string | number | boolean | undefined>;
@@ -83,20 +86,39 @@ const WRITE_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);
83
86
 
84
87
  const POSITIVE_CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
85
88
  const NEGATIVE_CACHE_TTL_MS = 60 * 1000; // 1 minute
89
+ const CATALOG_CACHE_TTL_MS = 60 * 1000; // 1 minute
86
90
 
87
91
  interface ServiceIdCacheEntry {
88
92
  value: string | null;
89
93
  expiresAt: number;
90
94
  }
91
95
 
96
+ /**
97
+ * A single entry from the live platform service catalog. Returned by
98
+ * {@link TopoloClient.listServices}. Shape mirrors the public-safe projection
99
+ * from TopoloAuth `GET /api/services/catalog`.
100
+ */
101
+ export interface TopoloServiceCatalogEntry {
102
+ serviceId: string;
103
+ slug: string | null;
104
+ name: string | null;
105
+ description: string | null;
106
+ baseUrl: string | null;
107
+ status: string | null;
108
+ surfaceKind: string | null;
109
+ launchable: boolean;
110
+ }
111
+
92
112
  export class TopoloClient {
93
113
  private readonly credential: TopoloCredential;
94
114
  private readonly agent: AgentIdentity;
95
- private readonly serviceUrls?: Partial<Record<ServiceId, string>>;
115
+ private readonly serviceUrls?: Record<string, string>;
116
+ private readonly serviceIds?: Record<string, string>;
96
117
  private readonly requireConfirmForWrites: boolean;
97
118
  private readonly timeoutMs: number;
98
119
  private readonly fetchImpl: typeof fetch;
99
120
  private readonly debug?: (event: TopoloDebugEvent) => void;
121
+ private serviceCatalogCache: { entries: TopoloServiceCatalogEntry[]; expiresAt: number } | null = null;
100
122
 
101
123
  /**
102
124
  * Process-wide cache for slug → service-id lookups. Keyed by
@@ -120,6 +142,7 @@ export class TopoloClient {
120
142
  this.credential = options.credential;
121
143
  this.agent = options.agent;
122
144
  this.serviceUrls = options.serviceUrls;
145
+ this.serviceIds = options.serviceIds;
123
146
  this.requireConfirmForWrites = options.requireConfirmForWrites !== false;
124
147
  this.timeoutMs = options.timeoutMs ?? 30_000;
125
148
  this.fetchImpl = options.fetch ?? fetch;
@@ -149,7 +172,8 @@ export class TopoloClient {
149
172
  );
150
173
  }
151
174
 
152
- const baseUrl = resolveServiceUrl(opts.service, this.serviceUrls);
175
+ const resolvedService = await this.resolveRequestService(opts.service);
176
+ const baseUrl = resolvedService.baseUrl;
153
177
  const url = new URL(opts.path, baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`);
154
178
  if (opts.query) {
155
179
  for (const [k, v] of Object.entries(opts.query)) {
@@ -160,7 +184,7 @@ export class TopoloClient {
160
184
  const headers = new Headers();
161
185
  headers.set('Accept', 'application/json');
162
186
  if (opts.body !== undefined) headers.set('Content-Type', 'application/json');
163
- const platformServiceId = PLATFORM_SERVICE_IDS[opts.service];
187
+ const platformServiceId = resolveConfiguredServiceId(resolvedService.serviceKey, this.serviceIds);
164
188
  if (platformServiceId) headers.set('X-Service-ID', platformServiceId);
165
189
 
166
190
  applyAuthHeaders(headers, this.credential);
@@ -179,7 +203,7 @@ export class TopoloClient {
179
203
 
180
204
  const urlStr = url.toString();
181
205
  const startedAt = Date.now();
182
- this.emit({ phase: 'request', method, service: opts.service, path: opts.path, url: urlStr, requestId });
206
+ this.emit({ phase: 'request', method, service: resolvedService.serviceKey, path: opts.path, url: urlStr, requestId });
183
207
 
184
208
  let res: Response;
185
209
  try {
@@ -193,7 +217,7 @@ export class TopoloClient {
193
217
  this.emit({
194
218
  phase: 'error',
195
219
  method,
196
- service: opts.service,
220
+ service: resolvedService.serviceKey,
197
221
  path: opts.path,
198
222
  url: urlStr,
199
223
  requestId,
@@ -216,7 +240,7 @@ export class TopoloClient {
216
240
  this.emit({
217
241
  phase: 'error',
218
242
  method,
219
- service: opts.service,
243
+ service: resolvedService.serviceKey,
220
244
  path: opts.path,
221
245
  url: urlStr,
222
246
  requestId,
@@ -231,13 +255,13 @@ export class TopoloClient {
231
255
  extractRequired(parsed),
232
256
  );
233
257
  }
234
- throw new TopoloHttpError(opts.service, opts.path, res.status, parsed);
258
+ throw new TopoloHttpError(resolvedService.serviceKey, opts.path, res.status, parsed);
235
259
  }
236
260
 
237
261
  this.emit({
238
262
  phase: 'response',
239
263
  method,
240
- service: opts.service,
264
+ service: resolvedService.serviceKey,
241
265
  path: opts.path,
242
266
  url: urlStr,
243
267
  requestId,
@@ -248,13 +272,39 @@ export class TopoloClient {
248
272
  return parsed as T;
249
273
  }
250
274
 
275
+ private async resolveRequestService(service: string): Promise<{ serviceKey: string; baseUrl: string }> {
276
+ const serviceKey = normalizeLookupKey(service);
277
+ if (!serviceKey) {
278
+ throw new TopoloSdkError('unknown_service', 'service is required');
279
+ }
280
+
281
+ const overrideUrl = resolveConfiguredServiceUrl(serviceKey, this.serviceUrls);
282
+ if (overrideUrl) return { serviceKey, baseUrl: overrideUrl };
283
+
284
+ const staticUrl = DEFAULT_SERVICE_URLS[serviceKey as ServiceId];
285
+ if (staticUrl) return { serviceKey, baseUrl: staticUrl };
286
+
287
+ const live = await this.resolveLiveCallableService(serviceKey);
288
+ if (live) return live;
289
+
290
+ throw new TopoloSdkError(
291
+ 'unknown_service',
292
+ `Unknown Topolo service "${service}". Run service discovery first and pass a callable service id or slug.`,
293
+ );
294
+ }
295
+
296
+ private async resolveLiveCallableService(serviceKey: string): Promise<{ serviceKey: string; baseUrl: string } | null> {
297
+ const catalog = await this.listServices();
298
+ const match = catalog.find((entry) => catalogEntryMatches(entry, serviceKey));
299
+ if (!match?.baseUrl || !isCallableCatalogEntry(match)) return null;
300
+ return {
301
+ serviceKey: normalizeLookupKey(match.slug || match.name || match.serviceId) || serviceKey,
302
+ baseUrl: match.baseUrl,
303
+ };
304
+ }
305
+
251
306
  /**
252
- * Resolve a service-slug → opaque service-id via TopoloAuth.
253
- *
254
- * Replaces the SDK-baked `PLATFORM_SERVICE_IDS` map (deprecated in 0.3.0,
255
- * removed in 0.4.0). Slug shape accepted: either the canonical Auth slug
256
- * (e.g. `topolo-notify`) or the SDK short id (`notify`) — Auth's resolver
257
- * understands both.
307
+ * Resolve a service-slug → opaque service-id via caller config or TopoloAuth.
258
308
  *
259
309
  * Cache: 5 minutes per process per `(slug, auth_base_url)`. Negative
260
310
  * results (404) are cached for 60s so a typo doesn't hot-loop the
@@ -270,6 +320,9 @@ export class TopoloClient {
270
320
  const normalized = slug.trim().toLowerCase();
271
321
  if (!normalized) return null;
272
322
 
323
+ const configuredServiceId = resolveConfiguredServiceId(normalized, this.serviceIds);
324
+ if (configuredServiceId) return configuredServiceId;
325
+
273
326
  const authBase = resolveServiceUrl('auth', this.serviceUrls);
274
327
  const cacheKey = `${authBase}|${normalized}`;
275
328
  const now = Date.now();
@@ -318,6 +371,88 @@ export class TopoloClient {
318
371
  return value;
319
372
  }
320
373
 
374
+ /**
375
+ * List the platform service catalog from the live registry
376
+ * (`GET /api/services/catalog` on TopoloAuth). Credential-aware: the
377
+ * caller's credential is attached, so an authenticated caller in an
378
+ * entitled org also receives the `developer_only` services its role
379
+ * permits; an unauthenticated/agent caller receives `active` only.
380
+ *
381
+ * Resilient: on any network/HTTP failure this falls back to the
382
+ * build-time generated `APPLICATIONS` snapshot so offline/degraded
383
+ * callers still get a usable (if static) catalog. The live result is
384
+ * authoritative; the snapshot is a floor, not a cap.
385
+ */
386
+ async listServices(): Promise<TopoloServiceCatalogEntry[]> {
387
+ const now = Date.now();
388
+ if (this.serviceCatalogCache && this.serviceCatalogCache.expiresAt > now) {
389
+ return this.serviceCatalogCache.entries;
390
+ }
391
+
392
+ try {
393
+ const res = await this.request<
394
+ | { data?: { services?: unknown[] } }
395
+ | { services?: unknown[] }
396
+ >({ service: 'auth', method: 'GET', path: '/api/services/catalog' });
397
+ const raw =
398
+ (res as { data?: { services?: unknown[] } }).data?.services ??
399
+ (res as { services?: unknown[] }).services;
400
+ if (Array.isArray(raw) && raw.length > 0) {
401
+ const entries: TopoloServiceCatalogEntry[] = [];
402
+ for (const item of raw) {
403
+ const s = item as Record<string, unknown>;
404
+ const serviceId =
405
+ typeof s.service_id === 'string'
406
+ ? s.service_id
407
+ : typeof s.id === 'string'
408
+ ? s.id
409
+ : null;
410
+ if (!serviceId) continue;
411
+ entries.push({
412
+ serviceId,
413
+ slug: typeof s.app_slug === 'string' ? s.app_slug : null,
414
+ name: typeof s.name === 'string' ? s.name : null,
415
+ description:
416
+ typeof s.description === 'string' ? s.description : null,
417
+ baseUrl: typeof s.base_url === 'string' ? s.base_url : null,
418
+ status: typeof s.status === 'string' ? s.status : null,
419
+ surfaceKind:
420
+ typeof s.surface_kind === 'string' ? s.surface_kind : null,
421
+ launchable: s.launchable === true,
422
+ });
423
+ }
424
+ if (entries.length > 0) {
425
+ this.serviceCatalogCache = {
426
+ entries,
427
+ expiresAt: now + CATALOG_CACHE_TTL_MS,
428
+ };
429
+ return entries;
430
+ }
431
+ }
432
+ } catch {
433
+ // Fall through to the generated snapshot.
434
+ }
435
+ return this.generatedCatalogFallback();
436
+ }
437
+
438
+ private generatedCatalogFallback(): TopoloServiceCatalogEntry[] {
439
+ const apps = Object.values(APPLICATIONS) as ReadonlyArray<{
440
+ id: string;
441
+ name?: string;
442
+ productionUrl?: string | null;
443
+ }>;
444
+ return apps.map((app) => ({
445
+ serviceId: app.id,
446
+ slug: app.id,
447
+ name: app.name ?? app.id,
448
+ description: null,
449
+ baseUrl: app.productionUrl ?? null,
450
+ status: 'active',
451
+ surfaceKind: 'application',
452
+ launchable: Boolean(app.productionUrl),
453
+ }));
454
+ }
455
+
321
456
  /**
322
457
  * Introspect the current credential. Returns the caller's resolved identity,
323
458
  * organization, and permission set. Used by CLI `whoami` and by MCP to gate
@@ -362,6 +497,84 @@ export class TopoloClient {
362
497
  }
363
498
  }
364
499
 
500
+ function resolveConfiguredServiceId(
501
+ slug: string,
502
+ serviceIds?: Record<string, string>,
503
+ ): string | null {
504
+ for (const serviceKey of serviceKeyCandidates(slug)) {
505
+ const configured = serviceIds?.[serviceKey as ServiceId];
506
+ if (typeof configured === 'string' && configured.trim()) return configured.trim();
507
+
508
+ const envKey = `TOPOLO_SERVICE_ID_${serviceKey.replace(/\./g, '_').toUpperCase()}`;
509
+ const envValue = typeof process !== 'undefined' ? process.env?.[envKey] : undefined;
510
+ if (typeof envValue === 'string' && envValue.trim()) return envValue.trim();
511
+ }
512
+
513
+ return null;
514
+ }
515
+
516
+ function resolveConfiguredServiceUrl(
517
+ slug: string,
518
+ serviceUrls?: Record<string, string>,
519
+ ): string | null {
520
+ for (const serviceKey of serviceKeyCandidates(slug)) {
521
+ const configured = serviceUrls?.[serviceKey];
522
+ if (typeof configured === 'string' && configured.trim()) return configured.trim();
523
+
524
+ const envKey = `TOPOLO_SERVICE_URL_${serviceKey.replace(/\./g, '_').toUpperCase()}`;
525
+ const envValue = typeof process !== 'undefined' ? process.env?.[envKey] : undefined;
526
+ if (typeof envValue === 'string' && envValue.trim()) return envValue.trim();
527
+ }
528
+
529
+ return null;
530
+ }
531
+
532
+ function serviceKeyCandidates(slug: string): string[] {
533
+ const normalized = normalizeLookupKey(slug);
534
+ const withoutTopolo = normalized.startsWith('topolo-')
535
+ ? normalized.slice('topolo-'.length)
536
+ : normalized;
537
+ return [...new Set([
538
+ normalized,
539
+ normalized.replace(/-/g, '_'),
540
+ withoutTopolo,
541
+ withoutTopolo.replace(/-/g, '_'),
542
+ ])];
543
+ }
544
+
545
+ function normalizeLookupKey(value: string): string {
546
+ return String(value || '').trim().toLowerCase();
547
+ }
548
+
549
+ function catalogEntryMatches(entry: TopoloServiceCatalogEntry, needle: string): boolean {
550
+ return catalogEntryLookupKeys(entry).includes(needle);
551
+ }
552
+
553
+ function catalogEntryLookupKeys(entry: TopoloServiceCatalogEntry): string[] {
554
+ const values = [entry.serviceId, entry.slug, entry.name].filter((value): value is string => (
555
+ typeof value === 'string' && value.trim().length > 0
556
+ ));
557
+ const keys = new Set<string>();
558
+ for (const value of values) {
559
+ const normalized = normalizeLookupKey(value);
560
+ if (!normalized) continue;
561
+ keys.add(normalized);
562
+ keys.add(normalized.replace(/-/g, '_'));
563
+ if (normalized.startsWith('topolo-')) {
564
+ const withoutTopolo = normalized.slice('topolo-'.length);
565
+ keys.add(withoutTopolo);
566
+ keys.add(withoutTopolo.replace(/-/g, '_'));
567
+ }
568
+ }
569
+ return [...keys];
570
+ }
571
+
572
+ function isCallableCatalogEntry(entry: TopoloServiceCatalogEntry): boolean {
573
+ const kind = normalizeLookupKey(entry.surfaceKind || '');
574
+ if (kind === 'api' || kind === 'technical_service' || kind === 'service') return true;
575
+ return Boolean(entry.slug && Object.prototype.hasOwnProperty.call(DEFAULT_SERVICE_URLS, catalogEntryLookupKeys(entry)[0] ?? ''));
576
+ }
577
+
365
578
  interface AuthMePayload {
366
579
  credentialType?: 'access_token' | 'api_key';
367
580
  user: RawUser;
package/src/index.ts CHANGED
@@ -4,6 +4,7 @@ export {
4
4
  type RequestOptions,
5
5
  type CredentialIntrospection,
6
6
  type TopoloDebugEvent,
7
+ type TopoloServiceCatalogEntry,
7
8
  } from "./client.js";
8
9
  export type { TopoloCredential, AgentIdentity } from "./auth.js";
9
10
  export {
@@ -11,7 +12,6 @@ export {
11
12
  APPLICATIONS,
12
13
  APPLICATION_REQUIREMENTS,
13
14
  APPLICATION_REQUIREMENTS_VERSION,
14
- PLATFORM_SERVICE_IDS,
15
15
  auditAllApplicationRequirements,
16
16
  auditApplicationRequirements,
17
17
  applicationRequirementScopes,
@@ -1,4 +1,4 @@
1
- import { describe, expect, it, vi } from 'vitest';
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
2
  import { TopoloClient } from '../client.js';
3
3
  import { P2PModule } from './p2p.js';
4
4
 
@@ -8,6 +8,12 @@ function stubFetch(responses: Array<{ status?: number; body?: unknown }>) {
8
8
  const calls: Array<{ url: string; init: RequestInit }> = [];
9
9
  const impl = vi.fn(async (url: string, init: RequestInit) => {
10
10
  calls.push({ url, init });
11
+ if (String(url).includes('/api/services/by-slug/')) {
12
+ return new Response(JSON.stringify({ success: true, data: { service_id: 'service_p2p' } }), {
13
+ status: 200,
14
+ headers: { 'Content-Type': 'application/json' },
15
+ });
16
+ }
11
17
  const next = responses.shift() ?? { status: 200, body: { success: true, data: {} } };
12
18
  return new Response(JSON.stringify(next.body ?? { success: true, data: {} }), {
13
19
  status: next.status ?? 200,
@@ -17,6 +23,10 @@ function stubFetch(responses: Array<{ status?: number; body?: unknown }>) {
17
23
  return { impl: impl as unknown as typeof fetch, calls };
18
24
  }
19
25
 
26
+ function requestCalls(calls: Array<{ url: string; init: RequestInit }>) {
27
+ return calls.filter((call) => !call.url.includes('/api/services/by-slug/'));
28
+ }
29
+
20
30
  function clientWith(impl: typeof fetch) {
21
31
  return new TopoloClient({
22
32
  credential: { kind: 'access_token', accessToken: 'tok_test' },
@@ -26,6 +36,10 @@ function clientWith(impl: typeof fetch) {
26
36
  }
27
37
 
28
38
  describe('P2PModule.listDirectory', () => {
39
+ beforeEach(() => {
40
+ TopoloClient.clearServiceIdCache();
41
+ });
42
+
29
43
  it('GETs /api/directory/capabilities and unwraps the envelope', async () => {
30
44
  const { impl, calls } = stubFetch([
31
45
  {
@@ -37,7 +51,7 @@ describe('P2PModule.listDirectory', () => {
37
51
  capability_id: 'cap_1',
38
52
  publisher_org_id: 'org_a',
39
53
  app_id: 'app_x',
40
- service_id: 'srv_x',
54
+ service_id: 'service_x',
41
55
  name: 'Demo',
42
56
  description: 'A capability',
43
57
  category: 'other',
@@ -60,10 +74,11 @@ describe('P2PModule.listDirectory', () => {
60
74
  const capabilities = await p2p.listDirectory({ limit: 10 });
61
75
  expect(capabilities).toHaveLength(1);
62
76
  expect(capabilities[0]!.capability_id).toBe('cap_1');
63
- const url = new URL(calls[0]!.url);
77
+ const call = requestCalls(calls)[0]!;
78
+ const url = new URL(call.url);
64
79
  expect(url.pathname).toBe('/api/directory/capabilities');
65
80
  expect(url.searchParams.get('limit')).toBe('10');
66
- expect((calls[0]!.init.method ?? 'GET').toUpperCase()).toBe('GET');
81
+ expect((call.init.method ?? 'GET').toUpperCase()).toBe('GET');
67
82
  });
68
83
  });
69
84
 
@@ -78,7 +93,7 @@ describe('P2PModule.publishCapability', () => {
78
93
  capability_id: 'cap_z',
79
94
  publisher_org_id: 'org_p',
80
95
  app_id: 'app_x',
81
- service_id: 'srv_x',
96
+ service_id: 'service_x',
82
97
  name: 'Demo',
83
98
  description: 'A capability',
84
99
  category: 'other',
@@ -101,7 +116,7 @@ describe('P2PModule.publishCapability', () => {
101
116
  capability_id: 'cap_z',
102
117
  publisher_org_id: 'org_p',
103
118
  app_id: 'app_x',
104
- service_id: 'srv_x',
119
+ service_id: 'service_x',
105
120
  name: 'Demo',
106
121
  description: 'A capability',
107
122
  category: 'other',
@@ -114,13 +129,14 @@ describe('P2PModule.publishCapability', () => {
114
129
  });
115
130
  expect(capability.capability_id).toBe('cap_z');
116
131
 
117
- const sent = JSON.parse(String(calls[0]!.init.body));
132
+ const call = requestCalls(calls)[0]!;
133
+ const sent = JSON.parse(String(call.init.body));
118
134
  expect(sent.publisher_org_id).toBe('org_p');
119
135
  expect(sent.acting_for_org_id).toBe('org_p');
120
136
  expect(sent.public_directory_visible).toBe(true);
121
137
  expect(sent.requires_human_approval).toBe(false);
122
138
  expect(sent.trust_level).toBe('standard');
123
- const url = new URL(calls[0]!.url);
139
+ const url = new URL(call.url);
124
140
  expect(url.pathname).toBe('/api/p2p/internal/capabilities/sync');
125
141
  expect((calls[0]!.init.method ?? '').toUpperCase()).toBe('POST');
126
142
  });
@@ -134,7 +150,7 @@ describe('P2PModule.publishCapability', () => {
134
150
  capability_id: 'cap_other',
135
151
  publisher_org_id: 'org_target',
136
152
  app_id: 'app_x',
137
- service_id: 'srv_x',
153
+ service_id: 'service_x',
138
154
  name: 'Demo',
139
155
  description: '...',
140
156
  category: 'other',
@@ -144,7 +160,7 @@ describe('P2PModule.publishCapability', () => {
144
160
  source_revision: 'rev_1',
145
161
  acting_for_org_id: 'org_target',
146
162
  });
147
- const sent = JSON.parse(String(calls[0]!.init.body));
163
+ const sent = JSON.parse(String(requestCalls(calls)[0]!.init.body));
148
164
  expect(sent.acting_for_org_id).toBe('org_target');
149
165
  });
150
166
  });
@@ -211,7 +227,7 @@ describe('P2PModule.releaseHold', () => {
211
227
  const result = await p2p.releaseHold('hold_1', 'seller', { reason: 'arbitration' });
212
228
  expect(result.resolution).toBe('seller');
213
229
  expect(result.cancel_entries[0]!.entry_id).toBe('led_1');
214
- const sent = JSON.parse(String(calls[0]!.init.body));
230
+ const sent = JSON.parse(String(requestCalls(calls)[0]!.init.body));
215
231
  expect(sent.resolution).toBe('seller');
216
232
  expect(sent.reason).toBe('arbitration');
217
233
  });
@@ -5,6 +5,7 @@
5
5
  export const SERVICES = {
6
6
  agent: "https://agent-api.topolo.app",
7
7
  auth: "https://auth.topolo.app",
8
+ backup: "https://backup.topolo.app",
8
9
  books: "https://books.topolo.app",
9
10
  bugfix: "https://bugfix.topolo.app",
10
11
  bytes: "https://bytes-api.topolo.app",
@@ -15,7 +16,6 @@ export const SERVICES = {
15
16
  compose: "https://compose.topolo.app",
16
17
  consent: "https://consent.topolo.app",
17
18
  crm: "https://crm-api.topolo.app",
18
- customer_success: "https://success.topolo.app",
19
19
  developers: "https://developers.topolo.app",
20
20
  director: "https://director.topolo.app",
21
21
  feed: "https://feed-api.topolo.app",
@@ -29,20 +29,24 @@ export const SERVICES = {
29
29
  localize: "https://localize.topolo.app",
30
30
  mail: "https://mail.topolo.app",
31
31
  mdm: "https://mdm-api.topolo.app",
32
- messaging: "https://messages.topolo.app",
32
+ messages: "https://messages.topolo.app",
33
33
  nexus: "https://nexus-api.topolo.app",
34
34
  notify: "https://notify.topolo.app",
35
+ observability_watch: "https://observability-watch.topolo.app",
35
36
  one: "https://one.topolo.app",
36
37
  p2p: "https://p2p.topolo.app",
37
38
  pay: "https://pay.topolo.app",
38
39
  people: "https://people.topolo.app",
40
+ pulse: "https://pulse.topolo.app",
39
41
  "quro.api": "https://api.ol0.me",
40
42
  "quro.redirect": "https://ol0.me",
41
43
  roadmapper: "https://roadmapper-api.topolo.app",
42
44
  sign: "https://sign.topolo.app",
43
45
  social_studio: "https://studio-api.topolo.app",
44
46
  socialize: "https://socialize-api.topolo.app",
47
+ spaces: "https://spaces.topolo.app",
45
48
  status: "https://status.topolo.app",
49
+ success: "https://success.topolo.app",
46
50
  support: "https://support.topolo.app",
47
51
  survey: "https://survey.topolo.app",
48
52
  voice: "https://voice.topolo.app",
@@ -50,35 +54,3 @@ export const SERVICES = {
50
54
  } as const;
51
55
 
52
56
  export type ServiceId = keyof typeof SERVICES;
53
-
54
- /**
55
- * @deprecated since 0.3.0 — opaque service ids are no longer baked into the
56
- * SDK. Use `TopoloClient.getServiceIdBySlug(slug)` which hits
57
- * `GET /api/services/by-slug/:slug` on TopoloAuth at runtime. This map is
58
- * still populated as a build-time snapshot for backward compatibility and
59
- * will be removed in 0.4.0. Stale entries are expected — do not rely on
60
- * presence/absence for catalog membership.
61
- */
62
- export const PLATFORM_SERVICE_IDS: Partial<Record<ServiceId, string>> = {
63
- books: "srv_0g260hbN8oqv",
64
- bugfix: "srv_kQ1t8qUSDgNs",
65
- compose: "srv_aR3ETYmgygmd",
66
- consent: "srv_8FIGR7Q7zP9r",
67
- crm: "srv_iCwM4jGXcwlj",
68
- customer_success: "srv_RWpOxkgZdZbH",
69
- developers: "srv_NjDq9G9qyyjr",
70
- director: "srv_wRCnLLbtvmVT",
71
- flow: "srv_GBCuKkHhnRXv",
72
- forms: "srv_UE7IcGe80J5o",
73
- home: "srv_hE6HBldVzjth",
74
- insights: "srv_DgbBMwWSIhmC",
75
- inventory: "srv_hmvjbv3ZxDnh",
76
- localize: "srv_Ze1g9OmXuZjF",
77
- notify: "srv_9pRdkIjYyKX0",
78
- p2p: "srv_4BzeLqH0wCve",
79
- people: "srv_VDMBAonA5fKb",
80
- sign: "srv_Bd8pRw5ZqRBT",
81
- status: "srv_topolo_status",
82
- survey: "srv_RCOi7CJkwf7R",
83
- voice: "srv_BZANHkshGAK5",
84
- };