@topolo/sdk 0.7.0 → 0.8.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/errors.ts DELETED
@@ -1,39 +0,0 @@
1
- export class TopoloSdkError extends Error {
2
- readonly code: string;
3
- constructor(code: string, message: string) {
4
- super(message);
5
- this.name = 'TopoloSdkError';
6
- this.code = code;
7
- }
8
- }
9
-
10
- export class TopoloAuthError extends TopoloSdkError {
11
- constructor(message: string, code: string = 'auth_error') {
12
- super(code, message);
13
- this.name = 'TopoloAuthError';
14
- }
15
- }
16
-
17
- export class TopoloPermissionError extends TopoloSdkError {
18
- readonly required: string[];
19
- constructor(message: string, required: string[]) {
20
- super('permission_denied', message);
21
- this.name = 'TopoloPermissionError';
22
- this.required = required;
23
- }
24
- }
25
-
26
- export class TopoloHttpError extends TopoloSdkError {
27
- readonly status: number;
28
- readonly body: unknown;
29
- readonly service: string;
30
- readonly path: string;
31
- constructor(service: string, path: string, status: number, body: unknown, message?: string) {
32
- super('http_error', message ?? `HTTP ${status} from ${service}${path}`);
33
- this.name = 'TopoloHttpError';
34
- this.status = status;
35
- this.body = body;
36
- this.service = service;
37
- this.path = path;
38
- }
39
- }
package/src/index.test.ts DELETED
@@ -1,69 +0,0 @@
1
- import { describe, expect, it, vi } from 'vitest';
2
- import {
3
- createTopolo,
4
- CrmModule,
5
- IdentityModule,
6
- TopoloClient,
7
- TopoloAuthError,
8
- TopoloHttpError,
9
- TopoloPermissionError,
10
- } from './index.js';
11
-
12
- const AGENT = { clientName: 'topolo-sdk-test', clientVersion: '0.0.0' };
13
-
14
- describe('createTopolo', () => {
15
- it('returns a ready client plus identity + crm module surfaces', () => {
16
- const topolo = createTopolo({
17
- credential: { kind: 'access_token', accessToken: 'tok' },
18
- agent: AGENT,
19
- fetch: vi.fn() as unknown as typeof fetch,
20
- });
21
-
22
- expect(topolo.client).toBeInstanceOf(TopoloClient);
23
- expect(topolo.identity).toBeInstanceOf(IdentityModule);
24
- expect(topolo.crm).toBeInstanceOf(CrmModule);
25
- });
26
-
27
- it('wires every module to the same underlying client', async () => {
28
- const calls: string[] = [];
29
- const impl = vi.fn(async (url: string) => {
30
- calls.push(String(url));
31
- if (String(url).includes('/api/services/by-slug/')) {
32
- return new Response(JSON.stringify({ success: true, data: { app_id: 'service_auth' } }), {
33
- status: 200,
34
- headers: { 'Content-Type': 'application/json' },
35
- });
36
- }
37
- if (String(url).includes('/api/services/catalog')) {
38
- return new Response(JSON.stringify({ data: { services: [] } }), {
39
- status: 200,
40
- headers: { 'Content-Type': 'application/json' },
41
- });
42
- }
43
- return new Response(
44
- JSON.stringify({ data: { user: { id: 'u_1', email: 'a@b.c' } } }),
45
- { status: 200, headers: { 'Content-Type': 'application/json' } },
46
- );
47
- }) as unknown as typeof fetch;
48
-
49
- TopoloClient.clearAppIdCache();
50
- const topolo = createTopolo({
51
- credential: { kind: 'access_token', accessToken: 'tok' },
52
- agent: AGENT,
53
- fetch: impl,
54
- appIds: { auth: 'service_auth' },
55
- });
56
-
57
- const me = await topolo.identity.whoami();
58
- expect(me.user.id).toBe('u_1');
59
- expect(calls.some((u) => u.includes('/api/auth/me'))).toBe(true);
60
- });
61
- });
62
-
63
- describe('package surface re-exports', () => {
64
- it('re-exports the error classes from the barrel', () => {
65
- expect(new TopoloAuthError('x').name).toBe('TopoloAuthError');
66
- expect(new TopoloHttpError('auth', '/p', 500, null).name).toBe('TopoloHttpError');
67
- expect(new TopoloPermissionError('nope', []).name).toBe('TopoloPermissionError');
68
- });
69
- });
package/src/index.ts DELETED
@@ -1,125 +0,0 @@
1
- export {
2
- TopoloClient,
3
- type TopoloClientOptions,
4
- type RequestOptions,
5
- type CredentialIntrospection,
6
- type TopoloDebugEvent,
7
- type TopoloServiceCatalogEntry,
8
- type TopoloApiKeyResourceMetadata,
9
- type TopoloActionCatalogEntry,
10
- } from "./client.js";
11
- export type { TopoloCredential, AgentIdentity } from "./auth.js";
12
- export {
13
- DEFAULT_APP_URLS,
14
- APPLICATIONS,
15
- APPLICATION_REQUIREMENTS,
16
- APPLICATION_REQUIREMENTS_VERSION,
17
- auditAllApplicationRequirements,
18
- auditApplicationRequirements,
19
- applicationRequirementScopes,
20
- requirementsForApplication,
21
- resolveCatalogServiceUrl,
22
- resolveServiceUrl,
23
- type ApplicationCatalogEntry,
24
- type ApplicationDeployTarget,
25
- type ApplicationId,
26
- type ApplicationRequirement,
27
- type ApplicationRequirementAudit,
28
- type ApplicationRequirementFinding,
29
- type ApplicationRequirementMigrationItem,
30
- type ApplicationRequirementScore,
31
- type ApplicationRequirementScope,
32
- type ApplicationRequirementStatus,
33
- type ApplicationRequirementsAuditReport,
34
- type ServiceCatalogUrlEntry,
35
- type AppApiId,
36
- } from "./services.js";
37
- export {
38
- TopoloSdkError,
39
- TopoloAuthError,
40
- TopoloPermissionError,
41
- TopoloHttpError,
42
- } from "./errors.js";
43
- export {
44
- CrmModule,
45
- type CrmContactSummary,
46
- type ListContactsOptions,
47
- type ListContactsResult,
48
- } from "./modules/crm.js";
49
- export { IdentityModule } from "./modules/identity.js";
50
- export {
51
- P2PModule,
52
- type P2PCapability,
53
- type P2PPricingModel,
54
- type PublishCapabilityInput,
55
- type P2PActionRow,
56
- type P2PActionRun,
57
- type P2PActionRequestInput,
58
- type P2PActionRequestResult,
59
- type P2PActionDetail,
60
- type P2PHold,
61
- type P2PLedgerEntry,
62
- type P2PPolicy,
63
- type P2PConnection,
64
- type P2PSettlementGroup,
65
- type P2PSettlementBatch,
66
- } from "./modules/p2p.js";
67
- export {
68
- TopoloOAuth,
69
- type OAuthHelperOptions,
70
- type DeviceAuthorizationResponse,
71
- type TokenResponse,
72
- } from "./oauth.js";
73
- export {
74
- TOPOLO_WIDGET_ENDPOINT,
75
- TOPOLO_WIDGET_REFRESH_INTERVAL_MS,
76
- createTopoloWidgetError,
77
- createTopoloWidgetResponse,
78
- defineTopoloChartWidget,
79
- defineTopoloNotificationsWidget,
80
- defineTopoloQuickActionsWidget,
81
- defineTopoloRecentActivityWidget,
82
- defineTopoloStatsWidget,
83
- validateTopoloWidgetResponse,
84
- type TopoloChartWidget,
85
- type TopoloNotificationsWidget,
86
- type TopoloQuickActionsWidget,
87
- type TopoloRecentActivityWidget,
88
- type TopoloStatsWidget,
89
- type TopoloWidget,
90
- type TopoloWidgetActionItem,
91
- type TopoloWidgetActivityItem,
92
- type TopoloWidgetApiResponse,
93
- type TopoloWidgetBase,
94
- type TopoloWidgetConfig,
95
- type TopoloWidgetDraft,
96
- type TopoloWidgetMetric,
97
- type TopoloWidgetNotificationItem,
98
- type TopoloWidgetResponseOptions,
99
- type TopoloWidgetSnapshotMetadata,
100
- type TopoloWidgetSnapshotMetric,
101
- type TopoloWidgetType,
102
- type TopoloWidgetValidationResult,
103
- } from "./widgets.js";
104
-
105
- import { TopoloClient, type TopoloClientOptions } from "./client.js";
106
- import { CrmModule } from "./modules/crm.js";
107
- import { IdentityModule } from "./modules/identity.js";
108
-
109
- /**
110
- * Convenience factory that returns a ready-to-use typed Topolo client plus
111
- * domain module surfaces (identity, crm, ...).
112
- *
113
- * Phase 1 exposes identity + crm; subsequent phases add more modules
114
- * incrementally as each app's API contract stabilizes.
115
- */
116
- export function createTopolo(options: TopoloClientOptions) {
117
- const client = new TopoloClient(options);
118
- return {
119
- client,
120
- identity: new IdentityModule(client),
121
- crm: new CrmModule(client),
122
- };
123
- }
124
-
125
- export type Topolo = ReturnType<typeof createTopolo>;
@@ -1,208 +0,0 @@
1
- import { beforeEach, describe, expect, it, vi } from 'vitest';
2
- import { TopoloClient } from '../client.js';
3
- import { CrmModule } from './crm.js';
4
-
5
- const AGENT = { clientName: 'topolo-sdk-test', clientVersion: '0.0.0' };
6
-
7
- function stubFetch(responses: Array<{ status?: number; body?: unknown }>) {
8
- const calls: Array<{ url: string; init: RequestInit }> = [];
9
- const impl = vi.fn(async (url: string, init: RequestInit) => {
10
- calls.push({ url, init });
11
- if (String(url).includes('/api/services/by-slug/')) {
12
- return new Response(JSON.stringify({ success: true, data: { app_id: 'service_crm' } }), {
13
- status: 200,
14
- headers: { 'Content-Type': 'application/json' },
15
- });
16
- }
17
- if (String(url).includes('/api/services/catalog')) {
18
- return new Response(
19
- JSON.stringify({
20
- data: {
21
- services: [
22
- {
23
- app_id: 'app_crm',
24
- app_slug: 'topolo-crm',
25
- name: 'Topolo CRM',
26
- api_base_url: 'https://crm.topolo.app',
27
- launch_url: 'https://crm.topolo.app',
28
- surface_kind: 'api',
29
- status: 'active',
30
- granted_permissions: ['app_crm.contacts:read'],
31
- },
32
- ],
33
- },
34
- }),
35
- { status: 200, headers: { 'Content-Type': 'application/json' } },
36
- );
37
- }
38
- const next = responses.shift() ?? { status: 200, body: { data: {} } };
39
- return new Response(JSON.stringify(next.body ?? {}), {
40
- status: next.status ?? 200,
41
- headers: { 'Content-Type': 'application/json' },
42
- });
43
- });
44
- return { impl: impl as unknown as typeof fetch, calls };
45
- }
46
-
47
- function requestCalls(calls: Array<{ url: string; init: RequestInit }>) {
48
- return calls.filter(
49
- (call) =>
50
- !call.url.includes('/api/services/by-slug/') &&
51
- !call.url.includes('/api/services/catalog'),
52
- );
53
- }
54
-
55
- function clientWith(impl: typeof fetch) {
56
- return new TopoloClient({
57
- credential: { kind: 'access_token', accessToken: 'tok_test' },
58
- agent: AGENT,
59
- fetch: impl,
60
- });
61
- }
62
-
63
- beforeEach(() => {
64
- TopoloClient.clearAppIdCache();
65
- });
66
-
67
- describe('CrmModule.listContacts', () => {
68
- it('maps snake_case rows into camelCase summaries and forwards query params', async () => {
69
- const { impl, calls } = stubFetch([
70
- {
71
- body: {
72
- contacts: [
73
- {
74
- id: 'c_1',
75
- first: 'Ada',
76
- last: 'Lovelace',
77
- email: 'ada@example.com',
78
- phone: '+1',
79
- company: 'Analytical',
80
- lead_status: 'new',
81
- lifecycle_stage: 'lead',
82
- updated_at: '2026-01-01T00:00:00.000Z',
83
- },
84
- ],
85
- total: 42,
86
- page: 2,
87
- pageSize: 25,
88
- },
89
- },
90
- ]);
91
- const crm = new CrmModule(clientWith(impl));
92
-
93
- const result = await crm.listContacts({ q: 'ada', page: 2, pageSize: 25 });
94
-
95
- expect(result.contacts).toEqual([
96
- {
97
- id: 'c_1',
98
- firstName: 'Ada',
99
- lastName: 'Lovelace',
100
- email: 'ada@example.com',
101
- phone: '+1',
102
- company: 'Analytical',
103
- leadStatus: 'new',
104
- lifecycleStage: 'lead',
105
- updatedAt: '2026-01-01T00:00:00.000Z',
106
- },
107
- ]);
108
- expect(result.total).toBe(42);
109
- expect(result.page).toBe(2);
110
- expect(result.pageSize).toBe(25);
111
-
112
- const url = new URL(requestCalls(calls)[0]!.url);
113
- expect(url.pathname).toBe('/api/contacts');
114
- expect(url.searchParams.get('q')).toBe('ada');
115
- expect(url.searchParams.get('page')).toBe('2');
116
- expect(url.searchParams.get('pageSize')).toBe('25');
117
- });
118
-
119
- it('reads rows from the data[] envelope and pagination block', async () => {
120
- const { impl } = stubFetch([
121
- {
122
- body: {
123
- data: [{ id: 'c_2' }],
124
- pagination: { total: 7, page: 3, pageSize: 10 },
125
- },
126
- },
127
- ]);
128
- const crm = new CrmModule(clientWith(impl));
129
-
130
- const result = await crm.listContacts();
131
-
132
- expect(result.contacts).toEqual([
133
- {
134
- id: 'c_2',
135
- firstName: null,
136
- lastName: null,
137
- email: null,
138
- phone: null,
139
- company: null,
140
- leadStatus: null,
141
- lifecycleStage: null,
142
- updatedAt: null,
143
- },
144
- ]);
145
- expect(result.total).toBe(7);
146
- expect(result.page).toBe(3);
147
- expect(result.pageSize).toBe(10);
148
- });
149
-
150
- it('falls back to row count + option defaults when no metadata is returned', async () => {
151
- const { impl } = stubFetch([{ body: { contacts: [{ id: 'c_3' }, { id: 'c_4' }] } }]);
152
- const crm = new CrmModule(clientWith(impl));
153
-
154
- const result = await crm.listContacts();
155
-
156
- expect(result.total).toBe(2);
157
- expect(result.page).toBe(1);
158
- expect(result.pageSize).toBe(2);
159
- });
160
-
161
- it('returns an empty list when the response has neither contacts nor data arrays', async () => {
162
- const { impl } = stubFetch([{ body: { unexpected: true } }]);
163
- const crm = new CrmModule(clientWith(impl));
164
-
165
- const result = await crm.listContacts();
166
-
167
- expect(result.contacts).toEqual([]);
168
- expect(result.total).toBe(0);
169
- });
170
- });
171
-
172
- describe('CrmModule.getContact', () => {
173
- it('rejects an empty contactId', async () => {
174
- const { impl } = stubFetch([]);
175
- const crm = new CrmModule(clientWith(impl));
176
- await expect(crm.getContact('')).rejects.toThrow('contactId is required');
177
- });
178
-
179
- it('unwraps a data-enveloped contact', async () => {
180
- const { impl, calls } = stubFetch([
181
- { body: { data: { id: 'c_9', first: 'Grace', last: 'Hopper' } } },
182
- ]);
183
- const crm = new CrmModule(clientWith(impl));
184
-
185
- const contact = await crm.getContact('c_9');
186
-
187
- expect(contact).toMatchObject({ id: 'c_9', firstName: 'Grace', lastName: 'Hopper' });
188
- expect(new URL(requestCalls(calls)[0]!.url).pathname).toBe('/api/contacts/c_9');
189
- });
190
-
191
- it('accepts a bare contact object (no data envelope)', async () => {
192
- const { impl } = stubFetch([{ body: { id: 'c_10', email: 'g@example.com' } }]);
193
- const crm = new CrmModule(clientWith(impl));
194
-
195
- const contact = await crm.getContact('c_10');
196
-
197
- expect(contact).toMatchObject({ id: 'c_10', email: 'g@example.com' });
198
- });
199
-
200
- it('url-encodes contact ids with special characters', async () => {
201
- const { impl, calls } = stubFetch([{ body: { data: { id: 'a/b c' } } }]);
202
- const crm = new CrmModule(clientWith(impl));
203
-
204
- await crm.getContact('a/b c');
205
-
206
- expect(requestCalls(calls)[0]!.url).toContain('/api/contacts/a%2Fb%20c');
207
- });
208
- });
@@ -1,110 +0,0 @@
1
- import type { TopoloClient } from '../client.js';
2
-
3
- export interface CrmContactSummary {
4
- id: string;
5
- firstName: string | null;
6
- lastName: string | null;
7
- email: string | null;
8
- phone: string | null;
9
- company: string | null;
10
- leadStatus: string | null;
11
- lifecycleStage: string | null;
12
- updatedAt: string | null;
13
- }
14
-
15
- export interface ListContactsOptions {
16
- /** Full-text search string. */
17
- q?: string;
18
- /** Page number, 1-indexed. */
19
- page?: number;
20
- /** Page size. */
21
- pageSize?: number;
22
- }
23
-
24
- export interface ListContactsResult {
25
- contacts: CrmContactSummary[];
26
- total: number;
27
- page: number;
28
- pageSize: number;
29
- }
30
-
31
- /**
32
- * CRM module — thin typed wrapper over the TopoloCRM HTTP API.
33
- *
34
- * Cross-org isolation: there is intentionally NO `orgId` parameter anywhere in
35
- * this module. The organization is derived entirely from the auth credential
36
- * by the CRM backend, which scopes all SQL queries by `org_slug`/`org_id`.
37
- */
38
- export class CrmModule {
39
- constructor(private readonly client: TopoloClient) {}
40
-
41
- async listContacts(options: ListContactsOptions = {}): Promise<ListContactsResult> {
42
- const res = await this.client.request<RawListResponse>({
43
- service: 'crm',
44
- path: '/api/contacts',
45
- query: {
46
- q: options.q,
47
- page: options.page,
48
- pageSize: options.pageSize,
49
- },
50
- });
51
-
52
- const rows = Array.isArray(res?.contacts)
53
- ? res.contacts
54
- : Array.isArray(res?.data)
55
- ? res.data
56
- : [];
57
-
58
- return {
59
- contacts: rows.map(toSummary),
60
- total: res?.total ?? res?.pagination?.total ?? rows.length,
61
- page: res?.page ?? res?.pagination?.page ?? options.page ?? 1,
62
- pageSize: res?.pageSize ?? res?.pagination?.pageSize ?? options.pageSize ?? rows.length,
63
- };
64
- }
65
-
66
- async getContact(contactId: string): Promise<CrmContactSummary | null> {
67
- if (!contactId) throw new Error('contactId is required');
68
- const res = await this.client.request<{ data?: RawContact } | RawContact>({
69
- service: 'crm',
70
- path: `/api/contacts/${encodeURIComponent(contactId)}`,
71
- });
72
- const row = (res as { data?: RawContact }).data ?? (res as RawContact);
73
- return row ? toSummary(row) : null;
74
- }
75
- }
76
-
77
- interface RawContact {
78
- id: string;
79
- first?: string | null;
80
- last?: string | null;
81
- email?: string | null;
82
- phone?: string | null;
83
- company?: string | null;
84
- lead_status?: string | null;
85
- lifecycle_stage?: string | null;
86
- updated_at?: string | null;
87
- }
88
-
89
- interface RawListResponse {
90
- contacts?: RawContact[];
91
- data?: RawContact[];
92
- total?: number;
93
- page?: number;
94
- pageSize?: number;
95
- pagination?: { total?: number; page?: number; pageSize?: number };
96
- }
97
-
98
- function toSummary(row: RawContact): CrmContactSummary {
99
- return {
100
- id: row.id,
101
- firstName: row.first ?? null,
102
- lastName: row.last ?? null,
103
- email: row.email ?? null,
104
- phone: row.phone ?? null,
105
- company: row.company ?? null,
106
- leadStatus: row.lead_status ?? null,
107
- lifecycleStage: row.lifecycle_stage ?? null,
108
- updatedAt: row.updated_at ?? null,
109
- };
110
- }