@topolo/cli 0.5.2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/generate-wrangler.mjs +4 -1
- package/dist/index.js +1111 -302
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -9,7 +9,7 @@ import { platform as platform2 } from "os";
|
|
|
9
9
|
import { createInterface } from "readline/promises";
|
|
10
10
|
import { stdin as input } from "process";
|
|
11
11
|
|
|
12
|
-
//
|
|
12
|
+
// node_modules/@topolo/sdk/dist/index.js
|
|
13
13
|
function applyAuthHeaders(headers, credential) {
|
|
14
14
|
if (credential.kind === "api_key") {
|
|
15
15
|
headers.set("X-Api-Key", credential.apiKey);
|
|
@@ -61,13 +61,13 @@ var TopoloHttpError = class extends TopoloSdkError {
|
|
|
61
61
|
body;
|
|
62
62
|
service;
|
|
63
63
|
path;
|
|
64
|
-
constructor(service,
|
|
65
|
-
super("http_error", message ?? `HTTP ${status} from ${service}${
|
|
64
|
+
constructor(service, path8, status, body, message) {
|
|
65
|
+
super("http_error", message ?? `HTTP ${status} from ${service}${path8}`);
|
|
66
66
|
this.name = "TopoloHttpError";
|
|
67
67
|
this.status = status;
|
|
68
68
|
this.body = body;
|
|
69
69
|
this.service = service;
|
|
70
|
-
this.path =
|
|
70
|
+
this.path = path8;
|
|
71
71
|
}
|
|
72
72
|
};
|
|
73
73
|
var APP_API_URLS = {
|
|
@@ -185,7 +185,7 @@ var APPLICATIONS = {
|
|
|
185
185
|
"appDir": "TopoloAuth",
|
|
186
186
|
"name": "topolo-auth",
|
|
187
187
|
"packageName": "topolo-auth",
|
|
188
|
-
"productionUrl": "https://auth.topolo.app",
|
|
188
|
+
"productionUrl": "https://auth.topolo.app/login",
|
|
189
189
|
"services": [
|
|
190
190
|
"auth"
|
|
191
191
|
],
|
|
@@ -273,7 +273,9 @@ var APPLICATIONS = {
|
|
|
273
273
|
"domains": [
|
|
274
274
|
"brand.topolo.app"
|
|
275
275
|
],
|
|
276
|
-
"routes": [
|
|
276
|
+
"routes": [
|
|
277
|
+
"brand.topolo.app"
|
|
278
|
+
],
|
|
277
279
|
"appId": "brand"
|
|
278
280
|
},
|
|
279
281
|
{
|
|
@@ -385,7 +387,9 @@ var APPLICATIONS = {
|
|
|
385
387
|
"domains": [
|
|
386
388
|
"campaigns.topolo.app"
|
|
387
389
|
],
|
|
388
|
-
"routes": [
|
|
390
|
+
"routes": [
|
|
391
|
+
"campaigns.topolo.app"
|
|
392
|
+
],
|
|
389
393
|
"appId": "campaigns"
|
|
390
394
|
},
|
|
391
395
|
{
|
|
@@ -1073,13 +1077,11 @@ var APPLICATIONS = {
|
|
|
1073
1077
|
{
|
|
1074
1078
|
"name": "topolo-nexus-dashboard",
|
|
1075
1079
|
"kind": "worker",
|
|
1076
|
-
"apiUrl":
|
|
1080
|
+
"apiUrl": null,
|
|
1077
1081
|
"domains": [
|
|
1078
1082
|
"nexus.topolo.app"
|
|
1079
1083
|
],
|
|
1080
|
-
"routes": [
|
|
1081
|
-
"nexus.topolo.app"
|
|
1082
|
-
],
|
|
1084
|
+
"routes": [],
|
|
1083
1085
|
"appId": null
|
|
1084
1086
|
},
|
|
1085
1087
|
{
|
|
@@ -2176,17 +2178,17 @@ function validateTopoloActionInput(schema, input3) {
|
|
|
2176
2178
|
validateNode(schema, input3, "$", schema, errors, 0);
|
|
2177
2179
|
return { valid: errors.length === 0, errors };
|
|
2178
2180
|
}
|
|
2179
|
-
function inspectSchema(schema,
|
|
2180
|
-
if (depth > 128) return { path:
|
|
2181
|
+
function inspectSchema(schema, path8, root, depth) {
|
|
2182
|
+
if (depth > 128) return { path: path8, code: "schema_depth", message: "Schema recursion exceeded 128 levels." };
|
|
2181
2183
|
const unsupportedKeyword = Object.keys(schema).find((keyword) => !SUPPORTED_SCHEMA_KEYWORDS.has(keyword));
|
|
2182
2184
|
if (unsupportedKeyword) {
|
|
2183
|
-
return { path:
|
|
2185
|
+
return { path: path8, code: "unsupported_schema_keyword", message: `Action schema uses unsupported keyword ${unsupportedKeyword}.` };
|
|
2184
2186
|
}
|
|
2185
2187
|
if (typeof schema.format === "string" && !["uri", "email", "date-time"].includes(schema.format)) {
|
|
2186
|
-
return { path:
|
|
2188
|
+
return { path: path8, code: "unsupported_schema_format", message: `Action schema uses unsupported format ${schema.format}.` };
|
|
2187
2189
|
}
|
|
2188
2190
|
if (typeof schema.$ref === "string" && !resolveLocalReference(root, schema.$ref)) {
|
|
2189
|
-
return { path:
|
|
2191
|
+
return { path: path8, code: "unsupported_schema_ref", message: `Cannot resolve schema reference ${schema.$ref}.` };
|
|
2190
2192
|
}
|
|
2191
2193
|
const children = [];
|
|
2192
2194
|
if (isRecord(schema.properties)) children.push(...Object.entries(schema.properties));
|
|
@@ -2202,7 +2204,7 @@ function inspectSchema(schema, path6, root, depth) {
|
|
|
2202
2204
|
);
|
|
2203
2205
|
for (const [key, child] of children) {
|
|
2204
2206
|
if (!isRecord(child)) continue;
|
|
2205
|
-
const issue = inspectSchema(child, `${
|
|
2207
|
+
const issue = inspectSchema(child, `${path8}.${key}`, root, depth + 1);
|
|
2206
2208
|
if (issue) return issue;
|
|
2207
2209
|
}
|
|
2208
2210
|
return null;
|
|
@@ -2210,129 +2212,129 @@ function inspectSchema(schema, path6, root, depth) {
|
|
|
2210
2212
|
function arraySchemaEntries(keyword, value) {
|
|
2211
2213
|
return Array.isArray(value) ? value.map((candidate, index) => [`${keyword}[${index}]`, candidate]) : [];
|
|
2212
2214
|
}
|
|
2213
|
-
function validateNode(schema, value,
|
|
2215
|
+
function validateNode(schema, value, path8, root, errors, depth) {
|
|
2214
2216
|
if (depth > 128) {
|
|
2215
|
-
errors.push({ path:
|
|
2217
|
+
errors.push({ path: path8, code: "schema_depth", message: "Schema recursion exceeded 128 levels." });
|
|
2216
2218
|
return;
|
|
2217
2219
|
}
|
|
2218
2220
|
if (typeof schema.$ref === "string") {
|
|
2219
2221
|
const resolved = resolveLocalReference(root, schema.$ref);
|
|
2220
2222
|
if (!resolved) {
|
|
2221
|
-
errors.push({ path:
|
|
2223
|
+
errors.push({ path: path8, code: "unsupported_schema_ref", message: `Cannot resolve schema reference ${schema.$ref}.` });
|
|
2222
2224
|
return;
|
|
2223
2225
|
}
|
|
2224
|
-
validateNode(resolved, value,
|
|
2226
|
+
validateNode(resolved, value, path8, root, errors, depth + 1);
|
|
2225
2227
|
}
|
|
2226
2228
|
if (Array.isArray(schema.allOf)) {
|
|
2227
2229
|
for (const candidate of schema.allOf) {
|
|
2228
|
-
if (isRecord(candidate)) validateNode(candidate, value,
|
|
2230
|
+
if (isRecord(candidate)) validateNode(candidate, value, path8, root, errors, depth + 1);
|
|
2229
2231
|
}
|
|
2230
2232
|
}
|
|
2231
2233
|
if (Array.isArray(schema.anyOf) && !schema.anyOf.some((candidate) => candidateMatches(candidate, value, root, depth))) {
|
|
2232
|
-
errors.push({ path:
|
|
2234
|
+
errors.push({ path: path8, code: "any_of", message: "Value does not match any allowed schema." });
|
|
2233
2235
|
return;
|
|
2234
2236
|
}
|
|
2235
2237
|
if (Array.isArray(schema.oneOf)) {
|
|
2236
2238
|
const matches = schema.oneOf.filter((candidate) => candidateMatches(candidate, value, root, depth)).length;
|
|
2237
2239
|
if (matches !== 1) {
|
|
2238
|
-
errors.push({ path:
|
|
2240
|
+
errors.push({ path: path8, code: "one_of", message: "Value must match exactly one allowed schema." });
|
|
2239
2241
|
return;
|
|
2240
2242
|
}
|
|
2241
2243
|
}
|
|
2242
2244
|
if (value === null && schema.nullable === true) return;
|
|
2243
2245
|
if (Object.prototype.hasOwnProperty.call(schema, "const") && !sameJsonValue(value, schema.const)) {
|
|
2244
|
-
errors.push({ path:
|
|
2246
|
+
errors.push({ path: path8, code: "const", message: `Value must equal ${JSON.stringify(schema.const)}.` });
|
|
2245
2247
|
return;
|
|
2246
2248
|
}
|
|
2247
2249
|
if (Array.isArray(schema.enum) && !schema.enum.some((candidate) => sameJsonValue(value, candidate))) {
|
|
2248
|
-
errors.push({ path:
|
|
2250
|
+
errors.push({ path: path8, code: "enum", message: `Value must be one of ${schema.enum.map((item) => JSON.stringify(item)).join(", ")}.` });
|
|
2249
2251
|
return;
|
|
2250
2252
|
}
|
|
2251
2253
|
if (!matchesType(schema.type, value)) {
|
|
2252
|
-
errors.push({ path:
|
|
2254
|
+
errors.push({ path: path8, code: "type", message: `Expected ${String(schema.type)}.` });
|
|
2253
2255
|
return;
|
|
2254
2256
|
}
|
|
2255
|
-
if (typeof value === "string") validateString(schema, value,
|
|
2256
|
-
if (typeof value === "number") validateNumber(schema, value,
|
|
2257
|
-
if (Array.isArray(value)) validateArray(schema, value,
|
|
2258
|
-
if (isRecord(value)) validateObject(schema, value,
|
|
2257
|
+
if (typeof value === "string") validateString(schema, value, path8, errors);
|
|
2258
|
+
if (typeof value === "number") validateNumber(schema, value, path8, errors);
|
|
2259
|
+
if (Array.isArray(value)) validateArray(schema, value, path8, root, errors, depth);
|
|
2260
|
+
if (isRecord(value)) validateObject(schema, value, path8, root, errors, depth);
|
|
2259
2261
|
}
|
|
2260
|
-
function validateObject(schema, value,
|
|
2262
|
+
function validateObject(schema, value, path8, root, errors, depth) {
|
|
2261
2263
|
if (typeof schema.minProperties === "number" && Object.keys(value).length < schema.minProperties) {
|
|
2262
|
-
errors.push({ path:
|
|
2264
|
+
errors.push({ path: path8, code: "min_properties", message: `Expected at least ${schema.minProperties} properties.` });
|
|
2263
2265
|
}
|
|
2264
2266
|
if (typeof schema.maxProperties === "number" && Object.keys(value).length > schema.maxProperties) {
|
|
2265
|
-
errors.push({ path:
|
|
2267
|
+
errors.push({ path: path8, code: "max_properties", message: `Expected at most ${schema.maxProperties} properties.` });
|
|
2266
2268
|
}
|
|
2267
2269
|
const properties = isRecord(schema.properties) ? schema.properties : {};
|
|
2268
2270
|
const required = Array.isArray(schema.required) ? schema.required.map(String) : [];
|
|
2269
2271
|
for (const key of required) {
|
|
2270
2272
|
if (!Object.prototype.hasOwnProperty.call(value, key)) {
|
|
2271
|
-
errors.push({ path: `${
|
|
2273
|
+
errors.push({ path: `${path8}.${key}`, code: "required", message: "Required property is missing." });
|
|
2272
2274
|
}
|
|
2273
2275
|
}
|
|
2274
2276
|
if (isRecord(schema.propertyNames)) {
|
|
2275
2277
|
for (const key of Object.keys(value)) {
|
|
2276
|
-
validateNode(schema.propertyNames, key, `${
|
|
2278
|
+
validateNode(schema.propertyNames, key, `${path8}.${key}`, root, errors, depth + 1);
|
|
2277
2279
|
}
|
|
2278
2280
|
}
|
|
2279
2281
|
for (const [key, child] of Object.entries(value)) {
|
|
2280
2282
|
const childSchema = properties[key];
|
|
2281
2283
|
if (isRecord(childSchema)) {
|
|
2282
|
-
validateNode(childSchema, child, `${
|
|
2284
|
+
validateNode(childSchema, child, `${path8}.${key}`, root, errors, depth + 1);
|
|
2283
2285
|
} else if (schema.additionalProperties === false) {
|
|
2284
|
-
errors.push({ path: `${
|
|
2286
|
+
errors.push({ path: `${path8}.${key}`, code: "additional_property", message: "Property is not declared by the action schema." });
|
|
2285
2287
|
} else if (isRecord(schema.additionalProperties)) {
|
|
2286
|
-
validateNode(schema.additionalProperties, child, `${
|
|
2288
|
+
validateNode(schema.additionalProperties, child, `${path8}.${key}`, root, errors, depth + 1);
|
|
2287
2289
|
}
|
|
2288
2290
|
}
|
|
2289
2291
|
}
|
|
2290
|
-
function validateArray(schema, value,
|
|
2292
|
+
function validateArray(schema, value, path8, root, errors, depth) {
|
|
2291
2293
|
if (typeof schema.minItems === "number" && value.length < schema.minItems) {
|
|
2292
|
-
errors.push({ path:
|
|
2294
|
+
errors.push({ path: path8, code: "min_items", message: `Expected at least ${schema.minItems} items.` });
|
|
2293
2295
|
}
|
|
2294
2296
|
if (typeof schema.maxItems === "number" && value.length > schema.maxItems) {
|
|
2295
|
-
errors.push({ path:
|
|
2297
|
+
errors.push({ path: path8, code: "max_items", message: `Expected at most ${schema.maxItems} items.` });
|
|
2296
2298
|
}
|
|
2297
2299
|
if (schema.uniqueItems === true) {
|
|
2298
|
-
const
|
|
2299
|
-
if (
|
|
2300
|
+
const unique4 = new Set(value.map((item) => JSON.stringify(item)));
|
|
2301
|
+
if (unique4.size !== value.length) errors.push({ path: path8, code: "unique_items", message: "Expected every item to be unique." });
|
|
2300
2302
|
}
|
|
2301
2303
|
if (isRecord(schema.items)) {
|
|
2302
|
-
value.forEach((item, index) => validateNode(schema.items, item, `${
|
|
2304
|
+
value.forEach((item, index) => validateNode(schema.items, item, `${path8}[${index}]`, root, errors, depth + 1));
|
|
2303
2305
|
}
|
|
2304
2306
|
}
|
|
2305
|
-
function validateString(schema, value,
|
|
2307
|
+
function validateString(schema, value, path8, errors) {
|
|
2306
2308
|
if (typeof schema.minLength === "number" && value.length < schema.minLength) {
|
|
2307
|
-
errors.push({ path:
|
|
2309
|
+
errors.push({ path: path8, code: "min_length", message: `Expected at least ${schema.minLength} characters.` });
|
|
2308
2310
|
}
|
|
2309
2311
|
if (typeof schema.maxLength === "number" && value.length > schema.maxLength) {
|
|
2310
|
-
errors.push({ path:
|
|
2312
|
+
errors.push({ path: path8, code: "max_length", message: `Expected at most ${schema.maxLength} characters.` });
|
|
2311
2313
|
}
|
|
2312
2314
|
if (typeof schema.pattern === "string") {
|
|
2313
2315
|
try {
|
|
2314
|
-
if (!new RegExp(schema.pattern).test(value)) errors.push({ path:
|
|
2316
|
+
if (!new RegExp(schema.pattern).test(value)) errors.push({ path: path8, code: "pattern", message: "Value does not match the required pattern." });
|
|
2315
2317
|
} catch {
|
|
2316
|
-
errors.push({ path:
|
|
2318
|
+
errors.push({ path: path8, code: "invalid_schema_pattern", message: "Action schema contains an invalid pattern." });
|
|
2317
2319
|
}
|
|
2318
2320
|
}
|
|
2319
|
-
if (schema.format === "uri" && !isUrl(value)) errors.push({ path:
|
|
2320
|
-
if (schema.format === "email" && !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(value)) errors.push({ path:
|
|
2321
|
-
if (schema.format === "date-time" && !isDateTime(value)) errors.push({ path:
|
|
2321
|
+
if (schema.format === "uri" && !isUrl(value)) errors.push({ path: path8, code: "format_uri", message: "Expected an absolute URL." });
|
|
2322
|
+
if (schema.format === "email" && !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(value)) errors.push({ path: path8, code: "format_email", message: "Expected an email address." });
|
|
2323
|
+
if (schema.format === "date-time" && !isDateTime(value)) errors.push({ path: path8, code: "format_date_time", message: "Expected an ISO-8601 date-time." });
|
|
2322
2324
|
}
|
|
2323
|
-
function validateNumber(schema, value,
|
|
2324
|
-
if (typeof schema.minimum === "number" && value < schema.minimum) errors.push({ path:
|
|
2325
|
-
if (typeof schema.maximum === "number" && value > schema.maximum) errors.push({ path:
|
|
2325
|
+
function validateNumber(schema, value, path8, errors) {
|
|
2326
|
+
if (typeof schema.minimum === "number" && value < schema.minimum) errors.push({ path: path8, code: "minimum", message: `Expected a value of at least ${schema.minimum}.` });
|
|
2327
|
+
if (typeof schema.maximum === "number" && value > schema.maximum) errors.push({ path: path8, code: "maximum", message: `Expected a value of at most ${schema.maximum}.` });
|
|
2326
2328
|
const exclusiveMinimum = typeof schema.exclusiveMinimum === "number" ? schema.exclusiveMinimum : schema.exclusiveMinimum === true && typeof schema.minimum === "number" ? schema.minimum : null;
|
|
2327
2329
|
const exclusiveMaximum = typeof schema.exclusiveMaximum === "number" ? schema.exclusiveMaximum : schema.exclusiveMaximum === true && typeof schema.maximum === "number" ? schema.maximum : null;
|
|
2328
2330
|
if (exclusiveMinimum !== null && value <= exclusiveMinimum) {
|
|
2329
|
-
errors.push({ path:
|
|
2331
|
+
errors.push({ path: path8, code: "exclusive_minimum", message: `Expected a value greater than ${exclusiveMinimum}.` });
|
|
2330
2332
|
}
|
|
2331
2333
|
if (exclusiveMaximum !== null && value >= exclusiveMaximum) {
|
|
2332
|
-
errors.push({ path:
|
|
2334
|
+
errors.push({ path: path8, code: "exclusive_maximum", message: `Expected a value less than ${exclusiveMaximum}.` });
|
|
2333
2335
|
}
|
|
2334
2336
|
if (typeof schema.multipleOf === "number" && !isMultipleOf(value, schema.multipleOf)) {
|
|
2335
|
-
errors.push({ path:
|
|
2337
|
+
errors.push({ path: path8, code: "multiple_of", message: `Expected a multiple of ${schema.multipleOf}.` });
|
|
2336
2338
|
}
|
|
2337
2339
|
}
|
|
2338
2340
|
function candidateMatches(candidate, value, root, depth) {
|
|
@@ -2843,14 +2845,14 @@ var TopoloClient = class _TopoloClient {
|
|
|
2843
2845
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
2844
2846
|
throw new TopoloSdkError("action_catalog_summary_unavailable", "Action catalog summary is unavailable.");
|
|
2845
2847
|
}
|
|
2846
|
-
const
|
|
2847
|
-
if (
|
|
2848
|
+
const record4 = raw;
|
|
2849
|
+
if (record4.algorithm !== "sha256-v1" || typeof record4.digest !== "string" || !/^[a-f0-9]{64}$/.test(record4.digest) || !Number.isSafeInteger(record4.actionCount) || Number(record4.actionCount) < 0) {
|
|
2848
2850
|
throw new TopoloSdkError("action_catalog_summary_unavailable", "Action catalog summary is invalid.");
|
|
2849
2851
|
}
|
|
2850
2852
|
return {
|
|
2851
2853
|
algorithm: "sha256-v1",
|
|
2852
|
-
digest:
|
|
2853
|
-
actionCount: Number(
|
|
2854
|
+
digest: record4.digest,
|
|
2855
|
+
actionCount: Number(record4.actionCount)
|
|
2854
2856
|
};
|
|
2855
2857
|
}
|
|
2856
2858
|
/** @deprecated Prefer searchActions() so callers retain the continuation cursor. */
|
|
@@ -3225,8 +3227,8 @@ function normalizeActionCatalogEntry(item) {
|
|
|
3225
3227
|
const name = pickActionString(a.name);
|
|
3226
3228
|
const appId = pickActionString(a.app_id) ?? pickActionString(a.appId);
|
|
3227
3229
|
const method = pickActionString(a.method)?.toUpperCase();
|
|
3228
|
-
const
|
|
3229
|
-
if (!actionId || !name || !appId || !method || !
|
|
3230
|
+
const path8 = pickActionString(a.path);
|
|
3231
|
+
if (!actionId || !name || !appId || !method || !path8 || !isHttpMethod(method)) {
|
|
3230
3232
|
return null;
|
|
3231
3233
|
}
|
|
3232
3234
|
return {
|
|
@@ -3241,7 +3243,7 @@ function normalizeActionCatalogEntry(item) {
|
|
|
3241
3243
|
appSlug: pickActionString(a.app_slug) ?? pickActionString(a.appSlug) ?? null,
|
|
3242
3244
|
appName: pickActionString(a.app_name) ?? pickActionString(a.appName) ?? null,
|
|
3243
3245
|
method,
|
|
3244
|
-
path:
|
|
3246
|
+
path: path8,
|
|
3245
3247
|
permissionName: pickActionString(a.permission_name) ?? pickActionString(a.permissionName) ?? "",
|
|
3246
3248
|
requiredPermission: pickActionString(a.required_permission) ?? pickActionString(a.requiredPermission) ?? "",
|
|
3247
3249
|
authorizationMode: pickAuthorizationMode(a.authorization_mode ?? a.authorizationMode),
|
|
@@ -3277,7 +3279,7 @@ function stringArray(value) {
|
|
|
3277
3279
|
}
|
|
3278
3280
|
function mapActionInput(action, input3) {
|
|
3279
3281
|
const remaining = { ...input3 || {} };
|
|
3280
|
-
const
|
|
3282
|
+
const path8 = action.path.replace(/\{([a-zA-Z0-9_]+)\}/g, (_match, key) => {
|
|
3281
3283
|
const value = remaining[key];
|
|
3282
3284
|
if (value === void 0 || value === null || value === "") {
|
|
3283
3285
|
throw new TopoloSdkError(
|
|
@@ -3294,14 +3296,14 @@ function mapActionInput(action, input3) {
|
|
|
3294
3296
|
delete remaining.body;
|
|
3295
3297
|
if (action.method === "GET") {
|
|
3296
3298
|
return {
|
|
3297
|
-
path:
|
|
3299
|
+
path: path8,
|
|
3298
3300
|
query: objectToQuery({ ...remaining, ...explicitQuery })
|
|
3299
3301
|
};
|
|
3300
3302
|
}
|
|
3301
3303
|
const query = objectToQuery(explicitQuery);
|
|
3302
3304
|
const body = explicitBody !== void 0 ? explicitBody : remaining;
|
|
3303
3305
|
return {
|
|
3304
|
-
path:
|
|
3306
|
+
path: path8,
|
|
3305
3307
|
...query ? { query } : {},
|
|
3306
3308
|
body
|
|
3307
3309
|
};
|
|
@@ -3471,8 +3473,8 @@ var TopoloOAuth = class {
|
|
|
3471
3473
|
if (params.clientSecret) body.set("client_secret", params.clientSecret);
|
|
3472
3474
|
return this.#postForm("/api/developer-oauth/token", body);
|
|
3473
3475
|
}
|
|
3474
|
-
async #postForm(
|
|
3475
|
-
const url = new URL(
|
|
3476
|
+
async #postForm(path8, body) {
|
|
3477
|
+
const url = new URL(path8, this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`);
|
|
3476
3478
|
const res = await this.fetchImpl(url.toString(), {
|
|
3477
3479
|
method: "POST",
|
|
3478
3480
|
headers: {
|
|
@@ -3488,7 +3490,7 @@ var TopoloOAuth = class {
|
|
|
3488
3490
|
const err = parsed;
|
|
3489
3491
|
throw new TopoloAuthError(err.error_description || err.error, err.error);
|
|
3490
3492
|
}
|
|
3491
|
-
throw new TopoloHttpError("auth",
|
|
3493
|
+
throw new TopoloHttpError("auth", path8, res.status, parsed);
|
|
3492
3494
|
}
|
|
3493
3495
|
return parsed;
|
|
3494
3496
|
}
|
|
@@ -3502,7 +3504,7 @@ function createTopolo(options) {
|
|
|
3502
3504
|
};
|
|
3503
3505
|
}
|
|
3504
3506
|
|
|
3505
|
-
//
|
|
3507
|
+
// node_modules/@topolo/sdk/dist/node/credential-store.js
|
|
3506
3508
|
import { mkdir, readFile, writeFile, chmod } from "fs/promises";
|
|
3507
3509
|
import { existsSync } from "fs";
|
|
3508
3510
|
import { homedir, platform } from "os";
|
|
@@ -3526,13 +3528,13 @@ var TopoloHttpError2 = class extends TopoloSdkError2 {
|
|
|
3526
3528
|
body;
|
|
3527
3529
|
service;
|
|
3528
3530
|
path;
|
|
3529
|
-
constructor(service,
|
|
3530
|
-
super("http_error", message ?? `HTTP ${status} from ${service}${
|
|
3531
|
+
constructor(service, path8, status, body, message) {
|
|
3532
|
+
super("http_error", message ?? `HTTP ${status} from ${service}${path8}`);
|
|
3531
3533
|
this.name = "TopoloHttpError";
|
|
3532
3534
|
this.status = status;
|
|
3533
3535
|
this.body = body;
|
|
3534
3536
|
this.service = service;
|
|
3535
|
-
this.path =
|
|
3537
|
+
this.path = path8;
|
|
3536
3538
|
}
|
|
3537
3539
|
};
|
|
3538
3540
|
var APP_API_URLS2 = {
|
|
@@ -3663,8 +3665,8 @@ var TopoloOAuth2 = class {
|
|
|
3663
3665
|
if (params.clientSecret) body.set("client_secret", params.clientSecret);
|
|
3664
3666
|
return this.#postForm("/api/developer-oauth/token", body);
|
|
3665
3667
|
}
|
|
3666
|
-
async #postForm(
|
|
3667
|
-
const url = new URL(
|
|
3668
|
+
async #postForm(path8, body) {
|
|
3669
|
+
const url = new URL(path8, this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`);
|
|
3668
3670
|
const res = await this.fetchImpl(url.toString(), {
|
|
3669
3671
|
method: "POST",
|
|
3670
3672
|
headers: {
|
|
@@ -3680,7 +3682,7 @@ var TopoloOAuth2 = class {
|
|
|
3680
3682
|
const err = parsed;
|
|
3681
3683
|
throw new TopoloAuthError2(err.error_description || err.error, err.error);
|
|
3682
3684
|
}
|
|
3683
|
-
throw new TopoloHttpError2("auth",
|
|
3685
|
+
throw new TopoloHttpError2("auth", path8, res.status, parsed);
|
|
3684
3686
|
}
|
|
3685
3687
|
return parsed;
|
|
3686
3688
|
}
|
|
@@ -3710,22 +3712,22 @@ function configPath() {
|
|
|
3710
3712
|
return join(base, "topolo", "config.json");
|
|
3711
3713
|
}
|
|
3712
3714
|
async function readConfig() {
|
|
3713
|
-
const
|
|
3714
|
-
if (!existsSync(
|
|
3715
|
+
const path8 = configPath();
|
|
3716
|
+
if (!existsSync(path8)) return {};
|
|
3715
3717
|
try {
|
|
3716
|
-
const raw = await readFile(
|
|
3718
|
+
const raw = await readFile(path8, "utf8");
|
|
3717
3719
|
return JSON.parse(raw);
|
|
3718
3720
|
} catch {
|
|
3719
3721
|
return {};
|
|
3720
3722
|
}
|
|
3721
3723
|
}
|
|
3722
3724
|
async function writeConfig(config) {
|
|
3723
|
-
const
|
|
3724
|
-
await mkdir(dirname(
|
|
3725
|
-
await writeFile(
|
|
3725
|
+
const path8 = configPath();
|
|
3726
|
+
await mkdir(dirname(path8), { recursive: true });
|
|
3727
|
+
await writeFile(path8, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
3726
3728
|
if (platform() !== "win32") {
|
|
3727
3729
|
try {
|
|
3728
|
-
await chmod(
|
|
3730
|
+
await chmod(path8, 384);
|
|
3729
3731
|
} catch {
|
|
3730
3732
|
}
|
|
3731
3733
|
}
|
|
@@ -4045,9 +4047,9 @@ function readCliVersion() {
|
|
|
4045
4047
|
join2(here, "package.json"),
|
|
4046
4048
|
join2(here, "..", "package.json")
|
|
4047
4049
|
];
|
|
4048
|
-
for (const
|
|
4050
|
+
for (const path8 of candidates) {
|
|
4049
4051
|
try {
|
|
4050
|
-
const pkg = JSON.parse(readFileSync(
|
|
4052
|
+
const pkg = JSON.parse(readFileSync(path8, "utf8"));
|
|
4051
4053
|
if (typeof pkg.version === "string" && pkg.version) return pkg.version;
|
|
4052
4054
|
} catch {
|
|
4053
4055
|
}
|
|
@@ -4406,7 +4408,7 @@ function registerApiCommand(program2) {
|
|
|
4406
4408
|
"-q, --query <kv...>",
|
|
4407
4409
|
"Query params as key=value (repeatable), e.g. --query source=cli page=2"
|
|
4408
4410
|
).option("--resource-type <type>", "Cataloged app-local workspace resource alias for resource-scoped API keys").option("--resource-id <id>", "Workspace resource id for resource-scoped API keys").option("--confirm", "Acknowledge a mutating request").option("--json", "Output JSON", true).option("--no-json", "Output human-readable text").action(
|
|
4409
|
-
async (service, method,
|
|
4411
|
+
async (service, method, path8, opts) => {
|
|
4410
4412
|
const mode = resolveOutputMode(opts.json);
|
|
4411
4413
|
try {
|
|
4412
4414
|
const upperMethod = method.toUpperCase();
|
|
@@ -4445,7 +4447,7 @@ function registerApiCommand(program2) {
|
|
|
4445
4447
|
const result = await topolo.client.request({
|
|
4446
4448
|
service,
|
|
4447
4449
|
method: httpMethod,
|
|
4448
|
-
path:
|
|
4450
|
+
path: path8,
|
|
4449
4451
|
body,
|
|
4450
4452
|
...query ? { query } : {},
|
|
4451
4453
|
...resource ? { resource } : {},
|
|
@@ -4609,13 +4611,13 @@ async function requiredClient() {
|
|
|
4609
4611
|
function parseJsonObject(value) {
|
|
4610
4612
|
let source = value;
|
|
4611
4613
|
if (value.startsWith("@")) {
|
|
4612
|
-
const
|
|
4613
|
-
if (!
|
|
4614
|
+
const path8 = value.slice(1);
|
|
4615
|
+
if (!path8) throw new Error("--data @file path must not be empty");
|
|
4614
4616
|
try {
|
|
4615
|
-
source = readFileSync2(
|
|
4617
|
+
source = readFileSync2(path8, "utf8");
|
|
4616
4618
|
} catch (err) {
|
|
4617
4619
|
const detail = err instanceof Error ? err.message : String(err);
|
|
4618
|
-
throw new Error(`Could not read --data file ${
|
|
4620
|
+
throw new Error(`Could not read --data file ${path8}: ${detail}`);
|
|
4619
4621
|
}
|
|
4620
4622
|
}
|
|
4621
4623
|
let parsed;
|
|
@@ -4690,7 +4692,7 @@ function formatActionPlan(plan) {
|
|
|
4690
4692
|
// src/commands/apps.ts
|
|
4691
4693
|
import { execSync } from "child_process";
|
|
4692
4694
|
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "fs";
|
|
4693
|
-
import
|
|
4695
|
+
import path5 from "path";
|
|
4694
4696
|
|
|
4695
4697
|
// src/commands/app-audit-local.ts
|
|
4696
4698
|
import {
|
|
@@ -5281,8 +5283,8 @@ function enrichFinding(application, finding2, evidence) {
|
|
|
5281
5283
|
evidence: verificationEvidence
|
|
5282
5284
|
});
|
|
5283
5285
|
}
|
|
5284
|
-
const usesNodeCheck = evidence.packageJsons.some((
|
|
5285
|
-
const scripts =
|
|
5286
|
+
const usesNodeCheck = evidence.packageJsons.some((record4) => {
|
|
5287
|
+
const scripts = record4.packageJson.scripts ?? {};
|
|
5286
5288
|
return Object.values(scripts).some((cmd) => typeof cmd === "string" && /node\s+--check/.test(cmd));
|
|
5287
5289
|
});
|
|
5288
5290
|
if (hasBuild && usesNodeCheck) {
|
|
@@ -5362,7 +5364,7 @@ function nativeDashboardWidgetEvidence(evidence) {
|
|
|
5362
5364
|
return unique(items);
|
|
5363
5365
|
}
|
|
5364
5366
|
function collectEvidence(repoRoot, application) {
|
|
5365
|
-
const appRepoPath = applicationSourcePath(application.appDir);
|
|
5367
|
+
const appRepoPath = applicationSourcePath(repoRoot, application.appDir);
|
|
5366
5368
|
const appRoot = path.join(repoRoot, appRepoPath);
|
|
5367
5369
|
const packageJsonPath = path.join(appRoot, "package.json");
|
|
5368
5370
|
const packageJson2 = readPackageJson(packageJsonPath);
|
|
@@ -5548,35 +5550,35 @@ function verificationEvidenceFor(evidence) {
|
|
|
5548
5550
|
"test:auth-contract": "test",
|
|
5549
5551
|
validate: "validate"
|
|
5550
5552
|
};
|
|
5551
|
-
for (const
|
|
5552
|
-
const scripts =
|
|
5553
|
-
const isRootPackage =
|
|
5553
|
+
for (const record4 of evidence.packageJsons) {
|
|
5554
|
+
const scripts = record4.packageJson.scripts ?? {};
|
|
5555
|
+
const isRootPackage = record4.path === evidence.packageJsonPath;
|
|
5554
5556
|
const seenCanonical = /* @__PURE__ */ new Set();
|
|
5555
5557
|
for (const [scriptName, canonical] of Object.entries(scriptAliases)) {
|
|
5556
5558
|
if (!scripts[scriptName]) continue;
|
|
5557
5559
|
if (seenCanonical.has(canonical)) continue;
|
|
5558
5560
|
seenCanonical.add(canonical);
|
|
5559
|
-
items.push(isRootPackage ? `script=${canonical}` : `script=${packageLabel(
|
|
5561
|
+
items.push(isRootPackage ? `script=${canonical}` : `script=${packageLabel(record4)}:${canonical}`);
|
|
5560
5562
|
}
|
|
5561
5563
|
}
|
|
5562
5564
|
return unique(items);
|
|
5563
5565
|
}
|
|
5564
5566
|
function dependencyNames(evidence) {
|
|
5565
5567
|
return new Set(
|
|
5566
|
-
evidence.packageJsons.flatMap((
|
|
5567
|
-
...Object.keys(
|
|
5568
|
-
...Object.keys(
|
|
5569
|
-
...Object.keys(
|
|
5568
|
+
evidence.packageJsons.flatMap((record4) => [
|
|
5569
|
+
...Object.keys(record4.packageJson.dependencies ?? {}),
|
|
5570
|
+
...Object.keys(record4.packageJson.devDependencies ?? {}),
|
|
5571
|
+
...Object.keys(record4.packageJson.peerDependencies ?? {})
|
|
5570
5572
|
])
|
|
5571
5573
|
);
|
|
5572
5574
|
}
|
|
5573
5575
|
function packageDependencyVersions(evidence, packageName) {
|
|
5574
5576
|
return unique(
|
|
5575
|
-
evidence.packageJsons.flatMap((
|
|
5577
|
+
evidence.packageJsons.flatMap((record4) => {
|
|
5576
5578
|
const specs = [
|
|
5577
|
-
|
|
5578
|
-
|
|
5579
|
-
|
|
5579
|
+
record4.packageJson.dependencies?.[packageName],
|
|
5580
|
+
record4.packageJson.devDependencies?.[packageName],
|
|
5581
|
+
record4.packageJson.peerDependencies?.[packageName]
|
|
5580
5582
|
];
|
|
5581
5583
|
return specs.filter((spec) => typeof spec === "string");
|
|
5582
5584
|
})
|
|
@@ -5604,7 +5606,7 @@ function matchingSystems(repoRoot, application) {
|
|
|
5604
5606
|
const docsRoot = topoloDocsRoot(repoRoot);
|
|
5605
5607
|
const systemsRoot = path.join(docsRoot, "src", "content", "systems");
|
|
5606
5608
|
if (!isDirectory(systemsRoot)) return [];
|
|
5607
|
-
const appRepoPath = applicationSourcePath(application.appDir);
|
|
5609
|
+
const appRepoPath = applicationSourcePath(repoRoot, application.appDir);
|
|
5608
5610
|
return readdirSync(systemsRoot).filter((entry) => entry.endsWith(".json")).map((entry) => readJson(path.join(systemsRoot, entry))).filter((system) => Boolean(system)).filter((system) => {
|
|
5609
5611
|
const repoPaths = [...system.repo_paths ?? [], ...system.repoPaths ?? []];
|
|
5610
5612
|
return repoPaths.some(
|
|
@@ -5627,11 +5629,19 @@ function existingDocs(repoRoot, systems) {
|
|
|
5627
5629
|
function topoloDocsRoot(repoRoot) {
|
|
5628
5630
|
return path.join(repoRoot, "system-apps", "TopoloDocs", "apps", "web");
|
|
5629
5631
|
}
|
|
5630
|
-
|
|
5632
|
+
var APPLICATION_SOURCE_ROOTS = [
|
|
5633
|
+
"apps",
|
|
5634
|
+
"system-apps",
|
|
5635
|
+
"topolo-platform/services",
|
|
5636
|
+
"topolo-platform/core",
|
|
5637
|
+
"topolo-platform/packages"
|
|
5638
|
+
];
|
|
5639
|
+
function applicationSourcePath(repoRoot, appDir) {
|
|
5631
5640
|
if (appDir === "TopoloCli" || appDir === "TopoloCLI") return "topolo-platform/packages/topolo-cli";
|
|
5632
|
-
|
|
5633
|
-
|
|
5634
|
-
|
|
5641
|
+
for (const root of APPLICATION_SOURCE_ROOTS) {
|
|
5642
|
+
const relative4 = `${root}/${appDir}`;
|
|
5643
|
+
if (isDirectory(path.join(repoRoot, relative4))) return relative4;
|
|
5644
|
+
}
|
|
5635
5645
|
return `apps/${appDir}`;
|
|
5636
5646
|
}
|
|
5637
5647
|
function docExists(contentRoot, docPath) {
|
|
@@ -5702,8 +5712,8 @@ function collectPackageJsons(root) {
|
|
|
5702
5712
|
function readPackageJson(filePath) {
|
|
5703
5713
|
return readJson(filePath);
|
|
5704
5714
|
}
|
|
5705
|
-
function packageLabel(
|
|
5706
|
-
return
|
|
5715
|
+
function packageLabel(record4) {
|
|
5716
|
+
return record4.packageJson.name ?? relativeTopoloPath(path.dirname(record4.path));
|
|
5707
5717
|
}
|
|
5708
5718
|
function sortedDirEntries(dir) {
|
|
5709
5719
|
return readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
@@ -5792,14 +5802,14 @@ import { constants } from "fs";
|
|
|
5792
5802
|
import { access } from "fs/promises";
|
|
5793
5803
|
import { relative, resolve } from "path";
|
|
5794
5804
|
function normalizeHomePath(value) {
|
|
5795
|
-
const
|
|
5796
|
-
if (!
|
|
5805
|
+
const path8 = String(value || "/dashboard").trim();
|
|
5806
|
+
if (!path8.startsWith("/") || path8.startsWith("//") || /[?#]/.test(path8)) {
|
|
5797
5807
|
throw new Error("Invalid home path. Use an absolute application path such as /dashboard.");
|
|
5798
5808
|
}
|
|
5799
|
-
if (["/", "/login", "/auth/callback", "/getting-started"].includes(
|
|
5809
|
+
if (["/", "/login", "/auth/callback", "/getting-started"].includes(path8)) {
|
|
5800
5810
|
throw new Error("Home path must be a protected product route, not a reserved entry route.");
|
|
5801
5811
|
}
|
|
5802
|
-
return
|
|
5812
|
+
return path8.replace(/\/$/, "") || "/dashboard";
|
|
5803
5813
|
}
|
|
5804
5814
|
function normalizeHomeLabel(value) {
|
|
5805
5815
|
const label = String(value || "Dashboard").trim();
|
|
@@ -5880,16 +5890,6 @@ function normalizeScaffoldProfile(value) {
|
|
|
5880
5890
|
if (value === "spatial-assets") return value;
|
|
5881
5891
|
throw new Error("Invalid scaffold profile. Use spatial-assets.");
|
|
5882
5892
|
}
|
|
5883
|
-
function resolveScaffoldResources(profile, options) {
|
|
5884
|
-
const profileResources = profile === "spatial-assets" ? { d1: true, r2: true, queue: true, workflow: true, containerProcessing: true } : { d1: false, r2: false, queue: false, workflow: false, containerProcessing: false };
|
|
5885
|
-
return {
|
|
5886
|
-
d1: Boolean(profileResources.d1 || options.withD1),
|
|
5887
|
-
r2: Boolean(profileResources.r2 || options.withR2),
|
|
5888
|
-
queue: Boolean(profileResources.queue || options.withQueue),
|
|
5889
|
-
workflow: Boolean(profileResources.workflow || options.withWorkflow),
|
|
5890
|
-
containerProcessing: Boolean(profileResources.containerProcessing || options.withContainerProcessing)
|
|
5891
|
-
};
|
|
5892
|
-
}
|
|
5893
5893
|
function normalizeContainerAppId(value, surfaceModel) {
|
|
5894
5894
|
const trimmed = String(value || "").trim();
|
|
5895
5895
|
if (surfaceModel === "mobile_module") {
|
|
@@ -5940,9 +5940,9 @@ function toTitle(value) {
|
|
|
5940
5940
|
function json(value) {
|
|
5941
5941
|
return JSON.stringify(value, null, 2);
|
|
5942
5942
|
}
|
|
5943
|
-
async function pathExists(
|
|
5943
|
+
async function pathExists(path8) {
|
|
5944
5944
|
try {
|
|
5945
|
-
await access(
|
|
5945
|
+
await access(path8, constants.F_OK);
|
|
5946
5946
|
return true;
|
|
5947
5947
|
} catch {
|
|
5948
5948
|
return false;
|
|
@@ -5950,8 +5950,8 @@ async function pathExists(path6) {
|
|
|
5950
5950
|
}
|
|
5951
5951
|
function resolveInside(root, filePath) {
|
|
5952
5952
|
const resolved = resolve(root, filePath);
|
|
5953
|
-
const
|
|
5954
|
-
if (
|
|
5953
|
+
const relativePath2 = relative(root, resolved);
|
|
5954
|
+
if (relativePath2.startsWith("..") || relativePath2 === "" || relativePath2.startsWith("/")) {
|
|
5955
5955
|
throw new Error(`Scaffold file path escapes the target root: ${filePath}`);
|
|
5956
5956
|
}
|
|
5957
5957
|
return resolved;
|
|
@@ -5978,48 +5978,6 @@ var STAGING_CLOUDFLARE_ACCOUNT_ID = "4f4e1c69a3830946f9fea7b1eb7531ac";
|
|
|
5978
5978
|
var DEVELOPMENT_CLOUDFLARE_ACCOUNT_ID = "0ebdeec262a245481a721fc4f9d6d798";
|
|
5979
5979
|
var CLOUDFLARE_COMPATIBILITY_DATE = "2026-05-05";
|
|
5980
5980
|
|
|
5981
|
-
// src/commands/app-scaffold-package-policy.ts
|
|
5982
|
-
var TOPOLO_PACKAGE_SCAFFOLD_POLICY = {
|
|
5983
|
-
"@topolo-io/api-key-management": "browser-baseline",
|
|
5984
|
-
"@topolo-io/app-funnel": "browser-baseline",
|
|
5985
|
-
"@topolo-io/app-launcher": "browser-baseline",
|
|
5986
|
-
"@topolo-io/app-manifest": "domain-opt-in",
|
|
5987
|
-
"@topolo-io/app-shell": "browser-baseline",
|
|
5988
|
-
"@topolo-io/auth-client": "browser-baseline",
|
|
5989
|
-
"@topolo-io/auth-middleware": "api-baseline",
|
|
5990
|
-
"@topolo-io/billing-manifest": "domain-opt-in",
|
|
5991
|
-
"@topolo-io/billing-ui": "domain-opt-in",
|
|
5992
|
-
"@topolo-io/bridge": "domain-opt-in",
|
|
5993
|
-
"@topolo-io/calendar": "domain-opt-in",
|
|
5994
|
-
"@topolo-io/chat-widget": "browser-baseline",
|
|
5995
|
-
"@topolo-io/data": "domain-opt-in",
|
|
5996
|
-
"@topolo-io/design-tokens": "browser-baseline",
|
|
5997
|
-
"@topolo-io/error-middleware": "api-baseline",
|
|
5998
|
-
"@topolo-io/mailbox": "domain-opt-in",
|
|
5999
|
-
"@topolo-io/mobile-error-middleware": "domain-opt-in",
|
|
6000
|
-
"@topolo-io/mobile-experience-contracts": "domain-opt-in",
|
|
6001
|
-
"@topolo-io/nexus-client": "api-baseline",
|
|
6002
|
-
"@topolo-io/node-canvas": "domain-opt-in",
|
|
6003
|
-
"@topolo-io/notifications": "browser-baseline",
|
|
6004
|
-
"@topolo-io/onboarding": "browser-baseline",
|
|
6005
|
-
"@topolo-io/platform-mcp": "tooling",
|
|
6006
|
-
"@topolo-io/resource-context": "browser-baseline",
|
|
6007
|
-
"@topolo-io/security-headers": "api-baseline",
|
|
6008
|
-
"@topolo-io/tour": "browser-baseline",
|
|
6009
|
-
"@topolo-io/ui-kit": "browser-baseline",
|
|
6010
|
-
"@topolo-io/widget-server": "api-baseline",
|
|
6011
|
-
"@topolo-io/worker-runtime": "api-baseline",
|
|
6012
|
-
"@topolo/cli": "tooling",
|
|
6013
|
-
"@topolo/login": "developer-baseline",
|
|
6014
|
-
"@topolo/mcp": "tooling",
|
|
6015
|
-
"@topolo/sdk": "api-baseline"
|
|
6016
|
-
};
|
|
6017
|
-
function packagesForScaffoldClass(scaffoldClass) {
|
|
6018
|
-
return Object.entries(TOPOLO_PACKAGE_SCAFFOLD_POLICY).filter(([, value]) => value === scaffoldClass).map(([name]) => name).sort();
|
|
6019
|
-
}
|
|
6020
|
-
var PLATFORM_BROWSER_TOPOLO_PACKAGES = packagesForScaffoldClass("browser-baseline");
|
|
6021
|
-
var PLATFORM_API_TOPOLO_PACKAGES = packagesForScaffoldClass("api-baseline");
|
|
6022
|
-
|
|
6023
5981
|
// src/commands/app-scaffold-package-template.ts
|
|
6024
5982
|
function packageJson(application, target, options) {
|
|
6025
5983
|
const dependencies = {};
|
|
@@ -6044,9 +6002,6 @@ function packageJson(application, target, options) {
|
|
|
6044
6002
|
devDependencies.autoprefixer = "10.5.0";
|
|
6045
6003
|
devDependencies.postcss = "8.5.6";
|
|
6046
6004
|
devDependencies.tailwindcss = "3.4.19";
|
|
6047
|
-
for (const packageName of PLATFORM_BROWSER_TOPOLO_PACKAGES) {
|
|
6048
|
-
dependencies[packageName] = topoloPackageVersion(packageName, target);
|
|
6049
|
-
}
|
|
6050
6005
|
dependencies["react-router-dom"] = "^7.6.2";
|
|
6051
6006
|
} else {
|
|
6052
6007
|
dependencies["@topolo/login"] = topoloPackageVersion("@topolo/login", target);
|
|
@@ -6055,9 +6010,6 @@ function packageJson(application, target, options) {
|
|
|
6055
6010
|
if (options.api) {
|
|
6056
6011
|
devDependencies["@cloudflare/workers-types"] = "^5.20260716.1";
|
|
6057
6012
|
if (target === "platform") {
|
|
6058
|
-
for (const packageName of PLATFORM_API_TOPOLO_PACKAGES) {
|
|
6059
|
-
dependencies[packageName] = topoloPackageVersion(packageName, target);
|
|
6060
|
-
}
|
|
6061
6013
|
dependencies.hono = "^4";
|
|
6062
6014
|
dependencies.zod = "^4.4.3";
|
|
6063
6015
|
scripts.deploy = `node -e 'console.error("\\nX Bare deploy is disabled. Use deploy:staging or deploy:production.\\n"); process.exit(1)'`;
|
|
@@ -6069,6 +6021,9 @@ function packageJson(application, target, options) {
|
|
|
6069
6021
|
}
|
|
6070
6022
|
devDependencies.wrangler = "^4.91.0";
|
|
6071
6023
|
}
|
|
6024
|
+
for (const packageName of options.packages) {
|
|
6025
|
+
dependencies[packageName] = topoloPackageVersion(packageName, target);
|
|
6026
|
+
}
|
|
6072
6027
|
if (target === "platform") {
|
|
6073
6028
|
const testDirs = [];
|
|
6074
6029
|
if (options.browser) testDirs.push("apps/web");
|
|
@@ -6090,7 +6045,7 @@ function packageJson(application, target, options) {
|
|
|
6090
6045
|
}
|
|
6091
6046
|
}
|
|
6092
6047
|
}
|
|
6093
|
-
if (target === "platform" &&
|
|
6048
|
+
if (target === "platform" && (options.api || options.browser)) {
|
|
6094
6049
|
const deployChecks = [
|
|
6095
6050
|
...options.api ? ["wrangler deploy --config apps/api/wrangler.toml --env staging --dry-run --outdir .wrangler/api-build"] : [],
|
|
6096
6051
|
...options.browser ? ["wrangler deploy --config apps/web/wrangler.toml --env staging --dry-run --outdir .wrangler/web-build"] : []
|
|
@@ -6174,6 +6129,320 @@ function resourceConfig(application, profile) {
|
|
|
6174
6129
|
};
|
|
6175
6130
|
}
|
|
6176
6131
|
|
|
6132
|
+
// src/commands/app-capability-registry.ts
|
|
6133
|
+
var APPLICATION_CAPABILITY_REGISTRY_VERSION = "1";
|
|
6134
|
+
var TOPOLO_APP_CAPABILITY_IDS = [
|
|
6135
|
+
"login",
|
|
6136
|
+
"launcher",
|
|
6137
|
+
"marketplace",
|
|
6138
|
+
"hosting",
|
|
6139
|
+
"api",
|
|
6140
|
+
"widget",
|
|
6141
|
+
"onboarding",
|
|
6142
|
+
"resource_management",
|
|
6143
|
+
"notifications",
|
|
6144
|
+
"notification_producer",
|
|
6145
|
+
"billing",
|
|
6146
|
+
"metering",
|
|
6147
|
+
"nexus_ai",
|
|
6148
|
+
"integrations",
|
|
6149
|
+
"webhooks",
|
|
6150
|
+
"automation",
|
|
6151
|
+
"agent_access",
|
|
6152
|
+
"data",
|
|
6153
|
+
"object_storage",
|
|
6154
|
+
"background_jobs",
|
|
6155
|
+
"durable_workflows",
|
|
6156
|
+
"privacy",
|
|
6157
|
+
"chat",
|
|
6158
|
+
"calendar",
|
|
6159
|
+
"mailbox",
|
|
6160
|
+
"mobile"
|
|
6161
|
+
];
|
|
6162
|
+
var bothTargets = ["platform", "developer"];
|
|
6163
|
+
var platformTarget = ["platform"];
|
|
6164
|
+
var APPLICATION_CAPABILITY_REGISTRY = {
|
|
6165
|
+
login: capability("login", "TopoloAuth", bothTargets, [], ["browser"], {
|
|
6166
|
+
platform: ["@topolo-io/auth-client", "@topolo-io/app-shell"],
|
|
6167
|
+
developer: ["@topolo/login"]
|
|
6168
|
+
}, ["openid", "profile", "email", "organization:read"], [], [], ["auth-client"], ["auth-login-smoke"]),
|
|
6169
|
+
launcher: capability("launcher", "TopoloDevelopers + TopoloAuth", bothTargets, ["login"], ["browser"], {
|
|
6170
|
+
platform: ["@topolo-io/app-funnel", "@topolo-io/app-launcher", "@topolo-io/app-shell", "@topolo-io/design-tokens", "@topolo-io/ui-kit"],
|
|
6171
|
+
developer: []
|
|
6172
|
+
}, [], [], [], ["launcher-registration"], ["launcher-catalog-smoke"]),
|
|
6173
|
+
marketplace: capability("marketplace", "TopoloDevelopers", bothTargets, ["launcher"], ["browser"], emptyPackages(), [], [], [], ["marketplace-registration"], ["marketplace-projection-check"]),
|
|
6174
|
+
hosting: capability("hosting", "TopoloCloudControl or external owner", bothTargets, [], [], emptyPackages(), [], [], [], ["environment-manifest", "health-contract"], ["environment-health-smoke"]),
|
|
6175
|
+
api: capability("api", "Application runtime + TopoloAuth", bothTargets, [], ["api"], {
|
|
6176
|
+
platform: ["@topolo-io/auth-middleware", "@topolo-io/error-middleware", "@topolo-io/security-headers", "@topolo-io/worker-runtime", "@topolo/sdk"],
|
|
6177
|
+
developer: ["@topolo/sdk"]
|
|
6178
|
+
}, ["entitlements:read"], ["workspace:read", "workspace:write"], [], ["protected-worker"], ["api-auth-allow-deny"]),
|
|
6179
|
+
widget: capability("widget", "Application runtime + TopoloDevelopers", bothTargets, ["api"], ["api"], {
|
|
6180
|
+
platform: ["@topolo-io/widget-server", "@topolo/sdk"],
|
|
6181
|
+
developer: ["@topolo/sdk"]
|
|
6182
|
+
}, [], ["workspace:read"], [], ["widget-route", "widget-action-contract"], ["widget-contract-smoke"]),
|
|
6183
|
+
onboarding: capability("onboarding", "TopoloAuth + shared onboarding packages", platformTarget, ["launcher"], ["browser"], {
|
|
6184
|
+
platform: ["@topolo-io/onboarding", "@topolo-io/tour"],
|
|
6185
|
+
developer: []
|
|
6186
|
+
}, [], [], [], ["getting-started-route"], ["onboarding-progress-smoke"]),
|
|
6187
|
+
resource_management: capability("resource_management", "TopoloAuth + application resource catalog", platformTarget, ["launcher"], ["browser"], {
|
|
6188
|
+
platform: ["@topolo-io/api-key-management", "@topolo-io/resource-context"],
|
|
6189
|
+
developer: []
|
|
6190
|
+
}, [], ["workspace:read"], [], ["resource-selector"], ["resource-binding-smoke"]),
|
|
6191
|
+
notifications: capability("notifications", "TopoloNotify + TopoloAuth", platformTarget, ["launcher"], ["browser"], {
|
|
6192
|
+
platform: ["@topolo-io/notifications"],
|
|
6193
|
+
developer: []
|
|
6194
|
+
}, [], [], [], ["notification-consumer"], ["notification-inbox-smoke"]),
|
|
6195
|
+
notification_producer: capability("notification_producer", "TopoloNotify + TopoloDevelopers", platformTarget, ["notifications", "api"], ["api"], {
|
|
6196
|
+
platform: ["@topolo-io/notifications", "@topolo-io/auth-middleware"],
|
|
6197
|
+
developer: []
|
|
6198
|
+
}, [], ["events:write"], [], ["notification-producer-adapter", "notification-contracts"], ["notification-delivery-smoke"]),
|
|
6199
|
+
billing: capability("billing", "TopoloPay + TopoloAuth", bothTargets, ["launcher"], ["browser"], {
|
|
6200
|
+
platform: ["@topolo-io/billing-manifest", "@topolo-io/billing-ui"],
|
|
6201
|
+
developer: []
|
|
6202
|
+
}, ["entitlements:read"], [], [], ["pricing-manifest"], ["pricing-registration-smoke"]),
|
|
6203
|
+
metering: capability("metering", "TopoloNexus", platformTarget, ["api"], ["api"], {
|
|
6204
|
+
platform: ["@topolo-io/nexus-client", "@topolo-io/worker-runtime"],
|
|
6205
|
+
developer: []
|
|
6206
|
+
}, [], ["usage:write"], [], ["nexus-usage-adapter"], ["usage-attribution-smoke"]),
|
|
6207
|
+
nexus_ai: capability("nexus_ai", "TopoloNexus", platformTarget, ["metering"], ["api"], {
|
|
6208
|
+
platform: ["@topolo-io/nexus-client"],
|
|
6209
|
+
developer: []
|
|
6210
|
+
}, [], ["usage:write"], [], ["nexus-ai-adapter"], ["ai-budget-rejection-smoke"]),
|
|
6211
|
+
integrations: capability("integrations", "TopoloNexus", platformTarget, ["api"], ["api"], {
|
|
6212
|
+
platform: ["@topolo-io/nexus-client"],
|
|
6213
|
+
developer: []
|
|
6214
|
+
}, [], [], [], ["integration-connection-adapter"], ["integration-connection-smoke"]),
|
|
6215
|
+
webhooks: capability("webhooks", "TopoloNotify", bothTargets, ["api"], ["api"], {
|
|
6216
|
+
platform: ["@topolo-io/notifications"],
|
|
6217
|
+
developer: []
|
|
6218
|
+
}, [], [], [], ["webhook-receiver"], ["webhook-signature-smoke"]),
|
|
6219
|
+
automation: capability("automation", "TopoloFlow + application runtime", platformTarget, ["api"], ["api"], {
|
|
6220
|
+
platform: ["@topolo/sdk"],
|
|
6221
|
+
developer: []
|
|
6222
|
+
}, [], [], [], ["flow-adapter"], ["flow-run-smoke"]),
|
|
6223
|
+
agent_access: capability("agent_access", "TopoloDevelopers + TopoloAgent", bothTargets, ["widget"], ["api"], {
|
|
6224
|
+
platform: ["@topolo-io/worker-runtime", "@topolo/sdk"],
|
|
6225
|
+
developer: ["@topolo/sdk"]
|
|
6226
|
+
}, [], ["workspace:read"], [], ["action-contracts"], ["credential-scoped-action-smoke"]),
|
|
6227
|
+
data: capability("data", "Application runtime through @topolo-io/data", platformTarget, ["api"], ["api"], {
|
|
6228
|
+
platform: ["@topolo-io/data"],
|
|
6229
|
+
developer: []
|
|
6230
|
+
}, [], ["workspace:read", "workspace:write"], ["d1"], ["workspace-migration", "data-port"], ["tenant-isolation-smoke"]),
|
|
6231
|
+
object_storage: capability("object_storage", "Application runtime through @topolo-io/data", platformTarget, ["data"], ["api"], {
|
|
6232
|
+
platform: ["@topolo-io/data"],
|
|
6233
|
+
developer: []
|
|
6234
|
+
}, [], ["workspace:read", "workspace:write"], ["r2"], ["object-store-port"], ["object-lifecycle-smoke"]),
|
|
6235
|
+
background_jobs: capability("background_jobs", "Application runtime", platformTarget, ["api"], ["api"], emptyPackages(), [], ["workspace:write"], ["queue"], ["queue-producer", "queue-consumer"], ["queue-execution-smoke"]),
|
|
6236
|
+
durable_workflows: capability("durable_workflows", "Application runtime + TopoloFlow", platformTarget, ["background_jobs"], ["api"], emptyPackages(), [], ["workspace:write"], ["workflow"], ["workflow-entrypoint"], ["workflow-execution-smoke"]),
|
|
6237
|
+
privacy: capability("privacy", "TopoloConsent + application data owner", platformTarget, ["api"], ["api"], emptyPackages(), [], [], [], ["privacy-adapter", "retention-declaration"], ["dsr-export-delete-smoke"]),
|
|
6238
|
+
chat: capability("chat", "TopoloChat", platformTarget, ["launcher"], ["browser"], {
|
|
6239
|
+
platform: ["@topolo-io/chat-widget"],
|
|
6240
|
+
developer: []
|
|
6241
|
+
}, [], [], [], ["chat-surface"], ["chat-surface-smoke"]),
|
|
6242
|
+
calendar: capability("calendar", "TopoloCalendar", platformTarget, ["launcher"], ["browser"], {
|
|
6243
|
+
platform: ["@topolo-io/calendar"],
|
|
6244
|
+
developer: []
|
|
6245
|
+
}, [], [], [], ["calendar-surface"], ["calendar-surface-smoke"]),
|
|
6246
|
+
mailbox: capability("mailbox", "TopoloMail", platformTarget, ["launcher"], ["browser"], {
|
|
6247
|
+
platform: ["@topolo-io/mailbox"],
|
|
6248
|
+
developer: []
|
|
6249
|
+
}, [], [], [], ["mailbox-surface"], ["mailbox-surface-smoke"]),
|
|
6250
|
+
mobile: capability("mobile", "TopoloOne + application native runtime", bothTargets, [], ["mobile"], {
|
|
6251
|
+
platform: ["@topolo-io/bridge", "@topolo-io/mobile-error-middleware", "@topolo-io/mobile-experience-contracts"],
|
|
6252
|
+
developer: []
|
|
6253
|
+
}, [], [], [], ["mobile-experience-metadata"], ["native-experience-smoke"])
|
|
6254
|
+
};
|
|
6255
|
+
var PROFILE_CAPABILITIES = {
|
|
6256
|
+
"spatial-assets": ["object_storage", "background_jobs", "durable_workflows"]
|
|
6257
|
+
};
|
|
6258
|
+
function resolveApplicationCapabilities(options) {
|
|
6259
|
+
const requested = options.requested ?? [];
|
|
6260
|
+
const unknown = requested.filter((id) => !isTopoloAppCapability(id));
|
|
6261
|
+
if (unknown.length > 0) {
|
|
6262
|
+
throw new Error(`Unknown application capability: ${unknown.join(", ")}. Supported capabilities: ${TOPOLO_APP_CAPABILITY_IDS.join(", ")}.`);
|
|
6263
|
+
}
|
|
6264
|
+
if (options.profile && !PROFILE_CAPABILITIES[options.profile]) {
|
|
6265
|
+
throw new Error(`Unknown capability profile: ${options.profile}.`);
|
|
6266
|
+
}
|
|
6267
|
+
const selected = /* @__PURE__ */ new Set();
|
|
6268
|
+
const add = (id) => {
|
|
6269
|
+
const definition = APPLICATION_CAPABILITY_REGISTRY[id];
|
|
6270
|
+
if (!definition.targets.includes(options.target)) {
|
|
6271
|
+
throw new Error(`Capability ${id} is not available to ${options.target} applications.`);
|
|
6272
|
+
}
|
|
6273
|
+
for (const dependency of definition.requires) add(dependency);
|
|
6274
|
+
selected.add(id);
|
|
6275
|
+
};
|
|
6276
|
+
if (options.browser) {
|
|
6277
|
+
add("login");
|
|
6278
|
+
add("launcher");
|
|
6279
|
+
add("hosting");
|
|
6280
|
+
if (options.target === "platform") {
|
|
6281
|
+
add("onboarding");
|
|
6282
|
+
add("resource_management");
|
|
6283
|
+
add("notifications");
|
|
6284
|
+
}
|
|
6285
|
+
}
|
|
6286
|
+
if (options.distributionModel === "marketplace" && options.browser) add("marketplace");
|
|
6287
|
+
if (options.api) {
|
|
6288
|
+
add("api");
|
|
6289
|
+
add("widget");
|
|
6290
|
+
if (options.target === "platform") add("agent_access");
|
|
6291
|
+
}
|
|
6292
|
+
if (options.mobile) add("mobile");
|
|
6293
|
+
for (const id of options.profile ? PROFILE_CAPABILITIES[options.profile] ?? [] : []) add(id);
|
|
6294
|
+
for (const id of requested) add(id);
|
|
6295
|
+
for (const id of selected) {
|
|
6296
|
+
const definition = APPLICATION_CAPABILITY_REGISTRY[id];
|
|
6297
|
+
if (definition.surfaces.includes("browser") && !options.browser) {
|
|
6298
|
+
throw new Error(`Capability ${id} requires a browser surface.`);
|
|
6299
|
+
}
|
|
6300
|
+
if (definition.surfaces.includes("api") && !options.api) {
|
|
6301
|
+
throw new Error(`Capability ${id} requires an API surface.`);
|
|
6302
|
+
}
|
|
6303
|
+
if (definition.surfaces.includes("mobile") && !options.mobile) {
|
|
6304
|
+
throw new Error(`Capability ${id} requires a mobile surface model.`);
|
|
6305
|
+
}
|
|
6306
|
+
}
|
|
6307
|
+
const ordered = TOPOLO_APP_CAPABILITY_IDS.filter((id) => selected.has(id));
|
|
6308
|
+
const definitions = ordered.map((id) => APPLICATION_CAPABILITY_REGISTRY[id]);
|
|
6309
|
+
const targetPackages = options.target === "platform" ? "platform" : "developer";
|
|
6310
|
+
return {
|
|
6311
|
+
registryVersion: APPLICATION_CAPABILITY_REGISTRY_VERSION,
|
|
6312
|
+
selected: ordered,
|
|
6313
|
+
declarations: ordered.map((id) => ({ id, version: 1 })),
|
|
6314
|
+
packages: unique2(definitions.flatMap((definition) => definition.packages[targetPackages])),
|
|
6315
|
+
authScopes: unique2(definitions.flatMap((definition) => definition.authScopes)),
|
|
6316
|
+
servicePermissions: unique2(definitions.flatMap((definition) => definition.servicePermissions)),
|
|
6317
|
+
resources: unique2(definitions.flatMap((definition) => definition.resources)),
|
|
6318
|
+
artifacts: unique2(definitions.flatMap((definition) => definition.artifacts)),
|
|
6319
|
+
stagingEvidence: unique2(definitions.flatMap((definition) => definition.stagingEvidence))
|
|
6320
|
+
};
|
|
6321
|
+
}
|
|
6322
|
+
function isTopoloAppCapability(value) {
|
|
6323
|
+
return TOPOLO_APP_CAPABILITY_IDS.includes(value);
|
|
6324
|
+
}
|
|
6325
|
+
function capability(id, owner, targets, requires, surfaces, packages, authScopes, servicePermissions, resources, artifacts, stagingEvidence) {
|
|
6326
|
+
return { id, version: 1, owner, targets, requires, surfaces, packages, authScopes, servicePermissions, resources, artifacts, stagingEvidence };
|
|
6327
|
+
}
|
|
6328
|
+
function emptyPackages() {
|
|
6329
|
+
return { platform: [], developer: [] };
|
|
6330
|
+
}
|
|
6331
|
+
function unique2(values) {
|
|
6332
|
+
return Array.from(new Set(values));
|
|
6333
|
+
}
|
|
6334
|
+
|
|
6335
|
+
// src/commands/app-capability-artifacts.ts
|
|
6336
|
+
function capabilityCompositionProjection(capabilities) {
|
|
6337
|
+
return {
|
|
6338
|
+
registryVersion: capabilities.registryVersion,
|
|
6339
|
+
selected: capabilities.declarations,
|
|
6340
|
+
artifacts: capabilities.artifacts,
|
|
6341
|
+
stagingEvidence: capabilities.stagingEvidence
|
|
6342
|
+
};
|
|
6343
|
+
}
|
|
6344
|
+
function capabilityContractProjection(capabilities) {
|
|
6345
|
+
return {
|
|
6346
|
+
registryVersion: APPLICATION_CAPABILITY_REGISTRY_VERSION,
|
|
6347
|
+
selected: capabilities.selected.map((id) => {
|
|
6348
|
+
const definition = APPLICATION_CAPABILITY_REGISTRY[id];
|
|
6349
|
+
return {
|
|
6350
|
+
id,
|
|
6351
|
+
version: definition.version,
|
|
6352
|
+
owner: definition.owner,
|
|
6353
|
+
packages: definition.packages,
|
|
6354
|
+
authScopes: definition.authScopes,
|
|
6355
|
+
servicePermissions: definition.servicePermissions,
|
|
6356
|
+
resources: definition.resources,
|
|
6357
|
+
artifacts: definition.artifacts,
|
|
6358
|
+
stagingEvidence: definition.stagingEvidence
|
|
6359
|
+
};
|
|
6360
|
+
})
|
|
6361
|
+
};
|
|
6362
|
+
}
|
|
6363
|
+
function defaultPricingManifest(options) {
|
|
6364
|
+
if (!options.appId) throw new Error("Billing composition requires a canonical appId.");
|
|
6365
|
+
return {
|
|
6366
|
+
appId: options.appId,
|
|
6367
|
+
developerOrgId: null,
|
|
6368
|
+
schemaVersion: 1,
|
|
6369
|
+
marketingCopy: { tagline: options.description, marketplaceBadges: ["free"] },
|
|
6370
|
+
meters: [],
|
|
6371
|
+
plans: [{
|
|
6372
|
+
id: "free",
|
|
6373
|
+
tier: "free",
|
|
6374
|
+
displayName: "Free",
|
|
6375
|
+
summary: `Start using ${options.displayName}.`,
|
|
6376
|
+
pricing: { model: "free" },
|
|
6377
|
+
bullets: ["Shared Topolo identity and application access"],
|
|
6378
|
+
isDefault: true
|
|
6379
|
+
}]
|
|
6380
|
+
};
|
|
6381
|
+
}
|
|
6382
|
+
|
|
6383
|
+
// src/commands/app-capability-adapter-template.ts
|
|
6384
|
+
function managedCapabilityAdapter(capabilities) {
|
|
6385
|
+
const notifications = capabilities.includes("notification_producer");
|
|
6386
|
+
const nexus = capabilities.some((capability2) => capability2 === "metering" || capability2 === "nexus_ai" || capability2 === "integrations");
|
|
6387
|
+
const webhooks = capabilities.includes("webhooks");
|
|
6388
|
+
if (!notifications && !nexus && !webhooks) return null;
|
|
6389
|
+
return [
|
|
6390
|
+
...notifications || nexus ? ["import { createServiceClient } from '@topolo-io/auth-middleware';"] : [],
|
|
6391
|
+
...notifications ? ["import { TopoloEvents } from '@topolo-io/notifications';"] : [],
|
|
6392
|
+
...webhooks ? ["import { WEBHOOK_SIGNATURE_HEADER, verifyWebhookSignature } from '@topolo-io/notifications';"] : [],
|
|
6393
|
+
...nexus ? ["import { createNexusClient } from '@topolo-io/nexus-client';"] : [],
|
|
6394
|
+
"",
|
|
6395
|
+
"export interface TopoloCapabilityEnv {",
|
|
6396
|
+
" APP_ID?: string;",
|
|
6397
|
+
" TOPOLO_SERVICE_CLIENT_SECRET?: string;",
|
|
6398
|
+
...notifications ? [" TOPOLO_NOTIFY_URL?: string;"] : [],
|
|
6399
|
+
...nexus ? [" NEXUS_GATEWAY_URL?: string;"] : [],
|
|
6400
|
+
...webhooks ? [" TOPOLO_WEBHOOK_SECRET?: string;"] : [],
|
|
6401
|
+
"}",
|
|
6402
|
+
...notifications ? [
|
|
6403
|
+
"",
|
|
6404
|
+
"export function createTopoloEvents(env: TopoloCapabilityEnv): TopoloEvents {",
|
|
6405
|
+
" if (!env.APP_ID) throw new Error('APP_ID is required for notification attribution.');",
|
|
6406
|
+
" if (!env.TOPOLO_NOTIFY_URL) throw new Error('TOPOLO_NOTIFY_URL is required for notification delivery.');",
|
|
6407
|
+
" return new TopoloEvents({",
|
|
6408
|
+
" appId: env.APP_ID,",
|
|
6409
|
+
" notifyBaseUrl: env.TOPOLO_NOTIFY_URL,",
|
|
6410
|
+
" serviceClient: createServiceClient(env),",
|
|
6411
|
+
" });",
|
|
6412
|
+
"}"
|
|
6413
|
+
] : [],
|
|
6414
|
+
...nexus ? [
|
|
6415
|
+
"",
|
|
6416
|
+
"export function createTopoloNexus(env: TopoloCapabilityEnv, organizationId: string) {",
|
|
6417
|
+
" if (!env.APP_ID) throw new Error('APP_ID is required for Nexus attribution.');",
|
|
6418
|
+
" if (!env.NEXUS_GATEWAY_URL) throw new Error('NEXUS_GATEWAY_URL is required for Nexus requests.');",
|
|
6419
|
+
" if (!organizationId) throw new Error('Verified organizationId is required for Nexus requests.');",
|
|
6420
|
+
" const serviceClient = createServiceClient(env);",
|
|
6421
|
+
" return createNexusClient({",
|
|
6422
|
+
" appId: env.APP_ID,",
|
|
6423
|
+
" gatewayUrl: env.NEXUS_GATEWAY_URL,",
|
|
6424
|
+
" organizationId,",
|
|
6425
|
+
" serviceTokenProvider: (scope) => serviceClient.getToken(scope ?? 'usage:write'),",
|
|
6426
|
+
" });",
|
|
6427
|
+
"}"
|
|
6428
|
+
] : [],
|
|
6429
|
+
...webhooks ? [
|
|
6430
|
+
"",
|
|
6431
|
+
"export async function verifyTopoloWebhookRequest(",
|
|
6432
|
+
" request: Request,",
|
|
6433
|
+
" env: TopoloCapabilityEnv,",
|
|
6434
|
+
"): Promise<{ ok: true; body: string } | { ok: false; response: Response }> {",
|
|
6435
|
+
" if (!env.TOPOLO_WEBHOOK_SECRET) return { ok: false, response: Response.json({ error: 'webhook_secret_missing' }, { status: 503 }) };",
|
|
6436
|
+
" const body = await request.text();",
|
|
6437
|
+
" const valid = await verifyWebhookSignature(env.TOPOLO_WEBHOOK_SECRET, body, request.headers.get(WEBHOOK_SIGNATURE_HEADER));",
|
|
6438
|
+
" if (!valid) return { ok: false, response: Response.json({ error: 'invalid_webhook_signature' }, { status: 401 }) };",
|
|
6439
|
+
" return { ok: true, body };",
|
|
6440
|
+
"}"
|
|
6441
|
+
] : [],
|
|
6442
|
+
""
|
|
6443
|
+
].join("\n");
|
|
6444
|
+
}
|
|
6445
|
+
|
|
6177
6446
|
// src/commands/app-scaffold-browser-template.ts
|
|
6178
6447
|
function readme(application, target, options) {
|
|
6179
6448
|
const displayName = toTitle(application.name);
|
|
@@ -7490,9 +7759,15 @@ function platformWorkerTs(application, options) {
|
|
|
7490
7759
|
];
|
|
7491
7760
|
const workflowExport = options.resources.workflow ? [
|
|
7492
7761
|
"",
|
|
7493
|
-
`export class ${resource.workflowClassName} extends WorkflowEntrypoint<Env,
|
|
7494
|
-
" async run(
|
|
7495
|
-
"
|
|
7762
|
+
`export class ${resource.workflowClassName} extends WorkflowEntrypoint<Env, CapabilityWorkflowInput> {`,
|
|
7763
|
+
" async run(event: Readonly<WorkflowEvent<CapabilityWorkflowInput>>, step: WorkflowStep): Promise<unknown> {",
|
|
7764
|
+
" if (event.payload.type !== 'capability.probe' || !event.payload.workspaceId) {",
|
|
7765
|
+
" throw new Error('Unsupported workflow message. Implement a vertical workflow contract before dispatching it.');",
|
|
7766
|
+
" }",
|
|
7767
|
+
" return step.do('capability probe', async () => ({",
|
|
7768
|
+
" status: 'completed',",
|
|
7769
|
+
" workspaceId: event.payload.workspaceId,",
|
|
7770
|
+
" }));",
|
|
7496
7771
|
" }",
|
|
7497
7772
|
"}"
|
|
7498
7773
|
] : [];
|
|
@@ -7527,6 +7802,21 @@ function platformWorkerTs(application, options) {
|
|
|
7527
7802
|
"export interface Env {",
|
|
7528
7803
|
...envBindings,
|
|
7529
7804
|
"}",
|
|
7805
|
+
...options.resources.queue ? [
|
|
7806
|
+
"",
|
|
7807
|
+
"interface CapabilityJob {",
|
|
7808
|
+
" type: 'capability.probe';",
|
|
7809
|
+
" workspaceId: string;",
|
|
7810
|
+
" payload?: unknown;",
|
|
7811
|
+
"}"
|
|
7812
|
+
] : [],
|
|
7813
|
+
...options.resources.workflow ? [
|
|
7814
|
+
"",
|
|
7815
|
+
"interface CapabilityWorkflowInput {",
|
|
7816
|
+
" type: 'capability.probe';",
|
|
7817
|
+
" workspaceId: string;",
|
|
7818
|
+
"}"
|
|
7819
|
+
] : [],
|
|
7530
7820
|
"",
|
|
7531
7821
|
"function buildWidgets(): TopoloWidgetDraft[] {",
|
|
7532
7822
|
" return [",
|
|
@@ -7623,6 +7913,14 @@ function platformWorkerTs(application, options) {
|
|
|
7623
7913
|
" bindings.__rt = rt;",
|
|
7624
7914
|
" return securedFetch(request, bindings, ctx as ExecutionContext);",
|
|
7625
7915
|
" }),",
|
|
7916
|
+
...options.resources.queue ? [
|
|
7917
|
+
" async queue(batch: MessageBatch<CapabilityJob>): Promise<void> {",
|
|
7918
|
+
" for (const message of batch.messages) {",
|
|
7919
|
+
" if (message.body?.type === 'capability.probe' && message.body.workspaceId) message.ack();",
|
|
7920
|
+
" else message.retry();",
|
|
7921
|
+
" }",
|
|
7922
|
+
" },"
|
|
7923
|
+
] : [],
|
|
7626
7924
|
"};",
|
|
7627
7925
|
"",
|
|
7628
7926
|
"function errorMessage(error: unknown): string {",
|
|
@@ -8040,14 +8338,24 @@ function topoloAppManifestJson(application, options) {
|
|
|
8040
8338
|
development: "https://nexus.topolo.dev",
|
|
8041
8339
|
staging: "https://nexus.stg.topolo.us",
|
|
8042
8340
|
production: "https://nexus.topolo.app"
|
|
8043
|
-
}
|
|
8341
|
+
},
|
|
8342
|
+
...options.payload.capabilities.includes("notification_producer") ? {
|
|
8343
|
+
TOPOLO_NOTIFY_URL: {
|
|
8344
|
+
development: "https://notify.topolo.dev",
|
|
8345
|
+
staging: "https://notify.stg.topolo.us",
|
|
8346
|
+
production: "https://notify.topolo.app"
|
|
8347
|
+
}
|
|
8348
|
+
} : {}
|
|
8044
8349
|
// No NEXUS_APP_ID: generate-wrangler binds APP_ID (the app_id) into every
|
|
8045
8350
|
// env, and worker-runtime reads env.APP_ID for Nexus usage attribution,
|
|
8046
8351
|
// so a second copy of the same identity would only be a chance to drift.
|
|
8047
8352
|
// It previously carried the SLUG, which reads as a working identity while
|
|
8048
8353
|
// Nexus attributes by app_id.
|
|
8049
8354
|
},
|
|
8050
|
-
secrets: [
|
|
8355
|
+
secrets: [
|
|
8356
|
+
"TOPOLO_SERVICE_CLIENT_SECRET",
|
|
8357
|
+
...options.payload.capabilities.includes("webhooks") ? ["TOPOLO_WEBHOOK_SECRET"] : []
|
|
8358
|
+
],
|
|
8051
8359
|
...Object.keys(bindings).length ? { bindings } : {}
|
|
8052
8360
|
};
|
|
8053
8361
|
}
|
|
@@ -8108,6 +8416,12 @@ function topoloAppManifestJson(application, options) {
|
|
|
8108
8416
|
}
|
|
8109
8417
|
},
|
|
8110
8418
|
targets,
|
|
8419
|
+
capabilityComposition: {
|
|
8420
|
+
registryVersion: options.payload.capabilityComposition.registryVersion,
|
|
8421
|
+
selected: options.payload.capabilityComposition.declarations,
|
|
8422
|
+
artifacts: options.payload.capabilityComposition.artifacts,
|
|
8423
|
+
stagingEvidence: options.payload.capabilityComposition.stagingEvidence
|
|
8424
|
+
},
|
|
8111
8425
|
registration: {
|
|
8112
8426
|
ownerType: options.payload.ownerType,
|
|
8113
8427
|
portfolio: options.payload.portfolio,
|
|
@@ -8126,7 +8440,14 @@ function topoloAppManifestJson(application, options) {
|
|
|
8126
8440
|
supportEmail: "support@topolo.io",
|
|
8127
8441
|
auth: {
|
|
8128
8442
|
callbackPath: "/auth/callback",
|
|
8129
|
-
requestedScopes:
|
|
8443
|
+
requestedScopes: unique3([
|
|
8444
|
+
"openid",
|
|
8445
|
+
"profile",
|
|
8446
|
+
"email",
|
|
8447
|
+
"organization:read",
|
|
8448
|
+
...options.payload.capabilityComposition.authScopes,
|
|
8449
|
+
...options.api ? [`${application.id}:read`] : []
|
|
8450
|
+
])
|
|
8130
8451
|
},
|
|
8131
8452
|
capabilities: options.payload.capabilities,
|
|
8132
8453
|
marketplace: {
|
|
@@ -8138,7 +8459,7 @@ function topoloAppManifestJson(application, options) {
|
|
|
8138
8459
|
docsUrl: `https://docs.topolo.app/applications/${slug}`
|
|
8139
8460
|
}
|
|
8140
8461
|
},
|
|
8141
|
-
notifications: { role: "consumer_only" },
|
|
8462
|
+
notifications: options.payload.capabilities.includes("notification_producer") ? { role: "producer", contracts: "topolo.notification-contracts.json" } : options.payload.capabilities.includes("notifications") ? { role: "consumer_only" } : { role: "internal_non_producer" },
|
|
8142
8463
|
...actions.length ? { actions } : {}
|
|
8143
8464
|
})}
|
|
8144
8465
|
`;
|
|
@@ -8514,12 +8835,18 @@ function serviceManifestJson(application, options) {
|
|
|
8514
8835
|
description: `Manage ${title} ${resourceLabel} data`,
|
|
8515
8836
|
resourcePattern: null
|
|
8516
8837
|
},
|
|
8517
|
-
{
|
|
8838
|
+
...options.servicePermissions.includes("usage:write") ? [{
|
|
8518
8839
|
id: `perm_${permissionPrefix}_usage_write`,
|
|
8519
8840
|
name: "usage:write",
|
|
8520
8841
|
description: `Record Nexus usage events for ${title}`,
|
|
8521
8842
|
resourcePattern: null
|
|
8522
|
-
}
|
|
8843
|
+
}] : [],
|
|
8844
|
+
...options.servicePermissions.includes("events:write") ? [{
|
|
8845
|
+
id: `perm_${permissionPrefix}_events_write`,
|
|
8846
|
+
name: "events:write",
|
|
8847
|
+
description: `Publish governed notification events for ${title}`,
|
|
8848
|
+
resourcePattern: null
|
|
8849
|
+
}] : []
|
|
8523
8850
|
],
|
|
8524
8851
|
apiKeyScopes: [
|
|
8525
8852
|
{
|
|
@@ -8534,12 +8861,18 @@ function serviceManifestJson(application, options) {
|
|
|
8534
8861
|
description: `Manage ${title} ${resourceLabel} data`,
|
|
8535
8862
|
resourcePattern: null
|
|
8536
8863
|
},
|
|
8537
|
-
{
|
|
8864
|
+
...options.servicePermissions.includes("usage:write") ? [{
|
|
8538
8865
|
id: `aks_${permissionPrefix}_usage_write`,
|
|
8539
8866
|
name: "usage.write",
|
|
8540
8867
|
description: `Record Nexus usage events for ${title}`,
|
|
8541
8868
|
resourcePattern: null
|
|
8542
|
-
}
|
|
8869
|
+
}] : [],
|
|
8870
|
+
...options.servicePermissions.includes("events:write") ? [{
|
|
8871
|
+
id: `aks_${permissionPrefix}_events_write`,
|
|
8872
|
+
name: "events.write",
|
|
8873
|
+
description: `Publish governed notification events for ${title}`,
|
|
8874
|
+
resourcePattern: null
|
|
8875
|
+
}] : []
|
|
8543
8876
|
],
|
|
8544
8877
|
roleBundles: {
|
|
8545
8878
|
admin: ["workspace:read", "workspace:write"],
|
|
@@ -8554,7 +8887,14 @@ function developerManifest(application, options) {
|
|
|
8554
8887
|
const callbackPath = "/auth/callback";
|
|
8555
8888
|
const localCallbackUrl = "http://localhost:5173/auth/callback";
|
|
8556
8889
|
const productionCallbackUrl = application.productionUrl ? `${application.productionUrl.replace(/\/$/, "")}${callbackPath}` : null;
|
|
8557
|
-
const requestedScopes =
|
|
8890
|
+
const requestedScopes = unique3([
|
|
8891
|
+
"openid",
|
|
8892
|
+
"profile",
|
|
8893
|
+
"email",
|
|
8894
|
+
"organization:read",
|
|
8895
|
+
...options.capabilityComposition.authScopes,
|
|
8896
|
+
...options.api ? [`${application.id}:read`] : []
|
|
8897
|
+
]);
|
|
8558
8898
|
return `${json({
|
|
8559
8899
|
schema: "https://topolo.app/schemas/topolo-app-manifest.v1.json",
|
|
8560
8900
|
id: application.id,
|
|
@@ -8569,17 +8909,9 @@ function developerManifest(application, options) {
|
|
|
8569
8909
|
app_id: application.appId,
|
|
8570
8910
|
capabilities: {
|
|
8571
8911
|
selected: options.capabilities,
|
|
8572
|
-
|
|
8573
|
-
|
|
8574
|
-
|
|
8575
|
-
"marketplace",
|
|
8576
|
-
"api",
|
|
8577
|
-
"widget",
|
|
8578
|
-
"billing",
|
|
8579
|
-
"nexus_ai",
|
|
8580
|
-
"agent_access",
|
|
8581
|
-
"hosting"
|
|
8582
|
-
]
|
|
8912
|
+
registry_version: options.capabilityComposition.registryVersion,
|
|
8913
|
+
declarations: options.capabilityComposition.declarations,
|
|
8914
|
+
supported_v1: TOPOLO_APP_CAPABILITY_IDS
|
|
8583
8915
|
},
|
|
8584
8916
|
auth: {
|
|
8585
8917
|
provider: "topolo",
|
|
@@ -8641,10 +8973,13 @@ function developerManifest(application, options) {
|
|
|
8641
8973
|
docsUrl: application.productionUrl || "https://developers.topolo.app"
|
|
8642
8974
|
}
|
|
8643
8975
|
},
|
|
8644
|
-
notifications: { role: "consumer_only" }
|
|
8976
|
+
notifications: options.capabilities.includes("notification_producer") ? { role: "producer", contracts: "topolo.notification-contracts.json" } : options.capabilities.includes("notifications") ? { role: "consumer_only" } : { role: "internal_non_producer" }
|
|
8645
8977
|
})}
|
|
8646
8978
|
`;
|
|
8647
8979
|
}
|
|
8980
|
+
function unique3(values) {
|
|
8981
|
+
return Array.from(new Set(values));
|
|
8982
|
+
}
|
|
8648
8983
|
function envExample(application) {
|
|
8649
8984
|
return [
|
|
8650
8985
|
"VITE_TOPOLO_AUTH_URL=https://auth.topolo.app",
|
|
@@ -8908,12 +9243,24 @@ function applicationScaffoldPayload(application, options = {}) {
|
|
|
8908
9243
|
const surfaceModel = normalizeSurfaceModel(options.surfaceModel);
|
|
8909
9244
|
const containerAppId = normalizeContainerAppId(options.containerAppId, surfaceModel);
|
|
8910
9245
|
const profile = normalizeScaffoldProfile(options.profile);
|
|
8911
|
-
const resources = resolveScaffoldResources(profile, options);
|
|
8912
|
-
if (target === "platform" && api && !profile) {
|
|
8913
|
-
resources.d1 = true;
|
|
8914
|
-
}
|
|
8915
9246
|
const ownerType = target === "platform" ? "topolo_first_party" : "third_party";
|
|
8916
9247
|
const distributionModel = target === "platform" ? "marketplace" : "organization_internal";
|
|
9248
|
+
const capabilityComposition = resolveApplicationCapabilities({
|
|
9249
|
+
target,
|
|
9250
|
+
browser,
|
|
9251
|
+
api,
|
|
9252
|
+
mobile: surfaceModel === "mobile_container" || surfaceModel === "mobile_module",
|
|
9253
|
+
distributionModel,
|
|
9254
|
+
requested: options.capabilities,
|
|
9255
|
+
profile
|
|
9256
|
+
});
|
|
9257
|
+
const resources = {
|
|
9258
|
+
d1: capabilityComposition.resources.includes("d1"),
|
|
9259
|
+
r2: capabilityComposition.resources.includes("r2"),
|
|
9260
|
+
queue: capabilityComposition.resources.includes("queue"),
|
|
9261
|
+
workflow: capabilityComposition.resources.includes("workflow"),
|
|
9262
|
+
containerProcessing: profile === "spatial-assets"
|
|
9263
|
+
};
|
|
8917
9264
|
const requestedAppDir = options.appDir ?? catalogApplication?.appDir ?? (target === "platform" ? `Topolo${toPascalCase(id)}` : `topolo-${slug}`);
|
|
8918
9265
|
const appDir = target === "platform" ? normalizeFirstPartyAppDir(requestedAppDir) : requestedAppDir;
|
|
8919
9266
|
const name = options.name ?? catalogApplication?.name ?? `topolo-${slug}`;
|
|
@@ -8933,7 +9280,7 @@ function applicationScaffoldPayload(application, options = {}) {
|
|
|
8933
9280
|
const appId = options.appId ?? defaultAppId;
|
|
8934
9281
|
const productionUrl = browser ? options.productionUrl ?? catalogApplication?.productionUrl ?? `https://${slug}.topolo.app` : null;
|
|
8935
9282
|
const scopes = scaffoldScopes({ browser, api });
|
|
8936
|
-
const capabilities =
|
|
9283
|
+
const capabilities = capabilityComposition.selected;
|
|
8937
9284
|
const ciWorkflowPath = target === "platform" ? ".github/workflows/topolo-conformance.yml" : null;
|
|
8938
9285
|
const ciWorkflow = target === "platform" ? topoloConformanceWorkflow({ id, appDir, audience }) : null;
|
|
8939
9286
|
const applicationPayload = {
|
|
@@ -8959,12 +9306,13 @@ function applicationScaffoldPayload(application, options = {}) {
|
|
|
8959
9306
|
experience,
|
|
8960
9307
|
distributionModel,
|
|
8961
9308
|
capabilities,
|
|
9309
|
+
capabilityComposition,
|
|
8962
9310
|
application: applicationPayload,
|
|
8963
9311
|
scopes,
|
|
8964
9312
|
requirements: APPLICATION_REQUIREMENTS.filter(
|
|
8965
9313
|
(requirement) => requirement.appliesTo.some((scope) => scopes.includes(scope))
|
|
8966
9314
|
),
|
|
8967
|
-
packages:
|
|
9315
|
+
packages: capabilityComposition.packages,
|
|
8968
9316
|
files: scaffoldFiles({
|
|
8969
9317
|
payload: {
|
|
8970
9318
|
ownerType,
|
|
@@ -8977,7 +9325,8 @@ function applicationScaffoldPayload(application, options = {}) {
|
|
|
8977
9325
|
resources,
|
|
8978
9326
|
experience,
|
|
8979
9327
|
distributionModel,
|
|
8980
|
-
capabilities
|
|
9328
|
+
capabilities,
|
|
9329
|
+
capabilityComposition
|
|
8981
9330
|
},
|
|
8982
9331
|
application: applicationPayload,
|
|
8983
9332
|
target,
|
|
@@ -9038,28 +9387,6 @@ function scaffoldScopes(options) {
|
|
|
9038
9387
|
}
|
|
9039
9388
|
return scopes;
|
|
9040
9389
|
}
|
|
9041
|
-
function scaffoldPackages(options) {
|
|
9042
|
-
if (options.target === "developer") {
|
|
9043
|
-
return options.browser ? ["@topolo/login"] : [];
|
|
9044
|
-
}
|
|
9045
|
-
const packages = /* @__PURE__ */ new Set();
|
|
9046
|
-
if (options.browser) {
|
|
9047
|
-
for (const packageName of PLATFORM_BROWSER_TOPOLO_PACKAGES) packages.add(packageName);
|
|
9048
|
-
}
|
|
9049
|
-
if (options.api) {
|
|
9050
|
-
for (const packageName of PLATFORM_API_TOPOLO_PACKAGES) packages.add(packageName);
|
|
9051
|
-
}
|
|
9052
|
-
return Array.from(packages);
|
|
9053
|
-
}
|
|
9054
|
-
function scaffoldCapabilities(options) {
|
|
9055
|
-
const capabilities = [];
|
|
9056
|
-
if (options.browser) {
|
|
9057
|
-
capabilities.push("login", "launcher");
|
|
9058
|
-
}
|
|
9059
|
-
if (options.distributionModel === "marketplace") capabilities.push("marketplace");
|
|
9060
|
-
if (options.api) capabilities.push("api", "widget");
|
|
9061
|
-
return capabilities;
|
|
9062
|
-
}
|
|
9063
9390
|
function scaffoldFiles(options) {
|
|
9064
9391
|
const { application, target } = options;
|
|
9065
9392
|
const slug = toSlug(application.id);
|
|
@@ -9099,9 +9426,31 @@ function scaffoldFiles(options) {
|
|
|
9099
9426
|
content: packageJson(application, target, {
|
|
9100
9427
|
browser: options.browser,
|
|
9101
9428
|
api: options.api,
|
|
9102
|
-
resources: options.payload.resources
|
|
9429
|
+
resources: options.payload.resources,
|
|
9430
|
+
packages: options.payload.capabilityComposition.packages
|
|
9103
9431
|
})
|
|
9104
9432
|
},
|
|
9433
|
+
{
|
|
9434
|
+
path: `${base}/topolo.capability-contract.json`,
|
|
9435
|
+
purpose: "Resolved application-factory contract for packages, permissions, resources, artifacts, and staging proof.",
|
|
9436
|
+
requiredSignals: [
|
|
9437
|
+
"registry version matches topolo.app.json capability composition",
|
|
9438
|
+
"every selected capability names its canonical owner and verification contract"
|
|
9439
|
+
],
|
|
9440
|
+
content: `${json(capabilityContractProjection(options.payload.capabilityComposition))}
|
|
9441
|
+
`
|
|
9442
|
+
},
|
|
9443
|
+
...options.payload.capabilities.includes("billing") ? [{
|
|
9444
|
+
path: `${base}/pricing-manifest.json`,
|
|
9445
|
+
purpose: "Canonical TopoloPay/Nexus pricing and meter registration manifest.",
|
|
9446
|
+
requiredSignals: ["contains a valid default plan", "remains the source for app-specific plan and meter design"],
|
|
9447
|
+
content: `${json(defaultPricingManifest({
|
|
9448
|
+
appId: application.appId ?? "",
|
|
9449
|
+
displayName: toTitle(application.name),
|
|
9450
|
+
description: options.payload.experience.tagline
|
|
9451
|
+
}))}
|
|
9452
|
+
`
|
|
9453
|
+
}] : [],
|
|
9105
9454
|
{
|
|
9106
9455
|
path: `${base}/README.md`,
|
|
9107
9456
|
purpose: "Starter README with install, run, and verification commands.",
|
|
@@ -9433,7 +9782,8 @@ function scaffoldFiles(options) {
|
|
|
9433
9782
|
],
|
|
9434
9783
|
content: serviceManifestJson(application, {
|
|
9435
9784
|
audience: options.payload.audience,
|
|
9436
|
-
tenancy: options.payload.tenancy
|
|
9785
|
+
tenancy: options.payload.tenancy,
|
|
9786
|
+
servicePermissions: options.payload.capabilityComposition.servicePermissions
|
|
9437
9787
|
})
|
|
9438
9788
|
}
|
|
9439
9789
|
);
|
|
@@ -9454,7 +9804,8 @@ function scaffoldFiles(options) {
|
|
|
9454
9804
|
surfaceModel: options.payload.surfaceModel,
|
|
9455
9805
|
containerAppId: options.payload.containerAppId,
|
|
9456
9806
|
distributionModel: options.payload.distributionModel,
|
|
9457
|
-
capabilities: options.payload.capabilities
|
|
9807
|
+
capabilities: options.payload.capabilities,
|
|
9808
|
+
capabilityComposition: options.payload.capabilityComposition
|
|
9458
9809
|
})
|
|
9459
9810
|
},
|
|
9460
9811
|
{
|
|
@@ -9547,6 +9898,7 @@ export default defineConfig({
|
|
|
9547
9898
|
}
|
|
9548
9899
|
}
|
|
9549
9900
|
if (options.api) {
|
|
9901
|
+
const capabilityAdapter = target === "platform" ? managedCapabilityAdapter(options.payload.capabilities) : null;
|
|
9550
9902
|
files.push(
|
|
9551
9903
|
...target === "platform" ? [] : [{
|
|
9552
9904
|
path: `${apiBase}/wrangler.toml`,
|
|
@@ -9571,7 +9923,13 @@ export default defineConfig({
|
|
|
9571
9923
|
profile: options.payload.profile,
|
|
9572
9924
|
resources: options.payload.resources
|
|
9573
9925
|
})
|
|
9574
|
-
}
|
|
9926
|
+
},
|
|
9927
|
+
...capabilityAdapter ? [{
|
|
9928
|
+
path: `${apiBase}/src/topolo-capabilities.ts`,
|
|
9929
|
+
purpose: "Managed adapters for selected Topolo notification, Nexus, AI, integration, and webhook capabilities.",
|
|
9930
|
+
requiredSignals: ["uses verified organization context", "uses environment-scoped gateways and keyless service identity", "verifies webhook signatures over the raw body"],
|
|
9931
|
+
content: capabilityAdapter
|
|
9932
|
+
}] : []
|
|
9575
9933
|
);
|
|
9576
9934
|
if (target === "platform") {
|
|
9577
9935
|
files.push(
|
|
@@ -9775,7 +10133,14 @@ function buildProvisioningRequest(payload, supportEmail) {
|
|
|
9775
10133
|
"http://localhost:5173/auth/callback",
|
|
9776
10134
|
payload.application.productionUrl ? `${payload.application.productionUrl.replace(/\/$/, "")}/auth/callback` : null
|
|
9777
10135
|
].filter(Boolean),
|
|
9778
|
-
requestedScopes:
|
|
10136
|
+
requestedScopes: Array.from(/* @__PURE__ */ new Set([
|
|
10137
|
+
"openid",
|
|
10138
|
+
"profile",
|
|
10139
|
+
"email",
|
|
10140
|
+
"organization:read",
|
|
10141
|
+
...payload.capabilityComposition.authScopes,
|
|
10142
|
+
...payload.application.appId ? [`${payload.application.id}:read`] : []
|
|
10143
|
+
])),
|
|
9779
10144
|
marketplace: {
|
|
9780
10145
|
intent: payload.distributionModel === "marketplace" ? "submit_when_ready" : "not_requested",
|
|
9781
10146
|
status: "preview",
|
|
@@ -9784,7 +10149,7 @@ function buildProvisioningRequest(payload, supportEmail) {
|
|
|
9784
10149
|
description: payload.experience.tagline,
|
|
9785
10150
|
docsUrl: payload.application.productionUrl || "https://developers.topolo.app"
|
|
9786
10151
|
},
|
|
9787
|
-
notifications: { role: "consumer_only" },
|
|
10152
|
+
notifications: payload.capabilities.includes("notification_producer") ? { role: "producer", contracts: "topolo.notification-contracts.json" } : payload.capabilities.includes("notifications") ? { role: "consumer_only" } : { role: "internal_non_producer" },
|
|
9788
10153
|
applicationId: payload.application.id,
|
|
9789
10154
|
name: payload.application.name,
|
|
9790
10155
|
description: payload.experience.tagline,
|
|
@@ -9852,18 +10217,18 @@ async function applyProvisionedAppId(manifestPath, appId) {
|
|
|
9852
10217
|
} catch {
|
|
9853
10218
|
return { changed: false, previous: null };
|
|
9854
10219
|
}
|
|
9855
|
-
let
|
|
10220
|
+
let json2;
|
|
9856
10221
|
try {
|
|
9857
|
-
|
|
10222
|
+
json2 = JSON.parse(raw);
|
|
9858
10223
|
} catch {
|
|
9859
10224
|
return { changed: false, previous: null };
|
|
9860
10225
|
}
|
|
9861
|
-
const previous = typeof
|
|
10226
|
+
const previous = typeof json2.appId === "string" ? json2.appId : null;
|
|
9862
10227
|
if (previous === appId) {
|
|
9863
10228
|
return { changed: false, previous };
|
|
9864
10229
|
}
|
|
9865
|
-
|
|
9866
|
-
await writeFile3(manifestPath, `${JSON.stringify(
|
|
10230
|
+
json2.appId = appId;
|
|
10231
|
+
await writeFile3(manifestPath, `${JSON.stringify(json2, null, 2)}
|
|
9867
10232
|
`, "utf8");
|
|
9868
10233
|
return { changed: true, previous };
|
|
9869
10234
|
}
|
|
@@ -9872,13 +10237,280 @@ async function applyProvisionedAppId(manifestPath, appId) {
|
|
|
9872
10237
|
import { join as join3 } from "path";
|
|
9873
10238
|
|
|
9874
10239
|
// src/commands/app-reconcile.ts
|
|
9875
|
-
import { readFile as
|
|
9876
|
-
import
|
|
10240
|
+
import { readFile as readFile4, writeFile as writeFile5 } from "fs/promises";
|
|
10241
|
+
import path4 from "path";
|
|
9877
10242
|
import { createInterface as createInterface2 } from "readline/promises";
|
|
9878
10243
|
import { stdin as input2, stdout as output } from "process";
|
|
10244
|
+
|
|
10245
|
+
// src/commands/app-capability-composition.ts
|
|
10246
|
+
import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile4 } from "fs/promises";
|
|
10247
|
+
import path3 from "path";
|
|
10248
|
+
async function reconcileApplicationCapabilitySource(options) {
|
|
10249
|
+
const manifestPath = path3.resolve(options.manifestPath || "topolo.app.json");
|
|
10250
|
+
const root = path3.dirname(manifestPath);
|
|
10251
|
+
const packagePath = path3.join(root, "package.json");
|
|
10252
|
+
const contractPath = path3.join(root, "topolo.capability-contract.json");
|
|
10253
|
+
const manifest = parseRecord(await readFile3(manifestPath, "utf8"), manifestPath);
|
|
10254
|
+
const registration = record(manifest.registration);
|
|
10255
|
+
if (!registration) throw new Error("topolo.app.json must declare registration before capability composition.");
|
|
10256
|
+
if (registration.ownerType !== options.ownerType) {
|
|
10257
|
+
throw new Error(`${String(registration.ownerType)} capabilities cannot be composed through the ${options.ownerType} command surface.`);
|
|
10258
|
+
}
|
|
10259
|
+
const target = options.ownerType === "topolo_first_party" ? "platform" : "developer";
|
|
10260
|
+
const surfaces = resolveManifestSurfaces(manifest, registration);
|
|
10261
|
+
const existing = Array.isArray(registration.capabilities) ? registration.capabilities.filter((value) => typeof value === "string") : [];
|
|
10262
|
+
const capabilities = resolveApplicationCapabilities({
|
|
10263
|
+
target,
|
|
10264
|
+
browser: surfaces.browser,
|
|
10265
|
+
api: surfaces.api,
|
|
10266
|
+
mobile: surfaces.mobile,
|
|
10267
|
+
distributionModel: registration.distributionModel === "marketplace" ? "marketplace" : "organization_internal",
|
|
10268
|
+
requested: [...existing, ...options.additions ?? []]
|
|
10269
|
+
});
|
|
10270
|
+
const expectedComposition = capabilityCompositionProjection(capabilities);
|
|
10271
|
+
const expectedNotifications = capabilities.selected.includes("notification_producer") ? { role: "producer", contracts: "topolo.notification-contracts.json" } : capabilities.selected.includes("notifications") ? { role: "consumer_only" } : { role: "internal_non_producer" };
|
|
10272
|
+
const changes = [];
|
|
10273
|
+
compareChange(changes, "topolo.app.json#capabilityComposition", manifest.capabilityComposition, expectedComposition);
|
|
10274
|
+
compareChange(changes, "topolo.app.json#registration.capabilities", registration.capabilities, capabilities.selected);
|
|
10275
|
+
compareChange(changes, "topolo.app.json#notifications", manifest.notifications, expectedNotifications);
|
|
10276
|
+
const packageJson2 = parseRecord(await readFile3(packagePath, "utf8"), packagePath);
|
|
10277
|
+
const dependencies = record(packageJson2.dependencies) ?? {};
|
|
10278
|
+
const nextDependencies = { ...dependencies };
|
|
10279
|
+
for (const packageName of capabilities.packages) {
|
|
10280
|
+
if (typeof nextDependencies[packageName] !== "string") {
|
|
10281
|
+
nextDependencies[packageName] = packageVersion(packageName, target);
|
|
10282
|
+
}
|
|
10283
|
+
}
|
|
10284
|
+
const sortedDependencies = sortRecord(nextDependencies);
|
|
10285
|
+
compareChange(changes, "package.json#dependencies", dependencies, sortedDependencies);
|
|
10286
|
+
const expectedContract = capabilityContractProjection(capabilities);
|
|
10287
|
+
const existingContract = await readOptionalJson(contractPath);
|
|
10288
|
+
compareChange(changes, "topolo.capability-contract.json", existingContract, expectedContract);
|
|
10289
|
+
const adapter = target === "platform" ? managedCapabilityAdapter(capabilities.selected) : null;
|
|
10290
|
+
const apiDirectory = resolveApiDirectory(root, manifest, target);
|
|
10291
|
+
const adapterPath = path3.join(apiDirectory, "src", "topolo-capabilities.ts");
|
|
10292
|
+
if (adapter) compareChange(changes, relativePath(root, adapterPath), await readOptionalText(adapterPath), adapter);
|
|
10293
|
+
const pricingPath = path3.join(root, "pricing-manifest.json");
|
|
10294
|
+
const pricingManifest = capabilities.selected.includes("billing") ? defaultPricingManifest({
|
|
10295
|
+
appId: String(manifest.appId || manifest.app_id || "").trim(),
|
|
10296
|
+
displayName: String(registration.displayName || manifest.name || "Application"),
|
|
10297
|
+
description: String(registration.description || `${String(registration.displayName || manifest.name || "Application")} on Topolo.`)
|
|
10298
|
+
}) : null;
|
|
10299
|
+
if (pricingManifest) {
|
|
10300
|
+
compareChange(changes, "pricing-manifest.json", await readOptionalJson(pricingPath), pricingManifest);
|
|
10301
|
+
}
|
|
10302
|
+
const missingResources = capabilities.resources.filter((resource) => !manifestProvidesResource(manifest, resource));
|
|
10303
|
+
const verticalConfiguration = [];
|
|
10304
|
+
for (const [capability2, fileName, description] of VERTICAL_CAPABILITY_FILES) {
|
|
10305
|
+
if (capabilities.selected.includes(capability2) && !await fileExists(path3.join(root, fileName))) {
|
|
10306
|
+
verticalConfiguration.push(`${fileName}: ${description}`);
|
|
10307
|
+
}
|
|
10308
|
+
}
|
|
10309
|
+
if (options.mode === "apply" && changes.length > 0) {
|
|
10310
|
+
manifest.capabilityComposition = expectedComposition;
|
|
10311
|
+
registration.capabilities = capabilities.selected;
|
|
10312
|
+
manifest.registration = registration;
|
|
10313
|
+
manifest.notifications = expectedNotifications;
|
|
10314
|
+
packageJson2.dependencies = sortedDependencies;
|
|
10315
|
+
await writeJson(manifestPath, manifest);
|
|
10316
|
+
await writeJson(packagePath, packageJson2);
|
|
10317
|
+
await writeJson(contractPath, expectedContract);
|
|
10318
|
+
if (pricingManifest) await writeJson(pricingPath, pricingManifest);
|
|
10319
|
+
if (adapter) {
|
|
10320
|
+
await mkdir3(path3.dirname(adapterPath), { recursive: true });
|
|
10321
|
+
await writeFile4(adapterPath, adapter, "utf8");
|
|
10322
|
+
}
|
|
10323
|
+
}
|
|
10324
|
+
const blocked = missingResources.length > 0 || verticalConfiguration.length > 0;
|
|
10325
|
+
return {
|
|
10326
|
+
mode: options.mode,
|
|
10327
|
+
applied: options.mode === "apply" && changes.length > 0,
|
|
10328
|
+
status: blocked ? "blocked" : options.mode === "apply" || changes.length === 0 ? "in_sync" : "drift",
|
|
10329
|
+
manifestPath,
|
|
10330
|
+
packagePath,
|
|
10331
|
+
contractPath,
|
|
10332
|
+
capabilities,
|
|
10333
|
+
changes,
|
|
10334
|
+
missingResources,
|
|
10335
|
+
verticalConfiguration
|
|
10336
|
+
};
|
|
10337
|
+
}
|
|
10338
|
+
function registerApplicationCapabilityCommands(parent, ownerType) {
|
|
10339
|
+
parent.command("add-capability <capabilities...>").description("Compose approved shared capabilities into an existing application source tree.").option("--manifest <path>", "Path to topolo.app.json", "topolo.app.json").option("--check", "Report source changes without writing").option("--apply", "Write canonical manifest, package, and managed capability artifacts").option("--json", "Output JSON").option("--no-json", "Output human-readable details").action(async (capabilities, options) => {
|
|
10340
|
+
const outputMode = resolveOutputMode(options.json);
|
|
10341
|
+
try {
|
|
10342
|
+
if (options.check && options.apply) throw new Error("Choose exactly one of --check or --apply.");
|
|
10343
|
+
const result = await reconcileApplicationCapabilitySource({
|
|
10344
|
+
manifestPath: options.manifest,
|
|
10345
|
+
ownerType,
|
|
10346
|
+
mode: options.apply ? "apply" : "check",
|
|
10347
|
+
additions: capabilities
|
|
10348
|
+
});
|
|
10349
|
+
emit(result, outputMode, () => formatCapabilitySource(result));
|
|
10350
|
+
if (result.status !== "in_sync" && !options.apply) process.exitCode = 2;
|
|
10351
|
+
if (result.status === "blocked") process.exitCode = 2;
|
|
10352
|
+
} catch (error) {
|
|
10353
|
+
fail(error, outputMode);
|
|
10354
|
+
}
|
|
10355
|
+
});
|
|
10356
|
+
parent.command("doctor").description("Check declared capabilities against packages, resources, managed artifacts, and vertical insertion points.").option("--manifest <path>", "Path to topolo.app.json", "topolo.app.json").option("--json", "Output JSON").option("--no-json", "Output human-readable details").action(async (options) => {
|
|
10357
|
+
const outputMode = resolveOutputMode(options.json);
|
|
10358
|
+
try {
|
|
10359
|
+
const result = await reconcileApplicationCapabilitySource({ manifestPath: options.manifest, ownerType, mode: "check" });
|
|
10360
|
+
emit(result, outputMode, () => formatCapabilitySource(result));
|
|
10361
|
+
if (result.status !== "in_sync") process.exitCode = 2;
|
|
10362
|
+
} catch (error) {
|
|
10363
|
+
fail(error, outputMode);
|
|
10364
|
+
}
|
|
10365
|
+
});
|
|
10366
|
+
parent.command("upgrade").description("Upgrade an application to the current capability registry without changing its selected product capabilities.").option("--manifest <path>", "Path to topolo.app.json", "topolo.app.json").option("--check", "Report source changes without writing").option("--apply", "Apply the registry upgrade").option("--json", "Output JSON").option("--no-json", "Output human-readable details").action(async (options) => {
|
|
10367
|
+
const outputMode = resolveOutputMode(options.json);
|
|
10368
|
+
try {
|
|
10369
|
+
if (options.check && options.apply) throw new Error("Choose exactly one of --check or --apply.");
|
|
10370
|
+
const result = await reconcileApplicationCapabilitySource({
|
|
10371
|
+
manifestPath: options.manifest,
|
|
10372
|
+
ownerType,
|
|
10373
|
+
mode: options.apply ? "apply" : "check"
|
|
10374
|
+
});
|
|
10375
|
+
emit(result, outputMode, () => formatCapabilitySource(result));
|
|
10376
|
+
if (result.status !== "in_sync" && !options.apply) process.exitCode = 2;
|
|
10377
|
+
if (result.status === "blocked") process.exitCode = 2;
|
|
10378
|
+
} catch (error) {
|
|
10379
|
+
fail(error, outputMode);
|
|
10380
|
+
}
|
|
10381
|
+
});
|
|
10382
|
+
}
|
|
10383
|
+
function formatCapabilitySource(result) {
|
|
10384
|
+
const lines = [
|
|
10385
|
+
`Capability source: ${result.status}`,
|
|
10386
|
+
`Registry: ${result.capabilities.registryVersion}`,
|
|
10387
|
+
`Selected: ${result.capabilities.selected.join(", ")}`
|
|
10388
|
+
];
|
|
10389
|
+
for (const change of result.changes) lines.push(`- ${change.path}`);
|
|
10390
|
+
if (result.missingResources.length > 0) lines.push(`Missing provisioned resources: ${result.missingResources.join(", ")}`);
|
|
10391
|
+
for (const item of result.verticalConfiguration) lines.push(`Vertical configuration required: ${item}`);
|
|
10392
|
+
return lines.join("\n");
|
|
10393
|
+
}
|
|
10394
|
+
function resolveManifestSurfaces(manifest, registration) {
|
|
10395
|
+
const targets = record(manifest.targets) ?? {};
|
|
10396
|
+
const targetRecords = Object.values(targets).map(record).filter((value) => Boolean(value));
|
|
10397
|
+
const runtime = record(manifest.runtime);
|
|
10398
|
+
return {
|
|
10399
|
+
browser: targetRecords.some((target) => Boolean(target.assets) || target.dir === "apps/web" || target.kind === "web") || runtime?.browser === true,
|
|
10400
|
+
api: targetRecords.some((target) => target.dir === "apps/api" || target.kind === "worker" && !target.assets) || runtime?.api === true,
|
|
10401
|
+
mobile: registration.surfaceModel === "mobile_container" || registration.surfaceModel === "mobile_module"
|
|
10402
|
+
};
|
|
10403
|
+
}
|
|
10404
|
+
function manifestProvidesResource(manifest, resource) {
|
|
10405
|
+
const targets = record(manifest.targets) ?? {};
|
|
10406
|
+
for (const value of Object.values(targets)) {
|
|
10407
|
+
const target = record(value);
|
|
10408
|
+
const bindings = record(target?.bindings);
|
|
10409
|
+
if (!bindings) continue;
|
|
10410
|
+
if (resource === "d1" && Array.isArray(bindings.d1) && bindings.d1.length > 0) {
|
|
10411
|
+
return bindings.d1.every((value2) => {
|
|
10412
|
+
const binding = record(value2);
|
|
10413
|
+
const configurations = ["development", "staging", "production"].map((environment) => record(binding?.[environment])).filter((configuration) => Boolean(configuration));
|
|
10414
|
+
return configurations.length > 0 && configurations.every((configuration) => typeof configuration.databaseId === "string" && configuration.databaseId.length > 0 && configuration.databaseId !== "provision-after-scaffold");
|
|
10415
|
+
});
|
|
10416
|
+
}
|
|
10417
|
+
if (resource === "r2" && Array.isArray(bindings.r2) && bindings.r2.length > 0) return true;
|
|
10418
|
+
if (resource === "queue" && record(bindings.queues)) return true;
|
|
10419
|
+
if (resource === "workflow" && Array.isArray(bindings.workflows) && bindings.workflows.length > 0) return true;
|
|
10420
|
+
}
|
|
10421
|
+
return false;
|
|
10422
|
+
}
|
|
10423
|
+
function packageVersion(packageName, target) {
|
|
10424
|
+
if (target === "platform") return "latest";
|
|
10425
|
+
if (packageName === "@topolo/login") return "^0.1.0";
|
|
10426
|
+
if (packageName === "@topolo/sdk") return "^0.8.1";
|
|
10427
|
+
throw new Error(`Capability registry exposed private package ${packageName} to a third-party application.`);
|
|
10428
|
+
}
|
|
10429
|
+
function compareChange(changes, changePath, before, after) {
|
|
10430
|
+
if (stableJson(before) !== stableJson(after)) changes.push({ path: changePath, before, after });
|
|
10431
|
+
}
|
|
9879
10432
|
function record(value) {
|
|
9880
10433
|
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
9881
10434
|
}
|
|
10435
|
+
function parseRecord(raw, filePath) {
|
|
10436
|
+
const value = JSON.parse(raw);
|
|
10437
|
+
const parsed = record(value);
|
|
10438
|
+
if (!parsed) throw new Error(`${filePath} must contain a JSON object.`);
|
|
10439
|
+
return parsed;
|
|
10440
|
+
}
|
|
10441
|
+
async function readOptionalJson(filePath) {
|
|
10442
|
+
try {
|
|
10443
|
+
return JSON.parse(await readFile3(filePath, "utf8"));
|
|
10444
|
+
} catch (error) {
|
|
10445
|
+
if (error.code === "ENOENT") return null;
|
|
10446
|
+
throw error;
|
|
10447
|
+
}
|
|
10448
|
+
}
|
|
10449
|
+
async function readOptionalText(filePath) {
|
|
10450
|
+
try {
|
|
10451
|
+
return await readFile3(filePath, "utf8");
|
|
10452
|
+
} catch (error) {
|
|
10453
|
+
if (error.code === "ENOENT") return null;
|
|
10454
|
+
throw error;
|
|
10455
|
+
}
|
|
10456
|
+
}
|
|
10457
|
+
async function fileExists(filePath) {
|
|
10458
|
+
try {
|
|
10459
|
+
await readFile3(filePath);
|
|
10460
|
+
return true;
|
|
10461
|
+
} catch (error) {
|
|
10462
|
+
if (error.code === "ENOENT") return false;
|
|
10463
|
+
throw error;
|
|
10464
|
+
}
|
|
10465
|
+
}
|
|
10466
|
+
async function writeJson(filePath, value) {
|
|
10467
|
+
await writeFile4(filePath, `${JSON.stringify(value, null, 2)}
|
|
10468
|
+
`, "utf8");
|
|
10469
|
+
}
|
|
10470
|
+
function sortRecord(value) {
|
|
10471
|
+
return Object.fromEntries(Object.entries(value).sort(([left], [right]) => left.localeCompare(right)));
|
|
10472
|
+
}
|
|
10473
|
+
function stableJson(value) {
|
|
10474
|
+
if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
|
|
10475
|
+
if (value && typeof value === "object") {
|
|
10476
|
+
return `{${Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => `${JSON.stringify(key)}:${stableJson(item)}`).join(",")}}`;
|
|
10477
|
+
}
|
|
10478
|
+
return JSON.stringify(value) ?? "undefined";
|
|
10479
|
+
}
|
|
10480
|
+
var VERTICAL_CAPABILITY_FILES = [
|
|
10481
|
+
["notification_producer", "topolo.notification-contracts.json", "define app-owned events and recipient policies"],
|
|
10482
|
+
["metering", "topolo.metering.json", "define app-owned meters and attribution semantics"],
|
|
10483
|
+
["nexus_ai", "topolo.ai.json", "define AI use cases, budget behavior, and model policy"],
|
|
10484
|
+
["integrations", "topolo.integrations.json", "define the external providers and connection purposes this app uses"],
|
|
10485
|
+
["webhooks", "topolo.webhooks.json", "define signed event routes, replay policy, and delivery ownership"],
|
|
10486
|
+
["automation", "topolo.automation.json", "define app-owned triggers and TopoloFlow actions"],
|
|
10487
|
+
["data", "topolo.data.json", "define the workspace data model and migration ownership"],
|
|
10488
|
+
["object_storage", "topolo.object-storage.json", "define object lifecycle, access, and deletion policy"],
|
|
10489
|
+
["background_jobs", "topolo.jobs.json", "define accepted job types, retry policy, and idempotency"],
|
|
10490
|
+
["durable_workflows", "topolo.workflows.json", "define workflow inputs, steps, and recovery semantics"],
|
|
10491
|
+
["privacy", "topolo.privacy.json", "define app-owned retention, export, and deletion adapters"],
|
|
10492
|
+
["chat", "topolo.chat.json", "define where the shared chat surface appears in this product"],
|
|
10493
|
+
["calendar", "topolo.calendar.json", "define the app-owned scheduling surface and resource mapping"],
|
|
10494
|
+
["mailbox", "topolo.mailbox.json", "define the app-owned mailbox surface and message context"]
|
|
10495
|
+
];
|
|
10496
|
+
function resolveApiDirectory(root, manifest, target) {
|
|
10497
|
+
if (target === "developer") return root;
|
|
10498
|
+
for (const value of Object.values(record(manifest.targets) ?? {})) {
|
|
10499
|
+
const targetRecord = record(value);
|
|
10500
|
+
if (targetRecord?.kind === "worker" && !targetRecord.assets && typeof targetRecord.dir === "string") {
|
|
10501
|
+
return path3.resolve(root, targetRecord.dir);
|
|
10502
|
+
}
|
|
10503
|
+
}
|
|
10504
|
+
return path3.join(root, "apps", "api");
|
|
10505
|
+
}
|
|
10506
|
+
function relativePath(root, filePath) {
|
|
10507
|
+
return path3.relative(root, filePath).split(path3.sep).join("/");
|
|
10508
|
+
}
|
|
10509
|
+
|
|
10510
|
+
// src/commands/app-reconcile.ts
|
|
10511
|
+
function record2(value) {
|
|
10512
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
10513
|
+
}
|
|
9882
10514
|
function environmentValue(value, environment) {
|
|
9883
10515
|
if (typeof value === "string") return value;
|
|
9884
10516
|
if (!value) return null;
|
|
@@ -9903,7 +10535,7 @@ function resolveManifestApplicationUrl(manifest, environment) {
|
|
|
9903
10535
|
return `https://${target.subdomain}.${domain}`;
|
|
9904
10536
|
}
|
|
9905
10537
|
function assertRegistration(value) {
|
|
9906
|
-
const registration =
|
|
10538
|
+
const registration = record2(value);
|
|
9907
10539
|
const required = [
|
|
9908
10540
|
"ownerType",
|
|
9909
10541
|
"applicationId",
|
|
@@ -9932,9 +10564,9 @@ function titleFromSlug(value) {
|
|
|
9932
10564
|
return value.replace(/^topolo[-_]/, "").split(/[-_]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
|
|
9933
10565
|
}
|
|
9934
10566
|
async function completeApplicationRegistrationInteractively(options) {
|
|
9935
|
-
const manifestPath =
|
|
9936
|
-
const manifest = JSON.parse(await
|
|
9937
|
-
const existing =
|
|
10567
|
+
const manifestPath = path4.resolve(options.manifestPath || "topolo.app.json");
|
|
10568
|
+
const manifest = JSON.parse(await readFile4(manifestPath, "utf8"));
|
|
10569
|
+
const existing = record2(manifest.registration) || {};
|
|
9938
10570
|
const appId = String(manifest.appId || manifest.app_id || "").trim();
|
|
9939
10571
|
const slug = String(existing.serviceSlug || manifest.name || "").trim();
|
|
9940
10572
|
const defaultName = titleFromSlug(slug);
|
|
@@ -9968,19 +10600,19 @@ async function completeApplicationRegistrationInteractively(options) {
|
|
|
9968
10600
|
containerAppId: existing.containerAppId,
|
|
9969
10601
|
distributionModel: existing.distributionModel || (options.expectedOwnerType === "topolo_first_party" ? "marketplace" : "organization_internal"),
|
|
9970
10602
|
version: String(existing.version || "1.0.0"),
|
|
9971
|
-
pricing:
|
|
10603
|
+
pricing: record2(existing.pricing) || { type: "free", monthlyPrice: null },
|
|
9972
10604
|
supportEmail,
|
|
9973
10605
|
productionUrl,
|
|
9974
10606
|
demoUrl: typeof existing.demoUrl === "string" ? existing.demoUrl : void 0,
|
|
9975
10607
|
repoUrl: typeof existing.repoUrl === "string" ? existing.repoUrl : void 0,
|
|
9976
10608
|
tags: Array.isArray(existing.tags) ? existing.tags : [],
|
|
9977
10609
|
screenshots: Array.isArray(existing.screenshots) ? existing.screenshots : [],
|
|
9978
|
-
auth:
|
|
10610
|
+
auth: record2(existing.auth) || {
|
|
9979
10611
|
callbackPath: "/auth/callback",
|
|
9980
10612
|
requestedScopes: ["openid", "profile", "email", "organization:read"]
|
|
9981
10613
|
},
|
|
9982
10614
|
capabilities: Array.isArray(existing.capabilities) ? existing.capabilities : ["login", "launcher"],
|
|
9983
|
-
marketplace:
|
|
10615
|
+
marketplace: record2(existing.marketplace) || {
|
|
9984
10616
|
intent: options.expectedOwnerType === "topolo_first_party" ? "submit_when_ready" : "not_requested",
|
|
9985
10617
|
status: "preview",
|
|
9986
10618
|
shortBlurb: description,
|
|
@@ -10004,7 +10636,7 @@ async function completeApplicationRegistrationInteractively(options) {
|
|
|
10004
10636
|
changed = true;
|
|
10005
10637
|
}
|
|
10006
10638
|
if (!appId) throw new Error("topolo.app.json must declare the immutable appId before reconciliation.");
|
|
10007
|
-
if (changed) await
|
|
10639
|
+
if (changed) await writeFile5(manifestPath, `${JSON.stringify(manifest, null, 2)}
|
|
10008
10640
|
`, "utf8");
|
|
10009
10641
|
return { changed, manifestPath };
|
|
10010
10642
|
} finally {
|
|
@@ -10012,8 +10644,8 @@ async function completeApplicationRegistrationInteractively(options) {
|
|
|
10012
10644
|
}
|
|
10013
10645
|
}
|
|
10014
10646
|
async function loadApplicationReconcileRequest(options) {
|
|
10015
|
-
const manifestPath =
|
|
10016
|
-
const raw = await
|
|
10647
|
+
const manifestPath = path4.resolve(options.manifestPath || "topolo.app.json");
|
|
10648
|
+
const raw = await readFile4(manifestPath, "utf8");
|
|
10017
10649
|
const manifest = JSON.parse(raw);
|
|
10018
10650
|
assertRegistration(manifest.registration);
|
|
10019
10651
|
const registration = manifest.registration;
|
|
@@ -10157,6 +10789,7 @@ function registerApplicationReconcileCommand(parent, ownerType, dependencies = {
|
|
|
10157
10789
|
const loadRequest = dependencies.loadRequest || loadApplicationReconcileRequest;
|
|
10158
10790
|
const reconcileRequest = dependencies.reconcileRequest || reconcileApplicationRequest;
|
|
10159
10791
|
const confirm = dependencies.confirm || confirmApplicationReconcile;
|
|
10792
|
+
const reconcileSource = dependencies.reconcileSource || reconcileApplicationCapabilitySource;
|
|
10160
10793
|
parent.command("reconcile").description("Check or apply the declarative application registration in topolo.app.json.").option("--manifest <path>", "Path to topolo.app.json", "topolo.app.json").option("--check", "Report drift without mutating remote data").option("--dry-run", "Alias for --check").option("--apply", "Apply mutable drift and required projections").option("--interactive", "Use the human confirmation flow").option("--non-interactive", "Disable all prompts").option("--yes", "Accept the interactive apply confirmation").option("--json", "Output JSON").option("--no-json", "Output human-readable details").action(async (options) => {
|
|
10161
10794
|
const outputMode = resolveOutputMode(options.json);
|
|
10162
10795
|
try {
|
|
@@ -10167,6 +10800,20 @@ function registerApplicationReconcileCommand(parent, ownerType, dependencies = {
|
|
|
10167
10800
|
expectedOwnerType: ownerType
|
|
10168
10801
|
});
|
|
10169
10802
|
}
|
|
10803
|
+
const source = await reconcileSource({
|
|
10804
|
+
manifestPath: options.manifest,
|
|
10805
|
+
ownerType,
|
|
10806
|
+
mode: selection.mode
|
|
10807
|
+
});
|
|
10808
|
+
if (selection.mode === "apply" && source.status === "blocked") {
|
|
10809
|
+
emit(
|
|
10810
|
+
{ manifest: source.manifestPath, source, reconciliation: null },
|
|
10811
|
+
outputMode,
|
|
10812
|
+
() => formatCapabilitySource(source)
|
|
10813
|
+
);
|
|
10814
|
+
process.exitCode = 2;
|
|
10815
|
+
return;
|
|
10816
|
+
}
|
|
10170
10817
|
const loaded = await loadRequest({
|
|
10171
10818
|
manifestPath: options.manifest,
|
|
10172
10819
|
expectedOwnerType: ownerType,
|
|
@@ -10184,11 +10831,11 @@ function registerApplicationReconcileCommand(parent, ownerType, dependencies = {
|
|
|
10184
10831
|
}
|
|
10185
10832
|
}
|
|
10186
10833
|
emit(
|
|
10187
|
-
{ manifest: loaded.manifestPath, reconciliation: result },
|
|
10834
|
+
{ manifest: loaded.manifestPath, source, reconciliation: result },
|
|
10188
10835
|
outputMode,
|
|
10189
10836
|
() => formatApplicationReconcile(result, loaded.manifestPath)
|
|
10190
10837
|
);
|
|
10191
|
-
if (result.mode === "check" && result.plan.status !== "in_sync") process.exitCode = 2;
|
|
10838
|
+
if (source.status === "blocked" || result.mode === "check" && (result.plan.status !== "in_sync" || source.status !== "in_sync")) process.exitCode = 2;
|
|
10192
10839
|
} catch (error) {
|
|
10193
10840
|
fail(error, outputMode);
|
|
10194
10841
|
}
|
|
@@ -10226,6 +10873,7 @@ function registerAppsCommand(program2) {
|
|
|
10226
10873
|
}
|
|
10227
10874
|
});
|
|
10228
10875
|
registerApplicationReconcileCommand(apps, "third_party");
|
|
10876
|
+
registerApplicationCapabilityCommands(apps, "third_party");
|
|
10229
10877
|
apps.command("get <application>").description("Show one Topolo application available to the current credential.").option("--json", "Output JSON", true).option("--no-json", "Output human-readable details").action(async (application, opts) => {
|
|
10230
10878
|
const mode = resolveOutputMode(opts.json);
|
|
10231
10879
|
try {
|
|
@@ -10295,7 +10943,7 @@ function registerAppsCommand(program2) {
|
|
|
10295
10943
|
).option("--json", "Output JSON").option("--no-json", "Output human-readable checklist").option("--app-dir <path>", "Application directory under the scaffold root").option("--name <name>", "Application display/package source name").option("--package-name <name>", "package.json name").option("--production-url <url>", "Production browser URL").option("--app-id <id>", "Stable SDK/Auth app id for API-backed apps").option("--audience <audience>", "Developer app audience: business or personal").option("--tenancy <tenancy>", "Developer app tenancy: organization or household").option(
|
|
10296
10944
|
"--surface-model <surfaceModel>",
|
|
10297
10945
|
"Developer app surface model: web_app, mobile_container, mobile_module, or messaging_surface"
|
|
10298
|
-
).option("--container-app-id <appId>", "Container app id when --surface-model mobile_module is used").option("--write", "Write the generated scaffold files to disk").option("--repo-root <path>", "Directory where scaffold files should be written").option("--force", "Overwrite existing scaffold files when --write is set").option("--with-api", "Include API/service files for developer scaffolds").option("--no-api", "Do not include API/service requirements").option("--no-browser", "Do not include browser-shell requirements").option(
|
|
10946
|
+
).option("--container-app-id <appId>", "Container app id when --surface-model mobile_module is used").option("--capability <id...>", "Compose one or more approved Topolo capabilities").option("--write", "Write the generated scaffold files to disk").option("--repo-root <path>", "Directory where scaffold files should be written").option("--force", "Overwrite existing scaffold files when --write is set").option("--with-api", "Include API/service files for developer scaffolds").option("--no-api", "Do not include API/service requirements").option("--no-browser", "Do not include browser-shell requirements").option(
|
|
10299
10947
|
"--provision",
|
|
10300
10948
|
"Register the app in the current Topolo developer workspace using the authenticated session"
|
|
10301
10949
|
).action(async (application, opts) => {
|
|
@@ -10314,7 +10962,8 @@ function registerAppsCommand(program2) {
|
|
|
10314
10962
|
audience: resolveScaffoldAudience(opts.audience),
|
|
10315
10963
|
tenancy: resolveScaffoldTenancy(opts.tenancy),
|
|
10316
10964
|
surfaceModel: resolveScaffoldSurfaceModel(opts.surfaceModel),
|
|
10317
|
-
containerAppId: opts.containerAppId
|
|
10965
|
+
containerAppId: opts.containerAppId,
|
|
10966
|
+
capabilities: opts.capability
|
|
10318
10967
|
});
|
|
10319
10968
|
const writeResult = opts.write ? await writeApplicationScaffold(payload, { repoRoot: opts.repoRoot, force: opts.force }) : null;
|
|
10320
10969
|
const provisionResult = opts.provision ? await provisionApplicationScaffold(payload) : null;
|
|
@@ -10398,7 +11047,7 @@ function applicationAuditPayload(application, options = {}) {
|
|
|
10398
11047
|
}
|
|
10399
11048
|
function recordApplicationEvidence(application, options = {}) {
|
|
10400
11049
|
const appId = resolveApplication(application).id;
|
|
10401
|
-
const root =
|
|
11050
|
+
const root = path5.resolve(options.appRoot ?? process.cwd());
|
|
10402
11051
|
if (!existsSync4(root)) {
|
|
10403
11052
|
throw new Error(`Application root does not exist: ${root}`);
|
|
10404
11053
|
}
|
|
@@ -10410,8 +11059,8 @@ function recordApplicationEvidence(application, options = {}) {
|
|
|
10410
11059
|
commit = void 0;
|
|
10411
11060
|
}
|
|
10412
11061
|
}
|
|
10413
|
-
const evidenceDir =
|
|
10414
|
-
const evidencePath =
|
|
11062
|
+
const evidenceDir = path5.join(root, ".topolo");
|
|
11063
|
+
const evidencePath = path5.join(evidenceDir, "evidence.json");
|
|
10415
11064
|
let before = null;
|
|
10416
11065
|
if (existsSync4(evidencePath)) {
|
|
10417
11066
|
try {
|
|
@@ -10711,17 +11360,181 @@ function statusesAtOrWorse(threshold) {
|
|
|
10711
11360
|
|
|
10712
11361
|
// src/commands/platform.ts
|
|
10713
11362
|
import { readdirSync as readdirSync2, readFileSync as readFileSync6, statSync as statSync2 } from "fs";
|
|
10714
|
-
import
|
|
11363
|
+
import path7 from "path";
|
|
11364
|
+
|
|
11365
|
+
// src/commands/app-capability-provision.ts
|
|
11366
|
+
import { readFile as readFile5, writeFile as writeFile6 } from "fs/promises";
|
|
11367
|
+
import path6 from "path";
|
|
11368
|
+
async function provisionApplicationResources(options) {
|
|
11369
|
+
const manifestPath = path6.resolve(options.manifestPath || "topolo.app.json");
|
|
11370
|
+
const manifest = parseRecord2(await readFile5(manifestPath, "utf8"), manifestPath);
|
|
11371
|
+
const environments = requiredRecord(manifest.environments, "topolo.app.json#environments");
|
|
11372
|
+
const environment = requiredRecord(environments[options.environment], `topolo.app.json#environments.${options.environment}`);
|
|
11373
|
+
const accountId = requiredString(environment.accountId, `topolo.app.json#environments.${options.environment}.accountId`);
|
|
11374
|
+
const token = options.token ?? process.env.CLOUDFLARE_API_TOKEN;
|
|
11375
|
+
if (!token) throw new Error("CLOUDFLARE_API_TOKEN is required to inspect or provision Cloudflare application resources.");
|
|
11376
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
11377
|
+
const operations = [];
|
|
11378
|
+
let changed = false;
|
|
11379
|
+
for (const target of Object.values(record3(manifest.targets) ?? {})) {
|
|
11380
|
+
const bindings = record3(record3(target)?.bindings);
|
|
11381
|
+
if (!bindings) continue;
|
|
11382
|
+
for (const bindingValue of array(bindings.d1)) {
|
|
11383
|
+
const binding = record3(bindingValue);
|
|
11384
|
+
const config = record3(binding?.[options.environment]);
|
|
11385
|
+
if (!config) continue;
|
|
11386
|
+
const name = requiredString(config.databaseName, `D1 ${options.environment} databaseName`);
|
|
11387
|
+
const databases = await cloudflareList(fetchImpl, token, `/accounts/${accountId}/d1/database?name=${encodeURIComponent(name)}&per_page=10000`);
|
|
11388
|
+
const existing = databases.find((item) => item.name === name);
|
|
11389
|
+
if (existing) {
|
|
11390
|
+
const id = requiredString(existing.uuid ?? existing.id, `D1 database ${name} id`);
|
|
11391
|
+
const mismatched = config.databaseId !== id;
|
|
11392
|
+
operations.push({ kind: "d1", name, status: mismatched && options.mode === "check" ? "mismatch" : "exists", id });
|
|
11393
|
+
if (mismatched && options.mode === "apply") {
|
|
11394
|
+
config.databaseId = id;
|
|
11395
|
+
changed = true;
|
|
11396
|
+
}
|
|
11397
|
+
} else if (options.mode === "apply") {
|
|
11398
|
+
const created = await cloudflareCreate(fetchImpl, token, `/accounts/${accountId}/d1/database`, { name });
|
|
11399
|
+
const id = requiredString(created.uuid ?? created.id, `created D1 database ${name} id`);
|
|
11400
|
+
config.databaseId = id;
|
|
11401
|
+
changed = true;
|
|
11402
|
+
operations.push({ kind: "d1", name, status: "created", id });
|
|
11403
|
+
} else {
|
|
11404
|
+
operations.push({ kind: "d1", name, status: "missing" });
|
|
11405
|
+
}
|
|
11406
|
+
}
|
|
11407
|
+
for (const bindingValue of array(bindings.r2)) {
|
|
11408
|
+
const config = record3(record3(bindingValue)?.[options.environment]);
|
|
11409
|
+
if (!config) continue;
|
|
11410
|
+
const name = requiredString(config.bucketName, `R2 ${options.environment} bucketName`);
|
|
11411
|
+
const buckets = await cloudflareList(fetchImpl, token, `/accounts/${accountId}/r2/buckets?name_contains=${encodeURIComponent(name)}&per_page=1000`);
|
|
11412
|
+
const exists = buckets.some((item) => item.name === name);
|
|
11413
|
+
if (exists) operations.push({ kind: "r2", name, status: "exists" });
|
|
11414
|
+
else if (options.mode === "apply") {
|
|
11415
|
+
await cloudflareCreate(fetchImpl, token, `/accounts/${accountId}/r2/buckets`, { name });
|
|
11416
|
+
operations.push({ kind: "r2", name, status: "created" });
|
|
11417
|
+
} else operations.push({ kind: "r2", name, status: "missing" });
|
|
11418
|
+
}
|
|
11419
|
+
const queues = record3(bindings.queues);
|
|
11420
|
+
const queueNames = /* @__PURE__ */ new Set();
|
|
11421
|
+
for (const bindingValue of [...array(queues?.producers), ...array(queues?.consumers)]) {
|
|
11422
|
+
const config = record3(record3(bindingValue)?.[options.environment]);
|
|
11423
|
+
if (config && typeof config.queue === "string") queueNames.add(config.queue);
|
|
11424
|
+
}
|
|
11425
|
+
if (queueNames.size > 0) {
|
|
11426
|
+
const existingQueues = await cloudflareList(fetchImpl, token, `/accounts/${accountId}/queues?page=1&per_page=1000`);
|
|
11427
|
+
for (const name of queueNames) {
|
|
11428
|
+
const exists = existingQueues.some((item) => item.queue_name === name || item.name === name);
|
|
11429
|
+
if (exists) operations.push({ kind: "queue", name, status: "exists" });
|
|
11430
|
+
else if (options.mode === "apply") {
|
|
11431
|
+
await cloudflareCreate(fetchImpl, token, `/accounts/${accountId}/queues`, { queue_name: name });
|
|
11432
|
+
operations.push({ kind: "queue", name, status: "created" });
|
|
11433
|
+
} else operations.push({ kind: "queue", name, status: "missing" });
|
|
11434
|
+
}
|
|
11435
|
+
}
|
|
11436
|
+
}
|
|
11437
|
+
if (changed) await writeFile6(manifestPath, `${JSON.stringify(manifest, null, 2)}
|
|
11438
|
+
`, "utf8");
|
|
11439
|
+
const drift = operations.some((operation) => operation.status === "missing" || operation.status === "mismatch");
|
|
11440
|
+
return {
|
|
11441
|
+
mode: options.mode,
|
|
11442
|
+
environment: options.environment,
|
|
11443
|
+
manifestPath,
|
|
11444
|
+
accountId,
|
|
11445
|
+
changed,
|
|
11446
|
+
status: drift ? "drift" : "in_sync",
|
|
11447
|
+
operations
|
|
11448
|
+
};
|
|
11449
|
+
}
|
|
11450
|
+
function registerApplicationResourceProvisionCommand(parent) {
|
|
11451
|
+
parent.command("provision-resources").description("Inspect or create declared Cloudflare resources for one explicit application environment.").requiredOption("--environment <environment>", "development, staging, or production").option("--manifest <path>", "Path to topolo.app.json", "topolo.app.json").option("--check", "Inspect without creating resources").option("--apply", "Create missing resources and write provider identifiers").option("--confirm-production", "Confirm production resource creation").option("--json", "Output JSON").option("--no-json", "Output human-readable details").action(async (options) => {
|
|
11452
|
+
const outputMode = resolveOutputMode(options.json);
|
|
11453
|
+
try {
|
|
11454
|
+
if (!isEnvironment(options.environment)) throw new Error("Environment must be development, staging, or production.");
|
|
11455
|
+
if (options.check && options.apply) throw new Error("Choose exactly one of --check or --apply.");
|
|
11456
|
+
if (options.environment === "production" && options.apply && !options.confirmProduction) {
|
|
11457
|
+
throw new Error("Production resource creation requires --confirm-production.");
|
|
11458
|
+
}
|
|
11459
|
+
const result = await provisionApplicationResources({
|
|
11460
|
+
manifestPath: options.manifest,
|
|
11461
|
+
environment: options.environment,
|
|
11462
|
+
mode: options.apply ? "apply" : "check"
|
|
11463
|
+
});
|
|
11464
|
+
emit(result, outputMode, () => formatResourceProvision(result));
|
|
11465
|
+
if (result.status !== "in_sync") process.exitCode = 2;
|
|
11466
|
+
} catch (error) {
|
|
11467
|
+
fail(error, outputMode);
|
|
11468
|
+
}
|
|
11469
|
+
});
|
|
11470
|
+
}
|
|
11471
|
+
function formatResourceProvision(result) {
|
|
11472
|
+
return [
|
|
11473
|
+
`Application resources: ${result.status}`,
|
|
11474
|
+
`Environment: ${result.environment}`,
|
|
11475
|
+
...result.operations.map((operation) => `- ${operation.kind} ${operation.name}: ${operation.status}`)
|
|
11476
|
+
].join("\n");
|
|
11477
|
+
}
|
|
11478
|
+
async function cloudflareList(fetchImpl, token, resourcePath) {
|
|
11479
|
+
const result = await cloudflareRequest(fetchImpl, token, resourcePath, "GET");
|
|
11480
|
+
if (Array.isArray(result)) return result.map(record3).filter((value) => Boolean(value));
|
|
11481
|
+
const resultRecord = record3(result);
|
|
11482
|
+
const values = array(resultRecord?.buckets ?? resultRecord?.queues ?? resultRecord?.databases);
|
|
11483
|
+
return values.map(record3).filter((value) => Boolean(value));
|
|
11484
|
+
}
|
|
11485
|
+
async function cloudflareCreate(fetchImpl, token, resourcePath, body) {
|
|
11486
|
+
const result = await cloudflareRequest(fetchImpl, token, resourcePath, "POST", body);
|
|
11487
|
+
return requiredRecord(result, `Cloudflare create response for ${resourcePath}`);
|
|
11488
|
+
}
|
|
11489
|
+
async function cloudflareRequest(fetchImpl, token, resourcePath, method, body) {
|
|
11490
|
+
const response = await fetchImpl(`https://api.cloudflare.com/client/v4${resourcePath}`, {
|
|
11491
|
+
method,
|
|
11492
|
+
headers: { Authorization: `Bearer ${token}`, ...body ? { "Content-Type": "application/json" } : {} },
|
|
11493
|
+
...body ? { body: JSON.stringify(body) } : {}
|
|
11494
|
+
});
|
|
11495
|
+
const payload = await response.json().catch(() => null);
|
|
11496
|
+
const envelope = record3(payload);
|
|
11497
|
+
if (!response.ok || envelope?.success === false) {
|
|
11498
|
+
throw new Error(`Cloudflare ${method} ${resourcePath} failed with HTTP ${response.status}.`);
|
|
11499
|
+
}
|
|
11500
|
+
return envelope && "result" in envelope ? envelope.result : payload;
|
|
11501
|
+
}
|
|
11502
|
+
function isEnvironment(value) {
|
|
11503
|
+
return value === "development" || value === "staging" || value === "production";
|
|
11504
|
+
}
|
|
11505
|
+
function parseRecord2(raw, filePath) {
|
|
11506
|
+
const value = JSON.parse(raw);
|
|
11507
|
+
return requiredRecord(value, filePath);
|
|
11508
|
+
}
|
|
11509
|
+
function requiredRecord(value, label) {
|
|
11510
|
+
const valueRecord = record3(value);
|
|
11511
|
+
if (!valueRecord) throw new Error(`${label} must be an object.`);
|
|
11512
|
+
return valueRecord;
|
|
11513
|
+
}
|
|
11514
|
+
function requiredString(value, label) {
|
|
11515
|
+
if (typeof value !== "string" || !value.trim()) throw new Error(`${label} must be a non-empty string.`);
|
|
11516
|
+
return value;
|
|
11517
|
+
}
|
|
11518
|
+
function record3(value) {
|
|
11519
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
11520
|
+
}
|
|
11521
|
+
function array(value) {
|
|
11522
|
+
return Array.isArray(value) ? value : [];
|
|
11523
|
+
}
|
|
11524
|
+
|
|
11525
|
+
// src/commands/platform.ts
|
|
10715
11526
|
function registerPlatformCommand(program2) {
|
|
10716
11527
|
const platform3 = program2.command("platform").description("Internal Topolo first-party platform and personal app commands");
|
|
10717
11528
|
const platformApps = platform3.command("apps").description("Scaffold and operate first-party Topolo platform and personal applications");
|
|
10718
11529
|
registerApplicationReconcileCommand(platformApps, "topolo_first_party");
|
|
11530
|
+
registerApplicationCapabilityCommands(platformApps, "topolo_first_party");
|
|
11531
|
+
registerApplicationResourceProvisionCommand(platformApps);
|
|
10719
11532
|
platformApps.command("scaffold <application>").description(
|
|
10720
11533
|
"Generate a first-party Topolo scaffold for the platform or personal portfolio."
|
|
10721
11534
|
).option("--json", "Output JSON").option("--no-json", "Output human-readable checklist").option("--app-dir <path>", "Application repo name or apps/<repo> path").option("--name <name>", "Application display/package source name").option("--package-name <name>", "package.json name").option("--production-url <url>", "Production browser URL").option("--service <id>", "Stable SDK/Auth app id for API-backed apps").option("--portfolio <portfolio>", "First-party portfolio: platform or personal").option(
|
|
10722
11535
|
"--surface-model <surfaceModel>",
|
|
10723
11536
|
"First-party surface model: web_app, mobile_container, mobile_module, or messaging_surface"
|
|
10724
|
-
).option("--container-app-id <appId>", "Container app id when --surface-model mobile_module is used").option("--profile <profile>", "First-party scaffold profile such as spatial-assets").option("--
|
|
11537
|
+
).option("--container-app-id <appId>", "Container app id when --surface-model mobile_module is used").option("--profile <profile>", "First-party scaffold profile such as spatial-assets").option("--capability <id...>", "Compose one or more versioned platform capabilities").option("--home-path <path>", "Authenticated home route (default: /dashboard)").option("--home-label <label>", "Home navigation label (default: Dashboard)").option("--navigation-mode <mode>", "Product navigation: sidebar, topbar, or none").option("--workspace-switcher <mode>", "Workspace context selector: required, optional, or off").option("--ui-template <template>", "Shared UI starter: operations, index, workbench, or minimal").option("--tagline <text>", "Product-facing landing and home description").option("--write", "Write the generated scaffold files to disk").option("--repo-root <path>", "Directory where scaffold files should be written").option("--force", "Overwrite existing scaffold files when --write is set").option("--with-api", "Include the API/service baseline").option("--no-api", "Do not include API/service requirements").option("--no-browser", "Do not include browser-shell requirements").option(
|
|
10725
11538
|
"--provision",
|
|
10726
11539
|
"Provision the generated first-party app into Topolo Developers and Auth using the authenticated Topolo admin session"
|
|
10727
11540
|
).option(
|
|
@@ -10752,11 +11565,7 @@ function registerPlatformCommand(program2) {
|
|
|
10752
11565
|
surfaceModel: resolvePlatformSurfaceModel(opts.surfaceModel),
|
|
10753
11566
|
containerAppId: opts.containerAppId,
|
|
10754
11567
|
profile: opts.profile,
|
|
10755
|
-
|
|
10756
|
-
withR2: opts.withR2,
|
|
10757
|
-
withQueue: opts.withQueue,
|
|
10758
|
-
withWorkflow: opts.withWorkflow,
|
|
10759
|
-
withContainerProcessing: opts.withContainerProcessing,
|
|
11568
|
+
capabilities: opts.capability,
|
|
10760
11569
|
homePath: opts.homePath,
|
|
10761
11570
|
homeLabel: opts.homeLabel,
|
|
10762
11571
|
navigationMode: opts.navigationMode,
|
|
@@ -10829,12 +11638,12 @@ var SKIPPED_SCAN_DIRS = /* @__PURE__ */ new Set([
|
|
|
10829
11638
|
"output"
|
|
10830
11639
|
]);
|
|
10831
11640
|
function scanStagingProductionUrls(rootInput, allowPatterns) {
|
|
10832
|
-
const root =
|
|
11641
|
+
const root = path7.resolve(rootInput);
|
|
10833
11642
|
const allowlist = allowPatterns.map((pattern) => new RegExp(pattern));
|
|
10834
11643
|
const violations = [];
|
|
10835
11644
|
function shouldSkipFile(filePath) {
|
|
10836
|
-
const basename =
|
|
10837
|
-
const
|
|
11645
|
+
const basename = path7.basename(filePath);
|
|
11646
|
+
const relativePath2 = path7.relative(root, filePath);
|
|
10838
11647
|
if (basename === "package-lock.json" || basename === "pnpm-lock.yaml" || basename === "yarn.lock") return true;
|
|
10839
11648
|
if (basename === "topolo.cloudcontrol.json") return true;
|
|
10840
11649
|
if (basename === "wrangler.toml" || basename === "wrangler.jsonc") return true;
|
|
@@ -10843,29 +11652,29 @@ function scanStagingProductionUrls(rootInput, allowPatterns) {
|
|
|
10843
11652
|
if (basename.endsWith(".generated.ts")) return true;
|
|
10844
11653
|
if (basename.endsWith(".test.ts") || basename.endsWith(".test.tsx") || basename.endsWith(".spec.ts")) return true;
|
|
10845
11654
|
if (basename === ".env.production") return true;
|
|
10846
|
-
if (
|
|
10847
|
-
if (
|
|
10848
|
-
if (
|
|
10849
|
-
return !RUNTIME_EXTENSIONS.has(
|
|
11655
|
+
if (relativePath2 === path7.join("src", "environment.ts") || relativePath2 === path7.join("src", "environmentUrls.ts") || relativePath2.endsWith(path7.join("src", "environment.ts")) || relativePath2.endsWith(path7.join("src", "lib", "environment.ts")) || relativePath2 === path7.join("packages", "topolo-ui-kit", "src", "environmentUrls.ts")) return true;
|
|
11656
|
+
if (relativePath2.includes(`${path7.sep}test${path7.sep}`) || relativePath2.includes(`${path7.sep}tests${path7.sep}`) || relativePath2.includes(`${path7.sep}e2e${path7.sep}`)) return true;
|
|
11657
|
+
if (relativePath2.startsWith(`scripts${path7.sep}`) || relativePath2.includes(`${path7.sep}scripts${path7.sep}`)) return true;
|
|
11658
|
+
return !RUNTIME_EXTENSIONS.has(path7.extname(filePath));
|
|
10850
11659
|
}
|
|
10851
11660
|
function scanFile(filePath) {
|
|
10852
11661
|
if (shouldSkipFile(filePath)) return;
|
|
10853
|
-
const
|
|
11662
|
+
const relativePath2 = path7.relative(root, filePath);
|
|
10854
11663
|
const content = readFileSync6(filePath, "utf8");
|
|
10855
11664
|
const lines = content.split(/\r?\n/);
|
|
10856
11665
|
lines.forEach((line, index) => {
|
|
10857
11666
|
const matches = [...line.matchAll(PRODUCTION_TOPOLO_URL_PATTERN)].map((match) => match[0]);
|
|
10858
11667
|
for (const url of new Set(matches)) {
|
|
10859
|
-
const allowed = STAGING_TOPOLO_URL_PATTERN.test(line) || line.includes("PRODUCTION_") || line.includes("hostname ===") || line.includes(".split(") && line.includes(".join(") || allowlist.some((pattern) => pattern.test(`${
|
|
11668
|
+
const allowed = STAGING_TOPOLO_URL_PATTERN.test(line) || line.includes("PRODUCTION_") || line.includes("hostname ===") || line.includes(".split(") && line.includes(".join(") || allowlist.some((pattern) => pattern.test(`${relativePath2}:${index + 1}:${line}`));
|
|
10860
11669
|
if (!allowed) {
|
|
10861
|
-
violations.push({ file:
|
|
11670
|
+
violations.push({ file: relativePath2, line: index + 1, url, text: line.trim() });
|
|
10862
11671
|
}
|
|
10863
11672
|
}
|
|
10864
11673
|
});
|
|
10865
11674
|
}
|
|
10866
11675
|
function walk(dir) {
|
|
10867
11676
|
for (const entry of readdirSync2(dir, { withFileTypes: true })) {
|
|
10868
|
-
const fullPath =
|
|
11677
|
+
const fullPath = path7.join(dir, entry.name);
|
|
10869
11678
|
if (entry.isDirectory()) {
|
|
10870
11679
|
if (!SKIPPED_SCAN_DIRS.has(entry.name)) walk(fullPath);
|
|
10871
11680
|
continue;
|
|
@@ -11033,7 +11842,7 @@ var DEFAULT_SEED_BASE_URL = "https://seed.stg.topolo.us";
|
|
|
11033
11842
|
function seedBaseUrl() {
|
|
11034
11843
|
return process.env.TOPOLO_SEED_URL || DEFAULT_SEED_BASE_URL;
|
|
11035
11844
|
}
|
|
11036
|
-
async function authedFetch(
|
|
11845
|
+
async function authedFetch(path8, init = {}) {
|
|
11037
11846
|
const credential = await refreshCredentialIfNeeded("staging");
|
|
11038
11847
|
if (!credential) {
|
|
11039
11848
|
throw new Error("No staging credential configured. Run `topolo auth login --env staging` or set TOPOLO_STAGING_API_KEY.");
|
|
@@ -11043,7 +11852,7 @@ async function authedFetch(path6, init = {}) {
|
|
|
11043
11852
|
else headers.set("Authorization", `Bearer ${credential.accessToken}`);
|
|
11044
11853
|
headers.set("X-Topolo-Client", `topolo-cli/${CLI_VERSION}`);
|
|
11045
11854
|
if (init.body && !headers.has("Content-Type")) headers.set("Content-Type", "application/json");
|
|
11046
|
-
const response = await fetch(`${seedBaseUrl()}${
|
|
11855
|
+
const response = await fetch(`${seedBaseUrl()}${path8}`, { ...init, headers });
|
|
11047
11856
|
const text = await response.text();
|
|
11048
11857
|
let body;
|
|
11049
11858
|
try {
|
|
@@ -11320,7 +12129,7 @@ function createOAuthClientPayload(options) {
|
|
|
11320
12129
|
logo_url: options.logoUrl
|
|
11321
12130
|
};
|
|
11322
12131
|
}
|
|
11323
|
-
async function authFetchJson(
|
|
12132
|
+
async function authFetchJson(path8, init = {}) {
|
|
11324
12133
|
const environment = selectedEnvironment();
|
|
11325
12134
|
const credential = await refreshCredentialIfNeeded(environment);
|
|
11326
12135
|
if (!credential) {
|
|
@@ -11342,7 +12151,7 @@ async function authFetchJson(path6, init = {}) {
|
|
|
11342
12151
|
throw err;
|
|
11343
12152
|
}
|
|
11344
12153
|
headers.set("Authorization", `Bearer ${credential.accessToken}`);
|
|
11345
|
-
const response = await fetch(`${authBaseUrl}${
|
|
12154
|
+
const response = await fetch(`${authBaseUrl}${path8}`, { ...init, headers });
|
|
11346
12155
|
const text = await response.text();
|
|
11347
12156
|
const payload = text ? JSON.parse(text) : {};
|
|
11348
12157
|
if (!response.ok || payload.success === false) {
|
|
@@ -11358,7 +12167,7 @@ function normalizeList(values, fallback = []) {
|
|
|
11358
12167
|
}
|
|
11359
12168
|
|
|
11360
12169
|
// src/commands/integrations.ts
|
|
11361
|
-
import { readdir, readFile as
|
|
12170
|
+
import { readdir, readFile as readFile6, stat } from "fs/promises";
|
|
11362
12171
|
import { join as join4, relative as relative3 } from "path";
|
|
11363
12172
|
function registerIntegrationsCommand(program2) {
|
|
11364
12173
|
const integrations = program2.command("integrations").description("Generate and validate Topolo integration handoffs.");
|
|
@@ -11502,7 +12311,7 @@ function detectFramework(packageJson2, haystack) {
|
|
|
11502
12311
|
}
|
|
11503
12312
|
async function readPackageJson2(repoRoot) {
|
|
11504
12313
|
try {
|
|
11505
|
-
return JSON.parse(await
|
|
12314
|
+
return JSON.parse(await readFile6(join4(repoRoot, "package.json"), "utf8"));
|
|
11506
12315
|
} catch {
|
|
11507
12316
|
return null;
|
|
11508
12317
|
}
|
|
@@ -11525,7 +12334,7 @@ async function collectFiles(repoRoot) {
|
|
|
11525
12334
|
if (info.size > 25e4) continue;
|
|
11526
12335
|
result.push({
|
|
11527
12336
|
path: relative3(root, fullPath),
|
|
11528
|
-
content: await
|
|
12337
|
+
content: await readFile6(fullPath, "utf8")
|
|
11529
12338
|
});
|
|
11530
12339
|
}
|
|
11531
12340
|
}
|
|
@@ -11543,7 +12352,7 @@ function formatCheckResult(result) {
|
|
|
11543
12352
|
// src/commands/skills.ts
|
|
11544
12353
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
11545
12354
|
import { existsSync as existsSync5 } from "fs";
|
|
11546
|
-
import { mkdir as
|
|
12355
|
+
import { mkdir as mkdir4, copyFile, readFile as readFile7, writeFile as writeFile7, rm } from "fs/promises";
|
|
11547
12356
|
import { homedir as homedir2 } from "os";
|
|
11548
12357
|
import { dirname as dirname4, join as join5, resolve as resolve3 } from "path";
|
|
11549
12358
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
@@ -11579,7 +12388,7 @@ function codexSkillDir() {
|
|
|
11579
12388
|
return join5(homedir2(), ".codex", "skills", "topolo");
|
|
11580
12389
|
}
|
|
11581
12390
|
async function installSkillFile(sourceDir, targetDir) {
|
|
11582
|
-
await
|
|
12391
|
+
await mkdir4(targetDir, { recursive: true });
|
|
11583
12392
|
const target = join5(targetDir, "SKILL.md");
|
|
11584
12393
|
await copyFile(join5(sourceDir, "topolo", "SKILL.md"), target);
|
|
11585
12394
|
return target;
|
|
@@ -11597,11 +12406,11 @@ function registerViaOfficialCli(cli, mcp, agentName) {
|
|
|
11597
12406
|
return result.status === 0;
|
|
11598
12407
|
}
|
|
11599
12408
|
async function registerClaudeViaConfig(mcp) {
|
|
11600
|
-
const
|
|
12409
|
+
const path8 = join5(homedir2(), ".claude.json");
|
|
11601
12410
|
let config = {};
|
|
11602
|
-
if (existsSync5(
|
|
12411
|
+
if (existsSync5(path8)) {
|
|
11603
12412
|
try {
|
|
11604
|
-
config = JSON.parse(await
|
|
12413
|
+
config = JSON.parse(await readFile7(path8, "utf8"));
|
|
11605
12414
|
} catch {
|
|
11606
12415
|
config = {};
|
|
11607
12416
|
}
|
|
@@ -11614,13 +12423,13 @@ async function registerClaudeViaConfig(mcp) {
|
|
|
11614
12423
|
env: { TOPOLO_AGENT_NAME: AGENT_NAME_FOR.claude }
|
|
11615
12424
|
};
|
|
11616
12425
|
config.mcpServers = servers;
|
|
11617
|
-
await
|
|
12426
|
+
await writeFile7(path8, `${JSON.stringify(config, null, 2)}
|
|
11618
12427
|
`, "utf8");
|
|
11619
12428
|
}
|
|
11620
12429
|
async function registerCodexViaConfig(mcp) {
|
|
11621
|
-
const
|
|
12430
|
+
const path8 = join5(homedir2(), ".codex", "config.toml");
|
|
11622
12431
|
let text = "";
|
|
11623
|
-
if (existsSync5(
|
|
12432
|
+
if (existsSync5(path8)) text = await readFile7(path8, "utf8");
|
|
11624
12433
|
text = text.replace(
|
|
11625
12434
|
/(^|\n)\[mcp_servers\.topolo(?:\.[a-z]+)?\][^\[]*/g,
|
|
11626
12435
|
(match) => match.startsWith("\n") ? "\n" : ""
|
|
@@ -11636,8 +12445,8 @@ command = "${mcp.command}"${argsToml}
|
|
|
11636
12445
|
[mcp_servers.topolo.env]
|
|
11637
12446
|
TOPOLO_AGENT_NAME = "${AGENT_NAME_FOR.codex}"
|
|
11638
12447
|
`;
|
|
11639
|
-
await
|
|
11640
|
-
await
|
|
12448
|
+
await mkdir4(dirname4(path8), { recursive: true });
|
|
12449
|
+
await writeFile7(path8, `${text}${block}`, "utf8");
|
|
11641
12450
|
}
|
|
11642
12451
|
async function installForAgent(agent, sourceDir, mcp, opts) {
|
|
11643
12452
|
const skillDir = agent === "claude" ? claudeSkillDir(opts.projectDir) : codexSkillDir();
|