@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/README.md +17 -5
- package/dist/index.cjs +582 -252
- package/dist/index.d.cts +504 -287
- package/dist/index.d.ts +504 -287
- package/dist/index.js +582 -251
- package/package.json +5 -2
- package/src/application-requirements.ts +26 -11
- package/src/applications.generated.ts +399 -200
- package/src/client.test.ts +211 -19
- package/src/client.ts +234 -21
- package/src/index.ts +1 -1
- package/src/modules/p2p.test.ts +27 -11
- package/src/services.generated.ts +6 -34
- package/src/services.test.ts +17 -28
- package/src/services.ts +1 -2
- package/src/widgets.test.ts +7 -7
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
|
-
|
|
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?:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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?:
|
|
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
|
|
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 =
|
|
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:
|
|
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:
|
|
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:
|
|
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(
|
|
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:
|
|
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,
|
package/src/modules/p2p.test.ts
CHANGED
|
@@ -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: '
|
|
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
|
|
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((
|
|
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: '
|
|
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: '
|
|
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
|
|
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(
|
|
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: '
|
|
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
|
-
|
|
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
|
-
};
|