@topolo/sdk 0.4.1 → 0.4.3
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 +40 -13
- package/dist/index.cjs +209 -57
- package/dist/index.d.cts +47 -10
- package/dist/index.d.ts +47 -10
- package/dist/index.js +209 -57
- package/package.json +1 -1
- package/src/client.test.ts +231 -28
- package/src/client.ts +285 -70
- package/src/index.ts +1 -0
- package/src/modules/p2p.test.ts +25 -2
- package/src/services.ts +1 -1
package/src/client.ts
CHANGED
|
@@ -107,6 +107,29 @@ export interface TopoloServiceCatalogEntry {
|
|
|
107
107
|
status: string | null;
|
|
108
108
|
surfaceKind: string | null;
|
|
109
109
|
launchable: boolean;
|
|
110
|
+
permissions: string[];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface TopoloActionCatalogEntry {
|
|
114
|
+
actionId: string;
|
|
115
|
+
name: string;
|
|
116
|
+
toolName: string | null;
|
|
117
|
+
title: string;
|
|
118
|
+
description: string;
|
|
119
|
+
serviceId: string;
|
|
120
|
+
serviceSlug: string | null;
|
|
121
|
+
serviceName: string | null;
|
|
122
|
+
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
123
|
+
path: string;
|
|
124
|
+
permissionName: string;
|
|
125
|
+
requiredPermission: string;
|
|
126
|
+
inputSchema: Record<string, unknown>;
|
|
127
|
+
outputSchema: Record<string, unknown>;
|
|
128
|
+
readOnly: boolean;
|
|
129
|
+
destructive: boolean;
|
|
130
|
+
requiresConfirmation: boolean;
|
|
131
|
+
status: string | null;
|
|
132
|
+
sourceRevision: string | null;
|
|
110
133
|
}
|
|
111
134
|
|
|
112
135
|
export class TopoloClient {
|
|
@@ -119,6 +142,7 @@ export class TopoloClient {
|
|
|
119
142
|
private readonly fetchImpl: typeof fetch;
|
|
120
143
|
private readonly debug?: (event: TopoloDebugEvent) => void;
|
|
121
144
|
private serviceCatalogCache: { entries: TopoloServiceCatalogEntry[]; expiresAt: number } | null = null;
|
|
145
|
+
private actionCatalogCache: { entries: TopoloActionCatalogEntry[]; expiresAt: number } | null = null;
|
|
122
146
|
|
|
123
147
|
/**
|
|
124
148
|
* Process-wide cache for slug → service-id lookups. Keyed by
|
|
@@ -278,14 +302,17 @@ export class TopoloClient {
|
|
|
278
302
|
throw new TopoloSdkError('unknown_service', 'service is required');
|
|
279
303
|
}
|
|
280
304
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
305
|
+
if (serviceKey === 'auth') {
|
|
306
|
+
const overrideUrl = resolveConfiguredServiceUrl(serviceKey, this.serviceUrls);
|
|
307
|
+
if (overrideUrl) return { serviceKey, baseUrl: overrideUrl };
|
|
308
|
+
return { serviceKey, baseUrl: DEFAULT_SERVICE_URLS.auth };
|
|
309
|
+
}
|
|
286
310
|
|
|
287
311
|
const live = await this.resolveLiveCallableService(serviceKey);
|
|
288
|
-
if (live)
|
|
312
|
+
if (live) {
|
|
313
|
+
const overrideUrl = resolveConfiguredServiceUrl(serviceKey, this.serviceUrls);
|
|
314
|
+
return overrideUrl ? { ...live, baseUrl: overrideUrl } : live;
|
|
315
|
+
}
|
|
289
316
|
|
|
290
317
|
throw new TopoloSdkError(
|
|
291
318
|
'unknown_service',
|
|
@@ -297,10 +324,7 @@ export class TopoloClient {
|
|
|
297
324
|
const catalog = await this.listServices();
|
|
298
325
|
const match = catalog.find((entry) => catalogEntryMatches(entry, serviceKey));
|
|
299
326
|
if (!match?.baseUrl || !isCallableCatalogEntry(match)) return null;
|
|
300
|
-
return {
|
|
301
|
-
serviceKey: normalizeLookupKey(match.slug || match.name || match.serviceId) || serviceKey,
|
|
302
|
-
baseUrl: match.baseUrl,
|
|
303
|
-
};
|
|
327
|
+
return { serviceKey, baseUrl: match.baseUrl };
|
|
304
328
|
}
|
|
305
329
|
|
|
306
330
|
/**
|
|
@@ -374,14 +398,12 @@ export class TopoloClient {
|
|
|
374
398
|
/**
|
|
375
399
|
* List the platform service catalog from the live registry
|
|
376
400
|
* (`GET /api/services/catalog` on TopoloAuth). Credential-aware: the
|
|
377
|
-
* caller's credential is attached, so
|
|
378
|
-
*
|
|
379
|
-
* permits; an unauthenticated/agent caller receives `active` only.
|
|
401
|
+
* caller's credential is attached, so the result only includes services the
|
|
402
|
+
* credential's organization and effective permissions can use.
|
|
380
403
|
*
|
|
381
|
-
*
|
|
382
|
-
*
|
|
383
|
-
*
|
|
384
|
-
* authoritative; the snapshot is a floor, not a cap.
|
|
404
|
+
* Security boundary: this never falls back to the generated global snapshot.
|
|
405
|
+
* The live Auth catalog is the authority because it is scoped to the caller's
|
|
406
|
+
* organization and effective permissions.
|
|
385
407
|
*/
|
|
386
408
|
async listServices(): Promise<TopoloServiceCatalogEntry[]> {
|
|
387
409
|
const now = Date.now();
|
|
@@ -389,53 +411,133 @@ export class TopoloClient {
|
|
|
389
411
|
return this.serviceCatalogCache.entries;
|
|
390
412
|
}
|
|
391
413
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
414
|
+
const res = await this.request<
|
|
415
|
+
| { data?: { services?: unknown[] } }
|
|
416
|
+
| { services?: unknown[] }
|
|
417
|
+
>({ service: 'auth', method: 'GET', path: '/api/services/catalog' });
|
|
418
|
+
const raw =
|
|
419
|
+
(res as { data?: { services?: unknown[] } }).data?.services ??
|
|
420
|
+
(res as { services?: unknown[] }).services;
|
|
421
|
+
if (!Array.isArray(raw)) {
|
|
422
|
+
throw new TopoloSdkError(
|
|
423
|
+
'service_catalog_unavailable',
|
|
424
|
+
'TopoloAuth did not return a service catalog for the current credential.',
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const entries: TopoloServiceCatalogEntry[] = [];
|
|
429
|
+
for (const item of raw) {
|
|
430
|
+
const s = item as Record<string, unknown>;
|
|
431
|
+
const serviceId =
|
|
432
|
+
typeof s.service_id === 'string'
|
|
433
|
+
? s.service_id
|
|
434
|
+
: typeof s.id === 'string'
|
|
435
|
+
? s.id
|
|
436
|
+
: null;
|
|
437
|
+
if (!serviceId) continue;
|
|
438
|
+
const permissions = Array.isArray(s.granted_permissions)
|
|
439
|
+
? s.granted_permissions
|
|
440
|
+
.filter((permission): permission is string => typeof permission === 'string' && permission.trim().length > 0)
|
|
441
|
+
: [];
|
|
442
|
+
entries.push({
|
|
443
|
+
serviceId,
|
|
444
|
+
slug: typeof s.app_slug === 'string' ? s.app_slug : null,
|
|
445
|
+
name: typeof s.name === 'string' ? s.name : null,
|
|
446
|
+
description:
|
|
447
|
+
typeof s.description === 'string' ? s.description : null,
|
|
448
|
+
baseUrl: typeof s.base_url === 'string' ? s.base_url : null,
|
|
449
|
+
status: typeof s.status === 'string' ? s.status : null,
|
|
450
|
+
surfaceKind:
|
|
451
|
+
typeof s.surface_kind === 'string' ? s.surface_kind : null,
|
|
452
|
+
launchable: s.launchable === true,
|
|
453
|
+
permissions,
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
this.serviceCatalogCache = {
|
|
457
|
+
entries,
|
|
458
|
+
expiresAt: now + CATALOG_CACHE_TTL_MS,
|
|
459
|
+
};
|
|
460
|
+
return entries;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* List callable actions from the live Auth catalog. The response is scoped to
|
|
465
|
+
* the caller's organization and effective permissions; no bundled/static app
|
|
466
|
+
* action list is used as an authorization source.
|
|
467
|
+
*/
|
|
468
|
+
async listActions(options: { service?: string } = {}): Promise<TopoloActionCatalogEntry[]> {
|
|
469
|
+
const now = Date.now();
|
|
470
|
+
if (!options.service && this.actionCatalogCache && this.actionCatalogCache.expiresAt > now) {
|
|
471
|
+
return this.actionCatalogCache.entries;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const res = await this.request<
|
|
475
|
+
| { data?: { actions?: unknown[] } }
|
|
476
|
+
| { actions?: unknown[] }
|
|
477
|
+
>({
|
|
478
|
+
service: 'auth',
|
|
479
|
+
method: 'GET',
|
|
480
|
+
path: '/api/actions/catalog',
|
|
481
|
+
...(options.service ? { query: { service: options.service } } : {}),
|
|
482
|
+
});
|
|
483
|
+
const raw =
|
|
484
|
+
(res as { data?: { actions?: unknown[] } }).data?.actions ??
|
|
485
|
+
(res as { actions?: unknown[] }).actions;
|
|
486
|
+
if (!Array.isArray(raw)) {
|
|
487
|
+
throw new TopoloSdkError(
|
|
488
|
+
'action_catalog_unavailable',
|
|
489
|
+
'TopoloAuth did not return an action catalog for the current credential.',
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const entries = raw
|
|
494
|
+
.map((item) => normalizeActionCatalogEntry(item))
|
|
495
|
+
.filter((entry): entry is TopoloActionCatalogEntry => Boolean(entry));
|
|
496
|
+
if (!options.service) {
|
|
497
|
+
this.actionCatalogCache = {
|
|
498
|
+
entries,
|
|
499
|
+
expiresAt: now + CATALOG_CACHE_TTL_MS,
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
return entries;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
async getAction(action: string): Promise<TopoloActionCatalogEntry> {
|
|
506
|
+
const needle = normalizeActionLookupKey(action);
|
|
507
|
+
const actions = await this.listActions();
|
|
508
|
+
const match = actions.find((entry) => actionEntryMatches(entry, needle));
|
|
509
|
+
if (!match) {
|
|
510
|
+
throw new TopoloSdkError(
|
|
511
|
+
'unknown_action',
|
|
512
|
+
`Unknown Topolo action "${action}". Run action discovery first and pass an action id, name, or tool name.`,
|
|
513
|
+
);
|
|
434
514
|
}
|
|
435
|
-
return
|
|
515
|
+
return match;
|
|
436
516
|
}
|
|
437
517
|
|
|
438
|
-
|
|
518
|
+
async callAction<T = unknown>(
|
|
519
|
+
action: string,
|
|
520
|
+
input: Record<string, unknown> = {},
|
|
521
|
+
options: { confirm?: boolean } = {},
|
|
522
|
+
): Promise<T> {
|
|
523
|
+
const entry = await this.getAction(action);
|
|
524
|
+
const mapped = mapActionInput(entry, input);
|
|
525
|
+
const service = entry.serviceSlug || entry.serviceId;
|
|
526
|
+
return this.request<T>({
|
|
527
|
+
service,
|
|
528
|
+
method: entry.method,
|
|
529
|
+
path: mapped.path,
|
|
530
|
+
...(mapped.query ? { query: mapped.query } : {}),
|
|
531
|
+
...(mapped.body !== undefined ? { body: mapped.body } : {}),
|
|
532
|
+
confirm: options.confirm === true,
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Bundled generated registry for tooling that audits platform packaging, not
|
|
538
|
+
* for deciding what the current credential may use.
|
|
539
|
+
*/
|
|
540
|
+
listBundledServices(): TopoloServiceCatalogEntry[] {
|
|
439
541
|
const apps = Object.values(APPLICATIONS) as ReadonlyArray<{
|
|
440
542
|
id: string;
|
|
441
543
|
name?: string;
|
|
@@ -447,9 +549,10 @@ export class TopoloClient {
|
|
|
447
549
|
name: app.name ?? app.id,
|
|
448
550
|
description: null,
|
|
449
551
|
baseUrl: app.productionUrl ?? null,
|
|
450
|
-
status: '
|
|
552
|
+
status: 'bundled',
|
|
451
553
|
surfaceKind: 'application',
|
|
452
554
|
launchable: Boolean(app.productionUrl),
|
|
555
|
+
permissions: [],
|
|
453
556
|
}));
|
|
454
557
|
}
|
|
455
558
|
|
|
@@ -543,7 +646,7 @@ function serviceKeyCandidates(slug: string): string[] {
|
|
|
543
646
|
}
|
|
544
647
|
|
|
545
648
|
function normalizeLookupKey(value: string): string {
|
|
546
|
-
return String(value || '').trim().toLowerCase();
|
|
649
|
+
return String(value || '').trim().toLowerCase().replace(/\s+/g, '-');
|
|
547
650
|
}
|
|
548
651
|
|
|
549
652
|
function catalogEntryMatches(entry: TopoloServiceCatalogEntry, needle: string): boolean {
|
|
@@ -558,12 +661,8 @@ function catalogEntryLookupKeys(entry: TopoloServiceCatalogEntry): string[] {
|
|
|
558
661
|
for (const value of values) {
|
|
559
662
|
const normalized = normalizeLookupKey(value);
|
|
560
663
|
if (!normalized) continue;
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
if (normalized.startsWith('topolo-')) {
|
|
564
|
-
const withoutTopolo = normalized.slice('topolo-'.length);
|
|
565
|
-
keys.add(withoutTopolo);
|
|
566
|
-
keys.add(withoutTopolo.replace(/-/g, '_'));
|
|
664
|
+
for (const candidate of serviceKeyCandidates(normalized)) {
|
|
665
|
+
keys.add(candidate);
|
|
567
666
|
}
|
|
568
667
|
}
|
|
569
668
|
return [...keys];
|
|
@@ -572,7 +671,123 @@ function catalogEntryLookupKeys(entry: TopoloServiceCatalogEntry): string[] {
|
|
|
572
671
|
function isCallableCatalogEntry(entry: TopoloServiceCatalogEntry): boolean {
|
|
573
672
|
const kind = normalizeLookupKey(entry.surfaceKind || '');
|
|
574
673
|
if (kind === 'api' || kind === 'technical_service' || kind === 'service') return true;
|
|
575
|
-
return Boolean(
|
|
674
|
+
return Boolean(
|
|
675
|
+
entry.baseUrl &&
|
|
676
|
+
catalogEntryLookupKeys(entry).some((key) => Object.prototype.hasOwnProperty.call(DEFAULT_SERVICE_URLS, key)),
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function normalizeActionCatalogEntry(item: unknown): TopoloActionCatalogEntry | null {
|
|
681
|
+
if (!item || typeof item !== 'object' || Array.isArray(item)) return null;
|
|
682
|
+
const a = item as Record<string, unknown>;
|
|
683
|
+
const actionId = pickActionString(a.action_id) ?? pickActionString(a.actionId);
|
|
684
|
+
const name = pickActionString(a.name);
|
|
685
|
+
const serviceId = pickActionString(a.service_id) ?? pickActionString(a.serviceId);
|
|
686
|
+
const method = pickActionString(a.method)?.toUpperCase();
|
|
687
|
+
const path = pickActionString(a.path);
|
|
688
|
+
if (!actionId || !name || !serviceId || !method || !path || !isHttpMethod(method)) {
|
|
689
|
+
return null;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
return {
|
|
693
|
+
actionId,
|
|
694
|
+
name,
|
|
695
|
+
toolName: pickActionString(a.tool_name) ?? pickActionString(a.toolName) ?? null,
|
|
696
|
+
title: pickActionString(a.title) ?? name,
|
|
697
|
+
description: pickActionString(a.description) ?? '',
|
|
698
|
+
serviceId,
|
|
699
|
+
serviceSlug: pickActionString(a.service_slug) ?? pickActionString(a.serviceSlug) ?? null,
|
|
700
|
+
serviceName: pickActionString(a.service_name) ?? pickActionString(a.serviceName) ?? null,
|
|
701
|
+
method,
|
|
702
|
+
path,
|
|
703
|
+
permissionName: pickActionString(a.permission_name) ?? pickActionString(a.permissionName) ?? '',
|
|
704
|
+
requiredPermission: pickActionString(a.required_permission) ?? pickActionString(a.requiredPermission) ?? '',
|
|
705
|
+
inputSchema: plainObject(a.input_schema ?? a.inputSchema),
|
|
706
|
+
outputSchema: plainObject(a.output_schema ?? a.outputSchema),
|
|
707
|
+
readOnly: Boolean(a.read_only ?? a.readOnly),
|
|
708
|
+
destructive: Boolean(a.destructive),
|
|
709
|
+
requiresConfirmation: Boolean(a.requires_confirmation ?? a.requiresConfirmation),
|
|
710
|
+
status: pickActionString(a.status),
|
|
711
|
+
sourceRevision: pickActionString(a.source_revision) ?? pickActionString(a.sourceRevision),
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
function isHttpMethod(value: string): value is TopoloActionCatalogEntry['method'] {
|
|
716
|
+
return value === 'GET' || value === 'POST' || value === 'PUT' || value === 'PATCH' || value === 'DELETE';
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
function pickActionString(value: unknown): string | null {
|
|
720
|
+
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
function plainObject(value: unknown): Record<string, unknown> {
|
|
724
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
725
|
+
? value as Record<string, unknown>
|
|
726
|
+
: {};
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
function normalizeActionLookupKey(value: string): string {
|
|
730
|
+
return String(value || '').trim().toLowerCase();
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function actionEntryMatches(entry: TopoloActionCatalogEntry, needle: string): boolean {
|
|
734
|
+
if (!needle) return false;
|
|
735
|
+
return [entry.actionId, entry.name, entry.toolName]
|
|
736
|
+
.filter((value): value is string => typeof value === 'string' && value.length > 0)
|
|
737
|
+
.some((value) => value.toLowerCase() === needle);
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
function mapActionInput(
|
|
741
|
+
action: TopoloActionCatalogEntry,
|
|
742
|
+
input: Record<string, unknown>,
|
|
743
|
+
): { path: string; query?: Record<string, string | number | boolean>; body?: unknown } {
|
|
744
|
+
const remaining: Record<string, unknown> = { ...(input || {}) };
|
|
745
|
+
const path = action.path.replace(/\{([a-zA-Z0-9_]+)\}/g, (_match, key: string) => {
|
|
746
|
+
const value = remaining[key];
|
|
747
|
+
if (value === undefined || value === null || value === '') {
|
|
748
|
+
throw new TopoloSdkError(
|
|
749
|
+
'invalid_action_input',
|
|
750
|
+
`Action "${action.name}" requires path parameter "${key}".`,
|
|
751
|
+
);
|
|
752
|
+
}
|
|
753
|
+
delete remaining[key];
|
|
754
|
+
return encodeURIComponent(String(value));
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
const explicitQuery = plainObject(remaining.query);
|
|
758
|
+
delete remaining.query;
|
|
759
|
+
const explicitBody = Object.prototype.hasOwnProperty.call(remaining, 'body')
|
|
760
|
+
? remaining.body
|
|
761
|
+
: undefined;
|
|
762
|
+
delete remaining.body;
|
|
763
|
+
|
|
764
|
+
if (action.method === 'GET') {
|
|
765
|
+
return {
|
|
766
|
+
path,
|
|
767
|
+
query: objectToQuery({ ...remaining, ...explicitQuery }),
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
const query = objectToQuery(explicitQuery);
|
|
772
|
+
const body = explicitBody !== undefined ? explicitBody : remaining;
|
|
773
|
+
return {
|
|
774
|
+
path,
|
|
775
|
+
...(query ? { query } : {}),
|
|
776
|
+
body,
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
function objectToQuery(value: Record<string, unknown>): Record<string, string | number | boolean> | undefined {
|
|
781
|
+
const query: Record<string, string | number | boolean> = {};
|
|
782
|
+
for (const [key, raw] of Object.entries(value)) {
|
|
783
|
+
if (raw === undefined || raw === null) continue;
|
|
784
|
+
if (typeof raw === 'string' || typeof raw === 'number' || typeof raw === 'boolean') {
|
|
785
|
+
query[key] = raw;
|
|
786
|
+
} else {
|
|
787
|
+
query[key] = JSON.stringify(raw);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
return Object.keys(query).length > 0 ? query : undefined;
|
|
576
791
|
}
|
|
577
792
|
|
|
578
793
|
interface AuthMePayload {
|
package/src/index.ts
CHANGED
package/src/modules/p2p.test.ts
CHANGED
|
@@ -14,6 +14,26 @@ function stubFetch(responses: Array<{ status?: number; body?: unknown }>) {
|
|
|
14
14
|
headers: { 'Content-Type': 'application/json' },
|
|
15
15
|
});
|
|
16
16
|
}
|
|
17
|
+
if (String(url).includes('/api/services/catalog')) {
|
|
18
|
+
return new Response(JSON.stringify({
|
|
19
|
+
data: {
|
|
20
|
+
services: [
|
|
21
|
+
{
|
|
22
|
+
service_id: 'srv_p2p',
|
|
23
|
+
app_slug: 'topolo-p2p',
|
|
24
|
+
name: 'Topolo P2P',
|
|
25
|
+
base_url: 'https://p2p.topolo.app',
|
|
26
|
+
surface_kind: 'api',
|
|
27
|
+
status: 'active',
|
|
28
|
+
granted_permissions: ['srv_p2p.directory:read'],
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
}), {
|
|
33
|
+
status: 200,
|
|
34
|
+
headers: { 'Content-Type': 'application/json' },
|
|
35
|
+
});
|
|
36
|
+
}
|
|
17
37
|
const next = responses.shift() ?? { status: 200, body: { success: true, data: {} } };
|
|
18
38
|
return new Response(JSON.stringify(next.body ?? { success: true, data: {} }), {
|
|
19
39
|
status: next.status ?? 200,
|
|
@@ -24,7 +44,10 @@ function stubFetch(responses: Array<{ status?: number; body?: unknown }>) {
|
|
|
24
44
|
}
|
|
25
45
|
|
|
26
46
|
function requestCalls(calls: Array<{ url: string; init: RequestInit }>) {
|
|
27
|
-
return calls.filter((call) =>
|
|
47
|
+
return calls.filter((call) =>
|
|
48
|
+
!call.url.includes('/api/services/by-slug/') &&
|
|
49
|
+
!call.url.includes('/api/services/catalog')
|
|
50
|
+
);
|
|
28
51
|
}
|
|
29
52
|
|
|
30
53
|
function clientWith(impl: typeof fetch) {
|
|
@@ -138,7 +161,7 @@ describe('P2PModule.publishCapability', () => {
|
|
|
138
161
|
expect(sent.trust_level).toBe('standard');
|
|
139
162
|
const url = new URL(call.url);
|
|
140
163
|
expect(url.pathname).toBe('/api/p2p/internal/capabilities/sync');
|
|
141
|
-
expect((
|
|
164
|
+
expect((call.init.method ?? '').toUpperCase()).toBe('POST');
|
|
142
165
|
});
|
|
143
166
|
|
|
144
167
|
it('honours an explicit acting_for_org_id when the platform key is publishing for another org', async () => {
|
package/src/services.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* `scripts/generate-services.mjs`; edit the JSON, not the generated file.
|
|
7
7
|
*
|
|
8
8
|
* Any entry can be overridden per-call via `TopoloClient` options, or globally
|
|
9
|
-
* via env vars like `
|
|
9
|
+
* via env vars like `TOPOLO_SERVICE_URL_MAIL` or `TOPOLO_SERVICE_URL_WEB_API`
|
|
10
10
|
* (dots in service IDs become underscores in env vars).
|
|
11
11
|
*
|
|
12
12
|
* IMPORTANT: The SDK NEVER accepts `orgId` as a parameter. Every request is
|