@topolo/cli 0.1.34 → 0.2.2
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 +60 -19
- package/dist/index.js +1011 -246
- package/package.json +6 -2
- package/skills/codex/AGENTS.md +25 -11
- package/skills/topolo/SKILL.md +70 -27
package/dist/index.js
CHANGED
|
@@ -50,6 +50,12 @@ var TopoloPermissionError = class extends TopoloSdkError {
|
|
|
50
50
|
this.required = required;
|
|
51
51
|
}
|
|
52
52
|
};
|
|
53
|
+
var TopoloConfirmationRequiredError = class extends TopoloSdkError {
|
|
54
|
+
constructor(message) {
|
|
55
|
+
super("confirmation_required", message);
|
|
56
|
+
this.name = "TopoloConfirmationRequiredError";
|
|
57
|
+
}
|
|
58
|
+
};
|
|
53
59
|
var TopoloHttpError = class extends TopoloSdkError {
|
|
54
60
|
status;
|
|
55
61
|
body;
|
|
@@ -1112,6 +1118,7 @@ var APPLICATIONS = {
|
|
|
1112
1118
|
"topolo.io"
|
|
1113
1119
|
],
|
|
1114
1120
|
"routes": [
|
|
1121
|
+
"one.topolo.io",
|
|
1115
1122
|
"topolo.io",
|
|
1116
1123
|
"www.topolo.io"
|
|
1117
1124
|
],
|
|
@@ -2065,7 +2072,6 @@ var TOPOLO_RESOURCE_TYPE_HEADER = "X-Topolo-Resource-Type";
|
|
|
2065
2072
|
var TOPOLO_RESOURCE_ID_HEADER = "X-Topolo-Resource-ID";
|
|
2066
2073
|
var POSITIVE_CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
2067
2074
|
var NEGATIVE_CACHE_TTL_MS = 60 * 1e3;
|
|
2068
|
-
var CATALOG_CACHE_TTL_MS = 60 * 1e3;
|
|
2069
2075
|
var TopoloClient = class _TopoloClient {
|
|
2070
2076
|
credential;
|
|
2071
2077
|
agent;
|
|
@@ -2074,9 +2080,9 @@ var TopoloClient = class _TopoloClient {
|
|
|
2074
2080
|
requireConfirmForWrites;
|
|
2075
2081
|
timeoutMs;
|
|
2076
2082
|
fetchImpl;
|
|
2083
|
+
refreshCredential;
|
|
2084
|
+
refreshInFlight = null;
|
|
2077
2085
|
debug;
|
|
2078
|
-
serviceCatalogCache = null;
|
|
2079
|
-
actionCatalogCache = null;
|
|
2080
2086
|
/**
|
|
2081
2087
|
* Process-wide cache for slug → app-id lookups. Keyed by
|
|
2082
2088
|
* `${authBaseUrl}|${slug}` so multiple `TopoloClient` instances pointing at
|
|
@@ -2101,6 +2107,7 @@ var TopoloClient = class _TopoloClient {
|
|
|
2101
2107
|
this.requireConfirmForWrites = options.requireConfirmForWrites !== false;
|
|
2102
2108
|
this.timeoutMs = options.timeoutMs ?? 3e4;
|
|
2103
2109
|
this.fetchImpl = options.fetch ?? fetch;
|
|
2110
|
+
this.refreshCredential = options.refreshCredential;
|
|
2104
2111
|
this.debug = options.debug;
|
|
2105
2112
|
}
|
|
2106
2113
|
emit(event) {
|
|
@@ -2118,11 +2125,11 @@ var TopoloClient = class _TopoloClient {
|
|
|
2118
2125
|
async request(opts) {
|
|
2119
2126
|
const method = opts.method ?? "GET";
|
|
2120
2127
|
if (this.requireConfirmForWrites && WRITE_METHODS.has(method) && !opts.confirm) {
|
|
2121
|
-
throw new
|
|
2128
|
+
throw new TopoloConfirmationRequiredError(
|
|
2122
2129
|
`Mutating ${method} requests require { confirm: true }. This guardrail prevents agents from issuing writes without an explicit human-in-the-loop acknowledgement.`
|
|
2123
2130
|
);
|
|
2124
2131
|
}
|
|
2125
|
-
const resolvedService = await this.resolveRequestService(opts.service);
|
|
2132
|
+
const resolvedService = opts.baseUrl ? { serviceKey: normalizeLookupKey(opts.service), baseUrl: requireHttpOrigin(opts.baseUrl) } : await this.resolveRequestService(opts.service);
|
|
2126
2133
|
const baseUrl = resolvedService.baseUrl;
|
|
2127
2134
|
const url = new URL(opts.path, baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`);
|
|
2128
2135
|
if (opts.query) {
|
|
@@ -2153,20 +2160,36 @@ var TopoloClient = class _TopoloClient {
|
|
|
2153
2160
|
headers.set(TOPOLO_RESOURCE_TYPE_HEADER, resourceType);
|
|
2154
2161
|
headers.set(TOPOLO_RESOURCE_ID_HEADER, resourceId);
|
|
2155
2162
|
}
|
|
2156
|
-
const controller = new AbortController();
|
|
2157
|
-
const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
2158
|
-
const signal = opts.signal ? mergeSignals(opts.signal, controller.signal) : controller.signal;
|
|
2159
2163
|
const urlStr = url.toString();
|
|
2160
2164
|
const startedAt = Date.now();
|
|
2161
2165
|
this.emit({ phase: "request", method, service: resolvedService.serviceKey, path: opts.path, url: urlStr, requestId });
|
|
2166
|
+
const fetchOnce = async () => {
|
|
2167
|
+
const controller = new AbortController();
|
|
2168
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
2169
|
+
const signal = opts.signal ? mergeSignals(opts.signal, controller.signal) : controller.signal;
|
|
2170
|
+
try {
|
|
2171
|
+
return await this.fetchImpl(urlStr, {
|
|
2172
|
+
method,
|
|
2173
|
+
headers: new Headers(headers),
|
|
2174
|
+
body: opts.body !== void 0 ? JSON.stringify(opts.body) : null,
|
|
2175
|
+
signal
|
|
2176
|
+
});
|
|
2177
|
+
} finally {
|
|
2178
|
+
clearTimeout(timeoutId);
|
|
2179
|
+
}
|
|
2180
|
+
};
|
|
2162
2181
|
let res;
|
|
2163
2182
|
try {
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2183
|
+
const attemptedCredential = this.credential;
|
|
2184
|
+
res = await fetchOnce();
|
|
2185
|
+
if (res.status === 401 && attemptedCredential.kind === "access_token" && this.refreshCredential) {
|
|
2186
|
+
await res.body?.cancel().catch(() => void 0);
|
|
2187
|
+
const refreshed = await this.refreshRejectedAccessToken(attemptedCredential.accessToken);
|
|
2188
|
+
if (refreshed) {
|
|
2189
|
+
applyAuthHeaders(headers, refreshed);
|
|
2190
|
+
res = await fetchOnce();
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2170
2193
|
} catch (err) {
|
|
2171
2194
|
this.emit({
|
|
2172
2195
|
phase: "error",
|
|
@@ -2179,8 +2202,6 @@ var TopoloClient = class _TopoloClient {
|
|
|
2179
2202
|
error: err instanceof Error ? err.message : String(err)
|
|
2180
2203
|
});
|
|
2181
2204
|
throw err;
|
|
2182
|
-
} finally {
|
|
2183
|
-
clearTimeout(timeoutId);
|
|
2184
2205
|
}
|
|
2185
2206
|
const contentType = res.headers.get("Content-Type") ?? "";
|
|
2186
2207
|
const parsed = contentType.includes("application/json") ? await res.json().catch(() => null) : await res.text().catch(() => null);
|
|
@@ -2219,6 +2240,19 @@ var TopoloClient = class _TopoloClient {
|
|
|
2219
2240
|
});
|
|
2220
2241
|
return parsed;
|
|
2221
2242
|
}
|
|
2243
|
+
async refreshRejectedAccessToken(attemptedAccessToken) {
|
|
2244
|
+
if (this.credential.kind === "access_token" && this.credential.accessToken !== attemptedAccessToken) {
|
|
2245
|
+
return this.credential;
|
|
2246
|
+
}
|
|
2247
|
+
if (!this.refreshCredential) return null;
|
|
2248
|
+
this.refreshInFlight ??= this.refreshCredential().then((credential) => {
|
|
2249
|
+
if (credential) this.credential = credential;
|
|
2250
|
+
return credential;
|
|
2251
|
+
}).catch(() => null).finally(() => {
|
|
2252
|
+
this.refreshInFlight = null;
|
|
2253
|
+
});
|
|
2254
|
+
return this.refreshInFlight;
|
|
2255
|
+
}
|
|
2222
2256
|
async resolveRequestService(service) {
|
|
2223
2257
|
const serviceKey = normalizeLookupKey(service);
|
|
2224
2258
|
if (!serviceKey) {
|
|
@@ -2244,9 +2278,8 @@ var TopoloClient = class _TopoloClient {
|
|
|
2244
2278
|
);
|
|
2245
2279
|
}
|
|
2246
2280
|
async resolveLiveCallableService(serviceKey) {
|
|
2247
|
-
const
|
|
2248
|
-
|
|
2249
|
-
if (!match) return null;
|
|
2281
|
+
const match = await this.getService(serviceKey).catch(() => null);
|
|
2282
|
+
if (!match || !catalogEntryMatches(match, serviceKey)) return null;
|
|
2250
2283
|
const baseUrl = resolveCatalogServiceUrl(match, {
|
|
2251
2284
|
serviceKey,
|
|
2252
2285
|
overrides: this.serviceUrls
|
|
@@ -2320,12 +2353,13 @@ var TopoloClient = class _TopoloClient {
|
|
|
2320
2353
|
* The live Auth catalog is the authority because it is scoped to the caller's
|
|
2321
2354
|
* organization and effective permissions.
|
|
2322
2355
|
*/
|
|
2323
|
-
async
|
|
2324
|
-
const
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2356
|
+
async searchServices(options = {}) {
|
|
2357
|
+
const res = await this.request({
|
|
2358
|
+
service: "developers",
|
|
2359
|
+
method: "GET",
|
|
2360
|
+
path: "/api/services/catalog",
|
|
2361
|
+
query: catalogSearchQuery(options)
|
|
2362
|
+
});
|
|
2329
2363
|
const raw = res.data?.services ?? res.services;
|
|
2330
2364
|
if (!Array.isArray(raw)) {
|
|
2331
2365
|
throw new TopoloSdkError(
|
|
@@ -2335,6 +2369,7 @@ var TopoloClient = class _TopoloClient {
|
|
|
2335
2369
|
}
|
|
2336
2370
|
const entries = [];
|
|
2337
2371
|
for (const item of raw) {
|
|
2372
|
+
if (!isRecord(item)) continue;
|
|
2338
2373
|
const s = item;
|
|
2339
2374
|
const appId = typeof s.app_id === "string" ? s.app_id : typeof s.id === "string" ? s.id : null;
|
|
2340
2375
|
if (!appId) continue;
|
|
@@ -2359,11 +2394,30 @@ var TopoloClient = class _TopoloClient {
|
|
|
2359
2394
|
apiKeyResources
|
|
2360
2395
|
});
|
|
2361
2396
|
}
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
};
|
|
2366
|
-
|
|
2397
|
+
const page = normalizeCatalogPage(
|
|
2398
|
+
res.data?.page ?? res.page
|
|
2399
|
+
);
|
|
2400
|
+
return { services: entries, page };
|
|
2401
|
+
}
|
|
2402
|
+
/** @deprecated Prefer searchServices() so callers retain the continuation cursor. */
|
|
2403
|
+
async listServices(options = {}) {
|
|
2404
|
+
return (await this.searchServices(options)).services;
|
|
2405
|
+
}
|
|
2406
|
+
async getService(application) {
|
|
2407
|
+
const needle = application.trim();
|
|
2408
|
+
if (!needle) throw new TopoloSdkError("unknown_service", "application is required");
|
|
2409
|
+
const res = await this.request({
|
|
2410
|
+
service: "developers",
|
|
2411
|
+
method: "GET",
|
|
2412
|
+
path: "/api/services/catalog",
|
|
2413
|
+
query: { application: needle }
|
|
2414
|
+
});
|
|
2415
|
+
const raw = res.data?.application ?? res.application;
|
|
2416
|
+
const entry = normalizeServiceCatalogEntry(raw);
|
|
2417
|
+
if (!entry) {
|
|
2418
|
+
throw new TopoloSdkError("unknown_service", `Unknown Topolo application "${application}".`);
|
|
2419
|
+
}
|
|
2420
|
+
return entry;
|
|
2367
2421
|
}
|
|
2368
2422
|
/**
|
|
2369
2423
|
* List callable actions from the live registry-canonical catalog (definitions
|
|
@@ -2371,16 +2425,15 @@ var TopoloClient = class _TopoloClient {
|
|
|
2371
2425
|
* caller's organization and effective permissions; no bundled/static app
|
|
2372
2426
|
* action list is used as an authorization source.
|
|
2373
2427
|
*/
|
|
2374
|
-
async
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
return this.actionCatalogCache.entries;
|
|
2428
|
+
async searchActions(options) {
|
|
2429
|
+
if (!options.service?.trim()) {
|
|
2430
|
+
throw new TopoloSdkError("service_required", "service is required for bounded action discovery");
|
|
2378
2431
|
}
|
|
2379
2432
|
const res = await this.request({
|
|
2380
2433
|
service: "developers",
|
|
2381
2434
|
method: "GET",
|
|
2382
2435
|
path: "/api/actions/catalog",
|
|
2383
|
-
|
|
2436
|
+
query: { service: options.service, ...catalogSearchQuery(options) }
|
|
2384
2437
|
});
|
|
2385
2438
|
const raw = res.data?.actions ?? res.actions;
|
|
2386
2439
|
if (!Array.isArray(raw)) {
|
|
@@ -2390,36 +2443,96 @@ var TopoloClient = class _TopoloClient {
|
|
|
2390
2443
|
);
|
|
2391
2444
|
}
|
|
2392
2445
|
const entries = raw.map((item) => normalizeActionCatalogEntry(item)).filter((entry) => Boolean(entry));
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2446
|
+
return {
|
|
2447
|
+
actions: entries,
|
|
2448
|
+
page: normalizeCatalogPage(
|
|
2449
|
+
res.data?.page ?? res.page
|
|
2450
|
+
)
|
|
2451
|
+
};
|
|
2452
|
+
}
|
|
2453
|
+
async getActionCatalogSummary(service) {
|
|
2454
|
+
const boundedService = service.trim();
|
|
2455
|
+
if (!boundedService) {
|
|
2456
|
+
throw new TopoloSdkError("service_required", "service is required for bounded action discovery");
|
|
2398
2457
|
}
|
|
2399
|
-
|
|
2458
|
+
const res = await this.request({
|
|
2459
|
+
service: "developers",
|
|
2460
|
+
method: "GET",
|
|
2461
|
+
path: "/api/actions/catalog",
|
|
2462
|
+
query: { service: boundedService, summary: "true" }
|
|
2463
|
+
});
|
|
2464
|
+
const raw = res.data?.summary ?? res.summary;
|
|
2465
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
2466
|
+
throw new TopoloSdkError("action_catalog_summary_unavailable", "Action catalog summary is unavailable.");
|
|
2467
|
+
}
|
|
2468
|
+
const record = raw;
|
|
2469
|
+
if (record.algorithm !== "sha256-v1" || typeof record.digest !== "string" || !/^[a-f0-9]{64}$/.test(record.digest) || !Number.isSafeInteger(record.actionCount) || Number(record.actionCount) < 0) {
|
|
2470
|
+
throw new TopoloSdkError("action_catalog_summary_unavailable", "Action catalog summary is invalid.");
|
|
2471
|
+
}
|
|
2472
|
+
return {
|
|
2473
|
+
algorithm: "sha256-v1",
|
|
2474
|
+
digest: record.digest,
|
|
2475
|
+
actionCount: Number(record.actionCount)
|
|
2476
|
+
};
|
|
2477
|
+
}
|
|
2478
|
+
/** @deprecated Prefer searchActions() so callers retain the continuation cursor. */
|
|
2479
|
+
async listActions(options) {
|
|
2480
|
+
if (!options) {
|
|
2481
|
+
throw new TopoloSdkError("service_required", "service is required for bounded action discovery");
|
|
2482
|
+
}
|
|
2483
|
+
return (await this.searchActions(options)).actions;
|
|
2400
2484
|
}
|
|
2401
2485
|
async getAction(action) {
|
|
2402
|
-
const
|
|
2403
|
-
|
|
2404
|
-
const match = actions.find((entry) => actionEntryMatches(entry, needle));
|
|
2405
|
-
if (!match) {
|
|
2486
|
+
const actionId = action.trim();
|
|
2487
|
+
if (!actionId) {
|
|
2406
2488
|
throw new TopoloSdkError(
|
|
2407
2489
|
"unknown_action",
|
|
2408
|
-
|
|
2490
|
+
"action id is required"
|
|
2409
2491
|
);
|
|
2410
2492
|
}
|
|
2493
|
+
let res;
|
|
2494
|
+
try {
|
|
2495
|
+
res = await this.request({
|
|
2496
|
+
service: "developers",
|
|
2497
|
+
method: "GET",
|
|
2498
|
+
path: "/api/actions/catalog",
|
|
2499
|
+
query: { actionId }
|
|
2500
|
+
});
|
|
2501
|
+
} catch (error) {
|
|
2502
|
+
if (error instanceof TopoloHttpError && error.status === 404) {
|
|
2503
|
+
throw new TopoloSdkError("unknown_action", `Unknown or unavailable Topolo action "${action}".`);
|
|
2504
|
+
}
|
|
2505
|
+
throw error;
|
|
2506
|
+
}
|
|
2507
|
+
const raw = res.data?.action ?? res.action;
|
|
2508
|
+
const match = normalizeActionCatalogEntry(raw);
|
|
2509
|
+
if (!match) throw new TopoloSdkError("unknown_action", `Unknown or unavailable Topolo action "${action}".`);
|
|
2411
2510
|
return match;
|
|
2412
2511
|
}
|
|
2413
2512
|
async callAction(action, input2 = {}, options = {}) {
|
|
2414
2513
|
const entry = await this.getAction(action);
|
|
2514
|
+
const agentAccess = entry.agentAccess ?? (entry.destructive ? "off" : entry.readOnly && !entry.requiresConfirmation ? "auto" : "confirm");
|
|
2515
|
+
if (agentAccess === "off") {
|
|
2516
|
+
throw new TopoloPermissionError(
|
|
2517
|
+
`Topolo action "${entry.actionId}" is not available for agent invocation.`,
|
|
2518
|
+
entry.requiredPermission ? [entry.requiredPermission] : []
|
|
2519
|
+
);
|
|
2520
|
+
}
|
|
2521
|
+
if ((agentAccess === "confirm" || entry.requiresConfirmation || entry.destructive) && options.confirm !== true) {
|
|
2522
|
+
throw new TopoloConfirmationRequiredError(
|
|
2523
|
+
`Topolo action "${entry.actionId}" requires { confirm: true } before invocation.`
|
|
2524
|
+
);
|
|
2525
|
+
}
|
|
2415
2526
|
const mapped = mapActionInput(entry, input2);
|
|
2416
2527
|
const service = entry.appSlug || entry.appId;
|
|
2417
2528
|
return this.request({
|
|
2418
2529
|
service,
|
|
2530
|
+
...entry.apiBaseUrl ? { baseUrl: entry.apiBaseUrl } : {},
|
|
2419
2531
|
method: entry.method,
|
|
2420
2532
|
path: mapped.path,
|
|
2421
2533
|
...mapped.query ? { query: mapped.query } : {},
|
|
2422
2534
|
...mapped.body !== void 0 ? { body: mapped.body } : {},
|
|
2535
|
+
...options.resource ? { resource: options.resource } : {},
|
|
2423
2536
|
headers: {
|
|
2424
2537
|
[TOPOLO_ACTION_ID_HEADER]: entry.actionId,
|
|
2425
2538
|
[TOPOLO_ACTION_NAME_HEADER]: entry.name
|
|
@@ -2557,6 +2670,18 @@ function serviceKeyCandidates2(slug) {
|
|
|
2557
2670
|
function normalizeLookupKey(value) {
|
|
2558
2671
|
return String(value || "").trim().toLowerCase().replace(/\s+/g, "-");
|
|
2559
2672
|
}
|
|
2673
|
+
function requireHttpOrigin(value) {
|
|
2674
|
+
let url;
|
|
2675
|
+
try {
|
|
2676
|
+
url = new URL(value);
|
|
2677
|
+
} catch {
|
|
2678
|
+
throw new TopoloSdkError("invalid_action_target", `Invalid action API origin "${value}".`);
|
|
2679
|
+
}
|
|
2680
|
+
if (url.protocol !== "https:" && url.protocol !== "http:" || url.username || url.password) {
|
|
2681
|
+
throw new TopoloSdkError("invalid_action_target", `Invalid action API origin "${value}".`);
|
|
2682
|
+
}
|
|
2683
|
+
return url.origin;
|
|
2684
|
+
}
|
|
2560
2685
|
function deriveDevelopersFromAuth(authUrl) {
|
|
2561
2686
|
if (!authUrl) return null;
|
|
2562
2687
|
try {
|
|
@@ -2583,6 +2708,47 @@ function catalogEntryLookupKeys(entry) {
|
|
|
2583
2708
|
}
|
|
2584
2709
|
return [...keys];
|
|
2585
2710
|
}
|
|
2711
|
+
function catalogSearchQuery(options) {
|
|
2712
|
+
const query = {};
|
|
2713
|
+
if (typeof options.query === "string" && options.query.trim()) query.query = options.query.trim();
|
|
2714
|
+
if (typeof options.cursor === "string" && options.cursor.trim()) query.cursor = options.cursor.trim();
|
|
2715
|
+
if (typeof options.limit === "number" && Number.isFinite(options.limit)) query.limit = Math.trunc(options.limit);
|
|
2716
|
+
return query;
|
|
2717
|
+
}
|
|
2718
|
+
function normalizeCatalogPage(value) {
|
|
2719
|
+
if (!isRecord(value)) return { nextCursor: null, hasMore: false };
|
|
2720
|
+
return {
|
|
2721
|
+
nextCursor: typeof value.nextCursor === "string" && value.nextCursor ? value.nextCursor : null,
|
|
2722
|
+
hasMore: value.hasMore === true
|
|
2723
|
+
};
|
|
2724
|
+
}
|
|
2725
|
+
function normalizeServiceCatalogEntry(item) {
|
|
2726
|
+
if (!isRecord(item)) return null;
|
|
2727
|
+
const appId = typeof item.app_id === "string" ? item.app_id : typeof item.id === "string" ? item.id : null;
|
|
2728
|
+
if (!appId) return null;
|
|
2729
|
+
const permissions = Array.isArray(item.granted_permissions) ? item.granted_permissions.filter(
|
|
2730
|
+
(permission) => typeof permission === "string" && permission.trim().length > 0
|
|
2731
|
+
) : [];
|
|
2732
|
+
const catalogApiKeyResources = normalizeApiKeyResourceMetadataList(
|
|
2733
|
+
item.api_key_resources ?? item.apiKeyResources
|
|
2734
|
+
);
|
|
2735
|
+
const apiKeyResource = normalizeApiKeyResourceMetadata(item.api_key_resource ?? item.apiKeyResource) ?? catalogApiKeyResources[0] ?? null;
|
|
2736
|
+
const apiKeyResources = catalogApiKeyResources.length > 0 ? catalogApiKeyResources : apiKeyResource ? [apiKeyResource] : [];
|
|
2737
|
+
return {
|
|
2738
|
+
appId,
|
|
2739
|
+
slug: typeof item.app_slug === "string" ? item.app_slug : null,
|
|
2740
|
+
name: typeof item.name === "string" ? item.name : null,
|
|
2741
|
+
description: typeof item.description === "string" ? item.description : null,
|
|
2742
|
+
apiBaseUrl: typeof item.api_base_url === "string" ? item.api_base_url : typeof item.apiBaseUrl === "string" ? item.apiBaseUrl : null,
|
|
2743
|
+
launchUrl: typeof item.launch_url === "string" ? item.launch_url : typeof item.launchUrl === "string" ? item.launchUrl : null,
|
|
2744
|
+
status: typeof item.status === "string" ? item.status : null,
|
|
2745
|
+
surfaceKind: typeof item.surface_kind === "string" ? item.surface_kind : null,
|
|
2746
|
+
launchable: item.launchable === true,
|
|
2747
|
+
permissions,
|
|
2748
|
+
apiKeyResource,
|
|
2749
|
+
apiKeyResources
|
|
2750
|
+
};
|
|
2751
|
+
}
|
|
2586
2752
|
function normalizeActionCatalogEntry(item) {
|
|
2587
2753
|
if (!item || typeof item !== "object" || Array.isArray(item)) return null;
|
|
2588
2754
|
const a = item;
|
|
@@ -2596,6 +2762,8 @@ function normalizeActionCatalogEntry(item) {
|
|
|
2596
2762
|
}
|
|
2597
2763
|
return {
|
|
2598
2764
|
actionId,
|
|
2765
|
+
target: pickActionString(a.target_key) ?? pickActionString(a.target) ?? null,
|
|
2766
|
+
apiBaseUrl: pickActionString(a.api_base_url) ?? pickActionString(a.apiBaseUrl) ?? null,
|
|
2599
2767
|
name,
|
|
2600
2768
|
toolName: pickActionString(a.tool_name) ?? pickActionString(a.toolName) ?? null,
|
|
2601
2769
|
title: pickActionString(a.title) ?? name,
|
|
@@ -2607,6 +2775,7 @@ function normalizeActionCatalogEntry(item) {
|
|
|
2607
2775
|
path: path4,
|
|
2608
2776
|
permissionName: pickActionString(a.permission_name) ?? pickActionString(a.permissionName) ?? "",
|
|
2609
2777
|
requiredPermission: pickActionString(a.required_permission) ?? pickActionString(a.requiredPermission) ?? "",
|
|
2778
|
+
authorizationMode: pickAuthorizationMode(a.authorization_mode ?? a.authorizationMode),
|
|
2610
2779
|
inputSchema: plainObject(a.input_schema ?? a.inputSchema),
|
|
2611
2780
|
outputSchema: plainObject(a.output_schema ?? a.outputSchema),
|
|
2612
2781
|
readOnly: Boolean(a.read_only ?? a.readOnly),
|
|
@@ -2626,16 +2795,12 @@ function pickActionString(value) {
|
|
|
2626
2795
|
function pickAgentAccess(value) {
|
|
2627
2796
|
return value === "auto" || value === "confirm" || value === "off" ? value : null;
|
|
2628
2797
|
}
|
|
2798
|
+
function pickAuthorizationMode(value) {
|
|
2799
|
+
return value === "runtime" ? "runtime" : "static";
|
|
2800
|
+
}
|
|
2629
2801
|
function plainObject(value) {
|
|
2630
2802
|
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
2631
2803
|
}
|
|
2632
|
-
function normalizeActionLookupKey(value) {
|
|
2633
|
-
return String(value || "").trim().toLowerCase();
|
|
2634
|
-
}
|
|
2635
|
-
function actionEntryMatches(entry, needle) {
|
|
2636
|
-
if (!needle) return false;
|
|
2637
|
-
return [entry.actionId, entry.name, entry.toolName].filter((value) => typeof value === "string" && value.length > 0).some((value) => value.toLowerCase() === needle);
|
|
2638
|
-
}
|
|
2639
2804
|
function mapActionInput(action, input2) {
|
|
2640
2805
|
const remaining = { ...input2 || {} };
|
|
2641
2806
|
const path4 = action.path.replace(/\{([a-zA-Z0-9_]+)\}/g, (_match, key) => {
|
|
@@ -3039,6 +3204,9 @@ var TopoloOAuth2 = class {
|
|
|
3039
3204
|
return parsed;
|
|
3040
3205
|
}
|
|
3041
3206
|
};
|
|
3207
|
+
function contextKey(contextType, organizationId) {
|
|
3208
|
+
return contextType === "organization" && organizationId ? `org:${organizationId}` : "personal";
|
|
3209
|
+
}
|
|
3042
3210
|
var ENVIRONMENT_DEFAULT_SERVICE_URLS = {
|
|
3043
3211
|
production: {
|
|
3044
3212
|
developers: "https://developers.topolo.app"
|
|
@@ -3132,6 +3300,18 @@ function writeProfileConfig(config, environment, profile) {
|
|
|
3132
3300
|
};
|
|
3133
3301
|
return { ...config, profiles };
|
|
3134
3302
|
}
|
|
3303
|
+
function writeContextCredential(config, environment, ctx, makeActive = true) {
|
|
3304
|
+
const key = contextKey(ctx.contextType, ctx.organizationId);
|
|
3305
|
+
const profile = profileForEnvironment(config, environment);
|
|
3306
|
+
const contexts = { ...profile.contexts ?? {}, [key]: ctx };
|
|
3307
|
+
const next = { contexts };
|
|
3308
|
+
if (makeActive) {
|
|
3309
|
+
next.activeContext = key;
|
|
3310
|
+
next.credential = ctx.credential;
|
|
3311
|
+
next.oauthClientId = ctx.oauthClientId ?? profile.oauthClientId;
|
|
3312
|
+
}
|
|
3313
|
+
return writeProfileConfig(config, environment, next);
|
|
3314
|
+
}
|
|
3135
3315
|
function clearProfileCredential(config, environment) {
|
|
3136
3316
|
const next = { ...config };
|
|
3137
3317
|
if (environment === "production") {
|
|
@@ -3163,7 +3343,7 @@ async function resolveCredential(environment = selectedEnvironment()) {
|
|
|
3163
3343
|
return profileForEnvironment(config, environment).credential ?? null;
|
|
3164
3344
|
}
|
|
3165
3345
|
var REFRESH_SKEW_SECONDS = 60;
|
|
3166
|
-
async function refreshCredentialIfNeeded(environment = selectedEnvironment()) {
|
|
3346
|
+
async function refreshCredentialIfNeeded(environment = selectedEnvironment(), options = {}) {
|
|
3167
3347
|
const prefix = envVarPrefix(environment);
|
|
3168
3348
|
if (process.env[`${prefix}_API_KEY`] || process.env[`${prefix}_ACCESS_TOKEN`] || process.env.TOPOLO_API_KEY || process.env.TOPOLO_ACCESS_TOKEN) {
|
|
3169
3349
|
return resolveCredential(environment);
|
|
@@ -3174,7 +3354,7 @@ async function refreshCredentialIfNeeded(environment = selectedEnvironment()) {
|
|
|
3174
3354
|
if (!cred || cred.kind !== "access_token") return cred ?? null;
|
|
3175
3355
|
if (!cred.refreshToken || !cred.expiresAt || !profile.oauthClientId) return cred;
|
|
3176
3356
|
const now = Math.floor(Date.now() / 1e3);
|
|
3177
|
-
if (cred.expiresAt - REFRESH_SKEW_SECONDS > now) return cred;
|
|
3357
|
+
if (!options.force && cred.expiresAt - REFRESH_SKEW_SECONDS > now) return cred;
|
|
3178
3358
|
try {
|
|
3179
3359
|
const oauth = new TopoloOAuth2({
|
|
3180
3360
|
serviceUrls: resolveServiceUrls(profile.serviceUrls, environment)
|
|
@@ -3189,9 +3369,20 @@ async function refreshCredentialIfNeeded(environment = selectedEnvironment()) {
|
|
|
3189
3369
|
...tokens.refresh_token ? { refreshToken: tokens.refresh_token } : { refreshToken: cred.refreshToken },
|
|
3190
3370
|
expiresAt: Math.floor(Date.now() / 1e3) + tokens.expires_in
|
|
3191
3371
|
};
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3372
|
+
const activeContext = profile.activeContext;
|
|
3373
|
+
const activeEntry = activeContext ? profile.contexts?.[activeContext] : void 0;
|
|
3374
|
+
const updated = activeContext && activeEntry ? writeProfileConfig(config, environment, {
|
|
3375
|
+
credential: refreshed,
|
|
3376
|
+
contexts: {
|
|
3377
|
+
...profile.contexts ?? {},
|
|
3378
|
+
[activeContext]: {
|
|
3379
|
+
...activeEntry,
|
|
3380
|
+
credential: refreshed,
|
|
3381
|
+
oauthClientId: activeEntry.oauthClientId ?? profile.oauthClientId
|
|
3382
|
+
}
|
|
3383
|
+
}
|
|
3384
|
+
}) : environment === "production" && !config.profiles?.production ? { ...config, credential: refreshed } : writeProfileConfig(config, environment, { credential: refreshed });
|
|
3385
|
+
await writeConfig(updated);
|
|
3195
3386
|
return refreshed;
|
|
3196
3387
|
} catch {
|
|
3197
3388
|
return cred;
|
|
@@ -3243,7 +3434,14 @@ function oauthClientRegistrationUrl(environment = selectedEnvironment()) {
|
|
|
3243
3434
|
function apiKeyIssuanceUrl(environment = selectedEnvironment()) {
|
|
3244
3435
|
return `${developerConsoleUrl(environment)}/credentials`;
|
|
3245
3436
|
}
|
|
3246
|
-
var DEFAULT_CLI_CLIENT_IDS = {
|
|
3437
|
+
var DEFAULT_CLI_CLIENT_IDS = {
|
|
3438
|
+
// First-party `topolo-cli` device-flow client, seeded per environment by Auth
|
|
3439
|
+
// (core/TopoloAuth/scripts/seed-first-party-oauth-clients.sql). Each env's
|
|
3440
|
+
// Auth must have this client before the CLI ships pointing at it.
|
|
3441
|
+
production: "topolo-cli",
|
|
3442
|
+
staging: "topolo-cli",
|
|
3443
|
+
development: "topolo-cli"
|
|
3444
|
+
};
|
|
3247
3445
|
function resolveOAuthClientId(config, environment = selectedEnvironment()) {
|
|
3248
3446
|
const envSpecific = process.env[`${envVarPrefix(environment)}_CLI_CLIENT_ID`];
|
|
3249
3447
|
if (envSpecific) return envSpecific;
|
|
@@ -3301,18 +3499,43 @@ function normalizeError(err) {
|
|
|
3301
3499
|
const code = err.code;
|
|
3302
3500
|
const hintMap = {
|
|
3303
3501
|
auth_error: "Run `topolo auth login` or set TOPOLO_API_KEY.",
|
|
3502
|
+
confirmation_required: "Review the action and rerun with --confirm to allow it.",
|
|
3304
3503
|
permission_denied: "Check your API key scopes or role assignments in TopoloDevelopers."
|
|
3305
3504
|
};
|
|
3306
3505
|
return {
|
|
3307
3506
|
message: err.message,
|
|
3308
3507
|
code,
|
|
3309
3508
|
hint: code ? hintMap[code] : void 0,
|
|
3310
|
-
exitCode: code === "auth_error" ? 2 : code === "permission_denied" ? 3 : 1
|
|
3509
|
+
exitCode: code === "auth_error" || code === "confirmation_required" ? 2 : code === "permission_denied" ? 3 : 1
|
|
3311
3510
|
};
|
|
3312
3511
|
}
|
|
3313
3512
|
return { message: String(err), exitCode: 1 };
|
|
3314
3513
|
}
|
|
3315
3514
|
|
|
3515
|
+
// src/version.ts
|
|
3516
|
+
import { readFileSync } from "fs";
|
|
3517
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
3518
|
+
import { fileURLToPath } from "url";
|
|
3519
|
+
function readCliVersion() {
|
|
3520
|
+
try {
|
|
3521
|
+
const here = dirname2(fileURLToPath(import.meta.url));
|
|
3522
|
+
const candidates = [
|
|
3523
|
+
join2(here, "package.json"),
|
|
3524
|
+
join2(here, "..", "package.json")
|
|
3525
|
+
];
|
|
3526
|
+
for (const path4 of candidates) {
|
|
3527
|
+
try {
|
|
3528
|
+
const pkg = JSON.parse(readFileSync(path4, "utf8"));
|
|
3529
|
+
if (typeof pkg.version === "string" && pkg.version) return pkg.version;
|
|
3530
|
+
} catch {
|
|
3531
|
+
}
|
|
3532
|
+
}
|
|
3533
|
+
} catch {
|
|
3534
|
+
}
|
|
3535
|
+
return "0.0.0";
|
|
3536
|
+
}
|
|
3537
|
+
var CLI_VERSION = readCliVersion();
|
|
3538
|
+
|
|
3316
3539
|
// src/commands/auth.ts
|
|
3317
3540
|
function registerAuthCommands(program2) {
|
|
3318
3541
|
const auth = program2.command("auth").description("Manage Topolo credentials");
|
|
@@ -3450,18 +3673,15 @@ async function runDeviceFlow(options) {
|
|
|
3450
3673
|
} finally {
|
|
3451
3674
|
spinner?.stop();
|
|
3452
3675
|
}
|
|
3676
|
+
const credential = {
|
|
3677
|
+
kind: "access_token",
|
|
3678
|
+
accessToken: token.access_token,
|
|
3679
|
+
...token.refresh_token ? { refreshToken: token.refresh_token } : {},
|
|
3680
|
+
expiresAt: Math.floor(Date.now() / 1e3) + token.expires_in
|
|
3681
|
+
};
|
|
3682
|
+
const context = await resolveLoginContext(credential, resolved.serviceUrls, options.clientId);
|
|
3453
3683
|
const updated = await readConfig();
|
|
3454
|
-
await writeConfig(
|
|
3455
|
-
writeProfileConfig(updated, options.environment, {
|
|
3456
|
-
credential: {
|
|
3457
|
-
kind: "access_token",
|
|
3458
|
-
accessToken: token.access_token,
|
|
3459
|
-
...token.refresh_token ? { refreshToken: token.refresh_token } : {},
|
|
3460
|
-
expiresAt: Math.floor(Date.now() / 1e3) + token.expires_in
|
|
3461
|
-
},
|
|
3462
|
-
oauthClientId: options.clientId
|
|
3463
|
-
})
|
|
3464
|
-
);
|
|
3684
|
+
await writeConfig(writeContextCredential(updated, options.environment, context));
|
|
3465
3685
|
emit(
|
|
3466
3686
|
{
|
|
3467
3687
|
ok: true,
|
|
@@ -3476,6 +3696,29 @@ async function runDeviceFlow(options) {
|
|
|
3476
3696
|
() => "\n \u2713 Signed in. Run `topolo whoami` to see your identity."
|
|
3477
3697
|
);
|
|
3478
3698
|
}
|
|
3699
|
+
async function resolveLoginContext(credential, serviceUrls, clientId) {
|
|
3700
|
+
try {
|
|
3701
|
+
const topolo = createTopolo({
|
|
3702
|
+
credential,
|
|
3703
|
+
agent: { clientName: "topolo-cli", clientVersion: CLI_VERSION },
|
|
3704
|
+
serviceUrls
|
|
3705
|
+
});
|
|
3706
|
+
const me = await topolo.identity.whoami();
|
|
3707
|
+
const org = me.organization;
|
|
3708
|
+
if (org?.id) {
|
|
3709
|
+
return {
|
|
3710
|
+
contextType: "organization",
|
|
3711
|
+
organizationId: org.id,
|
|
3712
|
+
organizationSlug: org.slug,
|
|
3713
|
+
organizationName: org.name,
|
|
3714
|
+
credential,
|
|
3715
|
+
oauthClientId: clientId
|
|
3716
|
+
};
|
|
3717
|
+
}
|
|
3718
|
+
} catch {
|
|
3719
|
+
}
|
|
3720
|
+
return { contextType: "personal", credential, oauthClientId: clientId };
|
|
3721
|
+
}
|
|
3479
3722
|
function tryOpenBrowser(url) {
|
|
3480
3723
|
try {
|
|
3481
3724
|
const os = platform2();
|
|
@@ -3540,30 +3783,6 @@ function sleep(ms) {
|
|
|
3540
3783
|
return new Promise((resolve4) => setTimeout(resolve4, ms));
|
|
3541
3784
|
}
|
|
3542
3785
|
|
|
3543
|
-
// src/version.ts
|
|
3544
|
-
import { readFileSync } from "fs";
|
|
3545
|
-
import { dirname as dirname2, join as join2 } from "path";
|
|
3546
|
-
import { fileURLToPath } from "url";
|
|
3547
|
-
function readCliVersion() {
|
|
3548
|
-
try {
|
|
3549
|
-
const here = dirname2(fileURLToPath(import.meta.url));
|
|
3550
|
-
const candidates = [
|
|
3551
|
-
join2(here, "package.json"),
|
|
3552
|
-
join2(here, "..", "package.json")
|
|
3553
|
-
];
|
|
3554
|
-
for (const path4 of candidates) {
|
|
3555
|
-
try {
|
|
3556
|
-
const pkg = JSON.parse(readFileSync(path4, "utf8"));
|
|
3557
|
-
if (typeof pkg.version === "string" && pkg.version) return pkg.version;
|
|
3558
|
-
} catch {
|
|
3559
|
-
}
|
|
3560
|
-
}
|
|
3561
|
-
} catch {
|
|
3562
|
-
}
|
|
3563
|
-
return "0.0.0";
|
|
3564
|
-
}
|
|
3565
|
-
var CLI_VERSION = readCliVersion();
|
|
3566
|
-
|
|
3567
3786
|
// src/client.ts
|
|
3568
3787
|
async function buildClient(options) {
|
|
3569
3788
|
const environment = selectedEnvironment();
|
|
@@ -3580,6 +3799,7 @@ async function buildClient(options) {
|
|
|
3580
3799
|
},
|
|
3581
3800
|
serviceUrls: profile.serviceUrls,
|
|
3582
3801
|
requireConfirmForWrites: options?.requireConfirmForWrites,
|
|
3802
|
+
...credential.kind === "access_token" && credential.refreshToken ? { refreshCredential: () => refreshCredentialIfNeeded(environment, { force: true }) } : {},
|
|
3583
3803
|
...isDebugEnabled() ? { debug: stderrDebugLogger } : {}
|
|
3584
3804
|
});
|
|
3585
3805
|
}
|
|
@@ -3642,93 +3862,6 @@ function formatPretty(info) {
|
|
|
3642
3862
|
return lines.join("\n");
|
|
3643
3863
|
}
|
|
3644
3864
|
|
|
3645
|
-
// src/commands/actions.ts
|
|
3646
|
-
function registerActionsCommand(program2) {
|
|
3647
|
-
const actions = program2.command("actions").description("List and call permission-scoped Topolo actions from the live catalog").option("-s, --service <service>", "Filter by app id, slug, or name").option("--json", "Output JSON", true).option("--no-json", "Output human-readable table").action(async (opts) => {
|
|
3648
|
-
const mode = resolveOutputMode(opts.json);
|
|
3649
|
-
try {
|
|
3650
|
-
const topolo = await buildClient();
|
|
3651
|
-
if (!topolo) {
|
|
3652
|
-
throw new Error("No credential configured. Run `topolo auth login` or set TOPOLO_API_KEY.");
|
|
3653
|
-
}
|
|
3654
|
-
const rows = await topolo.client.listActions({ service: opts.service });
|
|
3655
|
-
emit({ actions: rows }, mode, () => formatActionRows(rows));
|
|
3656
|
-
} catch (err) {
|
|
3657
|
-
fail(err, mode);
|
|
3658
|
-
}
|
|
3659
|
-
});
|
|
3660
|
-
actions.command("get <action>").description("Describe one Topolo action by action id, name, or tool name").option("--json", "Output JSON", true).option("--no-json", "Output human-readable text").action(async (action, opts) => {
|
|
3661
|
-
const mode = resolveOutputMode(opts.json);
|
|
3662
|
-
try {
|
|
3663
|
-
const topolo = await buildClient();
|
|
3664
|
-
if (!topolo) {
|
|
3665
|
-
throw new Error("No credential configured. Run `topolo auth login` or set TOPOLO_API_KEY.");
|
|
3666
|
-
}
|
|
3667
|
-
const entry = await topolo.client.getAction(action);
|
|
3668
|
-
emit(entry, mode, () => formatActionDetail(entry));
|
|
3669
|
-
} catch (err) {
|
|
3670
|
-
fail(err, mode);
|
|
3671
|
-
}
|
|
3672
|
-
});
|
|
3673
|
-
actions.command("call <action>").description("Call a permission-scoped Topolo action").option("-d, --data <json>", "JSON action input", "{}").option("--confirm", "Acknowledge a mutating action").option("--json", "Output JSON", true).option("--no-json", "Output human-readable text").action(async (action, opts) => {
|
|
3674
|
-
const mode = resolveOutputMode(opts.json);
|
|
3675
|
-
try {
|
|
3676
|
-
const topolo = await buildClient();
|
|
3677
|
-
if (!topolo) {
|
|
3678
|
-
throw new Error("No credential configured. Run `topolo auth login` or set TOPOLO_API_KEY.");
|
|
3679
|
-
}
|
|
3680
|
-
const input2 = parseJsonObject(opts.data || "{}");
|
|
3681
|
-
const result = await topolo.client.callAction(action, input2, { confirm: opts.confirm === true });
|
|
3682
|
-
emit(result, mode, () => JSON.stringify(result, null, 2));
|
|
3683
|
-
} catch (err) {
|
|
3684
|
-
fail(err, mode);
|
|
3685
|
-
}
|
|
3686
|
-
});
|
|
3687
|
-
}
|
|
3688
|
-
function parseJsonObject(value) {
|
|
3689
|
-
let parsed;
|
|
3690
|
-
try {
|
|
3691
|
-
parsed = JSON.parse(value);
|
|
3692
|
-
} catch {
|
|
3693
|
-
throw new Error("--data must be valid JSON");
|
|
3694
|
-
}
|
|
3695
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
3696
|
-
throw new Error("--data must be a JSON object");
|
|
3697
|
-
}
|
|
3698
|
-
return parsed;
|
|
3699
|
-
}
|
|
3700
|
-
function formatActionRows(rows) {
|
|
3701
|
-
if (rows.length === 0) return "No actions available for this credential.";
|
|
3702
|
-
const serviceWidth = Math.max(7, ...rows.map((row) => (row.appSlug || row.appId).length));
|
|
3703
|
-
const nameWidth = Math.max(6, ...rows.map((row) => row.name.length));
|
|
3704
|
-
const methodWidth = Math.max(6, ...rows.map((row) => row.method.length));
|
|
3705
|
-
const lines = [
|
|
3706
|
-
`${"SERVICE".padEnd(serviceWidth)} ${"ACTION".padEnd(nameWidth)} ${"METHOD".padEnd(methodWidth)} CONFIRM PERMISSION`,
|
|
3707
|
-
`${"-".repeat(serviceWidth)} ${"-".repeat(nameWidth)} ${"-".repeat(methodWidth)} ------- ----------`
|
|
3708
|
-
];
|
|
3709
|
-
for (const row of rows) {
|
|
3710
|
-
const confirmation = row.requiresConfirmation ? "yes" : "no";
|
|
3711
|
-
lines.push(
|
|
3712
|
-
`${(row.appSlug || row.appId).padEnd(serviceWidth)} ${row.name.padEnd(nameWidth)} ${row.method.padEnd(methodWidth)} ${confirmation.padEnd(7)} ${row.requiredPermission}`
|
|
3713
|
-
);
|
|
3714
|
-
}
|
|
3715
|
-
return lines.join("\n");
|
|
3716
|
-
}
|
|
3717
|
-
function formatActionDetail(action) {
|
|
3718
|
-
return [
|
|
3719
|
-
`Action: ${action.name}`,
|
|
3720
|
-
`ID: ${action.actionId}`,
|
|
3721
|
-
`Tool: ${action.toolName || "-"}`,
|
|
3722
|
-
`Service: ${action.appSlug || action.appId}`,
|
|
3723
|
-
`Method: ${action.method}`,
|
|
3724
|
-
`Path: ${action.path}`,
|
|
3725
|
-
`Permission: ${action.requiredPermission}`,
|
|
3726
|
-
`Confirmation: ${action.requiresConfirmation ? "required" : "not required"}`,
|
|
3727
|
-
"",
|
|
3728
|
-
action.description
|
|
3729
|
-
].join("\n");
|
|
3730
|
-
}
|
|
3731
|
-
|
|
3732
3865
|
// src/commands/api.ts
|
|
3733
3866
|
var STATIC_APP_IDS = new Set(Object.keys(DEFAULT_APP_URLS));
|
|
3734
3867
|
var HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"];
|
|
@@ -3811,25 +3944,132 @@ function resolveConfiguredServiceUrl2(service, serviceUrls) {
|
|
|
3811
3944
|
withoutTopolo,
|
|
3812
3945
|
withoutTopolo.replace(/-/g, "_")
|
|
3813
3946
|
];
|
|
3814
|
-
for (const candidate of candidates) {
|
|
3815
|
-
const url = serviceUrls?.[candidate];
|
|
3816
|
-
if (typeof url === "string" && url.trim()) return url.trim();
|
|
3947
|
+
for (const candidate of candidates) {
|
|
3948
|
+
const url = serviceUrls?.[candidate];
|
|
3949
|
+
if (typeof url === "string" && url.trim()) return url.trim();
|
|
3950
|
+
}
|
|
3951
|
+
return void 0;
|
|
3952
|
+
}
|
|
3953
|
+
function parseQueryPairs(pairs) {
|
|
3954
|
+
if (!pairs || pairs.length === 0) return void 0;
|
|
3955
|
+
const out = {};
|
|
3956
|
+
for (const pair of pairs) {
|
|
3957
|
+
const eq = pair.indexOf("=");
|
|
3958
|
+
if (eq <= 0) {
|
|
3959
|
+
throw new Error(`Invalid --query entry "${pair}". Expected key=value.`);
|
|
3960
|
+
}
|
|
3961
|
+
const key = pair.slice(0, eq);
|
|
3962
|
+
const value = pair.slice(eq + 1);
|
|
3963
|
+
out[key] = value;
|
|
3964
|
+
}
|
|
3965
|
+
return out;
|
|
3966
|
+
}
|
|
3967
|
+
|
|
3968
|
+
// src/commands/actions.ts
|
|
3969
|
+
function registerActionsCommand(program2) {
|
|
3970
|
+
const actions = program2.command("actions").description("Search permission-scoped Topolo actions for one application").option("-s, --service <service>", "App id, slug, or name (required for search)").option("-q, --query <query>", "Filter by action id, name, or title").option("--cursor <cursor>", "Continuation cursor from the previous page").option("--limit <limit>", "Page size (1-100)", "25").option("--json", "Output JSON").option("--no-json", "Output human-readable table").action(async (opts) => {
|
|
3971
|
+
const mode = resolveOutputMode(opts.json);
|
|
3972
|
+
try {
|
|
3973
|
+
const topolo = await buildClient();
|
|
3974
|
+
if (!topolo) {
|
|
3975
|
+
throw new Error("No credential configured. Run `topolo auth login` or set TOPOLO_API_KEY.");
|
|
3976
|
+
}
|
|
3977
|
+
if (!opts.service?.trim()) throw new Error("--service is required for action search");
|
|
3978
|
+
const result = await topolo.client.searchActions({
|
|
3979
|
+
service: opts.service.trim(),
|
|
3980
|
+
query: opts.query,
|
|
3981
|
+
cursor: opts.cursor,
|
|
3982
|
+
limit: parsePageSize(opts.limit)
|
|
3983
|
+
});
|
|
3984
|
+
emit(result, mode, () => formatActionRows(result.actions, result.page.nextCursor));
|
|
3985
|
+
} catch (err) {
|
|
3986
|
+
fail(err, mode);
|
|
3987
|
+
}
|
|
3988
|
+
});
|
|
3989
|
+
actions.command("get <action>").description("Describe one Topolo action by exact action id").option("--json", "Output JSON").option("--no-json", "Output human-readable text").action(async (action, opts, command) => {
|
|
3990
|
+
const mode = resolveOutputMode(opts.json ?? command.parent?.opts().json);
|
|
3991
|
+
try {
|
|
3992
|
+
const topolo = await buildClient();
|
|
3993
|
+
if (!topolo) {
|
|
3994
|
+
throw new Error("No credential configured. Run `topolo auth login` or set TOPOLO_API_KEY.");
|
|
3995
|
+
}
|
|
3996
|
+
const entry = await topolo.client.getAction(action);
|
|
3997
|
+
emit(entry, mode, () => formatActionDetail(entry));
|
|
3998
|
+
} catch (err) {
|
|
3999
|
+
fail(err, mode);
|
|
4000
|
+
}
|
|
4001
|
+
});
|
|
4002
|
+
actions.command("call <action>").description("Call a permission-scoped Topolo action").option("-d, --data <json>", "JSON action input", "{}").option("--resource-type <type>", "App-local resource alias, such as workspace").option("--resource-id <id>", "Resource id for the selected resource type").option("--confirm", "Acknowledge a mutating action").option("--json", "Output JSON").option("--no-json", "Output human-readable text").action(async (action, opts, command) => {
|
|
4003
|
+
const mode = resolveOutputMode(opts.json ?? command.parent?.opts().json);
|
|
4004
|
+
try {
|
|
4005
|
+
const topolo = await buildClient();
|
|
4006
|
+
if (!topolo) {
|
|
4007
|
+
throw new Error("No credential configured. Run `topolo auth login` or set TOPOLO_API_KEY.");
|
|
4008
|
+
}
|
|
4009
|
+
const input2 = parseJsonObject(opts.data || "{}");
|
|
4010
|
+
const resource = parseResourceOption(opts.resourceType, opts.resourceId);
|
|
4011
|
+
const result = await topolo.client.callAction(action, input2, {
|
|
4012
|
+
confirm: opts.confirm === true,
|
|
4013
|
+
...resource ? { resource } : {}
|
|
4014
|
+
});
|
|
4015
|
+
emit(result, mode, () => JSON.stringify(result, null, 2));
|
|
4016
|
+
} catch (err) {
|
|
4017
|
+
fail(err, mode);
|
|
4018
|
+
}
|
|
4019
|
+
});
|
|
4020
|
+
}
|
|
4021
|
+
function parseJsonObject(value) {
|
|
4022
|
+
let parsed;
|
|
4023
|
+
try {
|
|
4024
|
+
parsed = JSON.parse(value);
|
|
4025
|
+
} catch {
|
|
4026
|
+
throw new Error("--data must be valid JSON");
|
|
4027
|
+
}
|
|
4028
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
4029
|
+
throw new Error("--data must be a JSON object");
|
|
4030
|
+
}
|
|
4031
|
+
return parsed;
|
|
4032
|
+
}
|
|
4033
|
+
function formatActionRows(rows, nextCursor = null) {
|
|
4034
|
+
if (rows.length === 0) return "No actions available for this credential.";
|
|
4035
|
+
const serviceWidth = Math.max(7, ...rows.map((row) => (row.appSlug || row.appId).length));
|
|
4036
|
+
const nameWidth = Math.max(6, ...rows.map((row) => row.name.length));
|
|
4037
|
+
const methodWidth = Math.max(6, ...rows.map((row) => row.method.length));
|
|
4038
|
+
const lines = [
|
|
4039
|
+
`${"SERVICE".padEnd(serviceWidth)} ${"ACTION".padEnd(nameWidth)} ${"METHOD".padEnd(methodWidth)} CONFIRM PERMISSION`,
|
|
4040
|
+
`${"-".repeat(serviceWidth)} ${"-".repeat(nameWidth)} ${"-".repeat(methodWidth)} ------- ----------`
|
|
4041
|
+
];
|
|
4042
|
+
for (const row of rows) {
|
|
4043
|
+
const confirmation = row.requiresConfirmation ? "yes" : "no";
|
|
4044
|
+
lines.push(
|
|
4045
|
+
`${(row.appSlug || row.appId).padEnd(serviceWidth)} ${row.name.padEnd(nameWidth)} ${row.method.padEnd(methodWidth)} ${confirmation.padEnd(7)} ${row.requiredPermission}`
|
|
4046
|
+
);
|
|
3817
4047
|
}
|
|
3818
|
-
|
|
4048
|
+
if (nextCursor) lines.push("", `Next cursor: ${nextCursor}`);
|
|
4049
|
+
return lines.join("\n");
|
|
3819
4050
|
}
|
|
3820
|
-
function
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
const eq = pair.indexOf("=");
|
|
3825
|
-
if (eq <= 0) {
|
|
3826
|
-
throw new Error(`Invalid --query entry "${pair}". Expected key=value.`);
|
|
3827
|
-
}
|
|
3828
|
-
const key = pair.slice(0, eq);
|
|
3829
|
-
const value = pair.slice(eq + 1);
|
|
3830
|
-
out[key] = value;
|
|
4051
|
+
function parsePageSize(value) {
|
|
4052
|
+
const parsed = Number.parseInt(value || "25", 10);
|
|
4053
|
+
if (!Number.isFinite(parsed) || parsed < 1 || parsed > 100) {
|
|
4054
|
+
throw new Error("--limit must be an integer between 1 and 100");
|
|
3831
4055
|
}
|
|
3832
|
-
return
|
|
4056
|
+
return parsed;
|
|
4057
|
+
}
|
|
4058
|
+
function formatActionDetail(action) {
|
|
4059
|
+
return [
|
|
4060
|
+
`Action: ${action.name}`,
|
|
4061
|
+
`ID: ${action.actionId}`,
|
|
4062
|
+
`Tool: ${action.toolName || "-"}`,
|
|
4063
|
+
`Service: ${action.appSlug || action.appId}`,
|
|
4064
|
+
`Target: ${action.target || "-"}`,
|
|
4065
|
+
`API URL: ${action.apiBaseUrl || "-"}`,
|
|
4066
|
+
`Method: ${action.method}`,
|
|
4067
|
+
`Path: ${action.path}`,
|
|
4068
|
+
`Permission: ${action.requiredPermission}`,
|
|
4069
|
+
`Confirmation: ${action.requiresConfirmation ? "required" : "not required"}`,
|
|
4070
|
+
"",
|
|
4071
|
+
action.description
|
|
4072
|
+
].join("\n");
|
|
3833
4073
|
}
|
|
3834
4074
|
|
|
3835
4075
|
// src/commands/apps.ts
|
|
@@ -5073,6 +5313,12 @@ function firstPartyAppPath(appDir, _audience) {
|
|
|
5073
5313
|
return `apps/${appDir}`;
|
|
5074
5314
|
}
|
|
5075
5315
|
|
|
5316
|
+
// src/commands/app-scaffold-cloudflare-constants.ts
|
|
5317
|
+
var CLOUDFLARE_ACCOUNT_ID = "49ef1ba682ad8cfd720c86699ae17521";
|
|
5318
|
+
var STAGING_CLOUDFLARE_ACCOUNT_ID = "4f4e1c69a3830946f9fea7b1eb7531ac";
|
|
5319
|
+
var DEVELOPMENT_CLOUDFLARE_ACCOUNT_ID = "0ebdeec262a245481a721fc4f9d6d798";
|
|
5320
|
+
var CLOUDFLARE_COMPATIBILITY_DATE = "2026-05-05";
|
|
5321
|
+
|
|
5076
5322
|
// src/commands/app-scaffold-package-template.ts
|
|
5077
5323
|
function packageJson(application, target, options) {
|
|
5078
5324
|
const dependencies = {};
|
|
@@ -5111,16 +5357,43 @@ function packageJson(application, target, options) {
|
|
|
5111
5357
|
devDependencies["@cloudflare/workers-types"] = "^4.20260413.1";
|
|
5112
5358
|
if (target === "platform") {
|
|
5113
5359
|
dependencies["@topolo-io/worker-runtime"] = topoloPackageVersion("@topolo-io/worker-runtime", target);
|
|
5360
|
+
dependencies["@topolo-io/auth-middleware"] = topoloPackageVersion("@topolo-io/auth-middleware", target);
|
|
5361
|
+
dependencies["@topolo-io/error-middleware"] = topoloPackageVersion("@topolo-io/error-middleware", target);
|
|
5362
|
+
dependencies["@topolo-io/security-headers"] = topoloPackageVersion("@topolo-io/security-headers", target);
|
|
5363
|
+
dependencies["@topolo-io/resource-context"] = topoloPackageVersion("@topolo-io/resource-context", target);
|
|
5364
|
+
dependencies["@topolo-io/data"] = topoloPackageVersion("@topolo-io/data", target);
|
|
5114
5365
|
dependencies["@topolo/sdk"] = topoloPackageVersion("@topolo/sdk", target);
|
|
5115
5366
|
dependencies.hono = "^4";
|
|
5367
|
+
dependencies.zod = "^4.4.3";
|
|
5116
5368
|
scripts.deploy = `node -e 'console.error("\\nX Bare deploy is disabled. Use deploy:staging or deploy:production.\\n"); process.exit(1)'`;
|
|
5117
|
-
scripts["deploy:staging"] =
|
|
5118
|
-
scripts["deploy:production"] =
|
|
5369
|
+
scripts["deploy:staging"] = `DEPLOY_ENVIRONMENT=staging CLOUDFLARE_ACCOUNT_ID=${STAGING_CLOUDFLARE_ACCOUNT_ID} python3 .github/scripts/standard_deploy.py local-deploy`;
|
|
5370
|
+
scripts["deploy:production"] = `DEPLOY_ENVIRONMENT=production CLOUDFLARE_ACCOUNT_ID=${CLOUDFLARE_ACCOUNT_ID} python3 .github/scripts/standard_deploy.py local-deploy`;
|
|
5119
5371
|
} else {
|
|
5120
5372
|
dependencies["@topolo/sdk"] = topoloPackageVersion("@topolo/sdk", target);
|
|
5121
5373
|
scripts.deploy = "npm run build && wrangler deploy";
|
|
5122
5374
|
}
|
|
5123
|
-
devDependencies.wrangler = "^4.
|
|
5375
|
+
devDependencies.wrangler = "^4.91.0";
|
|
5376
|
+
}
|
|
5377
|
+
if (target === "platform") {
|
|
5378
|
+
const testDirs = [];
|
|
5379
|
+
if (options.browser) testDirs.push("apps/web");
|
|
5380
|
+
if (options.api) testDirs.push("apps/api");
|
|
5381
|
+
if (testDirs.length > 0) {
|
|
5382
|
+
const chain = (withCoverage) => testDirs.map((dir, index) => {
|
|
5383
|
+
const enter = index === 0 ? `cd ${dir}` : `cd ../${dir.split("/").pop()}`;
|
|
5384
|
+
return `${enter} && vitest run --config vitest.config.ts --passWithNoTests${withCoverage ? " --coverage" : ""}`;
|
|
5385
|
+
}).join(" && ");
|
|
5386
|
+
scripts.test = chain(false);
|
|
5387
|
+
scripts.coverage = chain(true);
|
|
5388
|
+
scripts.typecheck = testDirs.map((dir) => `tsc --noEmit -p ${dir}/tsconfig.json`).join(" && ");
|
|
5389
|
+
devDependencies.vitest = "^3.2.4";
|
|
5390
|
+
devDependencies["@vitest/coverage-v8"] = "^3.2.4";
|
|
5391
|
+
if (options.browser) {
|
|
5392
|
+
devDependencies.jsdom = "^29.1.1";
|
|
5393
|
+
devDependencies["@testing-library/react"] = "^16.3.2";
|
|
5394
|
+
devDependencies["@testing-library/user-event"] = "^14.6.1";
|
|
5395
|
+
}
|
|
5396
|
+
}
|
|
5124
5397
|
}
|
|
5125
5398
|
if (target === "platform" && hasScaffoldResources(options.resources)) {
|
|
5126
5399
|
scripts["check:deploy-config"] = "wrangler deploy --config apps/api/wrangler.toml --env staging --dry-run --outdir .wrangler/api-build && wrangler deploy --config apps/web/wrangler.toml --env staging --dry-run --outdir .wrangler/web-build";
|
|
@@ -5140,17 +5413,21 @@ function topoloPackageVersion(packageName, target) {
|
|
|
5140
5413
|
if (target === "developer") {
|
|
5141
5414
|
if (packageName === "@topolo/login") return "^0.1.0";
|
|
5142
5415
|
if (packageName === "@topolo-io/auth-client") return "^1.0.0";
|
|
5143
|
-
if (packageName === "@topolo/sdk") return "^0.
|
|
5416
|
+
if (packageName === "@topolo/sdk") return "^0.8.1";
|
|
5144
5417
|
return "^0.1.0";
|
|
5145
5418
|
}
|
|
5146
5419
|
const platformPackageVersions = {
|
|
5147
|
-
"@topolo/sdk": "^0.
|
|
5420
|
+
"@topolo/sdk": "^0.8.1",
|
|
5148
5421
|
"@topolo/login": "^0.1.0",
|
|
5149
5422
|
"@topolo-io/app-funnel": "^0.1.6",
|
|
5150
5423
|
"@topolo-io/app-launcher": "^0.1.15",
|
|
5151
5424
|
"@topolo-io/app-shell": "^0.5.54",
|
|
5152
5425
|
"@topolo-io/auth-client": "^1.2.1",
|
|
5153
|
-
"@topolo-io/auth-middleware": "^0.
|
|
5426
|
+
"@topolo-io/auth-middleware": "^0.4.0",
|
|
5427
|
+
"@topolo-io/error-middleware": "^0.6.0",
|
|
5428
|
+
"@topolo-io/security-headers": "^0.2.2",
|
|
5429
|
+
"@topolo-io/resource-context": "^0.3.0",
|
|
5430
|
+
"@topolo-io/data": "^0.1.2",
|
|
5154
5431
|
"@topolo-io/worker-runtime": "^0.1.11",
|
|
5155
5432
|
"@topolo-io/design-tokens": "^0.1.8",
|
|
5156
5433
|
"@topolo-io/onboarding": "^0.9.5",
|
|
@@ -5186,7 +5463,12 @@ function resourceConfig(application, profile) {
|
|
|
5186
5463
|
workflowProductionName: `topolo-${slug}-import`,
|
|
5187
5464
|
workflowStagingName: `topolo-${slug}-import-staging`,
|
|
5188
5465
|
containerProductionName: `topolo-${slug}-processor`,
|
|
5189
|
-
containerStagingName: `topolo-${slug}-processor-staging
|
|
5466
|
+
containerStagingName: `topolo-${slug}-processor-staging`,
|
|
5467
|
+
d1DevelopmentName: `topolo-${slug}-core-development`,
|
|
5468
|
+
r2DevelopmentName: `topolo-${slug}-assets-development`,
|
|
5469
|
+
queueDevelopmentName: `topolo-${slug}-processing-development`,
|
|
5470
|
+
workflowDevelopmentName: `topolo-${slug}-import-development`,
|
|
5471
|
+
containerDevelopmentName: `topolo-${slug}-processor-development`
|
|
5190
5472
|
};
|
|
5191
5473
|
}
|
|
5192
5474
|
return {
|
|
@@ -5204,7 +5486,12 @@ function resourceConfig(application, profile) {
|
|
|
5204
5486
|
workflowProductionName: `topolo-${slug}-workflow`,
|
|
5205
5487
|
workflowStagingName: `topolo-${slug}-workflow-staging`,
|
|
5206
5488
|
containerProductionName: `topolo-${slug}-processor`,
|
|
5207
|
-
containerStagingName: `topolo-${slug}-processor-staging
|
|
5489
|
+
containerStagingName: `topolo-${slug}-processor-staging`,
|
|
5490
|
+
d1DevelopmentName: `topolo-${slug}-core-development`,
|
|
5491
|
+
r2DevelopmentName: `topolo-${slug}-assets-development`,
|
|
5492
|
+
queueDevelopmentName: `topolo-${slug}-queue-development`,
|
|
5493
|
+
workflowDevelopmentName: `topolo-${slug}-workflow-development`,
|
|
5494
|
+
containerDevelopmentName: `topolo-${slug}-processor-development`
|
|
5208
5495
|
};
|
|
5209
5496
|
}
|
|
5210
5497
|
|
|
@@ -5377,6 +5664,64 @@ function tsconfigJson(api, target) {
|
|
|
5377
5664
|
})}
|
|
5378
5665
|
`;
|
|
5379
5666
|
}
|
|
5667
|
+
var TSCONFIG_TEST_EXCLUDE = [
|
|
5668
|
+
"src/**/*.test.ts",
|
|
5669
|
+
"src/**/*.test.tsx",
|
|
5670
|
+
"src/**/*.test.mts",
|
|
5671
|
+
"src/**/*.spec.ts",
|
|
5672
|
+
"src/**/*.spec.tsx",
|
|
5673
|
+
"test"
|
|
5674
|
+
];
|
|
5675
|
+
function webTsconfigJson() {
|
|
5676
|
+
return `${json({
|
|
5677
|
+
compilerOptions: {
|
|
5678
|
+
target: "ES2022",
|
|
5679
|
+
useDefineForClassFields: true,
|
|
5680
|
+
lib: ["ES2022", "DOM", "DOM.Iterable"],
|
|
5681
|
+
allowJs: false,
|
|
5682
|
+
skipLibCheck: true,
|
|
5683
|
+
esModuleInterop: true,
|
|
5684
|
+
allowSyntheticDefaultImports: true,
|
|
5685
|
+
strict: true,
|
|
5686
|
+
forceConsistentCasingInFileNames: true,
|
|
5687
|
+
module: "ESNext",
|
|
5688
|
+
moduleResolution: "Bundler",
|
|
5689
|
+
resolveJsonModule: true,
|
|
5690
|
+
isolatedModules: true,
|
|
5691
|
+
noEmit: true,
|
|
5692
|
+
jsx: "react-jsx",
|
|
5693
|
+
types: ["vite/client"]
|
|
5694
|
+
},
|
|
5695
|
+
include: ["src"],
|
|
5696
|
+
references: [],
|
|
5697
|
+
exclude: TSCONFIG_TEST_EXCLUDE
|
|
5698
|
+
})}
|
|
5699
|
+
`;
|
|
5700
|
+
}
|
|
5701
|
+
function apiTsconfigJson() {
|
|
5702
|
+
return `${json({
|
|
5703
|
+
compilerOptions: {
|
|
5704
|
+
target: "ES2022",
|
|
5705
|
+
lib: ["ES2022", "DOM", "DOM.Iterable", "WebWorker"],
|
|
5706
|
+
allowJs: false,
|
|
5707
|
+
skipLibCheck: true,
|
|
5708
|
+
esModuleInterop: true,
|
|
5709
|
+
allowSyntheticDefaultImports: true,
|
|
5710
|
+
strict: true,
|
|
5711
|
+
forceConsistentCasingInFileNames: true,
|
|
5712
|
+
module: "ESNext",
|
|
5713
|
+
moduleResolution: "Bundler",
|
|
5714
|
+
resolveJsonModule: true,
|
|
5715
|
+
isolatedModules: true,
|
|
5716
|
+
noEmit: true,
|
|
5717
|
+
types: ["@cloudflare/workers-types", "node"]
|
|
5718
|
+
},
|
|
5719
|
+
include: ["src"],
|
|
5720
|
+
references: [],
|
|
5721
|
+
exclude: TSCONFIG_TEST_EXCLUDE
|
|
5722
|
+
})}
|
|
5723
|
+
`;
|
|
5724
|
+
}
|
|
5380
5725
|
function gitignore() {
|
|
5381
5726
|
return [
|
|
5382
5727
|
"node_modules/",
|
|
@@ -6118,11 +6463,6 @@ function developerAppTsx(application) {
|
|
|
6118
6463
|
].join("\n");
|
|
6119
6464
|
}
|
|
6120
6465
|
|
|
6121
|
-
// src/commands/app-scaffold-cloudflare-constants.ts
|
|
6122
|
-
var CLOUDFLARE_ACCOUNT_ID = "49ef1ba682ad8cfd720c86699ae17521";
|
|
6123
|
-
var STAGING_CLOUDFLARE_ACCOUNT_ID = "4f4e1c69a3830946f9fea7b1eb7531ac";
|
|
6124
|
-
var CLOUDFLARE_COMPATIBILITY_DATE = "2026-05-05";
|
|
6125
|
-
|
|
6126
6466
|
// src/commands/app-scaffold-worker-template.ts
|
|
6127
6467
|
function webWorkerTs(application) {
|
|
6128
6468
|
const serviceName = toTitle(application.name);
|
|
@@ -6177,6 +6517,29 @@ function platformWebWranglerToml(application) {
|
|
|
6177
6517
|
'binding = "API"',
|
|
6178
6518
|
`service = "topolo-${slug}-api"`,
|
|
6179
6519
|
"",
|
|
6520
|
+
"[env.development]",
|
|
6521
|
+
`name = "topolo-${slug}-development"`,
|
|
6522
|
+
`account_id = "${DEVELOPMENT_CLOUDFLARE_ACCOUNT_ID}"`,
|
|
6523
|
+
"workers_dev = true",
|
|
6524
|
+
"",
|
|
6525
|
+
"[env.development.vars]",
|
|
6526
|
+
`APP_ID = "${application.appId ?? ""}"`,
|
|
6527
|
+
`VITE_TOPOLO_APP_ID = "${application.appId ?? ""}"`,
|
|
6528
|
+
"",
|
|
6529
|
+
"[[env.development.routes]]",
|
|
6530
|
+
`pattern = "${slug}.topolo.dev"`,
|
|
6531
|
+
"custom_domain = true",
|
|
6532
|
+
"",
|
|
6533
|
+
"[env.development.assets]",
|
|
6534
|
+
'directory = "./dist"',
|
|
6535
|
+
'binding = "ASSETS"',
|
|
6536
|
+
'not_found_handling = "single-page-application"',
|
|
6537
|
+
'run_worker_first = ["/api/*"]',
|
|
6538
|
+
"",
|
|
6539
|
+
"[[env.development.services]]",
|
|
6540
|
+
'binding = "API"',
|
|
6541
|
+
`service = "topolo-${slug}-api-development"`,
|
|
6542
|
+
"",
|
|
6180
6543
|
"[env.staging]",
|
|
6181
6544
|
`name = "topolo-${slug}-staging"`,
|
|
6182
6545
|
`account_id = "${STAGING_CLOUDFLARE_ACCOUNT_ID}"`,
|
|
@@ -6248,6 +6611,22 @@ function platformApiWranglerToml(application, resources, profile) {
|
|
|
6248
6611
|
""
|
|
6249
6612
|
];
|
|
6250
6613
|
appendResourceWranglerBlocks(lines, resources, resource, "production");
|
|
6614
|
+
lines.push(
|
|
6615
|
+
"[env.development]",
|
|
6616
|
+
`name = "topolo-${slug}-api-development"`,
|
|
6617
|
+
`account_id = "${DEVELOPMENT_CLOUDFLARE_ACCOUNT_ID}"`,
|
|
6618
|
+
"workers_dev = true",
|
|
6619
|
+
"",
|
|
6620
|
+
"[env.development.vars]",
|
|
6621
|
+
'AUTH_BASE_URL = "https://auth.topolo.dev"',
|
|
6622
|
+
'NEXUS_GATEWAY_URL = "https://nexus.topolo.dev"',
|
|
6623
|
+
`NEXUS_APP_ID = "${slug}"`,
|
|
6624
|
+
`APP_ID = "${application.appId ?? ""}"`,
|
|
6625
|
+
`VITE_TOPOLO_APP_ID = "${application.appId ?? ""}"`,
|
|
6626
|
+
'ENVIRONMENT = "development"',
|
|
6627
|
+
""
|
|
6628
|
+
);
|
|
6629
|
+
appendResourceWranglerBlocks(lines, resources, resource, "development");
|
|
6251
6630
|
lines.push(
|
|
6252
6631
|
"[env.staging]",
|
|
6253
6632
|
`name = "topolo-${slug}-api-staging"`,
|
|
@@ -6281,13 +6660,14 @@ function platformApiWranglerToml(application, resources, profile) {
|
|
|
6281
6660
|
return lines.join("\n");
|
|
6282
6661
|
}
|
|
6283
6662
|
function appendResourceWranglerBlocks(lines, resources, resource, environment) {
|
|
6284
|
-
const prefix = environment === "staging" ? "env.staging." : environment === "production-env" ? "env.production." : "";
|
|
6285
|
-
const envName = environment === "staging" ? "Staging" : "Production";
|
|
6663
|
+
const prefix = environment === "staging" ? "env.staging." : environment === "development" ? "env.development." : environment === "production-env" ? "env.production." : "";
|
|
6664
|
+
const envName = environment === "staging" ? "Staging" : environment === "development" ? "Development" : "Production";
|
|
6665
|
+
const pick = (dev, staging, prod) => environment === "development" ? dev : environment === "staging" ? staging : prod;
|
|
6286
6666
|
if (resources.d1) {
|
|
6287
6667
|
lines.push(
|
|
6288
6668
|
`[[${prefix}d1_databases]]`,
|
|
6289
6669
|
`binding = "${resource.d1Binding}"`,
|
|
6290
|
-
`database_name = "${
|
|
6670
|
+
`database_name = "${pick(resource.d1DevelopmentName, resource.d1StagingName, resource.d1ProductionName)}"`,
|
|
6291
6671
|
'database_id = "provision-after-scaffold"',
|
|
6292
6672
|
'migrations_dir = "migrations"',
|
|
6293
6673
|
""
|
|
@@ -6297,7 +6677,7 @@ function appendResourceWranglerBlocks(lines, resources, resource, environment) {
|
|
|
6297
6677
|
lines.push(
|
|
6298
6678
|
`[[${prefix}r2_buckets]]`,
|
|
6299
6679
|
`binding = "${resource.r2Binding}"`,
|
|
6300
|
-
`bucket_name = "${
|
|
6680
|
+
`bucket_name = "${pick(resource.r2DevelopmentName, resource.r2StagingName, resource.r2ProductionName)}"`,
|
|
6301
6681
|
""
|
|
6302
6682
|
);
|
|
6303
6683
|
}
|
|
@@ -6305,10 +6685,10 @@ function appendResourceWranglerBlocks(lines, resources, resource, environment) {
|
|
|
6305
6685
|
lines.push(
|
|
6306
6686
|
`[[${prefix}queues.producers]]`,
|
|
6307
6687
|
`binding = "${resource.queueBinding}"`,
|
|
6308
|
-
`queue = "${
|
|
6688
|
+
`queue = "${pick(resource.queueDevelopmentName, resource.queueStagingName, resource.queueProductionName)}"`,
|
|
6309
6689
|
"",
|
|
6310
6690
|
`[[${prefix}queues.consumers]]`,
|
|
6311
|
-
`queue = "${
|
|
6691
|
+
`queue = "${pick(resource.queueDevelopmentName, resource.queueStagingName, resource.queueProductionName)}"`,
|
|
6312
6692
|
""
|
|
6313
6693
|
);
|
|
6314
6694
|
}
|
|
@@ -6316,7 +6696,7 @@ function appendResourceWranglerBlocks(lines, resources, resource, environment) {
|
|
|
6316
6696
|
lines.push(
|
|
6317
6697
|
`[[${prefix}workflows]]`,
|
|
6318
6698
|
`binding = "${resource.workflowBinding}"`,
|
|
6319
|
-
`name = "${
|
|
6699
|
+
`name = "${pick(resource.workflowDevelopmentName, resource.workflowStagingName, resource.workflowProductionName)}"`,
|
|
6320
6700
|
`class_name = "${resource.workflowClassName}"`,
|
|
6321
6701
|
""
|
|
6322
6702
|
);
|
|
@@ -6514,6 +6894,8 @@ function platformWorkerTs(application, options) {
|
|
|
6514
6894
|
"import { Hono } from 'hono';",
|
|
6515
6895
|
"import { createTopoloWidgetResponse, type TopoloWidgetDraft } from '@topolo/sdk';",
|
|
6516
6896
|
"import { createTopoloWorkerRuntime, type TopoloRuntimeContext } from '@topolo-io/worker-runtime';",
|
|
6897
|
+
"import { withSecurityHeaders } from '@topolo-io/security-headers';",
|
|
6898
|
+
"import { withErrorReporting } from '@topolo-io/error-middleware';",
|
|
6517
6899
|
"import { SCAFFOLD_APP_ID, SCAFFOLD_APP_NAME, SCAFFOLD_SERVICE_SLUG, resolveRuntimeAppId } from './app-identity';",
|
|
6518
6900
|
...options.resources.workflow ? ["import { WorkflowEntrypoint, type WorkflowEvent, type WorkflowStep } from 'cloudflare:workers';"] : [],
|
|
6519
6901
|
"",
|
|
@@ -6604,11 +6986,20 @@ function platformWorkerTs(application, options) {
|
|
|
6604
6986
|
" return handleRequest(c.req.raw, c.env, rt);",
|
|
6605
6987
|
"});",
|
|
6606
6988
|
"",
|
|
6989
|
+
"// Platform security headers + structured error reporting wrap the whole app",
|
|
6990
|
+
"// (outermost), so headers are stamped even on error-middleware 5xx responses.",
|
|
6991
|
+
"const securedFetch = withSecurityHeaders(",
|
|
6992
|
+
" withErrorReporting(",
|
|
6993
|
+
" (request: Request, env: Env, ctx: ExecutionContext) => api.fetch(request, env, ctx),",
|
|
6994
|
+
" { appName: SCAFFOLD_APP_NAME, appId: SCAFFOLD_SERVICE_SLUG },",
|
|
6995
|
+
" ),",
|
|
6996
|
+
") as (request: Request, env: Env, ctx: ExecutionContext) => Response | Promise<Response>;",
|
|
6997
|
+
"",
|
|
6607
6998
|
"export default {",
|
|
6608
6999
|
" fetch: runtime.worker((request, env, ctx, rt) => {",
|
|
6609
7000
|
" const bindings = env as Env & { __rt?: TopoloRuntimeContext<Env> };",
|
|
6610
7001
|
" bindings.__rt = rt;",
|
|
6611
|
-
" return
|
|
7002
|
+
" return securedFetch(request, bindings, ctx as ExecutionContext);",
|
|
6612
7003
|
" }),",
|
|
6613
7004
|
"};",
|
|
6614
7005
|
"",
|
|
@@ -6961,9 +7352,12 @@ function topoloAppManifestJson(application, options) {
|
|
|
6961
7352
|
$schema: "./../../topolo-platform/tooling/app-manifest-schema.json",
|
|
6962
7353
|
name: `topolo-${slug}`,
|
|
6963
7354
|
appId: application.appId,
|
|
6964
|
-
primaryTarget: "web",
|
|
6965
7355
|
compatibilityDate: CLOUDFLARE_COMPATIBILITY_DATE,
|
|
6966
7356
|
environments: {
|
|
7357
|
+
development: {
|
|
7358
|
+
accountId: DEVELOPMENT_CLOUDFLARE_ACCOUNT_ID,
|
|
7359
|
+
domain: "topolo.dev"
|
|
7360
|
+
},
|
|
6967
7361
|
staging: {
|
|
6968
7362
|
accountId: STAGING_CLOUDFLARE_ACCOUNT_ID,
|
|
6969
7363
|
domain: "stg.topolo.us"
|
|
@@ -7004,6 +7398,15 @@ function cloudcontrolJson(application, options) {
|
|
|
7004
7398
|
resource_name: resources.d1StagingName,
|
|
7005
7399
|
resource_id: "provision-after-scaffold",
|
|
7006
7400
|
migrations_dir: "migrations"
|
|
7401
|
+
},
|
|
7402
|
+
{
|
|
7403
|
+
resource_type: "d1",
|
|
7404
|
+
binding: resources.d1Binding,
|
|
7405
|
+
environment: "development",
|
|
7406
|
+
account_id: DEVELOPMENT_CLOUDFLARE_ACCOUNT_ID,
|
|
7407
|
+
resource_name: resources.d1DevelopmentName,
|
|
7408
|
+
resource_id: "provision-after-scaffold",
|
|
7409
|
+
migrations_dir: "migrations"
|
|
7007
7410
|
}
|
|
7008
7411
|
);
|
|
7009
7412
|
}
|
|
@@ -7624,6 +8027,74 @@ function envExample(application) {
|
|
|
7624
8027
|
function generateOpaqueAppId() {
|
|
7625
8028
|
return `app_${randomBytes(9).toString("base64url")}`;
|
|
7626
8029
|
}
|
|
8030
|
+
function standardScriptStub() {
|
|
8031
|
+
return `#!/usr/bin/env python3
|
|
8032
|
+
"""Canonical-fetching stub for .github/scripts/standard_*.py.
|
|
8033
|
+
|
|
8034
|
+
Replaces a vendored copy. At runtime, finds the canonical sibling in
|
|
8035
|
+
\`<repo-parent>/topolo-platform/tooling/<name>.py\` and execs it, passing argv
|
|
8036
|
+
through, so every consumer runs the current platform script on every
|
|
8037
|
+
invocation (no drifted per-app copies).
|
|
8038
|
+
|
|
8039
|
+
Override: set TOPOLO_PLATFORM_DIR to the absolute path of the topolo-platform
|
|
8040
|
+
repo when the canonical is in a non-default checkout layout (e.g. CI). If no
|
|
8041
|
+
canonical can be found, exits 2 with a clear message rather than silently
|
|
8042
|
+
falling back.
|
|
8043
|
+
"""
|
|
8044
|
+
from __future__ import annotations
|
|
8045
|
+
|
|
8046
|
+
import os
|
|
8047
|
+
import sys
|
|
8048
|
+
|
|
8049
|
+
STUB_NAME = os.path.basename(__file__) # standard_ci.py OR standard_deploy.py
|
|
8050
|
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
8051
|
+
|
|
8052
|
+
|
|
8053
|
+
def find_canonical() -> str | None:
|
|
8054
|
+
# 1. Explicit override (CI runners with non-standard layouts).
|
|
8055
|
+
override = os.environ.get('TOPOLO_PLATFORM_DIR')
|
|
8056
|
+
if override:
|
|
8057
|
+
path = os.path.join(override, 'tooling', STUB_NAME)
|
|
8058
|
+
if os.path.exists(path):
|
|
8059
|
+
return path
|
|
8060
|
+
# 2. Walk up looking for a sibling topolo-platform/tooling/<name>.py.
|
|
8061
|
+
cursor = HERE
|
|
8062
|
+
for _ in range(10):
|
|
8063
|
+
candidate = os.path.normpath(os.path.join(cursor, '..', 'topolo-platform', 'tooling', STUB_NAME))
|
|
8064
|
+
if os.path.exists(candidate):
|
|
8065
|
+
return candidate
|
|
8066
|
+
parent = os.path.dirname(cursor)
|
|
8067
|
+
if parent == cursor:
|
|
8068
|
+
break
|
|
8069
|
+
cursor = parent
|
|
8070
|
+
# 3. node_modules-based delivery (future): @topolo-io/standard-ci.
|
|
8071
|
+
cursor = HERE
|
|
8072
|
+
for _ in range(10):
|
|
8073
|
+
candidate = os.path.normpath(os.path.join(cursor, 'node_modules', '@topolo-io', 'standard-ci', 'bin', STUB_NAME))
|
|
8074
|
+
if os.path.exists(candidate):
|
|
8075
|
+
return candidate
|
|
8076
|
+
parent = os.path.dirname(cursor)
|
|
8077
|
+
if parent == cursor:
|
|
8078
|
+
break
|
|
8079
|
+
cursor = parent
|
|
8080
|
+
return None
|
|
8081
|
+
|
|
8082
|
+
|
|
8083
|
+
def main() -> None:
|
|
8084
|
+
canonical = find_canonical()
|
|
8085
|
+
if not canonical:
|
|
8086
|
+
sys.stderr.write(
|
|
8087
|
+
f'[topolo-canonical-stub] could not locate canonical {STUB_NAME}.\\n'
|
|
8088
|
+
f' Set TOPOLO_PLATFORM_DIR or checkout topolo-platform as a sibling of this app.\\n'
|
|
8089
|
+
)
|
|
8090
|
+
sys.exit(2)
|
|
8091
|
+
os.execv(sys.executable, [sys.executable, canonical, *sys.argv[1:]])
|
|
8092
|
+
|
|
8093
|
+
|
|
8094
|
+
if __name__ == '__main__':
|
|
8095
|
+
main()
|
|
8096
|
+
`;
|
|
8097
|
+
}
|
|
7627
8098
|
function applicationScaffoldPayload(application, options = {}) {
|
|
7628
8099
|
const target = normalizeTarget(options.target);
|
|
7629
8100
|
const portfolio = target === "platform" ? normalizePortfolio(options.portfolio) : null;
|
|
@@ -7642,6 +8113,9 @@ function applicationScaffoldPayload(application, options = {}) {
|
|
|
7642
8113
|
const containerAppId = normalizeContainerAppId(options.containerAppId, surfaceModel);
|
|
7643
8114
|
const profile = normalizeScaffoldProfile(options.profile);
|
|
7644
8115
|
const resources = resolveScaffoldResources(profile, options);
|
|
8116
|
+
if (target === "platform" && api && !profile) {
|
|
8117
|
+
resources.d1 = true;
|
|
8118
|
+
}
|
|
7645
8119
|
const ownerType = target === "platform" ? "topolo_first_party" : "third_party";
|
|
7646
8120
|
const distributionModel = target === "platform" ? "marketplace" : "organization_internal";
|
|
7647
8121
|
const appDir = options.appDir ?? catalogApplication?.appDir ?? (target === "platform" ? `Topolo${toPascalCase(id)}` : `topolo-${slug}`);
|
|
@@ -7788,6 +8262,28 @@ function scaffoldCapabilities(options) {
|
|
|
7788
8262
|
function scaffoldFiles(options) {
|
|
7789
8263
|
const { application, target } = options;
|
|
7790
8264
|
const slug = toSlug(application.id);
|
|
8265
|
+
const tablePrefix = slug.replace(/-/g, "_");
|
|
8266
|
+
const tenantColumn = options.payload.tenancy === "household" ? "household_id" : "organization_id";
|
|
8267
|
+
const workspacesMigrationSql = [
|
|
8268
|
+
`CREATE TABLE IF NOT EXISTS ${tablePrefix}_workspaces (`,
|
|
8269
|
+
" id TEXT PRIMARY KEY,",
|
|
8270
|
+
` ${tenantColumn} TEXT NOT NULL,`,
|
|
8271
|
+
" name TEXT NOT NULL,",
|
|
8272
|
+
" slug TEXT NOT NULL,",
|
|
8273
|
+
" created_by_user_id TEXT,",
|
|
8274
|
+
" created_by_email TEXT,",
|
|
8275
|
+
" created_at TEXT NOT NULL,",
|
|
8276
|
+
" updated_at TEXT NOT NULL,",
|
|
8277
|
+
` UNIQUE(${tenantColumn}, slug)`,
|
|
8278
|
+
");",
|
|
8279
|
+
"",
|
|
8280
|
+
`CREATE INDEX IF NOT EXISTS idx_${tablePrefix}_workspaces_${tenantColumn}`,
|
|
8281
|
+
` ON ${tablePrefix}_workspaces(${tenantColumn});`,
|
|
8282
|
+
"",
|
|
8283
|
+
`CREATE INDEX IF NOT EXISTS idx_${tablePrefix}_workspaces_slug`,
|
|
8284
|
+
` ON ${tablePrefix}_workspaces(${tenantColumn}, slug);`,
|
|
8285
|
+
""
|
|
8286
|
+
].join("\n");
|
|
7791
8287
|
const base = target === "platform" ? firstPartyAppPath(application.appDir, options.payload.audience) : application.appDir;
|
|
7792
8288
|
const browserBase = target === "platform" ? `${base}/apps/web` : base;
|
|
7793
8289
|
const apiBase = target === "platform" ? `${base}/apps/api` : base;
|
|
@@ -7845,15 +8341,6 @@ function scaffoldFiles(options) {
|
|
|
7845
8341
|
resources: options.payload.resources
|
|
7846
8342
|
})
|
|
7847
8343
|
},
|
|
7848
|
-
{
|
|
7849
|
-
path: `${base}/tsconfig.json`,
|
|
7850
|
-
purpose: "TypeScript configuration for the generated app.",
|
|
7851
|
-
requiredSignals: [
|
|
7852
|
-
"uses strict mode",
|
|
7853
|
-
"includes browser DOM types for React and Worker types for API-backed apps"
|
|
7854
|
-
],
|
|
7855
|
-
content: tsconfigJson(options.api, target)
|
|
7856
|
-
},
|
|
7857
8344
|
{
|
|
7858
8345
|
path: `${browserBase}/vite.config.ts`,
|
|
7859
8346
|
purpose: "Vite browser build configuration.",
|
|
@@ -7911,6 +8398,31 @@ function scaffoldFiles(options) {
|
|
|
7911
8398
|
content: stylesCss()
|
|
7912
8399
|
}
|
|
7913
8400
|
];
|
|
8401
|
+
if (target === "platform") {
|
|
8402
|
+
if (options.browser) {
|
|
8403
|
+
files.push({
|
|
8404
|
+
path: `${browserBase}/tsconfig.json`,
|
|
8405
|
+
purpose: "TypeScript configuration for the browser surface (DOM + vite types).",
|
|
8406
|
+
requiredSignals: ["uses strict mode", "includes browser DOM types for React"],
|
|
8407
|
+
content: webTsconfigJson()
|
|
8408
|
+
});
|
|
8409
|
+
}
|
|
8410
|
+
if (options.api) {
|
|
8411
|
+
files.push({
|
|
8412
|
+
path: `${apiBase}/tsconfig.json`,
|
|
8413
|
+
purpose: "TypeScript configuration for the API worker surface (Workers types).",
|
|
8414
|
+
requiredSignals: ["uses strict mode", "includes Cloudflare Workers types"],
|
|
8415
|
+
content: apiTsconfigJson()
|
|
8416
|
+
});
|
|
8417
|
+
}
|
|
8418
|
+
} else {
|
|
8419
|
+
files.push({
|
|
8420
|
+
path: `${base}/tsconfig.json`,
|
|
8421
|
+
purpose: "TypeScript configuration for the generated app.",
|
|
8422
|
+
requiredSignals: ["uses strict mode", "includes browser DOM types for React and Worker types for API-backed apps"],
|
|
8423
|
+
content: tsconfigJson(options.api, target)
|
|
8424
|
+
});
|
|
8425
|
+
}
|
|
7914
8426
|
if (target === "platform") {
|
|
7915
8427
|
files.push(
|
|
7916
8428
|
{
|
|
@@ -7951,6 +8463,33 @@ function scaffoldFiles(options) {
|
|
|
7951
8463
|
],
|
|
7952
8464
|
content: options.ciWorkflow ?? ""
|
|
7953
8465
|
},
|
|
8466
|
+
{
|
|
8467
|
+
path: `${base}/.github/scripts/standard_deploy.py`,
|
|
8468
|
+
purpose: "Canonical-fetching deploy stub (execs topolo-platform/tooling/standard_deploy.py).",
|
|
8469
|
+
requiredSignals: [
|
|
8470
|
+
"never a vendored copy of the canonical script",
|
|
8471
|
+
"runs the current platform standard_deploy.py on every invocation"
|
|
8472
|
+
],
|
|
8473
|
+
content: standardScriptStub()
|
|
8474
|
+
},
|
|
8475
|
+
{
|
|
8476
|
+
path: `${base}/.github/scripts/standard_ci.py`,
|
|
8477
|
+
purpose: "Canonical-fetching CI stub (execs topolo-platform/tooling/standard_ci.py).",
|
|
8478
|
+
requiredSignals: [
|
|
8479
|
+
"never a vendored copy of the canonical script",
|
|
8480
|
+
"runs the current platform standard_ci.py on every invocation"
|
|
8481
|
+
],
|
|
8482
|
+
content: standardScriptStub()
|
|
8483
|
+
},
|
|
8484
|
+
{
|
|
8485
|
+
path: `${base}/.github/scripts/standard_ops.py`,
|
|
8486
|
+
purpose: "Canonical-fetching ops stub (execs topolo-platform/tooling/standard_ops.py).",
|
|
8487
|
+
requiredSignals: [
|
|
8488
|
+
"never a vendored copy of the canonical script",
|
|
8489
|
+
"runs the current platform standard_ops.py on every invocation"
|
|
8490
|
+
],
|
|
8491
|
+
content: standardScriptStub()
|
|
8492
|
+
},
|
|
7954
8493
|
{
|
|
7955
8494
|
path: `system-apps/TopoloDocs/apps/web/src/content/systems/topolo-${slug}.json`,
|
|
7956
8495
|
purpose: "Canonical system registry entry.",
|
|
@@ -8074,6 +8613,25 @@ function scaffoldFiles(options) {
|
|
|
8074
8613
|
portfolio: options.payload.portfolio
|
|
8075
8614
|
})
|
|
8076
8615
|
});
|
|
8616
|
+
if (target === "platform") {
|
|
8617
|
+
files.push({
|
|
8618
|
+
path: `${browserBase}/vitest.config.ts`,
|
|
8619
|
+
purpose: "Vitest config for the browser surface.",
|
|
8620
|
+
requiredSignals: [
|
|
8621
|
+
"runs component/unit tests under jsdom",
|
|
8622
|
+
"keeps the generated browser surface testable from day one"
|
|
8623
|
+
],
|
|
8624
|
+
content: `import { defineConfig } from 'vitest/config';
|
|
8625
|
+
|
|
8626
|
+
export default defineConfig({
|
|
8627
|
+
test: {
|
|
8628
|
+
include: ['test/**/*.test.{ts,tsx}'],
|
|
8629
|
+
environment: 'jsdom',
|
|
8630
|
+
},
|
|
8631
|
+
});
|
|
8632
|
+
`
|
|
8633
|
+
});
|
|
8634
|
+
}
|
|
8077
8635
|
}
|
|
8078
8636
|
if (options.api) {
|
|
8079
8637
|
files.push(
|
|
@@ -8130,8 +8688,45 @@ function scaffoldFiles(options) {
|
|
|
8130
8688
|
"binds the frontend Worker to the API Worker for /api/* requests"
|
|
8131
8689
|
],
|
|
8132
8690
|
content: platformWebWranglerToml(application)
|
|
8691
|
+
},
|
|
8692
|
+
{
|
|
8693
|
+
path: `${apiBase}/vitest.config.ts`,
|
|
8694
|
+
purpose: "Vitest config for the API worker surface.",
|
|
8695
|
+
requiredSignals: [
|
|
8696
|
+
"runs worker/unit tests under the node environment",
|
|
8697
|
+
"keeps the generated service testable from day one"
|
|
8698
|
+
],
|
|
8699
|
+
content: `import { defineConfig } from 'vitest/config';
|
|
8700
|
+
|
|
8701
|
+
export default defineConfig({
|
|
8702
|
+
test: {
|
|
8703
|
+
include: ['test/**/*.test.{js,mjs,mts,ts,tsx}'],
|
|
8704
|
+
environment: 'node',
|
|
8705
|
+
},
|
|
8706
|
+
});
|
|
8707
|
+
`
|
|
8708
|
+
},
|
|
8709
|
+
{
|
|
8710
|
+
path: `${apiBase}/test/health.test.ts`,
|
|
8711
|
+
purpose: "Smoke test so the API test suite is non-empty from scaffold.",
|
|
8712
|
+
requiredSignals: [
|
|
8713
|
+
"asserts the scaffold boots",
|
|
8714
|
+
"is replaced by real route/repository tests as the surface grows"
|
|
8715
|
+
],
|
|
8716
|
+
content: "import { describe, expect, it } from 'vitest';\n\n// Smoke test so the API test suite is non-empty from day one. Replace with\n// real route/repository tests as the surface grows.\ndescribe('api scaffold', () => {\n it('boots', () => {\n expect(true).toBe(true);\n });\n});\n"
|
|
8133
8717
|
}
|
|
8134
8718
|
);
|
|
8719
|
+
if (options.payload.resources.d1) {
|
|
8720
|
+
files.push({
|
|
8721
|
+
path: `${apiBase}/migrations/0001_initial.sql`,
|
|
8722
|
+
purpose: "Canonical multi-tenant baseline: the app workspaces table.",
|
|
8723
|
+
requiredSignals: [
|
|
8724
|
+
`${tablePrefix}_workspaces carries ${tenantColumn} on every row`,
|
|
8725
|
+
"every domain table added later carries the tenant + workspace columns"
|
|
8726
|
+
],
|
|
8727
|
+
content: workspacesMigrationSql
|
|
8728
|
+
});
|
|
8729
|
+
}
|
|
8135
8730
|
}
|
|
8136
8731
|
}
|
|
8137
8732
|
return files;
|
|
@@ -8372,15 +8967,20 @@ var CLOUDCONTROL_LOCAL_APPLICATION = {
|
|
|
8372
8967
|
deployTargets: []
|
|
8373
8968
|
};
|
|
8374
8969
|
function registerAppsCommand(program2) {
|
|
8375
|
-
const apps = program2.command("apps").description("
|
|
8970
|
+
const apps = program2.command("apps").description("Search Topolo platform applications available to the current credential").option("-q, --query <query>", "Filter by app id, slug, or name").option("--cursor <cursor>", "Continuation cursor from the previous page").option("--limit <limit>", "Page size (1-100)", "25").option("--json", "Output JSON", true).option("--no-json", "Output human-readable table").action(async (opts) => {
|
|
8376
8971
|
const mode = resolveOutputMode(opts.json);
|
|
8377
8972
|
try {
|
|
8378
8973
|
const topolo = await buildClient();
|
|
8379
8974
|
if (!topolo) {
|
|
8380
8975
|
throw new Error("No credential configured. Run `topolo auth login` or set TOPOLO_API_KEY.");
|
|
8381
8976
|
}
|
|
8382
|
-
const
|
|
8383
|
-
|
|
8977
|
+
const result = await topolo.client.searchServices({
|
|
8978
|
+
query: opts.query,
|
|
8979
|
+
cursor: opts.cursor,
|
|
8980
|
+
limit: parsePageSize2(opts.limit)
|
|
8981
|
+
});
|
|
8982
|
+
const rows = applicationRowsFromCatalog(result.services);
|
|
8983
|
+
emit({ applications: rows, page: result.page }, mode, () => formatRows(rows));
|
|
8384
8984
|
} catch (err) {
|
|
8385
8985
|
fail(err, mode);
|
|
8386
8986
|
}
|
|
@@ -8392,7 +8992,8 @@ function registerAppsCommand(program2) {
|
|
|
8392
8992
|
if (!topolo) {
|
|
8393
8993
|
throw new Error("No credential configured. Run `topolo auth login` or set TOPOLO_API_KEY.");
|
|
8394
8994
|
}
|
|
8395
|
-
const
|
|
8995
|
+
const entry = await topolo.client.getService(application);
|
|
8996
|
+
const app = applicationRowsFromCatalog([entry])[0];
|
|
8396
8997
|
emit({ application: app }, mode, () => formatApplicationRow(app));
|
|
8397
8998
|
} catch (err) {
|
|
8398
8999
|
fail(err, mode);
|
|
@@ -8512,16 +9113,12 @@ function applicationRowsFromCatalog(catalog) {
|
|
|
8512
9113
|
deployTargets: 0
|
|
8513
9114
|
})).sort((left, right) => left.id.localeCompare(right.id));
|
|
8514
9115
|
}
|
|
8515
|
-
function
|
|
8516
|
-
const
|
|
8517
|
-
|
|
8518
|
-
|
|
8519
|
-
(row) => row.id.toLowerCase() === needle || row.name.toLowerCase() === needle || row.services.some((service) => service.toLowerCase() === needle)
|
|
8520
|
-
);
|
|
8521
|
-
if (!match) {
|
|
8522
|
-
throw new Error(`Unknown application "${application}". Run \`topolo apps --json\` to discover applications available to this credential.`);
|
|
9116
|
+
function parsePageSize2(value) {
|
|
9117
|
+
const parsed = Number.parseInt(value || "25", 10);
|
|
9118
|
+
if (!Number.isFinite(parsed) || parsed < 1 || parsed > 100) {
|
|
9119
|
+
throw new Error("--limit must be an integer between 1 and 100");
|
|
8523
9120
|
}
|
|
8524
|
-
return
|
|
9121
|
+
return parsed;
|
|
8525
9122
|
}
|
|
8526
9123
|
function resolveApplication(application) {
|
|
8527
9124
|
const id = application.trim().toLowerCase();
|
|
@@ -9074,7 +9671,7 @@ async function requirePlatformAdminSession() {
|
|
|
9074
9671
|
|
|
9075
9672
|
// src/commands/services.ts
|
|
9076
9673
|
function registerServicesCommand(program2) {
|
|
9077
|
-
program2.command("services").description("
|
|
9674
|
+
program2.command("services").description("Search Topolo services the SDK can route to").option("-q, --query <query>", "Filter by app id, slug, or name").option("--cursor <cursor>", "Continuation cursor from the previous page").option("--limit <limit>", "Page size (1-100)", "25").option("--json", "Output JSON", true).option("--no-json", "Output human-readable table").action(async (opts) => {
|
|
9078
9675
|
const mode = resolveOutputMode(opts.json);
|
|
9079
9676
|
try {
|
|
9080
9677
|
const environment = selectedEnvironment();
|
|
@@ -9083,8 +9680,13 @@ function registerServicesCommand(program2) {
|
|
|
9083
9680
|
if (!topolo) {
|
|
9084
9681
|
throw new Error("No credential configured. Run `topolo auth login` or set TOPOLO_API_KEY.");
|
|
9085
9682
|
}
|
|
9086
|
-
const
|
|
9087
|
-
|
|
9683
|
+
const result = await topolo.client.searchServices({
|
|
9684
|
+
query: opts.query,
|
|
9685
|
+
cursor: opts.cursor,
|
|
9686
|
+
limit: parsePageSize3(opts.limit)
|
|
9687
|
+
});
|
|
9688
|
+
const rows = buildRows(result.services, config.serviceUrls, environment);
|
|
9689
|
+
emit({ environment, services: rows, page: result.page }, mode, () => formatRows2(rows, environment, result.page.nextCursor));
|
|
9088
9690
|
} catch (err) {
|
|
9089
9691
|
fail(err, mode);
|
|
9090
9692
|
}
|
|
@@ -9111,7 +9713,7 @@ function buildRows(catalog, overrides, environment = selectedEnvironment()) {
|
|
|
9111
9713
|
};
|
|
9112
9714
|
}).sort((left, right) => left.id.localeCompare(right.id));
|
|
9113
9715
|
}
|
|
9114
|
-
function formatRows2(rows, environment = selectedEnvironment()) {
|
|
9716
|
+
function formatRows2(rows, environment = selectedEnvironment(), nextCursor = null) {
|
|
9115
9717
|
const idWidth = Math.max(3, ...rows.map((r) => r.id.length));
|
|
9116
9718
|
const urlWidth = Math.max(7, ...rows.map((r) => (r.effectiveUrl ?? "-").length));
|
|
9117
9719
|
const lines = [];
|
|
@@ -9126,8 +9728,14 @@ function formatRows2(rows, environment = selectedEnvironment()) {
|
|
|
9126
9728
|
`${r.id.padEnd(idWidth)} ${effectiveUrl.padEnd(urlWidth)} ${r.overridden ? "yes" : "".padEnd(8)} ${r.environmentSafe ? "yes" : "no"} ${apiKeyResource}`
|
|
9127
9729
|
);
|
|
9128
9730
|
}
|
|
9731
|
+
if (nextCursor) lines.push("", `Next cursor: ${nextCursor}`);
|
|
9129
9732
|
return lines.join("\n");
|
|
9130
9733
|
}
|
|
9734
|
+
function parsePageSize3(value) {
|
|
9735
|
+
const parsed = Number.parseInt(value || "25", 10);
|
|
9736
|
+
if (!Number.isFinite(parsed) || parsed < 1 || parsed > 100) throw new Error("--limit must be an integer between 1 and 100");
|
|
9737
|
+
return parsed;
|
|
9738
|
+
}
|
|
9131
9739
|
function isStagingUrl(value) {
|
|
9132
9740
|
const url = value.toLowerCase();
|
|
9133
9741
|
return url.includes(".stg.topolo.us") || url.includes("localhost") || url.includes("127.0.0.1") || url.includes("topolo-staging");
|
|
@@ -9900,6 +10508,162 @@ function formatStatus(rows) {
|
|
|
9900
10508
|
).join("\n");
|
|
9901
10509
|
}
|
|
9902
10510
|
|
|
10511
|
+
// src/commands/context.ts
|
|
10512
|
+
function makeClient(credential, serviceUrls) {
|
|
10513
|
+
return createTopolo({
|
|
10514
|
+
credential,
|
|
10515
|
+
agent: { clientName: "topolo-cli", clientVersion: CLI_VERSION },
|
|
10516
|
+
serviceUrls
|
|
10517
|
+
});
|
|
10518
|
+
}
|
|
10519
|
+
function unwrap(res) {
|
|
10520
|
+
const r = res ?? {};
|
|
10521
|
+
return r.data ?? r;
|
|
10522
|
+
}
|
|
10523
|
+
async function fetchMe(credential, serviceUrls) {
|
|
10524
|
+
const topolo = makeClient(credential, serviceUrls);
|
|
10525
|
+
const data = unwrap(await topolo.client.request({ service: "auth", method: "GET", path: "/api/auth/me" }));
|
|
10526
|
+
const user = data.user ?? {};
|
|
10527
|
+
const rawMemberships = user.organizationMemberships || data.organizations || [];
|
|
10528
|
+
const memberships = rawMemberships.filter((m) => m && m.id);
|
|
10529
|
+
const activeType = user.orgId ? "organization" : "personal";
|
|
10530
|
+
return { memberships, activeType, activeOrgId: user.orgId || void 0 };
|
|
10531
|
+
}
|
|
10532
|
+
function credentialFromTokens(tokens) {
|
|
10533
|
+
const accessToken = tokens.accessToken || tokens.access_token;
|
|
10534
|
+
const refreshToken = tokens.refreshToken || tokens.refresh_token;
|
|
10535
|
+
const expiresIn = tokens.expiresIn || tokens.expires_in;
|
|
10536
|
+
const expiresAtRaw = tokens.expiresAt || tokens.expires_at;
|
|
10537
|
+
return {
|
|
10538
|
+
kind: "access_token",
|
|
10539
|
+
accessToken,
|
|
10540
|
+
...refreshToken ? { refreshToken } : {},
|
|
10541
|
+
...expiresAtRaw ? { expiresAt: expiresAtRaw } : expiresIn ? { expiresAt: Math.floor(Date.now() / 1e3) + expiresIn } : {}
|
|
10542
|
+
};
|
|
10543
|
+
}
|
|
10544
|
+
async function exchangeForContext(credential, serviceUrls, target) {
|
|
10545
|
+
const topolo = makeClient(credential, serviceUrls);
|
|
10546
|
+
const data = unwrap(
|
|
10547
|
+
await topolo.client.request({
|
|
10548
|
+
service: "auth",
|
|
10549
|
+
method: "PUT",
|
|
10550
|
+
path: "/api/me/active-context",
|
|
10551
|
+
body: {
|
|
10552
|
+
contextType: target.type,
|
|
10553
|
+
contextId: target.type === "organization" ? target.id : null
|
|
10554
|
+
},
|
|
10555
|
+
confirm: true
|
|
10556
|
+
})
|
|
10557
|
+
);
|
|
10558
|
+
const tokens = data.tokens || {};
|
|
10559
|
+
return credentialFromTokens(tokens);
|
|
10560
|
+
}
|
|
10561
|
+
function labelFor(ctx) {
|
|
10562
|
+
if (ctx.contextType === "personal") return "Personal";
|
|
10563
|
+
return ctx.organizationName || ctx.organizationSlug || ctx.organizationId || "organization";
|
|
10564
|
+
}
|
|
10565
|
+
function registerContextCommand(program2) {
|
|
10566
|
+
const context = program2.command("context").description("Show and switch the active context (personal or an organization)");
|
|
10567
|
+
context.command("ls", { isDefault: true }).alias("list").description("List the contexts you can act in and which is active").option("--json", "Output JSON", true).option("--no-json", "Output human-readable text").action(async (opts) => {
|
|
10568
|
+
const mode = resolveOutputMode(opts.json);
|
|
10569
|
+
try {
|
|
10570
|
+
const environment = selectedEnvironment();
|
|
10571
|
+
const config = await readConfig();
|
|
10572
|
+
const profile = profileForEnvironment(config, environment);
|
|
10573
|
+
const stored = profile.contexts ?? {};
|
|
10574
|
+
const activeKey = profile.activeContext;
|
|
10575
|
+
const rows = /* @__PURE__ */ new Map();
|
|
10576
|
+
for (const [key, ctx] of Object.entries(stored)) {
|
|
10577
|
+
rows.set(key, { key, type: ctx.contextType, label: labelFor(ctx), signedIn: true, active: key === activeKey });
|
|
10578
|
+
}
|
|
10579
|
+
const cred = await refreshCredentialIfNeeded(environment);
|
|
10580
|
+
if (cred) {
|
|
10581
|
+
try {
|
|
10582
|
+
const me = await fetchMe(cred, resolvedProfileConfig(config, environment).serviceUrls);
|
|
10583
|
+
const personalKey = contextKey("personal");
|
|
10584
|
+
if (!rows.has(personalKey)) {
|
|
10585
|
+
rows.set(personalKey, { key: personalKey, type: "personal", label: "Personal", signedIn: false, active: personalKey === activeKey });
|
|
10586
|
+
}
|
|
10587
|
+
for (const m of me.memberships) {
|
|
10588
|
+
const key = contextKey("organization", m.id);
|
|
10589
|
+
if (!rows.has(key)) {
|
|
10590
|
+
rows.set(key, { key, type: "organization", label: m.name || m.slug || m.id, signedIn: false, active: key === activeKey });
|
|
10591
|
+
}
|
|
10592
|
+
}
|
|
10593
|
+
} catch {
|
|
10594
|
+
}
|
|
10595
|
+
}
|
|
10596
|
+
const list = [...rows.values()];
|
|
10597
|
+
emit({ environment, active: activeKey ?? null, contexts: list }, mode, () => {
|
|
10598
|
+
if (list.length === 0) return "No contexts yet. Run `topolo auth login` first.";
|
|
10599
|
+
const lines = [`Contexts (${environmentLabel(environment)}):`, ""];
|
|
10600
|
+
for (const r of list) {
|
|
10601
|
+
const marks = [r.active ? "active" : "", r.signedIn ? "signed in" : "available"].filter(Boolean).join(", ");
|
|
10602
|
+
lines.push(` ${r.active ? "*" : " "} ${r.label} (${r.type}) [${marks}]`);
|
|
10603
|
+
}
|
|
10604
|
+
lines.push("", "Switch with: topolo context use <organization|personal>");
|
|
10605
|
+
return lines.join("\n");
|
|
10606
|
+
});
|
|
10607
|
+
} catch (err) {
|
|
10608
|
+
fail(err, mode);
|
|
10609
|
+
}
|
|
10610
|
+
});
|
|
10611
|
+
context.command("use <target>").description('Switch the active context (an organization name/slug, or "personal")').option("--json", "Output JSON", true).option("--no-json", "Output human-readable text").action(async (target, opts) => {
|
|
10612
|
+
const mode = resolveOutputMode(opts.json);
|
|
10613
|
+
try {
|
|
10614
|
+
const environment = selectedEnvironment();
|
|
10615
|
+
const config = await readConfig();
|
|
10616
|
+
const profile = profileForEnvironment(config, environment);
|
|
10617
|
+
const serviceUrls = resolvedProfileConfig(config, environment).serviceUrls;
|
|
10618
|
+
const cred = await refreshCredentialIfNeeded(environment);
|
|
10619
|
+
if (!cred) {
|
|
10620
|
+
throw new Error("Not signed in. Run `topolo auth login` first.");
|
|
10621
|
+
}
|
|
10622
|
+
const wantPersonal = /^(personal|me)$/i.test(target.trim());
|
|
10623
|
+
let resolved;
|
|
10624
|
+
if (wantPersonal) {
|
|
10625
|
+
resolved = { contextType: "personal" };
|
|
10626
|
+
} else {
|
|
10627
|
+
const me = await fetchMe(cred, serviceUrls);
|
|
10628
|
+
const needle = target.trim().toLowerCase();
|
|
10629
|
+
const match = me.memberships.find(
|
|
10630
|
+
(m) => m.id === target || m.slug?.toLowerCase() === needle || m.name?.toLowerCase() === needle
|
|
10631
|
+
);
|
|
10632
|
+
if (!match) {
|
|
10633
|
+
const names = me.memberships.map((m) => m.slug || m.name || m.id);
|
|
10634
|
+
throw new Error(
|
|
10635
|
+
`You are not a member of "${target}". Available: personal${names.length ? ", " + names.join(", ") : ""}.`
|
|
10636
|
+
);
|
|
10637
|
+
}
|
|
10638
|
+
resolved = {
|
|
10639
|
+
contextType: "organization",
|
|
10640
|
+
organizationId: match.id,
|
|
10641
|
+
organizationSlug: match.slug,
|
|
10642
|
+
organizationName: match.name
|
|
10643
|
+
};
|
|
10644
|
+
}
|
|
10645
|
+
const key = contextKey(resolved.contextType, resolved.organizationId);
|
|
10646
|
+
const existing = profile.contexts?.[key];
|
|
10647
|
+
const newCredential = await exchangeForContext(cred, serviceUrls, {
|
|
10648
|
+
type: resolved.contextType,
|
|
10649
|
+
id: resolved.organizationId
|
|
10650
|
+
});
|
|
10651
|
+
resolved.credential = newCredential;
|
|
10652
|
+
resolved.oauthClientId = existing?.oauthClientId ?? profile.oauthClientId;
|
|
10653
|
+
const updated = writeContextCredential(config, environment, resolved, true);
|
|
10654
|
+
await writeConfig(updated);
|
|
10655
|
+
emit(
|
|
10656
|
+
{ environment, active: key, context: { type: resolved.contextType, label: labelFor(resolved) } },
|
|
10657
|
+
mode,
|
|
10658
|
+
() => `
|
|
10659
|
+
\u2713 Now acting as ${labelFor(resolved)} (${resolved.contextType}).`
|
|
10660
|
+
);
|
|
10661
|
+
} catch (err) {
|
|
10662
|
+
fail(err, mode);
|
|
10663
|
+
}
|
|
10664
|
+
});
|
|
10665
|
+
}
|
|
10666
|
+
|
|
9903
10667
|
// src/index.ts
|
|
9904
10668
|
var program = new Command();
|
|
9905
10669
|
var argv;
|
|
@@ -9929,6 +10693,7 @@ registerPlatformCommand(program);
|
|
|
9929
10693
|
registerServicesCommand(program);
|
|
9930
10694
|
registerSeedCommands(program);
|
|
9931
10695
|
registerSkillsCommand(program);
|
|
10696
|
+
registerContextCommand(program);
|
|
9932
10697
|
program.parseAsync(argv).catch((err) => {
|
|
9933
10698
|
process.stderr.write(`Unexpected error: ${err instanceof Error ? err.message : String(err)}
|
|
9934
10699
|
`);
|