@topolo/sdk 0.5.0 → 0.7.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 +3 -3
- package/dist/index.cjs +27 -24
- package/dist/index.d.cts +20 -20
- package/dist/index.d.ts +20 -20
- package/dist/index.js +26 -23
- package/dist/node/credential-store.cjs +471 -0
- package/dist/node/credential-store.d.cts +108 -0
- package/dist/node/credential-store.d.ts +108 -0
- package/dist/node/credential-store.js +427 -0
- package/package.json +7 -2
- package/src/application-requirements.ts +13 -13
- package/src/client.test.ts +7 -7
- package/src/client.ts +10 -10
- package/src/index.ts +2 -2
- package/src/node/credential-store.ts +378 -0
- package/src/oauth.ts +2 -2
- package/src/services.generated.ts +2 -2
- package/src/services.test.ts +15 -15
- package/src/services.ts +17 -17
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
// src/node/credential-store.ts
|
|
2
|
+
import { mkdir, readFile, writeFile, chmod } from "fs/promises";
|
|
3
|
+
import { existsSync } from "fs";
|
|
4
|
+
import { homedir, platform } from "os";
|
|
5
|
+
import { dirname, join } from "path";
|
|
6
|
+
|
|
7
|
+
// src/errors.ts
|
|
8
|
+
var TopoloSdkError = class extends Error {
|
|
9
|
+
code;
|
|
10
|
+
constructor(code, message) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = "TopoloSdkError";
|
|
13
|
+
this.code = code;
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
var TopoloAuthError = class extends TopoloSdkError {
|
|
17
|
+
constructor(message, code = "auth_error") {
|
|
18
|
+
super(code, message);
|
|
19
|
+
this.name = "TopoloAuthError";
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
var TopoloHttpError = class extends TopoloSdkError {
|
|
23
|
+
status;
|
|
24
|
+
body;
|
|
25
|
+
service;
|
|
26
|
+
path;
|
|
27
|
+
constructor(service, path, status, body, message) {
|
|
28
|
+
super("http_error", message ?? `HTTP ${status} from ${service}${path}`);
|
|
29
|
+
this.name = "TopoloHttpError";
|
|
30
|
+
this.status = status;
|
|
31
|
+
this.body = body;
|
|
32
|
+
this.service = service;
|
|
33
|
+
this.path = path;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// src/services.generated.ts
|
|
38
|
+
var APP_API_URLS = {
|
|
39
|
+
agent: "https://agent.topolo.app",
|
|
40
|
+
auth: "https://auth.topolo.app",
|
|
41
|
+
backup: "https://backup.topolo.app",
|
|
42
|
+
books: "https://books.topolo.app",
|
|
43
|
+
bugfix: "https://bugfix.topolo.app",
|
|
44
|
+
bytes: "https://bytes.topolo.app",
|
|
45
|
+
calendar: "https://calendar.topolo.app",
|
|
46
|
+
capacity: "https://capacity.topolo.app",
|
|
47
|
+
chat: "https://chat.topolo.app",
|
|
48
|
+
commerce: "https://commerce.topolo.app",
|
|
49
|
+
compose: "https://compose.topolo.app",
|
|
50
|
+
consent: "https://consent.topolo.app",
|
|
51
|
+
crm: "https://crm.topolo.app",
|
|
52
|
+
developers: "https://developers.topolo.app",
|
|
53
|
+
director: "https://director.topolo.app",
|
|
54
|
+
feed: "https://feed.topolo.app",
|
|
55
|
+
flow: "https://flow.topolo.app",
|
|
56
|
+
forecast: "https://forecast.topolo.app",
|
|
57
|
+
forms: "https://forms.topolo.app",
|
|
58
|
+
home: "https://home.topolo.app",
|
|
59
|
+
insights: "https://insights.topolo.app",
|
|
60
|
+
inventory: "https://inventory.topolo.app",
|
|
61
|
+
learn: "https://learn.topolo.app",
|
|
62
|
+
localize: "https://localize.topolo.app",
|
|
63
|
+
mail: "https://mail.topolo.app",
|
|
64
|
+
mdm: "https://mdm.topolo.app",
|
|
65
|
+
messages: "https://messages.topolo.app",
|
|
66
|
+
nexus: "https://nexus.topolo.app",
|
|
67
|
+
notify: "https://notify.topolo.app",
|
|
68
|
+
observability_watch: "https://observability-watch.topolo.app",
|
|
69
|
+
one: "https://www.topolo.app",
|
|
70
|
+
p2p: "https://p2p.topolo.app",
|
|
71
|
+
pay: "https://pay.topolo.app",
|
|
72
|
+
people: "https://people.topolo.app",
|
|
73
|
+
"quro.api": "https://api.ol0.me",
|
|
74
|
+
"quro.redirect": "https://ol0.me",
|
|
75
|
+
roadmapper: "https://roadmapper.topolo.app",
|
|
76
|
+
sign: "https://sign.topolo.app",
|
|
77
|
+
social_studio: "https://studio.topolo.app",
|
|
78
|
+
socialize: "https://socialize.topolo.app",
|
|
79
|
+
spaces: "https://spaces.topolo.app",
|
|
80
|
+
status: "https://status.topolo.app",
|
|
81
|
+
success: "https://success.topolo.app",
|
|
82
|
+
support: "https://support.topolo.app",
|
|
83
|
+
survey: "https://survey.topolo.app",
|
|
84
|
+
voice: "https://voice.topolo.app",
|
|
85
|
+
web: "https://web.topolo.app"
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// src/services.ts
|
|
89
|
+
var DEFAULT_APP_URLS = APP_API_URLS;
|
|
90
|
+
function resolveServiceUrl(service, overrides) {
|
|
91
|
+
const override = resolveServiceUrlOverride([service], overrides);
|
|
92
|
+
if (override) return override;
|
|
93
|
+
return DEFAULT_APP_URLS[service];
|
|
94
|
+
}
|
|
95
|
+
function resolveServiceUrlOverride(keys, overrides) {
|
|
96
|
+
for (const key of keys) {
|
|
97
|
+
const direct = overrides?.[key];
|
|
98
|
+
if (direct) return direct;
|
|
99
|
+
const envStyleKey = key.replace(/\./g, "_");
|
|
100
|
+
const envStyle = overrides?.[envStyleKey];
|
|
101
|
+
if (envStyle) return envStyle;
|
|
102
|
+
}
|
|
103
|
+
for (const key of keys) {
|
|
104
|
+
const envKey = `TOPOLO_APP_URL_${key.replace(/\./g, "_").toUpperCase()}`;
|
|
105
|
+
const envValue = typeof process !== "undefined" ? process.env?.[envKey] : void 0;
|
|
106
|
+
if (envValue) return envValue;
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// src/oauth.ts
|
|
112
|
+
var DEVICE_GRANT = "urn:ietf:params:oauth:grant-type:device_code";
|
|
113
|
+
var TopoloOAuth = class {
|
|
114
|
+
baseUrl;
|
|
115
|
+
fetchImpl;
|
|
116
|
+
constructor(options = {}) {
|
|
117
|
+
this.baseUrl = resolveServiceUrl("auth", options.serviceUrls);
|
|
118
|
+
this.fetchImpl = options.fetch ?? fetch;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* RFC 8628 device authorization grant — starts the flow by asking the server
|
|
122
|
+
* for a device_code + user_code. The caller prints the user_code +
|
|
123
|
+
* verification_uri and then polls `pollDeviceToken` until approval.
|
|
124
|
+
*/
|
|
125
|
+
async requestDeviceCode(params) {
|
|
126
|
+
const body = new URLSearchParams();
|
|
127
|
+
body.set("client_id", params.clientId);
|
|
128
|
+
if (params.scope) {
|
|
129
|
+
body.set("scope", Array.isArray(params.scope) ? params.scope.join(" ") : params.scope);
|
|
130
|
+
}
|
|
131
|
+
return this.#postForm("/api/developer-oauth/device_authorization", body);
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Polls the token endpoint once. Returns the token pair on success, or
|
|
135
|
+
* throws a `TopoloAuthError` whose `code` is one of the RFC 8628 poll
|
|
136
|
+
* states: `authorization_pending`, `slow_down`, `access_denied`,
|
|
137
|
+
* `expired_token`. Callers should back off on `slow_down`, wait at least
|
|
138
|
+
* one `interval` on `authorization_pending`, and give up on the rest.
|
|
139
|
+
*/
|
|
140
|
+
async pollDeviceToken(params) {
|
|
141
|
+
const body = new URLSearchParams();
|
|
142
|
+
body.set("grant_type", DEVICE_GRANT);
|
|
143
|
+
body.set("client_id", params.clientId);
|
|
144
|
+
body.set("device_code", params.deviceCode);
|
|
145
|
+
if (params.clientSecret) body.set("client_secret", params.clientSecret);
|
|
146
|
+
return this.#postForm("/api/developer-oauth/token", body);
|
|
147
|
+
}
|
|
148
|
+
/** Exchange an authorization code (with PKCE) for tokens. */
|
|
149
|
+
async exchangeAuthorizationCode(params) {
|
|
150
|
+
const body = new URLSearchParams();
|
|
151
|
+
body.set("grant_type", "authorization_code");
|
|
152
|
+
body.set("client_id", params.clientId);
|
|
153
|
+
body.set("code", params.code);
|
|
154
|
+
body.set("redirect_uri", params.redirectUri);
|
|
155
|
+
if (params.codeVerifier) body.set("code_verifier", params.codeVerifier);
|
|
156
|
+
if (params.clientSecret) body.set("client_secret", params.clientSecret);
|
|
157
|
+
return this.#postForm("/api/developer-oauth/token", body);
|
|
158
|
+
}
|
|
159
|
+
/** Rotate a refresh token for a new token pair. */
|
|
160
|
+
async refreshToken(params) {
|
|
161
|
+
const body = new URLSearchParams();
|
|
162
|
+
body.set("grant_type", "refresh_token");
|
|
163
|
+
body.set("client_id", params.clientId);
|
|
164
|
+
body.set("refresh_token", params.refreshToken);
|
|
165
|
+
if (params.clientSecret) body.set("client_secret", params.clientSecret);
|
|
166
|
+
return this.#postForm("/api/developer-oauth/token", body);
|
|
167
|
+
}
|
|
168
|
+
async #postForm(path, body) {
|
|
169
|
+
const url = new URL(path, this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`);
|
|
170
|
+
const res = await this.fetchImpl(url.toString(), {
|
|
171
|
+
method: "POST",
|
|
172
|
+
headers: {
|
|
173
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
174
|
+
Accept: "application/json"
|
|
175
|
+
},
|
|
176
|
+
body: body.toString()
|
|
177
|
+
});
|
|
178
|
+
const contentType = res.headers.get("Content-Type") ?? "";
|
|
179
|
+
const parsed = contentType.includes("application/json") ? await res.json().catch(() => null) : await res.text().catch(() => null);
|
|
180
|
+
if (!res.ok) {
|
|
181
|
+
if (parsed && typeof parsed === "object" && "error" in parsed) {
|
|
182
|
+
const err = parsed;
|
|
183
|
+
throw new TopoloAuthError(err.error_description || err.error, err.error);
|
|
184
|
+
}
|
|
185
|
+
throw new TopoloHttpError("auth", path, res.status, parsed);
|
|
186
|
+
}
|
|
187
|
+
return parsed;
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
// src/node/credential-store.ts
|
|
192
|
+
var TOPOLO_ENVIRONMENTS = ["production", "staging", "development", "local"];
|
|
193
|
+
function contextKey(contextType, organizationId) {
|
|
194
|
+
return contextType === "organization" && organizationId ? `org:${organizationId}` : "personal";
|
|
195
|
+
}
|
|
196
|
+
var ENVIRONMENT_DEFAULT_SERVICE_URLS = {
|
|
197
|
+
production: {
|
|
198
|
+
developers: "https://developers.topolo.app"
|
|
199
|
+
},
|
|
200
|
+
staging: {
|
|
201
|
+
auth: "https://auth.stg.topolo.us",
|
|
202
|
+
developers: "https://developers.stg.topolo.us",
|
|
203
|
+
seed: "https://seed.stg.topolo.us"
|
|
204
|
+
},
|
|
205
|
+
development: {
|
|
206
|
+
auth: "https://auth.topolo.dev",
|
|
207
|
+
developers: "https://developers.topolo.dev"
|
|
208
|
+
},
|
|
209
|
+
local: {}
|
|
210
|
+
};
|
|
211
|
+
function configPath() {
|
|
212
|
+
const override = process.env.TOPOLO_CONFIG_PATH;
|
|
213
|
+
if (override) return override;
|
|
214
|
+
const base = process.env.XDG_CONFIG_HOME || (platform() === "win32" ? join(homedir(), "AppData", "Roaming") : join(homedir(), ".config"));
|
|
215
|
+
return join(base, "topolo", "config.json");
|
|
216
|
+
}
|
|
217
|
+
async function readConfig() {
|
|
218
|
+
const path = configPath();
|
|
219
|
+
if (!existsSync(path)) return {};
|
|
220
|
+
try {
|
|
221
|
+
const raw = await readFile(path, "utf8");
|
|
222
|
+
return JSON.parse(raw);
|
|
223
|
+
} catch {
|
|
224
|
+
return {};
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
async function writeConfig(config) {
|
|
228
|
+
const path = configPath();
|
|
229
|
+
await mkdir(dirname(path), { recursive: true });
|
|
230
|
+
await writeFile(path, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
231
|
+
if (platform() !== "win32") {
|
|
232
|
+
try {
|
|
233
|
+
await chmod(path, 384);
|
|
234
|
+
} catch {
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
function normalizeEnvironment(value) {
|
|
239
|
+
const normalized = (value ?? "production").trim().toLowerCase();
|
|
240
|
+
if (normalized === "" || normalized === "production" || normalized === "prod") {
|
|
241
|
+
return "production";
|
|
242
|
+
}
|
|
243
|
+
if (normalized === "staging" || normalized === "stage" || normalized === "stg") {
|
|
244
|
+
return "staging";
|
|
245
|
+
}
|
|
246
|
+
if (normalized === "development" || normalized === "dev") {
|
|
247
|
+
return "development";
|
|
248
|
+
}
|
|
249
|
+
if (normalized === "local" || normalized === "localhost") {
|
|
250
|
+
return "local";
|
|
251
|
+
}
|
|
252
|
+
throw new Error(
|
|
253
|
+
`Unknown Topolo environment "${value}". Expected production, staging, development, or local.`
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
function selectedEnvironment() {
|
|
257
|
+
return normalizeEnvironment(process.env.TOPOLO_ENV);
|
|
258
|
+
}
|
|
259
|
+
function envVarPrefix(environment) {
|
|
260
|
+
if (environment === "production") return "TOPOLO_PRODUCTION";
|
|
261
|
+
if (environment === "staging") return "TOPOLO_STAGING";
|
|
262
|
+
if (environment === "development") return "TOPOLO_DEVELOPMENT";
|
|
263
|
+
return "TOPOLO_LOCAL";
|
|
264
|
+
}
|
|
265
|
+
function profileForEnvironment(config, environment = selectedEnvironment()) {
|
|
266
|
+
const profile = config.profiles?.[environment] ?? {};
|
|
267
|
+
if (environment === "production") {
|
|
268
|
+
return {
|
|
269
|
+
credential: config.credential,
|
|
270
|
+
oauthClientId: config.oauthClientId,
|
|
271
|
+
...profile,
|
|
272
|
+
serviceUrls: {
|
|
273
|
+
...config.serviceUrls ?? {},
|
|
274
|
+
...profile.serviceUrls ?? {}
|
|
275
|
+
},
|
|
276
|
+
agentName: profile.agentName ?? config.agentName
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
return profile;
|
|
280
|
+
}
|
|
281
|
+
function writeProfileConfig(config, environment, profile) {
|
|
282
|
+
const profiles = { ...config.profiles ?? {} };
|
|
283
|
+
profiles[environment] = {
|
|
284
|
+
...profiles[environment] ?? {},
|
|
285
|
+
...profile
|
|
286
|
+
};
|
|
287
|
+
return { ...config, profiles };
|
|
288
|
+
}
|
|
289
|
+
function writeContextCredential(config, environment, ctx, makeActive = true) {
|
|
290
|
+
const key = contextKey(ctx.contextType, ctx.organizationId);
|
|
291
|
+
const profile = profileForEnvironment(config, environment);
|
|
292
|
+
const contexts = { ...profile.contexts ?? {}, [key]: ctx };
|
|
293
|
+
const next = { contexts };
|
|
294
|
+
if (makeActive) {
|
|
295
|
+
next.activeContext = key;
|
|
296
|
+
next.credential = ctx.credential;
|
|
297
|
+
next.oauthClientId = ctx.oauthClientId ?? profile.oauthClientId;
|
|
298
|
+
}
|
|
299
|
+
return writeProfileConfig(config, environment, next);
|
|
300
|
+
}
|
|
301
|
+
function activateContext(config, environment, key) {
|
|
302
|
+
const profile = profileForEnvironment(config, environment);
|
|
303
|
+
const ctx = profile.contexts?.[key];
|
|
304
|
+
if (!ctx) return config;
|
|
305
|
+
return writeProfileConfig(config, environment, {
|
|
306
|
+
activeContext: key,
|
|
307
|
+
credential: ctx.credential,
|
|
308
|
+
oauthClientId: ctx.oauthClientId ?? profile.oauthClientId
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
function clearProfileCredential(config, environment) {
|
|
312
|
+
const next = { ...config };
|
|
313
|
+
if (environment === "production") {
|
|
314
|
+
delete next.credential;
|
|
315
|
+
delete next.oauthClientId;
|
|
316
|
+
}
|
|
317
|
+
const profiles = { ...config.profiles ?? {} };
|
|
318
|
+
const profile = { ...profiles[environment] ?? {} };
|
|
319
|
+
delete profile.credential;
|
|
320
|
+
delete profile.oauthClientId;
|
|
321
|
+
profiles[environment] = profile;
|
|
322
|
+
return { ...next, profiles };
|
|
323
|
+
}
|
|
324
|
+
function credentialSourceFromConfig(config, environment = selectedEnvironment()) {
|
|
325
|
+
const prefix = envVarPrefix(environment);
|
|
326
|
+
if (process.env[`${prefix}_API_KEY`]) return `env:${prefix}_API_KEY`;
|
|
327
|
+
if (process.env[`${prefix}_ACCESS_TOKEN`]) return `env:${prefix}_ACCESS_TOKEN`;
|
|
328
|
+
if (process.env.TOPOLO_API_KEY) return "env:TOPOLO_API_KEY";
|
|
329
|
+
if (process.env.TOPOLO_ACCESS_TOKEN) return "env:TOPOLO_ACCESS_TOKEN";
|
|
330
|
+
return profileForEnvironment(config, environment).credential ? "config-file" : "none";
|
|
331
|
+
}
|
|
332
|
+
async function resolveCredential(environment = selectedEnvironment()) {
|
|
333
|
+
const prefix = envVarPrefix(environment);
|
|
334
|
+
const apiKey = process.env[`${prefix}_API_KEY`] ?? process.env.TOPOLO_API_KEY;
|
|
335
|
+
if (apiKey) return { kind: "api_key", apiKey };
|
|
336
|
+
const accessToken = process.env[`${prefix}_ACCESS_TOKEN`] ?? process.env.TOPOLO_ACCESS_TOKEN;
|
|
337
|
+
if (accessToken) return { kind: "access_token", accessToken };
|
|
338
|
+
const config = await readConfig();
|
|
339
|
+
return profileForEnvironment(config, environment).credential ?? null;
|
|
340
|
+
}
|
|
341
|
+
var REFRESH_SKEW_SECONDS = 60;
|
|
342
|
+
async function refreshCredentialIfNeeded(environment = selectedEnvironment()) {
|
|
343
|
+
const prefix = envVarPrefix(environment);
|
|
344
|
+
if (process.env[`${prefix}_API_KEY`] || process.env[`${prefix}_ACCESS_TOKEN`] || process.env.TOPOLO_API_KEY || process.env.TOPOLO_ACCESS_TOKEN) {
|
|
345
|
+
return resolveCredential(environment);
|
|
346
|
+
}
|
|
347
|
+
const config = await readConfig();
|
|
348
|
+
const profile = profileForEnvironment(config, environment);
|
|
349
|
+
const cred = profile.credential;
|
|
350
|
+
if (!cred || cred.kind !== "access_token") return cred ?? null;
|
|
351
|
+
if (!cred.refreshToken || !cred.expiresAt || !profile.oauthClientId) return cred;
|
|
352
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
353
|
+
if (cred.expiresAt - REFRESH_SKEW_SECONDS > now) return cred;
|
|
354
|
+
try {
|
|
355
|
+
const oauth = new TopoloOAuth({
|
|
356
|
+
serviceUrls: resolveServiceUrls(profile.serviceUrls, environment)
|
|
357
|
+
});
|
|
358
|
+
const tokens = await oauth.refreshToken({
|
|
359
|
+
clientId: profile.oauthClientId,
|
|
360
|
+
refreshToken: cred.refreshToken
|
|
361
|
+
});
|
|
362
|
+
const refreshed = {
|
|
363
|
+
kind: "access_token",
|
|
364
|
+
accessToken: tokens.access_token,
|
|
365
|
+
...tokens.refresh_token ? { refreshToken: tokens.refresh_token } : { refreshToken: cred.refreshToken },
|
|
366
|
+
expiresAt: Math.floor(Date.now() / 1e3) + tokens.expires_in
|
|
367
|
+
};
|
|
368
|
+
await writeConfig(
|
|
369
|
+
environment === "production" && !config.profiles?.production ? { ...config, credential: refreshed } : writeProfileConfig(config, environment, { credential: refreshed })
|
|
370
|
+
);
|
|
371
|
+
return refreshed;
|
|
372
|
+
} catch {
|
|
373
|
+
return cred;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
function resolveAgentName(configAgentName) {
|
|
377
|
+
return process.env.TOPOLO_AGENT_NAME ?? configAgentName;
|
|
378
|
+
}
|
|
379
|
+
function resolveServiceUrls(configUrls, environment = selectedEnvironment()) {
|
|
380
|
+
const merged = {
|
|
381
|
+
...ENVIRONMENT_DEFAULT_SERVICE_URLS[environment],
|
|
382
|
+
...configUrls ?? {}
|
|
383
|
+
};
|
|
384
|
+
const prefix = `${envVarPrefix(environment)}_SERVICE_URL_`;
|
|
385
|
+
for (const [k, v] of Object.entries(process.env)) {
|
|
386
|
+
if (k.startsWith(prefix) && v) {
|
|
387
|
+
merged[k.slice(prefix.length).toLowerCase()] = v;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
for (const [k, v] of Object.entries(process.env)) {
|
|
391
|
+
if (k.startsWith("TOPOLO_SERVICE_URL_") && v) {
|
|
392
|
+
merged[k.slice("TOPOLO_SERVICE_URL_".length).toLowerCase()] = v;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
return Object.keys(merged).length > 0 ? merged : void 0;
|
|
396
|
+
}
|
|
397
|
+
function resolvedProfileConfig(config, environment = selectedEnvironment()) {
|
|
398
|
+
const profile = profileForEnvironment(config, environment);
|
|
399
|
+
return {
|
|
400
|
+
credential: profile.credential,
|
|
401
|
+
oauthClientId: profile.oauthClientId,
|
|
402
|
+
agentName: profile.agentName,
|
|
403
|
+
serviceUrls: resolveServiceUrls(profile.serviceUrls, environment)
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
export {
|
|
407
|
+
ENVIRONMENT_DEFAULT_SERVICE_URLS,
|
|
408
|
+
TOPOLO_ENVIRONMENTS,
|
|
409
|
+
activateContext,
|
|
410
|
+
clearProfileCredential,
|
|
411
|
+
configPath,
|
|
412
|
+
contextKey,
|
|
413
|
+
credentialSourceFromConfig,
|
|
414
|
+
envVarPrefix,
|
|
415
|
+
normalizeEnvironment,
|
|
416
|
+
profileForEnvironment,
|
|
417
|
+
readConfig,
|
|
418
|
+
refreshCredentialIfNeeded,
|
|
419
|
+
resolveAgentName,
|
|
420
|
+
resolveCredential,
|
|
421
|
+
resolveServiceUrls,
|
|
422
|
+
resolvedProfileConfig,
|
|
423
|
+
selectedEnvironment,
|
|
424
|
+
writeConfig,
|
|
425
|
+
writeContextCredential,
|
|
426
|
+
writeProfileConfig
|
|
427
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@topolo/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Typed client SDK for the Topolo platform. Used by TopoloCli, TopoloMCP, and third-party agents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -11,6 +11,11 @@
|
|
|
11
11
|
"types": "./dist/index.d.ts",
|
|
12
12
|
"import": "./dist/index.js",
|
|
13
13
|
"require": "./dist/index.cjs"
|
|
14
|
+
},
|
|
15
|
+
"./node": {
|
|
16
|
+
"types": "./dist/node/credential-store.d.ts",
|
|
17
|
+
"import": "./dist/node/credential-store.js",
|
|
18
|
+
"require": "./dist/node/credential-store.cjs"
|
|
14
19
|
}
|
|
15
20
|
},
|
|
16
21
|
"files": [
|
|
@@ -21,7 +26,7 @@
|
|
|
21
26
|
"generate:services": "node scripts/generate-services.mjs",
|
|
22
27
|
"check:generated": "node scripts/check-generated.mjs",
|
|
23
28
|
"prebuild": "node scripts/generate-services.mjs",
|
|
24
|
-
"build": "tsup src/index.ts --format esm,cjs --dts --clean",
|
|
29
|
+
"build": "tsup src/index.ts --format esm,cjs --dts --clean && tsup src/node/credential-store.ts --format esm,cjs --dts --out-dir dist/node",
|
|
25
30
|
"dev": "tsup src/index.ts --format esm,cjs --dts --watch",
|
|
26
31
|
"typecheck": "node scripts/generate-services.mjs && tsc --noEmit",
|
|
27
32
|
"test": "vitest run",
|
|
@@ -105,12 +105,12 @@ export const APPLICATION_REQUIREMENTS = [
|
|
|
105
105
|
id: 'shared-auth-boundary',
|
|
106
106
|
title: 'Use the shared Topolo auth boundary',
|
|
107
107
|
summary:
|
|
108
|
-
'Applications must derive identity, organization, scopes, and
|
|
108
|
+
'Applications must derive identity, organization, scopes, and app entitlement from TopoloAuth rather than inventing parallel auth state.',
|
|
109
109
|
appliesTo: ['browser', 'api', 'tooling', 'agent_surface'],
|
|
110
110
|
evidence: [
|
|
111
111
|
'@topolo/auth-client or @topolo-io/worker-runtime is used where applicable',
|
|
112
112
|
'No public command, tool, or SDK method accepts orgId',
|
|
113
|
-
'
|
|
113
|
+
'App slug and Auth app registration are documented for protected APIs',
|
|
114
114
|
],
|
|
115
115
|
implementation: [
|
|
116
116
|
'Use shared auth/runtime packages instead of copying token parsing, gateway, or browser storage flows',
|
|
@@ -144,21 +144,21 @@ export const APPLICATION_REQUIREMENTS = [
|
|
|
144
144
|
],
|
|
145
145
|
},
|
|
146
146
|
{
|
|
147
|
-
id: '
|
|
148
|
-
title: 'Register protected APIs as platform
|
|
147
|
+
id: 'app-registration-and-scopes',
|
|
148
|
+
title: 'Register protected APIs as platform apps',
|
|
149
149
|
summary:
|
|
150
|
-
'Callable app APIs need a stable
|
|
150
|
+
'Callable app APIs need a stable app ID, app slug, permissions, role bundles, and API-key scopes before agents or users rely on them.',
|
|
151
151
|
appliesTo: ['api'],
|
|
152
152
|
evidence: [
|
|
153
|
-
'Auth
|
|
154
|
-
'Permissions, role bundles, and API-key scopes exist for the
|
|
155
|
-
'SDK, CLI, and MCP
|
|
156
|
-
'CloudControl production HTTP targets classify agent reachability with
|
|
153
|
+
'Auth app catalog contains the app ID and slug',
|
|
154
|
+
'Permissions, role bundles, and API-key scopes exist for the app',
|
|
155
|
+
'SDK, CLI, and MCP app discovery include the callable production API',
|
|
156
|
+
'CloudControl production HTTP targets classify agent reachability with sdk_app_id or agent_callable:false',
|
|
157
157
|
],
|
|
158
158
|
implementation: [
|
|
159
|
-
'Add production Worker api_url metadata and
|
|
159
|
+
'Add production Worker api_url metadata and sdk_app_id for every agent-callable API target',
|
|
160
160
|
'Mark browser, documentation, and runtime-only Worker targets with agent_callable:false',
|
|
161
|
-
'Expose stable
|
|
161
|
+
'Expose stable app IDs through generated DEFAULT_APP_URLS and live catalog discovery',
|
|
162
162
|
'Keep typed SDK/CLI/MCP surfaces in sync when an API contract stabilizes',
|
|
163
163
|
],
|
|
164
164
|
},
|
|
@@ -410,7 +410,7 @@ function evaluateRequirement(
|
|
|
410
410
|
'Catalog metadata cannot verify shared shell, launcher, and account menu adoption.',
|
|
411
411
|
evidence,
|
|
412
412
|
);
|
|
413
|
-
case '
|
|
413
|
+
case 'app-registration-and-scopes': {
|
|
414
414
|
if (application.services.length > 0) {
|
|
415
415
|
evidence.push(`services=${application.services.join(',')}`);
|
|
416
416
|
return finding(
|
|
@@ -420,7 +420,7 @@ function evaluateRequirement(
|
|
|
420
420
|
evidence,
|
|
421
421
|
);
|
|
422
422
|
}
|
|
423
|
-
return finding(requirement, 'missing', 'No SDK
|
|
423
|
+
return finding(requirement, 'missing', 'No SDK app API id is registered for this callable API surface.', evidence);
|
|
424
424
|
}
|
|
425
425
|
case 'organization-scoped-data':
|
|
426
426
|
return finding(
|
package/src/client.test.ts
CHANGED
|
@@ -46,8 +46,8 @@ const ACTION_CATALOG = {
|
|
|
46
46
|
title: 'List messages',
|
|
47
47
|
description: 'List Mail messages',
|
|
48
48
|
app_id: 'app_mail',
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
app_slug: 'topolo-mail',
|
|
50
|
+
app_name: 'Topolo Mail',
|
|
51
51
|
method: 'GET',
|
|
52
52
|
path: '/api/mail/mailboxes/{mailboxId}/messages',
|
|
53
53
|
permission_name: 'messages:read',
|
|
@@ -65,8 +65,8 @@ const ACTION_CATALOG = {
|
|
|
65
65
|
title: 'Get message',
|
|
66
66
|
description: 'Get Mail message',
|
|
67
67
|
app_id: 'app_mail',
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
app_slug: 'topolo-mail',
|
|
69
|
+
app_name: 'Topolo Mail',
|
|
70
70
|
method: 'GET',
|
|
71
71
|
path: '/api/mail/mailboxes/{mailboxId}/messages/{messageId}',
|
|
72
72
|
permission_name: 'messages:read',
|
|
@@ -84,8 +84,8 @@ const ACTION_CATALOG = {
|
|
|
84
84
|
title: 'Send message',
|
|
85
85
|
description: 'Send Mail message',
|
|
86
86
|
app_id: 'app_mail',
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
app_slug: 'topolo-mail',
|
|
88
|
+
app_name: 'Topolo Mail',
|
|
89
89
|
method: 'POST',
|
|
90
90
|
path: '/api/mail/mailboxes/{mailboxId}/messages',
|
|
91
91
|
permission_name: 'messages:send',
|
|
@@ -395,7 +395,7 @@ describe('TopoloClient.getAppIdBySlug', () => {
|
|
|
395
395
|
});
|
|
396
396
|
}
|
|
397
397
|
|
|
398
|
-
it('uses configured
|
|
398
|
+
it('uses configured app ids without calling Auth', async () => {
|
|
399
399
|
const { impl, calls } = stubFetch([]);
|
|
400
400
|
const client = new TopoloClient({
|
|
401
401
|
credential: { kind: 'api_key', apiKey: 'x' },
|
package/src/client.ts
CHANGED
|
@@ -7,11 +7,11 @@ import {
|
|
|
7
7
|
} from './auth.js';
|
|
8
8
|
import { TopoloAuthError, TopoloHttpError, TopoloPermissionError, TopoloSdkError } from './errors.js';
|
|
9
9
|
import {
|
|
10
|
-
|
|
10
|
+
DEFAULT_APP_URLS,
|
|
11
11
|
APPLICATIONS,
|
|
12
12
|
resolveCatalogServiceUrl,
|
|
13
13
|
resolveServiceUrl,
|
|
14
|
-
type
|
|
14
|
+
type AppApiId,
|
|
15
15
|
} from './services.js';
|
|
16
16
|
|
|
17
17
|
export interface TopoloClientOptions {
|
|
@@ -140,8 +140,8 @@ export interface TopoloActionCatalogEntry {
|
|
|
140
140
|
title: string;
|
|
141
141
|
description: string;
|
|
142
142
|
appId: string;
|
|
143
|
-
|
|
144
|
-
|
|
143
|
+
appSlug: string | null;
|
|
144
|
+
appName: string | null;
|
|
145
145
|
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
146
146
|
path: string;
|
|
147
147
|
permissionName: string;
|
|
@@ -352,7 +352,7 @@ export class TopoloClient {
|
|
|
352
352
|
const derived = deriveDevelopersFromAuth(resolveConfiguredServiceUrl('auth', this.serviceUrls));
|
|
353
353
|
if (derived) return { serviceKey, baseUrl: derived };
|
|
354
354
|
}
|
|
355
|
-
return { serviceKey, baseUrl:
|
|
355
|
+
return { serviceKey, baseUrl: DEFAULT_APP_URLS[serviceKey as 'auth' | 'developers'] };
|
|
356
356
|
}
|
|
357
357
|
|
|
358
358
|
const live = await this.resolveLiveCallableService(serviceKey);
|
|
@@ -363,7 +363,7 @@ export class TopoloClient {
|
|
|
363
363
|
|
|
364
364
|
throw new TopoloSdkError(
|
|
365
365
|
'unknown_service',
|
|
366
|
-
`Unknown Topolo
|
|
366
|
+
`Unknown Topolo app API "${service}". Run app discovery first and pass a callable app id or slug.`,
|
|
367
367
|
);
|
|
368
368
|
}
|
|
369
369
|
|
|
@@ -603,7 +603,7 @@ export class TopoloClient {
|
|
|
603
603
|
): Promise<T> {
|
|
604
604
|
const entry = await this.getAction(action);
|
|
605
605
|
const mapped = mapActionInput(entry, input);
|
|
606
|
-
const service = entry.
|
|
606
|
+
const service = entry.appSlug || entry.appId;
|
|
607
607
|
return this.request<T>({
|
|
608
608
|
service,
|
|
609
609
|
method: entry.method,
|
|
@@ -693,7 +693,7 @@ function resolveConfiguredAppId(
|
|
|
693
693
|
appIds?: Record<string, string>,
|
|
694
694
|
): string | null {
|
|
695
695
|
for (const serviceKey of serviceKeyCandidates(slug)) {
|
|
696
|
-
const configured = appIds?.[serviceKey as
|
|
696
|
+
const configured = appIds?.[serviceKey as AppApiId];
|
|
697
697
|
if (typeof configured === 'string' && configured.trim()) return configured.trim();
|
|
698
698
|
|
|
699
699
|
const envKey = `TOPOLO_APP_ID_${serviceKey.replace(/\./g, '_').toUpperCase()}`;
|
|
@@ -847,8 +847,8 @@ function normalizeActionCatalogEntry(item: unknown): TopoloActionCatalogEntry |
|
|
|
847
847
|
title: pickActionString(a.title) ?? name,
|
|
848
848
|
description: pickActionString(a.description) ?? '',
|
|
849
849
|
appId,
|
|
850
|
-
|
|
851
|
-
|
|
850
|
+
appSlug: pickActionString(a.app_slug) ?? pickActionString(a.appSlug) ?? null,
|
|
851
|
+
appName: pickActionString(a.app_name) ?? pickActionString(a.appName) ?? null,
|
|
852
852
|
method,
|
|
853
853
|
path,
|
|
854
854
|
permissionName: pickActionString(a.permission_name) ?? pickActionString(a.permissionName) ?? '',
|
package/src/index.ts
CHANGED
|
@@ -10,7 +10,7 @@ export {
|
|
|
10
10
|
} from "./client.js";
|
|
11
11
|
export type { TopoloCredential, AgentIdentity } from "./auth.js";
|
|
12
12
|
export {
|
|
13
|
-
|
|
13
|
+
DEFAULT_APP_URLS,
|
|
14
14
|
APPLICATIONS,
|
|
15
15
|
APPLICATION_REQUIREMENTS,
|
|
16
16
|
APPLICATION_REQUIREMENTS_VERSION,
|
|
@@ -32,7 +32,7 @@ export {
|
|
|
32
32
|
type ApplicationRequirementStatus,
|
|
33
33
|
type ApplicationRequirementsAuditReport,
|
|
34
34
|
type ServiceCatalogUrlEntry,
|
|
35
|
-
type
|
|
35
|
+
type AppApiId,
|
|
36
36
|
} from "./services.js";
|
|
37
37
|
export {
|
|
38
38
|
TopoloSdkError,
|