@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.
@@ -0,0 +1,378 @@
1
+ import { mkdir, readFile, writeFile, chmod } from 'node:fs/promises';
2
+ import { existsSync } from 'node:fs';
3
+ import { homedir, platform } from 'node:os';
4
+ import { dirname, join } from 'node:path';
5
+ import { TopoloOAuth } from '../oauth.js';
6
+ import type { TopoloCredential } from '../auth.js';
7
+
8
+ /**
9
+ * Node-only credential store shared by the `topolo` CLI and `@topolo/mcp`.
10
+ *
11
+ * Both need to read (and refresh) the credential that `topolo auth login`
12
+ * writes to `$XDG_CONFIG_HOME/topolo/config.json`. This lives in a Node-only
13
+ * subpath (`@topolo/sdk/node`) so the isomorphic SDK core never imports
14
+ * `node:fs` — importing this module in a browser/worker bundle is a mistake.
15
+ *
16
+ * The CLI re-exports every symbol here from `packages/topolo-cli/src/config.ts`
17
+ * so existing call sites are unchanged; the MCP imports it directly to fall
18
+ * back to the on-disk credential when no `TOPOLO_*` env credential is set.
19
+ */
20
+
21
+ export const TOPOLO_ENVIRONMENTS = ['production', 'staging', 'development', 'local'] as const;
22
+ export type TopoloEnvironment = (typeof TOPOLO_ENVIRONMENTS)[number];
23
+
24
+ export interface TopoloConfig {
25
+ credential?: TopoloCredential;
26
+ /** OAuth client_id used for the most recent login — needed to refresh the access token. */
27
+ oauthClientId?: string;
28
+ /** Per-service URL overrides — written when user runs `topolo config set-url`. */
29
+ serviceUrls?: Record<string, string>;
30
+ /** Optional agent label propagated as `X-Topolo-Agent`. */
31
+ agentName?: string;
32
+ /** Environment-scoped profiles. Legacy top-level fields are treated as production. */
33
+ profiles?: Partial<Record<TopoloEnvironment, TopoloEnvironmentProfile>>;
34
+ }
35
+
36
+ export interface TopoloEnvironmentProfile {
37
+ credential?: TopoloCredential;
38
+ oauthClientId?: string;
39
+ serviceUrls?: Record<string, string>;
40
+ agentName?: string;
41
+ /**
42
+ * The context (`personal` or `org:<orgId>`) whose credential is currently
43
+ * active. `credential` above always mirrors `contexts[activeContext]`, so
44
+ * credential resolution + the MCP keep working unchanged while `contexts`
45
+ * lets the CLI hold and switch between multiple org/personal contexts.
46
+ */
47
+ activeContext?: string;
48
+ contexts?: Record<string, TopoloContextCredential>;
49
+ }
50
+
51
+ /** A signed-in context — personal, or a specific organization. */
52
+ export interface TopoloContextCredential {
53
+ contextType: 'personal' | 'organization';
54
+ organizationId?: string;
55
+ organizationSlug?: string;
56
+ organizationName?: string;
57
+ credential?: TopoloCredential;
58
+ oauthClientId?: string;
59
+ }
60
+
61
+ /** Stable key for a context within a profile's `contexts` map. */
62
+ export function contextKey(
63
+ contextType: 'personal' | 'organization',
64
+ organizationId?: string | null,
65
+ ): string {
66
+ return contextType === 'organization' && organizationId
67
+ ? `org:${organizationId}`
68
+ : 'personal';
69
+ }
70
+
71
+ /**
72
+ * Default service URLs per environment. `local` has no baked defaults — a
73
+ * local dev loop points services at ad-hoc ports, so it is driven entirely by
74
+ * `TOPOLO_LOCAL_SERVICE_URL_*` / `TOPOLO_SERVICE_URL_*` overrides.
75
+ */
76
+ export const ENVIRONMENT_DEFAULT_SERVICE_URLS: Record<
77
+ TopoloEnvironment,
78
+ Record<string, string>
79
+ > = {
80
+ production: {
81
+ developers: 'https://developers.topolo.app',
82
+ },
83
+ staging: {
84
+ auth: 'https://auth.stg.topolo.us',
85
+ developers: 'https://developers.stg.topolo.us',
86
+ seed: 'https://seed.stg.topolo.us',
87
+ },
88
+ development: {
89
+ auth: 'https://auth.topolo.dev',
90
+ developers: 'https://developers.topolo.dev',
91
+ },
92
+ local: {},
93
+ };
94
+
95
+ export function configPath(): string {
96
+ const override = process.env.TOPOLO_CONFIG_PATH;
97
+ if (override) return override;
98
+ const base = process.env.XDG_CONFIG_HOME
99
+ || (platform() === 'win32'
100
+ ? join(homedir(), 'AppData', 'Roaming')
101
+ : join(homedir(), '.config'));
102
+ return join(base, 'topolo', 'config.json');
103
+ }
104
+
105
+ export async function readConfig(): Promise<TopoloConfig> {
106
+ const path = configPath();
107
+ if (!existsSync(path)) return {};
108
+ try {
109
+ const raw = await readFile(path, 'utf8');
110
+ return JSON.parse(raw) as TopoloConfig;
111
+ } catch {
112
+ return {};
113
+ }
114
+ }
115
+
116
+ export async function writeConfig(config: TopoloConfig): Promise<void> {
117
+ const path = configPath();
118
+ await mkdir(dirname(path), { recursive: true });
119
+ await writeFile(path, JSON.stringify(config, null, 2) + '\n', 'utf8');
120
+ if (platform() !== 'win32') {
121
+ try {
122
+ await chmod(path, 0o600);
123
+ } catch {
124
+ /* non-fatal */
125
+ }
126
+ }
127
+ }
128
+
129
+ export function normalizeEnvironment(value: string | undefined): TopoloEnvironment {
130
+ const normalized = (value ?? 'production').trim().toLowerCase();
131
+ if (normalized === '' || normalized === 'production' || normalized === 'prod') {
132
+ return 'production';
133
+ }
134
+ if (normalized === 'staging' || normalized === 'stage' || normalized === 'stg') {
135
+ return 'staging';
136
+ }
137
+ if (normalized === 'development' || normalized === 'dev') {
138
+ return 'development';
139
+ }
140
+ if (normalized === 'local' || normalized === 'localhost') {
141
+ return 'local';
142
+ }
143
+ throw new Error(
144
+ `Unknown Topolo environment "${value}". Expected production, staging, development, or local.`,
145
+ );
146
+ }
147
+
148
+ export function selectedEnvironment(): TopoloEnvironment {
149
+ return normalizeEnvironment(process.env.TOPOLO_ENV);
150
+ }
151
+
152
+ export function envVarPrefix(environment: TopoloEnvironment): string {
153
+ if (environment === 'production') return 'TOPOLO_PRODUCTION';
154
+ if (environment === 'staging') return 'TOPOLO_STAGING';
155
+ if (environment === 'development') return 'TOPOLO_DEVELOPMENT';
156
+ return 'TOPOLO_LOCAL';
157
+ }
158
+
159
+ export function profileForEnvironment(
160
+ config: TopoloConfig,
161
+ environment: TopoloEnvironment = selectedEnvironment(),
162
+ ): TopoloEnvironmentProfile {
163
+ const profile = config.profiles?.[environment] ?? {};
164
+ if (environment === 'production') {
165
+ return {
166
+ credential: config.credential,
167
+ oauthClientId: config.oauthClientId,
168
+ ...profile,
169
+ serviceUrls: {
170
+ ...(config.serviceUrls ?? {}),
171
+ ...(profile.serviceUrls ?? {}),
172
+ },
173
+ agentName: profile.agentName ?? config.agentName,
174
+ };
175
+ }
176
+ return profile;
177
+ }
178
+
179
+ export function writeProfileConfig(
180
+ config: TopoloConfig,
181
+ environment: TopoloEnvironment,
182
+ profile: TopoloEnvironmentProfile,
183
+ ): TopoloConfig {
184
+ const profiles = { ...(config.profiles ?? {}) };
185
+ profiles[environment] = {
186
+ ...(profiles[environment] ?? {}),
187
+ ...profile,
188
+ };
189
+ return { ...config, profiles };
190
+ }
191
+
192
+ /**
193
+ * Store a context's credential in the profile's `contexts` map. When
194
+ * `makeActive` (default), also point the profile's active credential at it so
195
+ * credential resolution + the MCP use this context.
196
+ */
197
+ export function writeContextCredential(
198
+ config: TopoloConfig,
199
+ environment: TopoloEnvironment,
200
+ ctx: TopoloContextCredential,
201
+ makeActive = true,
202
+ ): TopoloConfig {
203
+ const key = contextKey(ctx.contextType, ctx.organizationId);
204
+ const profile = profileForEnvironment(config, environment);
205
+ const contexts = { ...(profile.contexts ?? {}), [key]: ctx };
206
+ const next: TopoloEnvironmentProfile = { contexts };
207
+ if (makeActive) {
208
+ next.activeContext = key;
209
+ next.credential = ctx.credential;
210
+ next.oauthClientId = ctx.oauthClientId ?? profile.oauthClientId;
211
+ }
212
+ return writeProfileConfig(config, environment, next);
213
+ }
214
+
215
+ /** Point the profile's active credential at an already-stored context. */
216
+ export function activateContext(
217
+ config: TopoloConfig,
218
+ environment: TopoloEnvironment,
219
+ key: string,
220
+ ): TopoloConfig {
221
+ const profile = profileForEnvironment(config, environment);
222
+ const ctx = profile.contexts?.[key];
223
+ if (!ctx) return config;
224
+ return writeProfileConfig(config, environment, {
225
+ activeContext: key,
226
+ credential: ctx.credential,
227
+ oauthClientId: ctx.oauthClientId ?? profile.oauthClientId,
228
+ });
229
+ }
230
+
231
+ export function clearProfileCredential(
232
+ config: TopoloConfig,
233
+ environment: TopoloEnvironment,
234
+ ): TopoloConfig {
235
+ const next = { ...config };
236
+ if (environment === 'production') {
237
+ delete next.credential;
238
+ delete next.oauthClientId;
239
+ }
240
+ const profiles = { ...(config.profiles ?? {}) };
241
+ const profile = { ...(profiles[environment] ?? {}) };
242
+ delete profile.credential;
243
+ delete profile.oauthClientId;
244
+ profiles[environment] = profile;
245
+ return { ...next, profiles };
246
+ }
247
+
248
+ export function credentialSourceFromConfig(
249
+ config: TopoloConfig,
250
+ environment: TopoloEnvironment = selectedEnvironment(),
251
+ ): string {
252
+ const prefix = envVarPrefix(environment);
253
+ if (process.env[`${prefix}_API_KEY`]) return `env:${prefix}_API_KEY`;
254
+ if (process.env[`${prefix}_ACCESS_TOKEN`]) return `env:${prefix}_ACCESS_TOKEN`;
255
+ if (process.env.TOPOLO_API_KEY) return 'env:TOPOLO_API_KEY';
256
+ if (process.env.TOPOLO_ACCESS_TOKEN) return 'env:TOPOLO_ACCESS_TOKEN';
257
+ return profileForEnvironment(config, environment).credential ? 'config-file' : 'none';
258
+ }
259
+
260
+ /**
261
+ * Resolve credentials with env-var precedence over on-disk config.
262
+ * Env vars are what agents set in their environment; config is what
263
+ * interactive users get after running `topolo auth login`.
264
+ */
265
+ export async function resolveCredential(
266
+ environment: TopoloEnvironment = selectedEnvironment(),
267
+ ): Promise<TopoloCredential | null> {
268
+ const prefix = envVarPrefix(environment);
269
+ const apiKey = process.env[`${prefix}_API_KEY`] ?? process.env.TOPOLO_API_KEY;
270
+ if (apiKey) return { kind: 'api_key', apiKey };
271
+
272
+ const accessToken = process.env[`${prefix}_ACCESS_TOKEN`] ?? process.env.TOPOLO_ACCESS_TOKEN;
273
+ if (accessToken) return { kind: 'access_token', accessToken };
274
+
275
+ const config = await readConfig();
276
+ return profileForEnvironment(config, environment).credential ?? null;
277
+ }
278
+
279
+ /**
280
+ * If the stored access_token is expired (or within the skew window) and a
281
+ * refresh_token + oauthClientId are present, exchange the refresh_token for a
282
+ * new pair and persist. On any failure returns the existing (stale) credential
283
+ * so the next API call fails naturally with auth_error — we don't nuke the
284
+ * user's config on a transient network blip.
285
+ */
286
+ const REFRESH_SKEW_SECONDS = 60;
287
+
288
+ export async function refreshCredentialIfNeeded(
289
+ environment: TopoloEnvironment = selectedEnvironment(),
290
+ ): Promise<TopoloCredential | null> {
291
+ const prefix = envVarPrefix(environment);
292
+ if (
293
+ process.env[`${prefix}_API_KEY`]
294
+ || process.env[`${prefix}_ACCESS_TOKEN`]
295
+ || process.env.TOPOLO_API_KEY
296
+ || process.env.TOPOLO_ACCESS_TOKEN
297
+ ) {
298
+ return resolveCredential(environment);
299
+ }
300
+
301
+ const config = await readConfig();
302
+ const profile = profileForEnvironment(config, environment);
303
+ const cred = profile.credential;
304
+ if (!cred || cred.kind !== 'access_token') return cred ?? null;
305
+ if (!cred.refreshToken || !cred.expiresAt || !profile.oauthClientId) return cred;
306
+
307
+ const now = Math.floor(Date.now() / 1000);
308
+ if (cred.expiresAt - REFRESH_SKEW_SECONDS > now) return cred;
309
+
310
+ try {
311
+ const oauth = new TopoloOAuth({
312
+ serviceUrls: resolveServiceUrls(profile.serviceUrls, environment) as Partial<
313
+ Record<'auth' | 'crm', string>
314
+ >,
315
+ });
316
+ const tokens = await oauth.refreshToken({
317
+ clientId: profile.oauthClientId,
318
+ refreshToken: cred.refreshToken,
319
+ });
320
+ const refreshed: TopoloCredential = {
321
+ kind: 'access_token',
322
+ accessToken: tokens.access_token,
323
+ ...(tokens.refresh_token
324
+ ? { refreshToken: tokens.refresh_token }
325
+ : { refreshToken: cred.refreshToken }),
326
+ expiresAt: Math.floor(Date.now() / 1000) + tokens.expires_in,
327
+ };
328
+ await writeConfig(
329
+ environment === 'production' && !config.profiles?.production
330
+ ? { ...config, credential: refreshed }
331
+ : writeProfileConfig(config, environment, { credential: refreshed }),
332
+ );
333
+ return refreshed;
334
+ } catch {
335
+ return cred;
336
+ }
337
+ }
338
+
339
+ export function resolveAgentName(configAgentName?: string): string | undefined {
340
+ return process.env.TOPOLO_AGENT_NAME ?? configAgentName;
341
+ }
342
+
343
+ export function resolveServiceUrls(
344
+ configUrls?: Record<string, string>,
345
+ environment: TopoloEnvironment = selectedEnvironment(),
346
+ ): Record<string, string> | undefined {
347
+ const merged: Record<string, string> = {
348
+ ...ENVIRONMENT_DEFAULT_SERVICE_URLS[environment],
349
+ ...(configUrls ?? {}),
350
+ };
351
+
352
+ // Env wins. Pull env-specific and global service URL vars that might be set.
353
+ const prefix = `${envVarPrefix(environment)}_SERVICE_URL_`;
354
+ for (const [k, v] of Object.entries(process.env)) {
355
+ if (k.startsWith(prefix) && v) {
356
+ merged[k.slice(prefix.length).toLowerCase()] = v;
357
+ }
358
+ }
359
+ for (const [k, v] of Object.entries(process.env)) {
360
+ if (k.startsWith('TOPOLO_SERVICE_URL_') && v) {
361
+ merged[k.slice('TOPOLO_SERVICE_URL_'.length).toLowerCase()] = v;
362
+ }
363
+ }
364
+ return Object.keys(merged).length > 0 ? merged : undefined;
365
+ }
366
+
367
+ export function resolvedProfileConfig(
368
+ config: TopoloConfig,
369
+ environment: TopoloEnvironment = selectedEnvironment(),
370
+ ): TopoloEnvironmentProfile {
371
+ const profile = profileForEnvironment(config, environment);
372
+ return {
373
+ credential: profile.credential,
374
+ oauthClientId: profile.oauthClientId,
375
+ agentName: profile.agentName,
376
+ serviceUrls: resolveServiceUrls(profile.serviceUrls, environment),
377
+ };
378
+ }
package/src/oauth.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { TopoloAuthError, TopoloHttpError } from './errors.js';
2
- import { resolveServiceUrl, type ServiceId } from './services.js';
2
+ import { resolveServiceUrl, type AppApiId } from './services.js';
3
3
 
4
4
  const DEVICE_GRANT = 'urn:ietf:params:oauth:grant-type:device_code';
5
5
 
@@ -22,7 +22,7 @@ export interface TokenResponse {
22
22
 
23
23
  export interface OAuthHelperOptions {
24
24
  /** Overrides for TopoloAuth URL. Staging/dev should pass a mapping. */
25
- serviceUrls?: Partial<Record<ServiceId, string>>;
25
+ serviceUrls?: Partial<Record<AppApiId, string>>;
26
26
  /** Injected fetch. Defaults to global fetch. */
27
27
  fetch?: typeof fetch;
28
28
  }
@@ -2,7 +2,7 @@
2
2
  // Source of truth: topolo.cloudcontrol.json in every app repo.
3
3
  // Regenerate: npm run generate:services
4
4
 
5
- export const SERVICES = {
5
+ export const APP_API_URLS = {
6
6
  agent: "https://agent.topolo.app",
7
7
  auth: "https://auth.topolo.app",
8
8
  backup: "https://backup.topolo.app",
@@ -52,4 +52,4 @@ export const SERVICES = {
52
52
  web: "https://web.topolo.app",
53
53
  } as const;
54
54
 
55
- export type ServiceId = keyof typeof SERVICES;
55
+ export type AppApiId = keyof typeof APP_API_URLS;
@@ -3,7 +3,7 @@ import {
3
3
  APPLICATION_REQUIREMENTS,
4
4
  APPLICATION_REQUIREMENTS_VERSION,
5
5
  APPLICATIONS,
6
- DEFAULT_SERVICE_URLS,
6
+ DEFAULT_APP_URLS,
7
7
  auditAllApplicationRequirements,
8
8
  auditApplicationRequirements,
9
9
  applicationRequirementScopes,
@@ -62,9 +62,9 @@ const EXPECTED_APPLICATION_IDS = [
62
62
  'web',
63
63
  ];
64
64
 
65
- describe('service registry', () => {
65
+ describe('app API registry', () => {
66
66
  it('includes every production worker API that belongs to a platform application', () => {
67
- expect(DEFAULT_SERVICE_URLS).toMatchObject({
67
+ expect(DEFAULT_APP_URLS).toMatchObject({
68
68
  calendar: 'https://calendar.topolo.app',
69
69
  books: 'https://books.topolo.app',
70
70
  consent: 'https://consent.topolo.app',
@@ -73,27 +73,27 @@ describe('service registry', () => {
73
73
  one: 'https://www.topolo.app',
74
74
  web: 'https://web.topolo.app',
75
75
  });
76
- expect(DEFAULT_SERVICE_URLS).toMatchObject({
76
+ expect(DEFAULT_APP_URLS).toMatchObject({
77
77
  success: 'https://success.topolo.app',
78
78
  forms: 'https://forms.topolo.app',
79
79
  survey: 'https://survey.topolo.app',
80
80
  });
81
- expect(DEFAULT_SERVICE_URLS).not.toHaveProperty('web.api');
82
- expect(DEFAULT_SERVICE_URLS).not.toHaveProperty('web.runtime');
81
+ expect(DEFAULT_APP_URLS).not.toHaveProperty('web.api');
82
+ expect(DEFAULT_APP_URLS).not.toHaveProperty('web.runtime');
83
83
  });
84
84
 
85
- it('does not bake opaque Auth service ids into the generated registry', async () => {
85
+ it('does not bake opaque Auth app ids into the generated registry', async () => {
86
86
  const services = await import('./services.js');
87
87
 
88
- expect(services).not.toHaveProperty('PLATFORM_SERVICE_IDS');
88
+ expect(services).not.toHaveProperty('PLATFORM_APP_IDS');
89
89
  });
90
90
 
91
- it('maps every callable service back to an application catalog entry', () => {
91
+ it('maps every callable app API back to an application catalog entry', () => {
92
92
  const applicationServices = new Set<string>(
93
93
  Object.values(APPLICATIONS).flatMap((application) => [...application.services]),
94
94
  );
95
95
 
96
- for (const appId of Object.keys(DEFAULT_SERVICE_URLS)) {
96
+ for (const appId of Object.keys(DEFAULT_APP_URLS)) {
97
97
  expect(applicationServices.has(appId)).toBe(true);
98
98
  }
99
99
  });
@@ -133,8 +133,8 @@ describe('application catalog', () => {
133
133
  expect(Object.keys(APPLICATIONS).sort()).toEqual(EXPECTED_APPLICATION_IDS);
134
134
  });
135
135
 
136
- it('only references registered service IDs from application metadata', () => {
137
- const registeredServices = new Set<string>(Object.keys(DEFAULT_SERVICE_URLS));
136
+ it('only references registered app API ids from application metadata', () => {
137
+ const registeredServices = new Set<string>(Object.keys(DEFAULT_APP_URLS));
138
138
 
139
139
  for (const application of Object.values(APPLICATIONS)) {
140
140
  for (const appId of application.services) {
@@ -159,7 +159,7 @@ describe('application catalog', () => {
159
159
  ).toBeNull();
160
160
  // topolo-web-runtime (Astro, sites.topolo.app) deploys from its own wrangler
161
161
  // config and is intentionally listed as non-callable, so it can never be an
162
- // agent-callable service.
162
+ // agent-callable app API.
163
163
  expect(
164
164
  APPLICATIONS.web.deployTargets.find((target) => target.name === 'topolo-web-runtime')?.appId,
165
165
  ).toBeNull();
@@ -214,7 +214,7 @@ describe('application requirements', () => {
214
214
  expect(adminRequirementIds).toContain('shared-shell-and-launcher');
215
215
  expect(adminRequirementIds).not.toContain('organization-scoped-data');
216
216
  expect(mailRequirementIds).toContain('organization-scoped-data');
217
- expect(mailRequirementIds).toContain('service-registration-and-scopes');
217
+ expect(mailRequirementIds).toContain('app-registration-and-scopes');
218
218
  expect(mailRequirementIds).toContain('native-dashboard-widget');
219
219
  });
220
220
 
@@ -226,7 +226,7 @@ describe('application requirements', () => {
226
226
  expect(audit.application.id).toBe('mail');
227
227
  expect(audit.score.total).toBe(audit.findings.length);
228
228
  expect(byRequirement.get('platform-metadata')?.status).toBe('met');
229
- expect(byRequirement.get('service-registration-and-scopes')?.status).toBe('partial');
229
+ expect(byRequirement.get('app-registration-and-scopes')?.status).toBe('partial');
230
230
  expect(byRequirement.get('canonical-docs')?.status).toBe('needs_review');
231
231
  });
232
232
 
package/src/services.ts CHANGED
@@ -1,21 +1,21 @@
1
1
  /**
2
- * Service registry — base URLs for every Topolo platform service.
2
+ * App API registry — base URLs for callable Topolo platform APIs.
3
3
  *
4
4
  * Source of truth lives in each app's `topolo.cloudcontrol.json`. The
5
- * `SERVICES` map is regenerated from those files via
5
+ * `APP_API_URLS` map is regenerated from those files via
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 `TOPOLO_SERVICE_URL_MAIL` or `TOPOLO_SERVICE_URL_WEB_API`
10
- * (dots in service IDs become underscores in env vars).
9
+ * via env vars like `TOPOLO_APP_URL_MAIL` or `TOPOLO_APP_URL_WEB_API`
10
+ * (dots in app API keys become underscores in env vars).
11
11
  *
12
12
  * IMPORTANT: The SDK NEVER accepts `orgId` as a parameter. Every request is
13
13
  * scoped to the organization embedded in the auth credential (JWT claim or
14
14
  * API-key binding). This is the load-bearing cross-org isolation guarantee.
15
15
  */
16
16
  import {
17
- SERVICES,
18
- type ServiceId,
17
+ APP_API_URLS,
18
+ type AppApiId,
19
19
  } from './services.generated.js';
20
20
  import {
21
21
  APPLICATIONS,
@@ -40,12 +40,12 @@ export {
40
40
  type ApplicationRequirementsAuditReport,
41
41
  } from './application-requirements.js';
42
42
 
43
- export { SERVICES };
44
- export type { ServiceId };
43
+ export { APP_API_URLS };
44
+ export type { AppApiId };
45
45
  export { APPLICATIONS };
46
46
  export type { ApplicationCatalogEntry, ApplicationDeployTarget, ApplicationId };
47
47
 
48
- export const DEFAULT_SERVICE_URLS: Record<ServiceId, string> = SERVICES;
48
+ export const DEFAULT_APP_URLS: Record<AppApiId, string> = APP_API_URLS;
49
49
 
50
50
  export interface ServiceCatalogUrlEntry {
51
51
  appId?: string | null;
@@ -61,7 +61,7 @@ export function resolveCatalogServiceUrl(
61
61
  entry: ServiceCatalogUrlEntry,
62
62
  options: {
63
63
  serviceKey?: string;
64
- overrides?: Record<string, string> | Partial<Record<ServiceId, string>>;
64
+ overrides?: Record<string, string> | Partial<Record<AppApiId, string>>;
65
65
  } = {},
66
66
  ): string | null {
67
67
  const keys = catalogServiceKeyCandidates(entry, options.serviceKey);
@@ -75,13 +75,13 @@ export function resolveCatalogServiceUrl(
75
75
  }
76
76
 
77
77
  export function resolveServiceUrl(
78
- service: ServiceId,
79
- overrides?: Partial<Record<ServiceId, string>>,
78
+ service: AppApiId,
79
+ overrides?: Partial<Record<AppApiId, string>>,
80
80
  ): string {
81
81
  const override = resolveServiceUrlOverride([service], overrides);
82
82
  if (override) return override;
83
83
 
84
- return DEFAULT_SERVICE_URLS[service];
84
+ return DEFAULT_APP_URLS[service];
85
85
  }
86
86
 
87
87
  function catalogServiceKeyCandidates(entry: ServiceCatalogUrlEntry, preferred?: string): string[] {
@@ -110,19 +110,19 @@ function serviceKeyCandidates(value: string): string[] {
110
110
 
111
111
  function resolveServiceUrlOverride(
112
112
  keys: string[],
113
- overrides?: Record<string, string> | Partial<Record<ServiceId, string>>,
113
+ overrides?: Record<string, string> | Partial<Record<AppApiId, string>>,
114
114
  ): string | null {
115
115
  for (const key of keys) {
116
- const direct = overrides?.[key as ServiceId];
116
+ const direct = overrides?.[key as AppApiId];
117
117
  if (direct) return direct;
118
118
 
119
119
  const envStyleKey = key.replace(/\./g, '_');
120
- const envStyle = overrides?.[envStyleKey as ServiceId];
120
+ const envStyle = overrides?.[envStyleKey as AppApiId];
121
121
  if (envStyle) return envStyle;
122
122
  }
123
123
 
124
124
  for (const key of keys) {
125
- const envKey = `TOPOLO_SERVICE_URL_${key.replace(/\./g, '_').toUpperCase()}`;
125
+ const envKey = `TOPOLO_APP_URL_${key.replace(/\./g, '_').toUpperCase()}`;
126
126
  const envValue = typeof process !== 'undefined' ? process.env?.[envKey] : undefined;
127
127
  if (envValue) return envValue;
128
128
  }