@topolo/sdk 0.1.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 +123 -0
- package/dist/index.cjs +414 -0
- package/dist/index.d.cts +277 -0
- package/dist/index.d.ts +277 -0
- package/dist/index.js +376 -0
- package/package.json +33 -0
- package/src/auth.ts +48 -0
- package/src/client.ts +245 -0
- package/src/errors.ts +39 -0
- package/src/index.ts +49 -0
- package/src/modules/crm.ts +110 -0
- package/src/modules/identity.ts +18 -0
- package/src/oauth.ts +138 -0
- package/src/services.ts +41 -0
package/README.md
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# @topolo/sdk
|
|
2
|
+
|
|
3
|
+
Typed client SDK for the Topolo platform. Consumed by `@topolo/cli` and
|
|
4
|
+
`@topolo/mcp`, and available to any first- or third-party code that needs to
|
|
5
|
+
call Topolo APIs on a user's behalf.
|
|
6
|
+
|
|
7
|
+
This package is the single place where cross-org isolation is enforced on the
|
|
8
|
+
client side.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install @topolo/sdk
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Usage
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import { createTopolo } from '@topolo/sdk';
|
|
20
|
+
|
|
21
|
+
const topolo = createTopolo({
|
|
22
|
+
credential: { kind: 'api_key', apiKey: process.env.TOPOLO_API_KEY! },
|
|
23
|
+
agent: {
|
|
24
|
+
clientName: 'my-integration',
|
|
25
|
+
clientVersion: '1.0.0',
|
|
26
|
+
agentName: 'my-backend-service',
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const me = await topolo.identity.whoami();
|
|
31
|
+
console.log(me.organization?.slug);
|
|
32
|
+
|
|
33
|
+
const { contacts } = await topolo.crm.listContacts({ q: 'acme' });
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Design invariants
|
|
37
|
+
|
|
38
|
+
### No `orgId` parameter, anywhere
|
|
39
|
+
|
|
40
|
+
No public method, tool definition, or CLI command in the Topolo agent surface
|
|
41
|
+
accepts an `orgId` input. Every request derives the org from the credential —
|
|
42
|
+
a JWT `orgId` claim for access tokens, or the `organization_id` bound to an
|
|
43
|
+
API key at issuance.
|
|
44
|
+
|
|
45
|
+
This is enforced at every layer:
|
|
46
|
+
|
|
47
|
+
1. `@topolo/sdk` — no typed method takes an `orgId` arg.
|
|
48
|
+
2. `@topolo/cli` — no command takes an `--org` flag.
|
|
49
|
+
3. `@topolo/mcp` — no tool schema has an `orgId` field.
|
|
50
|
+
4. Each Topolo backend app re-derives `orgId` from the credential on every
|
|
51
|
+
request and binds it into SQL `WHERE` clauses.
|
|
52
|
+
|
|
53
|
+
### Write-action gating
|
|
54
|
+
|
|
55
|
+
`TopoloClient` rejects POST/PUT/PATCH/DELETE by default unless the call
|
|
56
|
+
passes `{ confirm: true }`. This prevents an agent from accidentally issuing
|
|
57
|
+
writes during casual exploration. Disable with `requireConfirmForWrites: false`
|
|
58
|
+
only for trusted surfaces.
|
|
59
|
+
|
|
60
|
+
### Audit headers
|
|
61
|
+
|
|
62
|
+
Every request sends:
|
|
63
|
+
|
|
64
|
+
- `X-Topolo-Client: <clientName>/<clientVersion>`
|
|
65
|
+
- `X-Topolo-Agent: <agentName>` (if set)
|
|
66
|
+
- `X-Topolo-Request-Id: <uuid>`
|
|
67
|
+
|
|
68
|
+
Combined with the credential identity, this makes every platform API call
|
|
69
|
+
traceable to a specific agent invocation.
|
|
70
|
+
|
|
71
|
+
## Modules (Phase 1)
|
|
72
|
+
|
|
73
|
+
| Module | Methods |
|
|
74
|
+
| ------------ | ------------------------------------- |
|
|
75
|
+
| `identity` | `whoami()` |
|
|
76
|
+
| `crm` | `listContacts()`, `getContact(id)` |
|
|
77
|
+
|
|
78
|
+
Additional modules land per-app as each backend contract stabilizes.
|
|
79
|
+
|
|
80
|
+
## Escape hatch
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
await topolo.client.request({
|
|
84
|
+
service: 'crm',
|
|
85
|
+
method: 'GET',
|
|
86
|
+
path: '/api/some/untyped/endpoint',
|
|
87
|
+
});
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Still routes through auth + audit headers. Mutating methods still require
|
|
91
|
+
`confirm: true`.
|
|
92
|
+
|
|
93
|
+
## Errors
|
|
94
|
+
|
|
95
|
+
| Class | Thrown for |
|
|
96
|
+
| ------------------------ | ---------------------------------------- |
|
|
97
|
+
| `TopoloAuthError` | 401 responses, missing credentials |
|
|
98
|
+
| `TopoloPermissionError` | 403 responses (includes `.required`) |
|
|
99
|
+
| `TopoloHttpError` | Other non-2xx responses |
|
|
100
|
+
| `TopoloSdkError` | Base class |
|
|
101
|
+
|
|
102
|
+
## Service URL overrides
|
|
103
|
+
|
|
104
|
+
Defaults target production. Override per-service for staging/dev:
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
createTopolo({
|
|
108
|
+
/* ... */
|
|
109
|
+
serviceUrls: {
|
|
110
|
+
crm: 'https://edge-crm-stg.topolo.workers.dev',
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Or via env: `TOPOLO_SERVICE_URL_CRM=...` (falls through `resolveServiceUrl`).
|
|
116
|
+
|
|
117
|
+
## Development
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
npm install
|
|
121
|
+
npm run build
|
|
122
|
+
npm run typecheck
|
|
123
|
+
```
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
CrmModule: () => CrmModule,
|
|
24
|
+
DEFAULT_SERVICE_URLS: () => DEFAULT_SERVICE_URLS,
|
|
25
|
+
IdentityModule: () => IdentityModule,
|
|
26
|
+
PLATFORM_SERVICE_IDS: () => PLATFORM_SERVICE_IDS,
|
|
27
|
+
TopoloAuthError: () => TopoloAuthError,
|
|
28
|
+
TopoloClient: () => TopoloClient,
|
|
29
|
+
TopoloHttpError: () => TopoloHttpError,
|
|
30
|
+
TopoloOAuth: () => TopoloOAuth,
|
|
31
|
+
TopoloPermissionError: () => TopoloPermissionError,
|
|
32
|
+
TopoloSdkError: () => TopoloSdkError,
|
|
33
|
+
createTopolo: () => createTopolo,
|
|
34
|
+
resolveServiceUrl: () => resolveServiceUrl
|
|
35
|
+
});
|
|
36
|
+
module.exports = __toCommonJS(index_exports);
|
|
37
|
+
|
|
38
|
+
// src/auth.ts
|
|
39
|
+
function applyAuthHeaders(headers, credential) {
|
|
40
|
+
if (credential.kind === "api_key") {
|
|
41
|
+
headers.set("X-Api-Key", credential.apiKey);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
headers.set("Authorization", `Bearer ${credential.accessToken}`);
|
|
45
|
+
}
|
|
46
|
+
function applyAuditHeaders(headers, agent, requestId) {
|
|
47
|
+
headers.set("X-Topolo-Client", `${agent.clientName}/${agent.clientVersion}`);
|
|
48
|
+
if (agent.agentName) headers.set("X-Topolo-Agent", agent.agentName);
|
|
49
|
+
headers.set("X-Topolo-Request-Id", requestId);
|
|
50
|
+
}
|
|
51
|
+
function generateRequestId() {
|
|
52
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
53
|
+
return crypto.randomUUID();
|
|
54
|
+
}
|
|
55
|
+
return `req_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// src/errors.ts
|
|
59
|
+
var TopoloSdkError = class extends Error {
|
|
60
|
+
code;
|
|
61
|
+
constructor(code, message) {
|
|
62
|
+
super(message);
|
|
63
|
+
this.name = "TopoloSdkError";
|
|
64
|
+
this.code = code;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
var TopoloAuthError = class extends TopoloSdkError {
|
|
68
|
+
constructor(message, code = "auth_error") {
|
|
69
|
+
super(code, message);
|
|
70
|
+
this.name = "TopoloAuthError";
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
var TopoloPermissionError = class extends TopoloSdkError {
|
|
74
|
+
required;
|
|
75
|
+
constructor(message, required) {
|
|
76
|
+
super("permission_denied", message);
|
|
77
|
+
this.name = "TopoloPermissionError";
|
|
78
|
+
this.required = required;
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
var TopoloHttpError = class extends TopoloSdkError {
|
|
82
|
+
status;
|
|
83
|
+
body;
|
|
84
|
+
service;
|
|
85
|
+
path;
|
|
86
|
+
constructor(service, path, status, body, message) {
|
|
87
|
+
super("http_error", message ?? `HTTP ${status} from ${service}${path}`);
|
|
88
|
+
this.name = "TopoloHttpError";
|
|
89
|
+
this.status = status;
|
|
90
|
+
this.body = body;
|
|
91
|
+
this.service = service;
|
|
92
|
+
this.path = path;
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// src/services.ts
|
|
97
|
+
var DEFAULT_SERVICE_URLS = {
|
|
98
|
+
auth: "https://auth.topolo.app",
|
|
99
|
+
crm: "https://topolo-crm-worker.topolo.workers.dev"
|
|
100
|
+
};
|
|
101
|
+
var PLATFORM_SERVICE_IDS = {
|
|
102
|
+
crm: "srv_iCwM4jGXcwlj"
|
|
103
|
+
};
|
|
104
|
+
function resolveServiceUrl(service, overrides) {
|
|
105
|
+
const override = overrides?.[service];
|
|
106
|
+
if (override) return override;
|
|
107
|
+
const envKey = `TOPOLO_SERVICE_URL_${service.toUpperCase()}`;
|
|
108
|
+
const envValue = typeof process !== "undefined" ? process.env?.[envKey] : void 0;
|
|
109
|
+
if (envValue) return envValue;
|
|
110
|
+
return DEFAULT_SERVICE_URLS[service];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// src/client.ts
|
|
114
|
+
var WRITE_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
115
|
+
var TopoloClient = class {
|
|
116
|
+
credential;
|
|
117
|
+
agent;
|
|
118
|
+
serviceUrls;
|
|
119
|
+
requireConfirmForWrites;
|
|
120
|
+
timeoutMs;
|
|
121
|
+
fetchImpl;
|
|
122
|
+
constructor(options) {
|
|
123
|
+
if (!options.credential) throw new TopoloAuthError("credential is required");
|
|
124
|
+
if (!options.agent?.clientName) throw new TopoloAuthError("agent.clientName is required");
|
|
125
|
+
this.credential = options.credential;
|
|
126
|
+
this.agent = options.agent;
|
|
127
|
+
this.serviceUrls = options.serviceUrls;
|
|
128
|
+
this.requireConfirmForWrites = options.requireConfirmForWrites !== false;
|
|
129
|
+
this.timeoutMs = options.timeoutMs ?? 3e4;
|
|
130
|
+
this.fetchImpl = options.fetch ?? fetch;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Low-level JSON request. Prefer the typed module helpers (identity, crm, ...)
|
|
134
|
+
* for anything a caller would reach for; this stays exported for escape-hatch
|
|
135
|
+
* use and for the generic `topolo api` / MCP passthrough tool.
|
|
136
|
+
*/
|
|
137
|
+
async request(opts) {
|
|
138
|
+
const method = opts.method ?? "GET";
|
|
139
|
+
if (this.requireConfirmForWrites && WRITE_METHODS.has(method) && !opts.confirm) {
|
|
140
|
+
throw new TopoloAuthError(
|
|
141
|
+
`Mutating ${method} requests require { confirm: true }. This guardrail prevents agents from issuing writes without an explicit human-in-the-loop acknowledgement.`
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
const baseUrl = resolveServiceUrl(opts.service, this.serviceUrls);
|
|
145
|
+
const url = new URL(opts.path, baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`);
|
|
146
|
+
if (opts.query) {
|
|
147
|
+
for (const [k, v] of Object.entries(opts.query)) {
|
|
148
|
+
if (v !== void 0) url.searchParams.set(k, String(v));
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
const headers = new Headers();
|
|
152
|
+
headers.set("Accept", "application/json");
|
|
153
|
+
if (opts.body !== void 0) headers.set("Content-Type", "application/json");
|
|
154
|
+
const platformServiceId = PLATFORM_SERVICE_IDS[opts.service];
|
|
155
|
+
if (platformServiceId) headers.set("X-Service-ID", platformServiceId);
|
|
156
|
+
applyAuthHeaders(headers, this.credential);
|
|
157
|
+
applyAuditHeaders(headers, this.agent, generateRequestId());
|
|
158
|
+
if (opts.headers) {
|
|
159
|
+
for (const [k, v] of Object.entries(opts.headers)) headers.set(k, v);
|
|
160
|
+
}
|
|
161
|
+
const controller = new AbortController();
|
|
162
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
163
|
+
const signal = opts.signal ? mergeSignals(opts.signal, controller.signal) : controller.signal;
|
|
164
|
+
let res;
|
|
165
|
+
try {
|
|
166
|
+
res = await this.fetchImpl(url.toString(), {
|
|
167
|
+
method,
|
|
168
|
+
headers,
|
|
169
|
+
body: opts.body !== void 0 ? JSON.stringify(opts.body) : null,
|
|
170
|
+
signal
|
|
171
|
+
});
|
|
172
|
+
} finally {
|
|
173
|
+
clearTimeout(timeoutId);
|
|
174
|
+
}
|
|
175
|
+
const contentType = res.headers.get("Content-Type") ?? "";
|
|
176
|
+
const parsed = contentType.includes("application/json") ? await res.json().catch(() => null) : await res.text().catch(() => null);
|
|
177
|
+
if (!res.ok) {
|
|
178
|
+
if (res.status === 401) throw new TopoloAuthError(describeError(parsed, "Unauthorized"));
|
|
179
|
+
if (res.status === 403) {
|
|
180
|
+
throw new TopoloPermissionError(
|
|
181
|
+
describeError(parsed, "Permission denied"),
|
|
182
|
+
extractRequired(parsed)
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
throw new TopoloHttpError(opts.service, opts.path, res.status, parsed);
|
|
186
|
+
}
|
|
187
|
+
return parsed;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Introspect the current credential. Returns the caller's resolved identity,
|
|
191
|
+
* organization, and permission set. Used by CLI `whoami` and by MCP to gate
|
|
192
|
+
* advertised tools by scope.
|
|
193
|
+
*
|
|
194
|
+
* Both JWT access tokens and platform API keys are resolved via the unified
|
|
195
|
+
* `GET /api/auth/me` endpoint on TopoloAuth, which does not require service
|
|
196
|
+
* credentials. The caller's possession of the credential secret is proof.
|
|
197
|
+
*/
|
|
198
|
+
async introspect() {
|
|
199
|
+
const res = await this.request({
|
|
200
|
+
service: "auth",
|
|
201
|
+
method: "GET",
|
|
202
|
+
path: "/api/auth/me"
|
|
203
|
+
});
|
|
204
|
+
const payload = res.data ?? res;
|
|
205
|
+
const user = payload.user;
|
|
206
|
+
const organization = payload.organization;
|
|
207
|
+
const kind = payload.credentialType === "api_key" ? "api_key" : "access_token";
|
|
208
|
+
return {
|
|
209
|
+
kind,
|
|
210
|
+
user: {
|
|
211
|
+
id: user.id,
|
|
212
|
+
email: user.email,
|
|
213
|
+
name: user.name ?? null,
|
|
214
|
+
role: user.role ?? (kind === "api_key" ? "service" : "member"),
|
|
215
|
+
permissions: payload.permissions ?? user.permissions ?? []
|
|
216
|
+
},
|
|
217
|
+
organization: organization ? {
|
|
218
|
+
id: organization.id,
|
|
219
|
+
slug: organization.slug,
|
|
220
|
+
name: organization.name ?? organization.slug
|
|
221
|
+
} : user.orgId && user.orgSlug ? { id: user.orgId, slug: user.orgSlug, name: user.orgSlug } : null
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
function describeError(body, fallback) {
|
|
226
|
+
if (body && typeof body === "object") {
|
|
227
|
+
const maybe = body;
|
|
228
|
+
return maybe.message ?? maybe.error ?? fallback;
|
|
229
|
+
}
|
|
230
|
+
return fallback;
|
|
231
|
+
}
|
|
232
|
+
function extractRequired(body) {
|
|
233
|
+
if (body && typeof body === "object") {
|
|
234
|
+
const maybe = body;
|
|
235
|
+
if (Array.isArray(maybe.required)) return maybe.required.map(String);
|
|
236
|
+
}
|
|
237
|
+
return [];
|
|
238
|
+
}
|
|
239
|
+
function mergeSignals(a, b) {
|
|
240
|
+
if (a.aborted) return a;
|
|
241
|
+
if (b.aborted) return b;
|
|
242
|
+
const controller = new AbortController();
|
|
243
|
+
const onAbort = () => controller.abort();
|
|
244
|
+
a.addEventListener("abort", onAbort, { once: true });
|
|
245
|
+
b.addEventListener("abort", onAbort, { once: true });
|
|
246
|
+
return controller.signal;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// src/modules/crm.ts
|
|
250
|
+
var CrmModule = class {
|
|
251
|
+
constructor(client) {
|
|
252
|
+
this.client = client;
|
|
253
|
+
}
|
|
254
|
+
client;
|
|
255
|
+
async listContacts(options = {}) {
|
|
256
|
+
const res = await this.client.request({
|
|
257
|
+
service: "crm",
|
|
258
|
+
path: "/api/contacts",
|
|
259
|
+
query: {
|
|
260
|
+
q: options.q,
|
|
261
|
+
page: options.page,
|
|
262
|
+
pageSize: options.pageSize
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
const rows = Array.isArray(res?.contacts) ? res.contacts : Array.isArray(res?.data) ? res.data : [];
|
|
266
|
+
return {
|
|
267
|
+
contacts: rows.map(toSummary),
|
|
268
|
+
total: res?.total ?? res?.pagination?.total ?? rows.length,
|
|
269
|
+
page: res?.page ?? res?.pagination?.page ?? options.page ?? 1,
|
|
270
|
+
pageSize: res?.pageSize ?? res?.pagination?.pageSize ?? options.pageSize ?? rows.length
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
async getContact(contactId) {
|
|
274
|
+
if (!contactId) throw new Error("contactId is required");
|
|
275
|
+
const res = await this.client.request({
|
|
276
|
+
service: "crm",
|
|
277
|
+
path: `/api/contacts/${encodeURIComponent(contactId)}`
|
|
278
|
+
});
|
|
279
|
+
const row = res.data ?? res;
|
|
280
|
+
return row ? toSummary(row) : null;
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
function toSummary(row) {
|
|
284
|
+
return {
|
|
285
|
+
id: row.id,
|
|
286
|
+
firstName: row.first ?? null,
|
|
287
|
+
lastName: row.last ?? null,
|
|
288
|
+
email: row.email ?? null,
|
|
289
|
+
phone: row.phone ?? null,
|
|
290
|
+
company: row.company ?? null,
|
|
291
|
+
leadStatus: row.lead_status ?? null,
|
|
292
|
+
lifecycleStage: row.lifecycle_stage ?? null,
|
|
293
|
+
updatedAt: row.updated_at ?? null
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// src/modules/identity.ts
|
|
298
|
+
var IdentityModule = class {
|
|
299
|
+
constructor(client) {
|
|
300
|
+
this.client = client;
|
|
301
|
+
}
|
|
302
|
+
client;
|
|
303
|
+
/** Returns the resolved user, organization, and permission set for the
|
|
304
|
+
* current credential. Equivalent to `TopoloClient.introspect()` — exposed
|
|
305
|
+
* on this module for symmetry with the other domain modules. */
|
|
306
|
+
whoami() {
|
|
307
|
+
return this.client.introspect();
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
// src/oauth.ts
|
|
312
|
+
var DEVICE_GRANT = "urn:ietf:params:oauth:grant-type:device_code";
|
|
313
|
+
var TopoloOAuth = class {
|
|
314
|
+
baseUrl;
|
|
315
|
+
fetchImpl;
|
|
316
|
+
constructor(options = {}) {
|
|
317
|
+
this.baseUrl = resolveServiceUrl("auth", options.serviceUrls);
|
|
318
|
+
this.fetchImpl = options.fetch ?? fetch;
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* RFC 8628 device authorization grant — starts the flow by asking the server
|
|
322
|
+
* for a device_code + user_code. The caller prints the user_code +
|
|
323
|
+
* verification_uri and then polls `pollDeviceToken` until approval.
|
|
324
|
+
*/
|
|
325
|
+
async requestDeviceCode(params) {
|
|
326
|
+
const body = new URLSearchParams();
|
|
327
|
+
body.set("client_id", params.clientId);
|
|
328
|
+
if (params.scope) {
|
|
329
|
+
body.set("scope", Array.isArray(params.scope) ? params.scope.join(" ") : params.scope);
|
|
330
|
+
}
|
|
331
|
+
return this.#postForm("/api/developer-oauth/device_authorization", body);
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Polls the token endpoint once. Returns the token pair on success, or
|
|
335
|
+
* throws a `TopoloAuthError` whose `code` is one of the RFC 8628 poll
|
|
336
|
+
* states: `authorization_pending`, `slow_down`, `access_denied`,
|
|
337
|
+
* `expired_token`. Callers should back off on `slow_down`, wait at least
|
|
338
|
+
* one `interval` on `authorization_pending`, and give up on the rest.
|
|
339
|
+
*/
|
|
340
|
+
async pollDeviceToken(params) {
|
|
341
|
+
const body = new URLSearchParams();
|
|
342
|
+
body.set("grant_type", DEVICE_GRANT);
|
|
343
|
+
body.set("client_id", params.clientId);
|
|
344
|
+
body.set("device_code", params.deviceCode);
|
|
345
|
+
if (params.clientSecret) body.set("client_secret", params.clientSecret);
|
|
346
|
+
return this.#postForm("/api/developer-oauth/token", body);
|
|
347
|
+
}
|
|
348
|
+
/** Exchange an authorization code (with PKCE) for tokens. */
|
|
349
|
+
async exchangeAuthorizationCode(params) {
|
|
350
|
+
const body = new URLSearchParams();
|
|
351
|
+
body.set("grant_type", "authorization_code");
|
|
352
|
+
body.set("client_id", params.clientId);
|
|
353
|
+
body.set("code", params.code);
|
|
354
|
+
body.set("redirect_uri", params.redirectUri);
|
|
355
|
+
if (params.codeVerifier) body.set("code_verifier", params.codeVerifier);
|
|
356
|
+
if (params.clientSecret) body.set("client_secret", params.clientSecret);
|
|
357
|
+
return this.#postForm("/api/developer-oauth/token", body);
|
|
358
|
+
}
|
|
359
|
+
/** Rotate a refresh token for a new token pair. */
|
|
360
|
+
async refreshToken(params) {
|
|
361
|
+
const body = new URLSearchParams();
|
|
362
|
+
body.set("grant_type", "refresh_token");
|
|
363
|
+
body.set("client_id", params.clientId);
|
|
364
|
+
body.set("refresh_token", params.refreshToken);
|
|
365
|
+
if (params.clientSecret) body.set("client_secret", params.clientSecret);
|
|
366
|
+
return this.#postForm("/api/developer-oauth/token", body);
|
|
367
|
+
}
|
|
368
|
+
async #postForm(path, body) {
|
|
369
|
+
const url = new URL(path, this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`);
|
|
370
|
+
const res = await this.fetchImpl(url.toString(), {
|
|
371
|
+
method: "POST",
|
|
372
|
+
headers: {
|
|
373
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
374
|
+
Accept: "application/json"
|
|
375
|
+
},
|
|
376
|
+
body: body.toString()
|
|
377
|
+
});
|
|
378
|
+
const contentType = res.headers.get("Content-Type") ?? "";
|
|
379
|
+
const parsed = contentType.includes("application/json") ? await res.json().catch(() => null) : await res.text().catch(() => null);
|
|
380
|
+
if (!res.ok) {
|
|
381
|
+
if (parsed && typeof parsed === "object" && "error" in parsed) {
|
|
382
|
+
const err = parsed;
|
|
383
|
+
throw new TopoloAuthError(err.error_description || err.error, err.error);
|
|
384
|
+
}
|
|
385
|
+
throw new TopoloHttpError("auth", path, res.status, parsed);
|
|
386
|
+
}
|
|
387
|
+
return parsed;
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
// src/index.ts
|
|
392
|
+
function createTopolo(options) {
|
|
393
|
+
const client = new TopoloClient(options);
|
|
394
|
+
return {
|
|
395
|
+
client,
|
|
396
|
+
identity: new IdentityModule(client),
|
|
397
|
+
crm: new CrmModule(client)
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
401
|
+
0 && (module.exports = {
|
|
402
|
+
CrmModule,
|
|
403
|
+
DEFAULT_SERVICE_URLS,
|
|
404
|
+
IdentityModule,
|
|
405
|
+
PLATFORM_SERVICE_IDS,
|
|
406
|
+
TopoloAuthError,
|
|
407
|
+
TopoloClient,
|
|
408
|
+
TopoloHttpError,
|
|
409
|
+
TopoloOAuth,
|
|
410
|
+
TopoloPermissionError,
|
|
411
|
+
TopoloSdkError,
|
|
412
|
+
createTopolo,
|
|
413
|
+
resolveServiceUrl
|
|
414
|
+
});
|