@topolo/sdk 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/index.cjs +149 -78
- package/dist/index.d.cts +81 -56
- package/dist/index.d.ts +81 -56
- package/dist/index.js +149 -77
- package/package.json +5 -2
- package/src/application-requirements.ts +10 -6
- package/src/applications.generated.ts +61 -40
- package/src/client.test.ts +108 -19
- package/src/client.ts +126 -8
- package/src/index.ts +1 -1
- package/src/modules/p2p.test.ts +27 -11
- package/src/services.generated.ts +2 -32
- package/src/services.test.ts +6 -23
- package/src/services.ts +1 -2
- package/src/widgets.test.ts +7 -7
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
|
});
|
|
@@ -36,12 +36,14 @@ export const SERVICES = {
|
|
|
36
36
|
p2p: "https://p2p.topolo.app",
|
|
37
37
|
pay: "https://pay.topolo.app",
|
|
38
38
|
people: "https://people.topolo.app",
|
|
39
|
+
pulse: "https://pulse.topolo.app",
|
|
39
40
|
"quro.api": "https://api.ol0.me",
|
|
40
41
|
"quro.redirect": "https://ol0.me",
|
|
41
42
|
roadmapper: "https://roadmapper-api.topolo.app",
|
|
42
43
|
sign: "https://sign.topolo.app",
|
|
43
44
|
social_studio: "https://studio-api.topolo.app",
|
|
44
45
|
socialize: "https://socialize-api.topolo.app",
|
|
46
|
+
spaces: "https://spaces.topolo.app",
|
|
45
47
|
status: "https://status.topolo.app",
|
|
46
48
|
support: "https://support.topolo.app",
|
|
47
49
|
survey: "https://survey.topolo.app",
|
|
@@ -50,35 +52,3 @@ export const SERVICES = {
|
|
|
50
52
|
} as const;
|
|
51
53
|
|
|
52
54
|
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
|
-
};
|
package/src/services.test.ts
CHANGED
|
@@ -14,20 +14,19 @@ const EXPECTED_APPLICATION_IDS = [
|
|
|
14
14
|
'admin',
|
|
15
15
|
'agent',
|
|
16
16
|
'auth',
|
|
17
|
+
'backup',
|
|
17
18
|
'books',
|
|
18
19
|
'bugfix',
|
|
19
20
|
'bytes',
|
|
20
21
|
'calendar',
|
|
21
22
|
'capacity',
|
|
22
23
|
'chat',
|
|
23
|
-
'cli',
|
|
24
24
|
'cloudcontrol',
|
|
25
25
|
'commerce',
|
|
26
26
|
'compose',
|
|
27
27
|
'consent',
|
|
28
28
|
'crm',
|
|
29
29
|
'customer_success',
|
|
30
|
-
'd1_backup',
|
|
31
30
|
'developers',
|
|
32
31
|
'director',
|
|
33
32
|
'docs',
|
|
@@ -41,7 +40,6 @@ const EXPECTED_APPLICATION_IDS = [
|
|
|
41
40
|
'learn',
|
|
42
41
|
'localize',
|
|
43
42
|
'mail',
|
|
44
|
-
'mcp',
|
|
45
43
|
'mdm',
|
|
46
44
|
'messaging',
|
|
47
45
|
'nexus',
|
|
@@ -57,6 +55,7 @@ const EXPECTED_APPLICATION_IDS = [
|
|
|
57
55
|
'sign',
|
|
58
56
|
'social_studio',
|
|
59
57
|
'socialize',
|
|
58
|
+
'spaces',
|
|
60
59
|
'status',
|
|
61
60
|
'support',
|
|
62
61
|
'survey',
|
|
@@ -84,20 +83,10 @@ describe('service registry', () => {
|
|
|
84
83
|
expect(DEFAULT_SERVICE_URLS).not.toHaveProperty('web.runtime');
|
|
85
84
|
});
|
|
86
85
|
|
|
87
|
-
it('
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
// is non-empty and that the values look like opaque service ids; specific
|
|
92
|
-
// ids are no longer load-bearing.
|
|
93
|
-
const { PLATFORM_SERVICE_IDS } = await import('./services.js');
|
|
94
|
-
|
|
95
|
-
const entries = Object.entries(PLATFORM_SERVICE_IDS);
|
|
96
|
-
expect(entries.length).toBeGreaterThan(0);
|
|
97
|
-
for (const [, id] of entries) {
|
|
98
|
-
expect(typeof id).toBe('string');
|
|
99
|
-
expect(id).toMatch(/^srv_/);
|
|
100
|
-
}
|
|
86
|
+
it('does not bake opaque Auth service ids into the generated registry', async () => {
|
|
87
|
+
const services = await import('./services.js');
|
|
88
|
+
|
|
89
|
+
expect(services).not.toHaveProperty('PLATFORM_SERVICE_IDS');
|
|
101
90
|
});
|
|
102
91
|
|
|
103
92
|
it('maps every callable service back to an application catalog entry', () => {
|
|
@@ -169,23 +158,17 @@ describe('application requirements', () => {
|
|
|
169
158
|
expect(applicationRequirementScopes(APPLICATIONS.mail)).toEqual(
|
|
170
159
|
expect.arrayContaining(['all', 'browser', 'api', 'agent_surface']),
|
|
171
160
|
);
|
|
172
|
-
expect(applicationRequirementScopes(APPLICATIONS.cli)).toEqual(
|
|
173
|
-
expect.arrayContaining(['all', 'tooling', 'agent_surface']),
|
|
174
|
-
);
|
|
175
161
|
});
|
|
176
162
|
|
|
177
163
|
it('filters requirements for a target application', () => {
|
|
178
164
|
const adminRequirementIds = requirementsForApplication('admin').map((requirement) => requirement.id);
|
|
179
165
|
const mailRequirementIds = requirementsForApplication('mail').map((requirement) => requirement.id);
|
|
180
|
-
const cliRequirementIds = requirementsForApplication('cli').map((requirement) => requirement.id);
|
|
181
166
|
|
|
182
167
|
expect(adminRequirementIds).toContain('shared-shell-and-launcher');
|
|
183
168
|
expect(adminRequirementIds).not.toContain('organization-scoped-data');
|
|
184
169
|
expect(mailRequirementIds).toContain('organization-scoped-data');
|
|
185
170
|
expect(mailRequirementIds).toContain('service-registration-and-scopes');
|
|
186
171
|
expect(mailRequirementIds).toContain('native-dashboard-widget');
|
|
187
|
-
expect(cliRequirementIds).toContain('agent-and-operator-surfaces');
|
|
188
|
-
expect(cliRequirementIds).not.toContain('service-registration-and-scopes');
|
|
189
172
|
});
|
|
190
173
|
|
|
191
174
|
it('audits one application against the catalog-backed requirements', () => {
|
package/src/services.ts
CHANGED
|
@@ -15,7 +15,6 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import {
|
|
17
17
|
SERVICES,
|
|
18
|
-
PLATFORM_SERVICE_IDS,
|
|
19
18
|
type ServiceId,
|
|
20
19
|
} from './services.generated.js';
|
|
21
20
|
import {
|
|
@@ -41,7 +40,7 @@ export {
|
|
|
41
40
|
type ApplicationRequirementsAuditReport,
|
|
42
41
|
} from './application-requirements.js';
|
|
43
42
|
|
|
44
|
-
export { SERVICES
|
|
43
|
+
export { SERVICES };
|
|
45
44
|
export type { ServiceId };
|
|
46
45
|
export { APPLICATIONS };
|
|
47
46
|
export type { ApplicationCatalogEntry, ApplicationDeployTarget, ApplicationId };
|
package/src/widgets.test.ts
CHANGED
|
@@ -15,7 +15,7 @@ describe("Topolo widget contract", () => {
|
|
|
15
15
|
|
|
16
16
|
it("stamps service metadata and timestamp onto widget drafts", () => {
|
|
17
17
|
const response = createTopoloWidgetResponse({
|
|
18
|
-
serviceId: "
|
|
18
|
+
serviceId: "service_bytes",
|
|
19
19
|
serviceName: "Bytes",
|
|
20
20
|
now: "2026-04-25T00:00:00.000Z",
|
|
21
21
|
widgets: [
|
|
@@ -30,7 +30,7 @@ describe("Topolo widget contract", () => {
|
|
|
30
30
|
success: true,
|
|
31
31
|
widgets: [
|
|
32
32
|
{
|
|
33
|
-
serviceId: "
|
|
33
|
+
serviceId: "service_bytes",
|
|
34
34
|
serviceName: "Bytes",
|
|
35
35
|
lastUpdated: "2026-04-25T00:00:00.000Z",
|
|
36
36
|
type: "stats",
|
|
@@ -42,12 +42,12 @@ describe("Topolo widget contract", () => {
|
|
|
42
42
|
|
|
43
43
|
it("keeps explicit widget metadata when an app needs multiple service cards", () => {
|
|
44
44
|
const response = createTopoloWidgetResponse({
|
|
45
|
-
serviceId: "
|
|
45
|
+
serviceId: "service_platform",
|
|
46
46
|
serviceName: "Platform",
|
|
47
47
|
now: "2026-04-25T00:00:00.000Z",
|
|
48
48
|
widgets: [
|
|
49
49
|
{
|
|
50
|
-
serviceId: "
|
|
50
|
+
serviceId: "service_crm",
|
|
51
51
|
serviceName: "CRM",
|
|
52
52
|
lastUpdated: "2026-04-24T23:00:00.000Z",
|
|
53
53
|
type: "stats",
|
|
@@ -57,7 +57,7 @@ describe("Topolo widget contract", () => {
|
|
|
57
57
|
});
|
|
58
58
|
|
|
59
59
|
expect(response.widgets[0]).toMatchObject({
|
|
60
|
-
serviceId: "
|
|
60
|
+
serviceId: "service_crm",
|
|
61
61
|
serviceName: "CRM",
|
|
62
62
|
lastUpdated: "2026-04-24T23:00:00.000Z",
|
|
63
63
|
});
|
|
@@ -73,7 +73,7 @@ describe("Topolo widget contract", () => {
|
|
|
73
73
|
|
|
74
74
|
it("validates a compliant response", () => {
|
|
75
75
|
const response = createTopoloWidgetResponse({
|
|
76
|
-
serviceId: "
|
|
76
|
+
serviceId: "service_crm",
|
|
77
77
|
serviceName: "CRM",
|
|
78
78
|
now: "2026-04-25T00:00:00.000Z",
|
|
79
79
|
widgets: [
|
|
@@ -102,7 +102,7 @@ describe("Topolo widget contract", () => {
|
|
|
102
102
|
|
|
103
103
|
it("keeps optional snapshot metadata for configurable TopoloOne datapoints", () => {
|
|
104
104
|
const response = createTopoloWidgetResponse({
|
|
105
|
-
serviceId: "
|
|
105
|
+
serviceId: "service_admin",
|
|
106
106
|
serviceName: "Admin",
|
|
107
107
|
now: "2026-04-28T00:00:00.000Z",
|
|
108
108
|
widgets: [
|