@topolo/cli 0.5.1 → 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 +911 -0
- package/dist/index.js +1286 -875
- package/package.json +4 -4
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 = {
|
|
@@ -2178,17 +2178,17 @@ function validateTopoloActionInput(schema, input3) {
|
|
|
2178
2178
|
validateNode(schema, input3, "$", schema, errors, 0);
|
|
2179
2179
|
return { valid: errors.length === 0, errors };
|
|
2180
2180
|
}
|
|
2181
|
-
function inspectSchema(schema,
|
|
2182
|
-
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." };
|
|
2183
2183
|
const unsupportedKeyword = Object.keys(schema).find((keyword) => !SUPPORTED_SCHEMA_KEYWORDS.has(keyword));
|
|
2184
2184
|
if (unsupportedKeyword) {
|
|
2185
|
-
return { path:
|
|
2185
|
+
return { path: path8, code: "unsupported_schema_keyword", message: `Action schema uses unsupported keyword ${unsupportedKeyword}.` };
|
|
2186
2186
|
}
|
|
2187
2187
|
if (typeof schema.format === "string" && !["uri", "email", "date-time"].includes(schema.format)) {
|
|
2188
|
-
return { path:
|
|
2188
|
+
return { path: path8, code: "unsupported_schema_format", message: `Action schema uses unsupported format ${schema.format}.` };
|
|
2189
2189
|
}
|
|
2190
2190
|
if (typeof schema.$ref === "string" && !resolveLocalReference(root, schema.$ref)) {
|
|
2191
|
-
return { path:
|
|
2191
|
+
return { path: path8, code: "unsupported_schema_ref", message: `Cannot resolve schema reference ${schema.$ref}.` };
|
|
2192
2192
|
}
|
|
2193
2193
|
const children = [];
|
|
2194
2194
|
if (isRecord(schema.properties)) children.push(...Object.entries(schema.properties));
|
|
@@ -2204,7 +2204,7 @@ function inspectSchema(schema, path5, root, depth) {
|
|
|
2204
2204
|
);
|
|
2205
2205
|
for (const [key, child] of children) {
|
|
2206
2206
|
if (!isRecord(child)) continue;
|
|
2207
|
-
const issue = inspectSchema(child, `${
|
|
2207
|
+
const issue = inspectSchema(child, `${path8}.${key}`, root, depth + 1);
|
|
2208
2208
|
if (issue) return issue;
|
|
2209
2209
|
}
|
|
2210
2210
|
return null;
|
|
@@ -2212,129 +2212,129 @@ function inspectSchema(schema, path5, root, depth) {
|
|
|
2212
2212
|
function arraySchemaEntries(keyword, value) {
|
|
2213
2213
|
return Array.isArray(value) ? value.map((candidate, index) => [`${keyword}[${index}]`, candidate]) : [];
|
|
2214
2214
|
}
|
|
2215
|
-
function validateNode(schema, value,
|
|
2215
|
+
function validateNode(schema, value, path8, root, errors, depth) {
|
|
2216
2216
|
if (depth > 128) {
|
|
2217
|
-
errors.push({ path:
|
|
2217
|
+
errors.push({ path: path8, code: "schema_depth", message: "Schema recursion exceeded 128 levels." });
|
|
2218
2218
|
return;
|
|
2219
2219
|
}
|
|
2220
2220
|
if (typeof schema.$ref === "string") {
|
|
2221
2221
|
const resolved = resolveLocalReference(root, schema.$ref);
|
|
2222
2222
|
if (!resolved) {
|
|
2223
|
-
errors.push({ path:
|
|
2223
|
+
errors.push({ path: path8, code: "unsupported_schema_ref", message: `Cannot resolve schema reference ${schema.$ref}.` });
|
|
2224
2224
|
return;
|
|
2225
2225
|
}
|
|
2226
|
-
validateNode(resolved, value,
|
|
2226
|
+
validateNode(resolved, value, path8, root, errors, depth + 1);
|
|
2227
2227
|
}
|
|
2228
2228
|
if (Array.isArray(schema.allOf)) {
|
|
2229
2229
|
for (const candidate of schema.allOf) {
|
|
2230
|
-
if (isRecord(candidate)) validateNode(candidate, value,
|
|
2230
|
+
if (isRecord(candidate)) validateNode(candidate, value, path8, root, errors, depth + 1);
|
|
2231
2231
|
}
|
|
2232
2232
|
}
|
|
2233
2233
|
if (Array.isArray(schema.anyOf) && !schema.anyOf.some((candidate) => candidateMatches(candidate, value, root, depth))) {
|
|
2234
|
-
errors.push({ path:
|
|
2234
|
+
errors.push({ path: path8, code: "any_of", message: "Value does not match any allowed schema." });
|
|
2235
2235
|
return;
|
|
2236
2236
|
}
|
|
2237
2237
|
if (Array.isArray(schema.oneOf)) {
|
|
2238
2238
|
const matches = schema.oneOf.filter((candidate) => candidateMatches(candidate, value, root, depth)).length;
|
|
2239
2239
|
if (matches !== 1) {
|
|
2240
|
-
errors.push({ path:
|
|
2240
|
+
errors.push({ path: path8, code: "one_of", message: "Value must match exactly one allowed schema." });
|
|
2241
2241
|
return;
|
|
2242
2242
|
}
|
|
2243
2243
|
}
|
|
2244
2244
|
if (value === null && schema.nullable === true) return;
|
|
2245
2245
|
if (Object.prototype.hasOwnProperty.call(schema, "const") && !sameJsonValue(value, schema.const)) {
|
|
2246
|
-
errors.push({ path:
|
|
2246
|
+
errors.push({ path: path8, code: "const", message: `Value must equal ${JSON.stringify(schema.const)}.` });
|
|
2247
2247
|
return;
|
|
2248
2248
|
}
|
|
2249
2249
|
if (Array.isArray(schema.enum) && !schema.enum.some((candidate) => sameJsonValue(value, candidate))) {
|
|
2250
|
-
errors.push({ path:
|
|
2250
|
+
errors.push({ path: path8, code: "enum", message: `Value must be one of ${schema.enum.map((item) => JSON.stringify(item)).join(", ")}.` });
|
|
2251
2251
|
return;
|
|
2252
2252
|
}
|
|
2253
2253
|
if (!matchesType(schema.type, value)) {
|
|
2254
|
-
errors.push({ path:
|
|
2254
|
+
errors.push({ path: path8, code: "type", message: `Expected ${String(schema.type)}.` });
|
|
2255
2255
|
return;
|
|
2256
2256
|
}
|
|
2257
|
-
if (typeof value === "string") validateString(schema, value,
|
|
2258
|
-
if (typeof value === "number") validateNumber(schema, value,
|
|
2259
|
-
if (Array.isArray(value)) validateArray(schema, value,
|
|
2260
|
-
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);
|
|
2261
2261
|
}
|
|
2262
|
-
function validateObject(schema, value,
|
|
2262
|
+
function validateObject(schema, value, path8, root, errors, depth) {
|
|
2263
2263
|
if (typeof schema.minProperties === "number" && Object.keys(value).length < schema.minProperties) {
|
|
2264
|
-
errors.push({ path:
|
|
2264
|
+
errors.push({ path: path8, code: "min_properties", message: `Expected at least ${schema.minProperties} properties.` });
|
|
2265
2265
|
}
|
|
2266
2266
|
if (typeof schema.maxProperties === "number" && Object.keys(value).length > schema.maxProperties) {
|
|
2267
|
-
errors.push({ path:
|
|
2267
|
+
errors.push({ path: path8, code: "max_properties", message: `Expected at most ${schema.maxProperties} properties.` });
|
|
2268
2268
|
}
|
|
2269
2269
|
const properties = isRecord(schema.properties) ? schema.properties : {};
|
|
2270
2270
|
const required = Array.isArray(schema.required) ? schema.required.map(String) : [];
|
|
2271
2271
|
for (const key of required) {
|
|
2272
2272
|
if (!Object.prototype.hasOwnProperty.call(value, key)) {
|
|
2273
|
-
errors.push({ path: `${
|
|
2273
|
+
errors.push({ path: `${path8}.${key}`, code: "required", message: "Required property is missing." });
|
|
2274
2274
|
}
|
|
2275
2275
|
}
|
|
2276
2276
|
if (isRecord(schema.propertyNames)) {
|
|
2277
2277
|
for (const key of Object.keys(value)) {
|
|
2278
|
-
validateNode(schema.propertyNames, key, `${
|
|
2278
|
+
validateNode(schema.propertyNames, key, `${path8}.${key}`, root, errors, depth + 1);
|
|
2279
2279
|
}
|
|
2280
2280
|
}
|
|
2281
2281
|
for (const [key, child] of Object.entries(value)) {
|
|
2282
2282
|
const childSchema = properties[key];
|
|
2283
2283
|
if (isRecord(childSchema)) {
|
|
2284
|
-
validateNode(childSchema, child, `${
|
|
2284
|
+
validateNode(childSchema, child, `${path8}.${key}`, root, errors, depth + 1);
|
|
2285
2285
|
} else if (schema.additionalProperties === false) {
|
|
2286
|
-
errors.push({ path: `${
|
|
2286
|
+
errors.push({ path: `${path8}.${key}`, code: "additional_property", message: "Property is not declared by the action schema." });
|
|
2287
2287
|
} else if (isRecord(schema.additionalProperties)) {
|
|
2288
|
-
validateNode(schema.additionalProperties, child, `${
|
|
2288
|
+
validateNode(schema.additionalProperties, child, `${path8}.${key}`, root, errors, depth + 1);
|
|
2289
2289
|
}
|
|
2290
2290
|
}
|
|
2291
2291
|
}
|
|
2292
|
-
function validateArray(schema, value,
|
|
2292
|
+
function validateArray(schema, value, path8, root, errors, depth) {
|
|
2293
2293
|
if (typeof schema.minItems === "number" && value.length < schema.minItems) {
|
|
2294
|
-
errors.push({ path:
|
|
2294
|
+
errors.push({ path: path8, code: "min_items", message: `Expected at least ${schema.minItems} items.` });
|
|
2295
2295
|
}
|
|
2296
2296
|
if (typeof schema.maxItems === "number" && value.length > schema.maxItems) {
|
|
2297
|
-
errors.push({ path:
|
|
2297
|
+
errors.push({ path: path8, code: "max_items", message: `Expected at most ${schema.maxItems} items.` });
|
|
2298
2298
|
}
|
|
2299
2299
|
if (schema.uniqueItems === true) {
|
|
2300
|
-
const
|
|
2301
|
-
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." });
|
|
2302
2302
|
}
|
|
2303
2303
|
if (isRecord(schema.items)) {
|
|
2304
|
-
value.forEach((item, index) => validateNode(schema.items, item, `${
|
|
2304
|
+
value.forEach((item, index) => validateNode(schema.items, item, `${path8}[${index}]`, root, errors, depth + 1));
|
|
2305
2305
|
}
|
|
2306
2306
|
}
|
|
2307
|
-
function validateString(schema, value,
|
|
2307
|
+
function validateString(schema, value, path8, errors) {
|
|
2308
2308
|
if (typeof schema.minLength === "number" && value.length < schema.minLength) {
|
|
2309
|
-
errors.push({ path:
|
|
2309
|
+
errors.push({ path: path8, code: "min_length", message: `Expected at least ${schema.minLength} characters.` });
|
|
2310
2310
|
}
|
|
2311
2311
|
if (typeof schema.maxLength === "number" && value.length > schema.maxLength) {
|
|
2312
|
-
errors.push({ path:
|
|
2312
|
+
errors.push({ path: path8, code: "max_length", message: `Expected at most ${schema.maxLength} characters.` });
|
|
2313
2313
|
}
|
|
2314
2314
|
if (typeof schema.pattern === "string") {
|
|
2315
2315
|
try {
|
|
2316
|
-
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." });
|
|
2317
2317
|
} catch {
|
|
2318
|
-
errors.push({ path:
|
|
2318
|
+
errors.push({ path: path8, code: "invalid_schema_pattern", message: "Action schema contains an invalid pattern." });
|
|
2319
2319
|
}
|
|
2320
2320
|
}
|
|
2321
|
-
if (schema.format === "uri" && !isUrl(value)) errors.push({ path:
|
|
2322
|
-
if (schema.format === "email" && !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(value)) errors.push({ path:
|
|
2323
|
-
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." });
|
|
2324
2324
|
}
|
|
2325
|
-
function validateNumber(schema, value,
|
|
2326
|
-
if (typeof schema.minimum === "number" && value < schema.minimum) errors.push({ path:
|
|
2327
|
-
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}.` });
|
|
2328
2328
|
const exclusiveMinimum = typeof schema.exclusiveMinimum === "number" ? schema.exclusiveMinimum : schema.exclusiveMinimum === true && typeof schema.minimum === "number" ? schema.minimum : null;
|
|
2329
2329
|
const exclusiveMaximum = typeof schema.exclusiveMaximum === "number" ? schema.exclusiveMaximum : schema.exclusiveMaximum === true && typeof schema.maximum === "number" ? schema.maximum : null;
|
|
2330
2330
|
if (exclusiveMinimum !== null && value <= exclusiveMinimum) {
|
|
2331
|
-
errors.push({ path:
|
|
2331
|
+
errors.push({ path: path8, code: "exclusive_minimum", message: `Expected a value greater than ${exclusiveMinimum}.` });
|
|
2332
2332
|
}
|
|
2333
2333
|
if (exclusiveMaximum !== null && value >= exclusiveMaximum) {
|
|
2334
|
-
errors.push({ path:
|
|
2334
|
+
errors.push({ path: path8, code: "exclusive_maximum", message: `Expected a value less than ${exclusiveMaximum}.` });
|
|
2335
2335
|
}
|
|
2336
2336
|
if (typeof schema.multipleOf === "number" && !isMultipleOf(value, schema.multipleOf)) {
|
|
2337
|
-
errors.push({ path:
|
|
2337
|
+
errors.push({ path: path8, code: "multiple_of", message: `Expected a multiple of ${schema.multipleOf}.` });
|
|
2338
2338
|
}
|
|
2339
2339
|
}
|
|
2340
2340
|
function candidateMatches(candidate, value, root, depth) {
|
|
@@ -2845,14 +2845,14 @@ var TopoloClient = class _TopoloClient {
|
|
|
2845
2845
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
2846
2846
|
throw new TopoloSdkError("action_catalog_summary_unavailable", "Action catalog summary is unavailable.");
|
|
2847
2847
|
}
|
|
2848
|
-
const
|
|
2849
|
-
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) {
|
|
2850
2850
|
throw new TopoloSdkError("action_catalog_summary_unavailable", "Action catalog summary is invalid.");
|
|
2851
2851
|
}
|
|
2852
2852
|
return {
|
|
2853
2853
|
algorithm: "sha256-v1",
|
|
2854
|
-
digest:
|
|
2855
|
-
actionCount: Number(
|
|
2854
|
+
digest: record4.digest,
|
|
2855
|
+
actionCount: Number(record4.actionCount)
|
|
2856
2856
|
};
|
|
2857
2857
|
}
|
|
2858
2858
|
/** @deprecated Prefer searchActions() so callers retain the continuation cursor. */
|
|
@@ -3227,8 +3227,8 @@ function normalizeActionCatalogEntry(item) {
|
|
|
3227
3227
|
const name = pickActionString(a.name);
|
|
3228
3228
|
const appId = pickActionString(a.app_id) ?? pickActionString(a.appId);
|
|
3229
3229
|
const method = pickActionString(a.method)?.toUpperCase();
|
|
3230
|
-
const
|
|
3231
|
-
if (!actionId || !name || !appId || !method || !
|
|
3230
|
+
const path8 = pickActionString(a.path);
|
|
3231
|
+
if (!actionId || !name || !appId || !method || !path8 || !isHttpMethod(method)) {
|
|
3232
3232
|
return null;
|
|
3233
3233
|
}
|
|
3234
3234
|
return {
|
|
@@ -3243,7 +3243,7 @@ function normalizeActionCatalogEntry(item) {
|
|
|
3243
3243
|
appSlug: pickActionString(a.app_slug) ?? pickActionString(a.appSlug) ?? null,
|
|
3244
3244
|
appName: pickActionString(a.app_name) ?? pickActionString(a.appName) ?? null,
|
|
3245
3245
|
method,
|
|
3246
|
-
path:
|
|
3246
|
+
path: path8,
|
|
3247
3247
|
permissionName: pickActionString(a.permission_name) ?? pickActionString(a.permissionName) ?? "",
|
|
3248
3248
|
requiredPermission: pickActionString(a.required_permission) ?? pickActionString(a.requiredPermission) ?? "",
|
|
3249
3249
|
authorizationMode: pickAuthorizationMode(a.authorization_mode ?? a.authorizationMode),
|
|
@@ -3279,7 +3279,7 @@ function stringArray(value) {
|
|
|
3279
3279
|
}
|
|
3280
3280
|
function mapActionInput(action, input3) {
|
|
3281
3281
|
const remaining = { ...input3 || {} };
|
|
3282
|
-
const
|
|
3282
|
+
const path8 = action.path.replace(/\{([a-zA-Z0-9_]+)\}/g, (_match, key) => {
|
|
3283
3283
|
const value = remaining[key];
|
|
3284
3284
|
if (value === void 0 || value === null || value === "") {
|
|
3285
3285
|
throw new TopoloSdkError(
|
|
@@ -3296,14 +3296,14 @@ function mapActionInput(action, input3) {
|
|
|
3296
3296
|
delete remaining.body;
|
|
3297
3297
|
if (action.method === "GET") {
|
|
3298
3298
|
return {
|
|
3299
|
-
path:
|
|
3299
|
+
path: path8,
|
|
3300
3300
|
query: objectToQuery({ ...remaining, ...explicitQuery })
|
|
3301
3301
|
};
|
|
3302
3302
|
}
|
|
3303
3303
|
const query = objectToQuery(explicitQuery);
|
|
3304
3304
|
const body = explicitBody !== void 0 ? explicitBody : remaining;
|
|
3305
3305
|
return {
|
|
3306
|
-
path:
|
|
3306
|
+
path: path8,
|
|
3307
3307
|
...query ? { query } : {},
|
|
3308
3308
|
body
|
|
3309
3309
|
};
|
|
@@ -3473,8 +3473,8 @@ var TopoloOAuth = class {
|
|
|
3473
3473
|
if (params.clientSecret) body.set("client_secret", params.clientSecret);
|
|
3474
3474
|
return this.#postForm("/api/developer-oauth/token", body);
|
|
3475
3475
|
}
|
|
3476
|
-
async #postForm(
|
|
3477
|
-
const url = new URL(
|
|
3476
|
+
async #postForm(path8, body) {
|
|
3477
|
+
const url = new URL(path8, this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`);
|
|
3478
3478
|
const res = await this.fetchImpl(url.toString(), {
|
|
3479
3479
|
method: "POST",
|
|
3480
3480
|
headers: {
|
|
@@ -3490,7 +3490,7 @@ var TopoloOAuth = class {
|
|
|
3490
3490
|
const err = parsed;
|
|
3491
3491
|
throw new TopoloAuthError(err.error_description || err.error, err.error);
|
|
3492
3492
|
}
|
|
3493
|
-
throw new TopoloHttpError("auth",
|
|
3493
|
+
throw new TopoloHttpError("auth", path8, res.status, parsed);
|
|
3494
3494
|
}
|
|
3495
3495
|
return parsed;
|
|
3496
3496
|
}
|
|
@@ -3504,7 +3504,7 @@ function createTopolo(options) {
|
|
|
3504
3504
|
};
|
|
3505
3505
|
}
|
|
3506
3506
|
|
|
3507
|
-
//
|
|
3507
|
+
// node_modules/@topolo/sdk/dist/node/credential-store.js
|
|
3508
3508
|
import { mkdir, readFile, writeFile, chmod } from "fs/promises";
|
|
3509
3509
|
import { existsSync } from "fs";
|
|
3510
3510
|
import { homedir, platform } from "os";
|
|
@@ -3528,13 +3528,13 @@ var TopoloHttpError2 = class extends TopoloSdkError2 {
|
|
|
3528
3528
|
body;
|
|
3529
3529
|
service;
|
|
3530
3530
|
path;
|
|
3531
|
-
constructor(service,
|
|
3532
|
-
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}`);
|
|
3533
3533
|
this.name = "TopoloHttpError";
|
|
3534
3534
|
this.status = status;
|
|
3535
3535
|
this.body = body;
|
|
3536
3536
|
this.service = service;
|
|
3537
|
-
this.path =
|
|
3537
|
+
this.path = path8;
|
|
3538
3538
|
}
|
|
3539
3539
|
};
|
|
3540
3540
|
var APP_API_URLS2 = {
|
|
@@ -3665,8 +3665,8 @@ var TopoloOAuth2 = class {
|
|
|
3665
3665
|
if (params.clientSecret) body.set("client_secret", params.clientSecret);
|
|
3666
3666
|
return this.#postForm("/api/developer-oauth/token", body);
|
|
3667
3667
|
}
|
|
3668
|
-
async #postForm(
|
|
3669
|
-
const url = new URL(
|
|
3668
|
+
async #postForm(path8, body) {
|
|
3669
|
+
const url = new URL(path8, this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`);
|
|
3670
3670
|
const res = await this.fetchImpl(url.toString(), {
|
|
3671
3671
|
method: "POST",
|
|
3672
3672
|
headers: {
|
|
@@ -3682,7 +3682,7 @@ var TopoloOAuth2 = class {
|
|
|
3682
3682
|
const err = parsed;
|
|
3683
3683
|
throw new TopoloAuthError2(err.error_description || err.error, err.error);
|
|
3684
3684
|
}
|
|
3685
|
-
throw new TopoloHttpError2("auth",
|
|
3685
|
+
throw new TopoloHttpError2("auth", path8, res.status, parsed);
|
|
3686
3686
|
}
|
|
3687
3687
|
return parsed;
|
|
3688
3688
|
}
|
|
@@ -3712,22 +3712,22 @@ function configPath() {
|
|
|
3712
3712
|
return join(base, "topolo", "config.json");
|
|
3713
3713
|
}
|
|
3714
3714
|
async function readConfig() {
|
|
3715
|
-
const
|
|
3716
|
-
if (!existsSync(
|
|
3715
|
+
const path8 = configPath();
|
|
3716
|
+
if (!existsSync(path8)) return {};
|
|
3717
3717
|
try {
|
|
3718
|
-
const raw = await readFile(
|
|
3718
|
+
const raw = await readFile(path8, "utf8");
|
|
3719
3719
|
return JSON.parse(raw);
|
|
3720
3720
|
} catch {
|
|
3721
3721
|
return {};
|
|
3722
3722
|
}
|
|
3723
3723
|
}
|
|
3724
3724
|
async function writeConfig(config) {
|
|
3725
|
-
const
|
|
3726
|
-
await mkdir(dirname(
|
|
3727
|
-
await writeFile(
|
|
3725
|
+
const path8 = configPath();
|
|
3726
|
+
await mkdir(dirname(path8), { recursive: true });
|
|
3727
|
+
await writeFile(path8, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
3728
3728
|
if (platform() !== "win32") {
|
|
3729
3729
|
try {
|
|
3730
|
-
await chmod(
|
|
3730
|
+
await chmod(path8, 384);
|
|
3731
3731
|
} catch {
|
|
3732
3732
|
}
|
|
3733
3733
|
}
|
|
@@ -4047,9 +4047,9 @@ function readCliVersion() {
|
|
|
4047
4047
|
join2(here, "package.json"),
|
|
4048
4048
|
join2(here, "..", "package.json")
|
|
4049
4049
|
];
|
|
4050
|
-
for (const
|
|
4050
|
+
for (const path8 of candidates) {
|
|
4051
4051
|
try {
|
|
4052
|
-
const pkg = JSON.parse(readFileSync(
|
|
4052
|
+
const pkg = JSON.parse(readFileSync(path8, "utf8"));
|
|
4053
4053
|
if (typeof pkg.version === "string" && pkg.version) return pkg.version;
|
|
4054
4054
|
} catch {
|
|
4055
4055
|
}
|
|
@@ -4408,7 +4408,7 @@ function registerApiCommand(program2) {
|
|
|
4408
4408
|
"-q, --query <kv...>",
|
|
4409
4409
|
"Query params as key=value (repeatable), e.g. --query source=cli page=2"
|
|
4410
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(
|
|
4411
|
-
async (service, method,
|
|
4411
|
+
async (service, method, path8, opts) => {
|
|
4412
4412
|
const mode = resolveOutputMode(opts.json);
|
|
4413
4413
|
try {
|
|
4414
4414
|
const upperMethod = method.toUpperCase();
|
|
@@ -4447,7 +4447,7 @@ function registerApiCommand(program2) {
|
|
|
4447
4447
|
const result = await topolo.client.request({
|
|
4448
4448
|
service,
|
|
4449
4449
|
method: httpMethod,
|
|
4450
|
-
path:
|
|
4450
|
+
path: path8,
|
|
4451
4451
|
body,
|
|
4452
4452
|
...query ? { query } : {},
|
|
4453
4453
|
...resource ? { resource } : {},
|
|
@@ -4611,13 +4611,13 @@ async function requiredClient() {
|
|
|
4611
4611
|
function parseJsonObject(value) {
|
|
4612
4612
|
let source = value;
|
|
4613
4613
|
if (value.startsWith("@")) {
|
|
4614
|
-
const
|
|
4615
|
-
if (!
|
|
4614
|
+
const path8 = value.slice(1);
|
|
4615
|
+
if (!path8) throw new Error("--data @file path must not be empty");
|
|
4616
4616
|
try {
|
|
4617
|
-
source = readFileSync2(
|
|
4617
|
+
source = readFileSync2(path8, "utf8");
|
|
4618
4618
|
} catch (err) {
|
|
4619
4619
|
const detail = err instanceof Error ? err.message : String(err);
|
|
4620
|
-
throw new Error(`Could not read --data file ${
|
|
4620
|
+
throw new Error(`Could not read --data file ${path8}: ${detail}`);
|
|
4621
4621
|
}
|
|
4622
4622
|
}
|
|
4623
4623
|
let parsed;
|
|
@@ -4691,8 +4691,8 @@ function formatActionPlan(plan) {
|
|
|
4691
4691
|
|
|
4692
4692
|
// src/commands/apps.ts
|
|
4693
4693
|
import { execSync } from "child_process";
|
|
4694
|
-
import { existsSync as
|
|
4695
|
-
import
|
|
4694
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "fs";
|
|
4695
|
+
import path5 from "path";
|
|
4696
4696
|
|
|
4697
4697
|
// src/commands/app-audit-local.ts
|
|
4698
4698
|
import {
|
|
@@ -5283,8 +5283,8 @@ function enrichFinding(application, finding2, evidence) {
|
|
|
5283
5283
|
evidence: verificationEvidence
|
|
5284
5284
|
});
|
|
5285
5285
|
}
|
|
5286
|
-
const usesNodeCheck = evidence.packageJsons.some((
|
|
5287
|
-
const scripts =
|
|
5286
|
+
const usesNodeCheck = evidence.packageJsons.some((record4) => {
|
|
5287
|
+
const scripts = record4.packageJson.scripts ?? {};
|
|
5288
5288
|
return Object.values(scripts).some((cmd) => typeof cmd === "string" && /node\s+--check/.test(cmd));
|
|
5289
5289
|
});
|
|
5290
5290
|
if (hasBuild && usesNodeCheck) {
|
|
@@ -5364,7 +5364,7 @@ function nativeDashboardWidgetEvidence(evidence) {
|
|
|
5364
5364
|
return unique(items);
|
|
5365
5365
|
}
|
|
5366
5366
|
function collectEvidence(repoRoot, application) {
|
|
5367
|
-
const appRepoPath = applicationSourcePath(application.appDir);
|
|
5367
|
+
const appRepoPath = applicationSourcePath(repoRoot, application.appDir);
|
|
5368
5368
|
const appRoot = path.join(repoRoot, appRepoPath);
|
|
5369
5369
|
const packageJsonPath = path.join(appRoot, "package.json");
|
|
5370
5370
|
const packageJson2 = readPackageJson(packageJsonPath);
|
|
@@ -5550,35 +5550,35 @@ function verificationEvidenceFor(evidence) {
|
|
|
5550
5550
|
"test:auth-contract": "test",
|
|
5551
5551
|
validate: "validate"
|
|
5552
5552
|
};
|
|
5553
|
-
for (const
|
|
5554
|
-
const scripts =
|
|
5555
|
-
const isRootPackage =
|
|
5553
|
+
for (const record4 of evidence.packageJsons) {
|
|
5554
|
+
const scripts = record4.packageJson.scripts ?? {};
|
|
5555
|
+
const isRootPackage = record4.path === evidence.packageJsonPath;
|
|
5556
5556
|
const seenCanonical = /* @__PURE__ */ new Set();
|
|
5557
5557
|
for (const [scriptName, canonical] of Object.entries(scriptAliases)) {
|
|
5558
5558
|
if (!scripts[scriptName]) continue;
|
|
5559
5559
|
if (seenCanonical.has(canonical)) continue;
|
|
5560
5560
|
seenCanonical.add(canonical);
|
|
5561
|
-
items.push(isRootPackage ? `script=${canonical}` : `script=${packageLabel(
|
|
5561
|
+
items.push(isRootPackage ? `script=${canonical}` : `script=${packageLabel(record4)}:${canonical}`);
|
|
5562
5562
|
}
|
|
5563
5563
|
}
|
|
5564
5564
|
return unique(items);
|
|
5565
5565
|
}
|
|
5566
5566
|
function dependencyNames(evidence) {
|
|
5567
5567
|
return new Set(
|
|
5568
|
-
evidence.packageJsons.flatMap((
|
|
5569
|
-
...Object.keys(
|
|
5570
|
-
...Object.keys(
|
|
5571
|
-
...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 ?? {})
|
|
5572
5572
|
])
|
|
5573
5573
|
);
|
|
5574
5574
|
}
|
|
5575
5575
|
function packageDependencyVersions(evidence, packageName) {
|
|
5576
5576
|
return unique(
|
|
5577
|
-
evidence.packageJsons.flatMap((
|
|
5577
|
+
evidence.packageJsons.flatMap((record4) => {
|
|
5578
5578
|
const specs = [
|
|
5579
|
-
|
|
5580
|
-
|
|
5581
|
-
|
|
5579
|
+
record4.packageJson.dependencies?.[packageName],
|
|
5580
|
+
record4.packageJson.devDependencies?.[packageName],
|
|
5581
|
+
record4.packageJson.peerDependencies?.[packageName]
|
|
5582
5582
|
];
|
|
5583
5583
|
return specs.filter((spec) => typeof spec === "string");
|
|
5584
5584
|
})
|
|
@@ -5606,7 +5606,7 @@ function matchingSystems(repoRoot, application) {
|
|
|
5606
5606
|
const docsRoot = topoloDocsRoot(repoRoot);
|
|
5607
5607
|
const systemsRoot = path.join(docsRoot, "src", "content", "systems");
|
|
5608
5608
|
if (!isDirectory(systemsRoot)) return [];
|
|
5609
|
-
const appRepoPath = applicationSourcePath(application.appDir);
|
|
5609
|
+
const appRepoPath = applicationSourcePath(repoRoot, application.appDir);
|
|
5610
5610
|
return readdirSync(systemsRoot).filter((entry) => entry.endsWith(".json")).map((entry) => readJson(path.join(systemsRoot, entry))).filter((system) => Boolean(system)).filter((system) => {
|
|
5611
5611
|
const repoPaths = [...system.repo_paths ?? [], ...system.repoPaths ?? []];
|
|
5612
5612
|
return repoPaths.some(
|
|
@@ -5629,11 +5629,19 @@ function existingDocs(repoRoot, systems) {
|
|
|
5629
5629
|
function topoloDocsRoot(repoRoot) {
|
|
5630
5630
|
return path.join(repoRoot, "system-apps", "TopoloDocs", "apps", "web");
|
|
5631
5631
|
}
|
|
5632
|
-
|
|
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) {
|
|
5633
5640
|
if (appDir === "TopoloCli" || appDir === "TopoloCLI") return "topolo-platform/packages/topolo-cli";
|
|
5634
|
-
|
|
5635
|
-
|
|
5636
|
-
|
|
5641
|
+
for (const root of APPLICATION_SOURCE_ROOTS) {
|
|
5642
|
+
const relative4 = `${root}/${appDir}`;
|
|
5643
|
+
if (isDirectory(path.join(repoRoot, relative4))) return relative4;
|
|
5644
|
+
}
|
|
5637
5645
|
return `apps/${appDir}`;
|
|
5638
5646
|
}
|
|
5639
5647
|
function docExists(contentRoot, docPath) {
|
|
@@ -5704,8 +5712,8 @@ function collectPackageJsons(root) {
|
|
|
5704
5712
|
function readPackageJson(filePath) {
|
|
5705
5713
|
return readJson(filePath);
|
|
5706
5714
|
}
|
|
5707
|
-
function packageLabel(
|
|
5708
|
-
return
|
|
5715
|
+
function packageLabel(record4) {
|
|
5716
|
+
return record4.packageJson.name ?? relativeTopoloPath(path.dirname(record4.path));
|
|
5709
5717
|
}
|
|
5710
5718
|
function sortedDirEntries(dir) {
|
|
5711
5719
|
return readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
@@ -5794,14 +5802,14 @@ import { constants } from "fs";
|
|
|
5794
5802
|
import { access } from "fs/promises";
|
|
5795
5803
|
import { relative, resolve } from "path";
|
|
5796
5804
|
function normalizeHomePath(value) {
|
|
5797
|
-
const
|
|
5798
|
-
if (!
|
|
5805
|
+
const path8 = String(value || "/dashboard").trim();
|
|
5806
|
+
if (!path8.startsWith("/") || path8.startsWith("//") || /[?#]/.test(path8)) {
|
|
5799
5807
|
throw new Error("Invalid home path. Use an absolute application path such as /dashboard.");
|
|
5800
5808
|
}
|
|
5801
|
-
if (["/", "/login", "/auth/callback", "/getting-started"].includes(
|
|
5809
|
+
if (["/", "/login", "/auth/callback", "/getting-started"].includes(path8)) {
|
|
5802
5810
|
throw new Error("Home path must be a protected product route, not a reserved entry route.");
|
|
5803
5811
|
}
|
|
5804
|
-
return
|
|
5812
|
+
return path8.replace(/\/$/, "") || "/dashboard";
|
|
5805
5813
|
}
|
|
5806
5814
|
function normalizeHomeLabel(value) {
|
|
5807
5815
|
const label = String(value || "Dashboard").trim();
|
|
@@ -5882,16 +5890,6 @@ function normalizeScaffoldProfile(value) {
|
|
|
5882
5890
|
if (value === "spatial-assets") return value;
|
|
5883
5891
|
throw new Error("Invalid scaffold profile. Use spatial-assets.");
|
|
5884
5892
|
}
|
|
5885
|
-
function resolveScaffoldResources(profile, options) {
|
|
5886
|
-
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 };
|
|
5887
|
-
return {
|
|
5888
|
-
d1: Boolean(profileResources.d1 || options.withD1),
|
|
5889
|
-
r2: Boolean(profileResources.r2 || options.withR2),
|
|
5890
|
-
queue: Boolean(profileResources.queue || options.withQueue),
|
|
5891
|
-
workflow: Boolean(profileResources.workflow || options.withWorkflow),
|
|
5892
|
-
containerProcessing: Boolean(profileResources.containerProcessing || options.withContainerProcessing)
|
|
5893
|
-
};
|
|
5894
|
-
}
|
|
5895
5893
|
function normalizeContainerAppId(value, surfaceModel) {
|
|
5896
5894
|
const trimmed = String(value || "").trim();
|
|
5897
5895
|
if (surfaceModel === "mobile_module") {
|
|
@@ -5942,9 +5940,9 @@ function toTitle(value) {
|
|
|
5942
5940
|
function json(value) {
|
|
5943
5941
|
return JSON.stringify(value, null, 2);
|
|
5944
5942
|
}
|
|
5945
|
-
async function pathExists(
|
|
5943
|
+
async function pathExists(path8) {
|
|
5946
5944
|
try {
|
|
5947
|
-
await access(
|
|
5945
|
+
await access(path8, constants.F_OK);
|
|
5948
5946
|
return true;
|
|
5949
5947
|
} catch {
|
|
5950
5948
|
return false;
|
|
@@ -5952,8 +5950,8 @@ async function pathExists(path5) {
|
|
|
5952
5950
|
}
|
|
5953
5951
|
function resolveInside(root, filePath) {
|
|
5954
5952
|
const resolved = resolve(root, filePath);
|
|
5955
|
-
const
|
|
5956
|
-
if (
|
|
5953
|
+
const relativePath2 = relative(root, resolved);
|
|
5954
|
+
if (relativePath2.startsWith("..") || relativePath2 === "" || relativePath2.startsWith("/")) {
|
|
5957
5955
|
throw new Error(`Scaffold file path escapes the target root: ${filePath}`);
|
|
5958
5956
|
}
|
|
5959
5957
|
return resolved;
|
|
@@ -5980,48 +5978,6 @@ var STAGING_CLOUDFLARE_ACCOUNT_ID = "4f4e1c69a3830946f9fea7b1eb7531ac";
|
|
|
5980
5978
|
var DEVELOPMENT_CLOUDFLARE_ACCOUNT_ID = "0ebdeec262a245481a721fc4f9d6d798";
|
|
5981
5979
|
var CLOUDFLARE_COMPATIBILITY_DATE = "2026-05-05";
|
|
5982
5980
|
|
|
5983
|
-
// src/commands/app-scaffold-package-policy.ts
|
|
5984
|
-
var TOPOLO_PACKAGE_SCAFFOLD_POLICY = {
|
|
5985
|
-
"@topolo-io/api-key-management": "browser-baseline",
|
|
5986
|
-
"@topolo-io/app-funnel": "browser-baseline",
|
|
5987
|
-
"@topolo-io/app-launcher": "browser-baseline",
|
|
5988
|
-
"@topolo-io/app-manifest": "domain-opt-in",
|
|
5989
|
-
"@topolo-io/app-shell": "browser-baseline",
|
|
5990
|
-
"@topolo-io/auth-client": "browser-baseline",
|
|
5991
|
-
"@topolo-io/auth-middleware": "api-baseline",
|
|
5992
|
-
"@topolo-io/billing-manifest": "domain-opt-in",
|
|
5993
|
-
"@topolo-io/billing-ui": "domain-opt-in",
|
|
5994
|
-
"@topolo-io/bridge": "domain-opt-in",
|
|
5995
|
-
"@topolo-io/calendar": "domain-opt-in",
|
|
5996
|
-
"@topolo-io/chat-widget": "browser-baseline",
|
|
5997
|
-
"@topolo-io/data": "domain-opt-in",
|
|
5998
|
-
"@topolo-io/design-tokens": "browser-baseline",
|
|
5999
|
-
"@topolo-io/error-middleware": "api-baseline",
|
|
6000
|
-
"@topolo-io/mailbox": "domain-opt-in",
|
|
6001
|
-
"@topolo-io/mobile-error-middleware": "domain-opt-in",
|
|
6002
|
-
"@topolo-io/mobile-experience-contracts": "domain-opt-in",
|
|
6003
|
-
"@topolo-io/nexus-client": "api-baseline",
|
|
6004
|
-
"@topolo-io/node-canvas": "domain-opt-in",
|
|
6005
|
-
"@topolo-io/notifications": "browser-baseline",
|
|
6006
|
-
"@topolo-io/onboarding": "browser-baseline",
|
|
6007
|
-
"@topolo-io/platform-mcp": "tooling",
|
|
6008
|
-
"@topolo-io/resource-context": "browser-baseline",
|
|
6009
|
-
"@topolo-io/security-headers": "api-baseline",
|
|
6010
|
-
"@topolo-io/tour": "browser-baseline",
|
|
6011
|
-
"@topolo-io/ui-kit": "browser-baseline",
|
|
6012
|
-
"@topolo-io/widget-server": "api-baseline",
|
|
6013
|
-
"@topolo-io/worker-runtime": "api-baseline",
|
|
6014
|
-
"@topolo/cli": "tooling",
|
|
6015
|
-
"@topolo/login": "developer-baseline",
|
|
6016
|
-
"@topolo/mcp": "tooling",
|
|
6017
|
-
"@topolo/sdk": "api-baseline"
|
|
6018
|
-
};
|
|
6019
|
-
function packagesForScaffoldClass(scaffoldClass) {
|
|
6020
|
-
return Object.entries(TOPOLO_PACKAGE_SCAFFOLD_POLICY).filter(([, value]) => value === scaffoldClass).map(([name]) => name).sort();
|
|
6021
|
-
}
|
|
6022
|
-
var PLATFORM_BROWSER_TOPOLO_PACKAGES = packagesForScaffoldClass("browser-baseline");
|
|
6023
|
-
var PLATFORM_API_TOPOLO_PACKAGES = packagesForScaffoldClass("api-baseline");
|
|
6024
|
-
|
|
6025
5981
|
// src/commands/app-scaffold-package-template.ts
|
|
6026
5982
|
function packageJson(application, target, options) {
|
|
6027
5983
|
const dependencies = {};
|
|
@@ -6046,9 +6002,6 @@ function packageJson(application, target, options) {
|
|
|
6046
6002
|
devDependencies.autoprefixer = "10.5.0";
|
|
6047
6003
|
devDependencies.postcss = "8.5.6";
|
|
6048
6004
|
devDependencies.tailwindcss = "3.4.19";
|
|
6049
|
-
for (const packageName of PLATFORM_BROWSER_TOPOLO_PACKAGES) {
|
|
6050
|
-
dependencies[packageName] = topoloPackageVersion(packageName, target);
|
|
6051
|
-
}
|
|
6052
6005
|
dependencies["react-router-dom"] = "^7.6.2";
|
|
6053
6006
|
} else {
|
|
6054
6007
|
dependencies["@topolo/login"] = topoloPackageVersion("@topolo/login", target);
|
|
@@ -6057,9 +6010,6 @@ function packageJson(application, target, options) {
|
|
|
6057
6010
|
if (options.api) {
|
|
6058
6011
|
devDependencies["@cloudflare/workers-types"] = "^5.20260716.1";
|
|
6059
6012
|
if (target === "platform") {
|
|
6060
|
-
for (const packageName of PLATFORM_API_TOPOLO_PACKAGES) {
|
|
6061
|
-
dependencies[packageName] = topoloPackageVersion(packageName, target);
|
|
6062
|
-
}
|
|
6063
6013
|
dependencies.hono = "^4";
|
|
6064
6014
|
dependencies.zod = "^4.4.3";
|
|
6065
6015
|
scripts.deploy = `node -e 'console.error("\\nX Bare deploy is disabled. Use deploy:staging or deploy:production.\\n"); process.exit(1)'`;
|
|
@@ -6071,6 +6021,9 @@ function packageJson(application, target, options) {
|
|
|
6071
6021
|
}
|
|
6072
6022
|
devDependencies.wrangler = "^4.91.0";
|
|
6073
6023
|
}
|
|
6024
|
+
for (const packageName of options.packages) {
|
|
6025
|
+
dependencies[packageName] = topoloPackageVersion(packageName, target);
|
|
6026
|
+
}
|
|
6074
6027
|
if (target === "platform") {
|
|
6075
6028
|
const testDirs = [];
|
|
6076
6029
|
if (options.browser) testDirs.push("apps/web");
|
|
@@ -6092,7 +6045,7 @@ function packageJson(application, target, options) {
|
|
|
6092
6045
|
}
|
|
6093
6046
|
}
|
|
6094
6047
|
}
|
|
6095
|
-
if (target === "platform" &&
|
|
6048
|
+
if (target === "platform" && (options.api || options.browser)) {
|
|
6096
6049
|
const deployChecks = [
|
|
6097
6050
|
...options.api ? ["wrangler deploy --config apps/api/wrangler.toml --env staging --dry-run --outdir .wrangler/api-build"] : [],
|
|
6098
6051
|
...options.browser ? ["wrangler deploy --config apps/web/wrangler.toml --env staging --dry-run --outdir .wrangler/web-build"] : []
|
|
@@ -6176,6 +6129,320 @@ function resourceConfig(application, profile) {
|
|
|
6176
6129
|
};
|
|
6177
6130
|
}
|
|
6178
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
|
+
|
|
6179
6446
|
// src/commands/app-scaffold-browser-template.ts
|
|
6180
6447
|
function readme(application, target, options) {
|
|
6181
6448
|
const displayName = toTitle(application.name);
|
|
@@ -6480,11 +6747,20 @@ function appIdentityTs(application) {
|
|
|
6480
6747
|
"}",
|
|
6481
6748
|
"",
|
|
6482
6749
|
"export function getBrowserAuthBaseUrl(): string {",
|
|
6483
|
-
"
|
|
6484
|
-
"
|
|
6485
|
-
"
|
|
6486
|
-
"
|
|
6487
|
-
"
|
|
6750
|
+
" // A declared URL always wins; deployed builds set VITE_TOPOLO_AUTH_URL in",
|
|
6751
|
+
" // their per-environment build commands, so the host map below only covers",
|
|
6752
|
+
" // platform domains. Anything else fails loudly \u2014 an unknown host",
|
|
6753
|
+
" // (localhost, workers.dev previews) must never silently authenticate",
|
|
6754
|
+
" // against production Auth.",
|
|
6755
|
+
" const declared = import.meta.env.VITE_TOPOLO_AUTH_URL;",
|
|
6756
|
+
" if (declared) return declared.replace(/\\/+$/, '');",
|
|
6757
|
+
" const hostname = typeof window !== 'undefined' ? window.location.hostname : '';",
|
|
6758
|
+
" if (hostname.endsWith('.stg.topolo.us')) return 'https://auth.stg.topolo.us';",
|
|
6759
|
+
" if (hostname.endsWith('.topolo.dev')) return 'https://auth.topolo.dev';",
|
|
6760
|
+
" if (hostname.endsWith('.topolo.app')) return 'https://auth.topolo.app';",
|
|
6761
|
+
" throw new Error(",
|
|
6762
|
+
" `VITE_TOPOLO_AUTH_URL must be set for host \"${hostname || 'non-browser context'}\" \u2014 refusing to default to production Auth.`,",
|
|
6763
|
+
" );",
|
|
6488
6764
|
"}",
|
|
6489
6765
|
"",
|
|
6490
6766
|
"// Canonical browser app id. index.html bakes %VITE_TOPOLO_APP_ID% into",
|
|
@@ -7219,7 +7495,12 @@ import { createAccountLinkPayload, createTopoloAuthorizationUrl, exchangeTopoloC
|
|
|
7219
7495
|
|
|
7220
7496
|
type AuthState = 'ready' | 'missing_client' | 'authorizing' | 'authenticated' | 'error';
|
|
7221
7497
|
const APP_NAME = '${escapeSingleQuoted(appName)}';
|
|
7222
|
-
|
|
7498
|
+
// Environment-aware default: hardcoding the production host would point a
|
|
7499
|
+
// staging build at prod Auth whenever the VITE_ vars were dropped.
|
|
7500
|
+
const DEFAULT_AUTH_URL = window.location.hostname.endsWith('.stg.topolo.us')
|
|
7501
|
+
? 'https://auth.stg.topolo.us'
|
|
7502
|
+
: 'https://auth.topolo.app';
|
|
7503
|
+
const AUTH_URL = import.meta.env.VITE_TOPOLO_AUTH_URL ?? import.meta.env.VITE_TOPOLO_AUTH_BASE_URL ?? DEFAULT_AUTH_URL;
|
|
7223
7504
|
const CLIENT_ID = import.meta.env.VITE_TOPOLO_CLIENT_ID || '${defaultClientId}';
|
|
7224
7505
|
const REDIRECT_URI = import.meta.env.VITE_TOPOLO_REDIRECT_URI || \`\${window.location.origin}/auth/callback\`;
|
|
7225
7506
|
const REQUESTED_SCOPES = (import.meta.env.VITE_TOPOLO_SCOPES || 'openid profile email organization:read entitlements:read').split(/[\\s,]+/).filter(Boolean);
|
|
@@ -7318,235 +7599,6 @@ function webWorkerTs(application, api) {
|
|
|
7318
7599
|
""
|
|
7319
7600
|
].join("\n");
|
|
7320
7601
|
}
|
|
7321
|
-
function platformWebWranglerToml(application, api) {
|
|
7322
|
-
const slug = toSlug(application.id);
|
|
7323
|
-
const host = application.productionUrl ? new URL(application.productionUrl).host : `${slug}.topolo.app`;
|
|
7324
|
-
return [
|
|
7325
|
-
`name = "topolo-${slug}"`,
|
|
7326
|
-
'main = "src/worker.ts"',
|
|
7327
|
-
`compatibility_date = "${CLOUDFLARE_COMPATIBILITY_DATE}"`,
|
|
7328
|
-
'compatibility_flags = ["nodejs_compat"]',
|
|
7329
|
-
`account_id = "${CLOUDFLARE_ACCOUNT_ID}"`,
|
|
7330
|
-
"workers_dev = false",
|
|
7331
|
-
"",
|
|
7332
|
-
"[assets]",
|
|
7333
|
-
'directory = "./dist"',
|
|
7334
|
-
'binding = "ASSETS"',
|
|
7335
|
-
'not_found_handling = "single-page-application"',
|
|
7336
|
-
...api ? ['run_worker_first = ["/api/*"]'] : [],
|
|
7337
|
-
"",
|
|
7338
|
-
"[observability]",
|
|
7339
|
-
"enabled = true",
|
|
7340
|
-
"",
|
|
7341
|
-
"[[routes]]",
|
|
7342
|
-
`pattern = "${host}"`,
|
|
7343
|
-
"custom_domain = true",
|
|
7344
|
-
"",
|
|
7345
|
-
"[vars]",
|
|
7346
|
-
`APP_ID = "${application.appId ?? ""}"`,
|
|
7347
|
-
`VITE_TOPOLO_APP_ID = "${application.appId ?? ""}"`,
|
|
7348
|
-
"",
|
|
7349
|
-
...api ? [
|
|
7350
|
-
"[[services]]",
|
|
7351
|
-
'binding = "API"',
|
|
7352
|
-
`service = "topolo-${slug}-api"`,
|
|
7353
|
-
""
|
|
7354
|
-
] : [],
|
|
7355
|
-
"[env.development]",
|
|
7356
|
-
`name = "topolo-${slug}-development"`,
|
|
7357
|
-
`account_id = "${DEVELOPMENT_CLOUDFLARE_ACCOUNT_ID}"`,
|
|
7358
|
-
"workers_dev = true",
|
|
7359
|
-
"",
|
|
7360
|
-
"[env.development.vars]",
|
|
7361
|
-
`APP_ID = "${application.appId ?? ""}"`,
|
|
7362
|
-
`VITE_TOPOLO_APP_ID = "${application.appId ?? ""}"`,
|
|
7363
|
-
"",
|
|
7364
|
-
"[[env.development.routes]]",
|
|
7365
|
-
`pattern = "${slug}.topolo.dev"`,
|
|
7366
|
-
"custom_domain = true",
|
|
7367
|
-
"",
|
|
7368
|
-
"[env.development.assets]",
|
|
7369
|
-
'directory = "./dist"',
|
|
7370
|
-
'binding = "ASSETS"',
|
|
7371
|
-
'not_found_handling = "single-page-application"',
|
|
7372
|
-
...api ? ['run_worker_first = ["/api/*"]'] : [],
|
|
7373
|
-
"",
|
|
7374
|
-
...api ? [
|
|
7375
|
-
"[[env.development.services]]",
|
|
7376
|
-
'binding = "API"',
|
|
7377
|
-
`service = "topolo-${slug}-api-development"`,
|
|
7378
|
-
""
|
|
7379
|
-
] : [],
|
|
7380
|
-
"[env.staging]",
|
|
7381
|
-
`name = "topolo-${slug}-staging"`,
|
|
7382
|
-
`account_id = "${STAGING_CLOUDFLARE_ACCOUNT_ID}"`,
|
|
7383
|
-
"workers_dev = true",
|
|
7384
|
-
"",
|
|
7385
|
-
"[env.staging.vars]",
|
|
7386
|
-
`APP_ID = "${application.appId ?? ""}"`,
|
|
7387
|
-
`VITE_TOPOLO_APP_ID = "${application.appId ?? ""}"`,
|
|
7388
|
-
"",
|
|
7389
|
-
"[[env.staging.routes]]",
|
|
7390
|
-
`pattern = "${slug}.stg.topolo.us"`,
|
|
7391
|
-
"custom_domain = true",
|
|
7392
|
-
"",
|
|
7393
|
-
"[env.staging.assets]",
|
|
7394
|
-
'directory = "./dist"',
|
|
7395
|
-
'binding = "ASSETS"',
|
|
7396
|
-
'not_found_handling = "single-page-application"',
|
|
7397
|
-
...api ? ['run_worker_first = ["/api/*"]'] : [],
|
|
7398
|
-
"",
|
|
7399
|
-
...api ? [
|
|
7400
|
-
"[[env.staging.services]]",
|
|
7401
|
-
'binding = "API"',
|
|
7402
|
-
`service = "topolo-${slug}-api-staging"`,
|
|
7403
|
-
""
|
|
7404
|
-
] : [],
|
|
7405
|
-
"[env.production]",
|
|
7406
|
-
`name = "topolo-${slug}-production"`,
|
|
7407
|
-
`account_id = "${CLOUDFLARE_ACCOUNT_ID}"`,
|
|
7408
|
-
"workers_dev = false",
|
|
7409
|
-
"",
|
|
7410
|
-
"[env.production.vars]",
|
|
7411
|
-
`APP_ID = "${application.appId ?? ""}"`,
|
|
7412
|
-
`VITE_TOPOLO_APP_ID = "${application.appId ?? ""}"`,
|
|
7413
|
-
"",
|
|
7414
|
-
"[[env.production.routes]]",
|
|
7415
|
-
`pattern = "${host}"`,
|
|
7416
|
-
"custom_domain = true",
|
|
7417
|
-
"",
|
|
7418
|
-
"[env.production.assets]",
|
|
7419
|
-
'directory = "./dist"',
|
|
7420
|
-
'binding = "ASSETS"',
|
|
7421
|
-
'not_found_handling = "single-page-application"',
|
|
7422
|
-
...api ? ['run_worker_first = ["/api/*"]'] : [],
|
|
7423
|
-
"",
|
|
7424
|
-
...api ? [
|
|
7425
|
-
"[[env.production.services]]",
|
|
7426
|
-
'binding = "API"',
|
|
7427
|
-
`service = "topolo-${slug}-api-production"`,
|
|
7428
|
-
""
|
|
7429
|
-
] : []
|
|
7430
|
-
].join("\n");
|
|
7431
|
-
}
|
|
7432
|
-
function platformApiWranglerToml(application, resources, profile) {
|
|
7433
|
-
const slug = toSlug(application.id);
|
|
7434
|
-
const resource = resourceConfig(application, profile);
|
|
7435
|
-
const lines = [
|
|
7436
|
-
`name = "topolo-${slug}-api"`,
|
|
7437
|
-
'main = "src/worker.ts"',
|
|
7438
|
-
`compatibility_date = "${CLOUDFLARE_COMPATIBILITY_DATE}"`,
|
|
7439
|
-
'compatibility_flags = ["nodejs_compat"]',
|
|
7440
|
-
`account_id = "${CLOUDFLARE_ACCOUNT_ID}"`,
|
|
7441
|
-
"workers_dev = true",
|
|
7442
|
-
"",
|
|
7443
|
-
"[observability]",
|
|
7444
|
-
"enabled = true",
|
|
7445
|
-
"",
|
|
7446
|
-
"[vars]",
|
|
7447
|
-
'AUTH_BASE_URL = "https://auth.topolo.app"',
|
|
7448
|
-
'NEXUS_GATEWAY_URL = "https://nexus.topolo.app"',
|
|
7449
|
-
`NEXUS_APP_ID = "${slug}"`,
|
|
7450
|
-
`APP_ID = "${application.appId ?? ""}"`,
|
|
7451
|
-
`VITE_TOPOLO_APP_ID = "${application.appId ?? ""}"`,
|
|
7452
|
-
""
|
|
7453
|
-
];
|
|
7454
|
-
appendResourceWranglerBlocks(lines, resources, resource, "production");
|
|
7455
|
-
lines.push(
|
|
7456
|
-
"[env.development]",
|
|
7457
|
-
`name = "topolo-${slug}-api-development"`,
|
|
7458
|
-
`account_id = "${DEVELOPMENT_CLOUDFLARE_ACCOUNT_ID}"`,
|
|
7459
|
-
"workers_dev = true",
|
|
7460
|
-
"",
|
|
7461
|
-
"[env.development.vars]",
|
|
7462
|
-
'AUTH_BASE_URL = "https://auth.topolo.dev"',
|
|
7463
|
-
'NEXUS_GATEWAY_URL = "https://nexus.topolo.dev"',
|
|
7464
|
-
`NEXUS_APP_ID = "${slug}"`,
|
|
7465
|
-
`APP_ID = "${application.appId ?? ""}"`,
|
|
7466
|
-
`VITE_TOPOLO_APP_ID = "${application.appId ?? ""}"`,
|
|
7467
|
-
'ENVIRONMENT = "development"',
|
|
7468
|
-
""
|
|
7469
|
-
);
|
|
7470
|
-
appendResourceWranglerBlocks(lines, resources, resource, "development");
|
|
7471
|
-
lines.push(
|
|
7472
|
-
"[env.staging]",
|
|
7473
|
-
`name = "topolo-${slug}-api-staging"`,
|
|
7474
|
-
`account_id = "${STAGING_CLOUDFLARE_ACCOUNT_ID}"`,
|
|
7475
|
-
"workers_dev = true",
|
|
7476
|
-
"",
|
|
7477
|
-
"[env.staging.vars]",
|
|
7478
|
-
'AUTH_BASE_URL = "https://auth.stg.topolo.us"',
|
|
7479
|
-
'NEXUS_GATEWAY_URL = "https://nexus.stg.topolo.us"',
|
|
7480
|
-
`NEXUS_APP_ID = "${slug}"`,
|
|
7481
|
-
`APP_ID = "${application.appId ?? ""}"`,
|
|
7482
|
-
`VITE_TOPOLO_APP_ID = "${application.appId ?? ""}"`,
|
|
7483
|
-
""
|
|
7484
|
-
);
|
|
7485
|
-
appendResourceWranglerBlocks(lines, resources, resource, "staging");
|
|
7486
|
-
lines.push(
|
|
7487
|
-
"[env.production]",
|
|
7488
|
-
`name = "topolo-${slug}-api-production"`,
|
|
7489
|
-
`account_id = "${CLOUDFLARE_ACCOUNT_ID}"`,
|
|
7490
|
-
"workers_dev = false",
|
|
7491
|
-
"",
|
|
7492
|
-
"[env.production.vars]",
|
|
7493
|
-
'AUTH_BASE_URL = "https://auth.topolo.app"',
|
|
7494
|
-
'NEXUS_GATEWAY_URL = "https://nexus.topolo.app"',
|
|
7495
|
-
`NEXUS_APP_ID = "${slug}"`,
|
|
7496
|
-
`APP_ID = "${application.appId ?? ""}"`,
|
|
7497
|
-
`VITE_TOPOLO_APP_ID = "${application.appId ?? ""}"`,
|
|
7498
|
-
""
|
|
7499
|
-
);
|
|
7500
|
-
appendResourceWranglerBlocks(lines, resources, resource, "production-env");
|
|
7501
|
-
return lines.join("\n");
|
|
7502
|
-
}
|
|
7503
|
-
function appendResourceWranglerBlocks(lines, resources, resource, environment) {
|
|
7504
|
-
const prefix = environment === "staging" ? "env.staging." : environment === "development" ? "env.development." : environment === "production-env" ? "env.production." : "";
|
|
7505
|
-
const envName = environment === "staging" ? "Staging" : environment === "development" ? "Development" : "Production";
|
|
7506
|
-
const pick = (dev, staging, prod) => environment === "development" ? dev : environment === "staging" ? staging : prod;
|
|
7507
|
-
if (resources.d1) {
|
|
7508
|
-
lines.push(
|
|
7509
|
-
`[[${prefix}d1_databases]]`,
|
|
7510
|
-
`binding = "${resource.d1Binding}"`,
|
|
7511
|
-
`database_name = "${pick(resource.d1DevelopmentName, resource.d1StagingName, resource.d1ProductionName)}"`,
|
|
7512
|
-
'database_id = "provision-after-scaffold"',
|
|
7513
|
-
'migrations_dir = "migrations"',
|
|
7514
|
-
""
|
|
7515
|
-
);
|
|
7516
|
-
}
|
|
7517
|
-
if (resources.r2) {
|
|
7518
|
-
lines.push(
|
|
7519
|
-
`[[${prefix}r2_buckets]]`,
|
|
7520
|
-
`binding = "${resource.r2Binding}"`,
|
|
7521
|
-
`bucket_name = "${pick(resource.r2DevelopmentName, resource.r2StagingName, resource.r2ProductionName)}"`,
|
|
7522
|
-
""
|
|
7523
|
-
);
|
|
7524
|
-
}
|
|
7525
|
-
if (resources.queue) {
|
|
7526
|
-
lines.push(
|
|
7527
|
-
`[[${prefix}queues.producers]]`,
|
|
7528
|
-
`binding = "${resource.queueBinding}"`,
|
|
7529
|
-
`queue = "${pick(resource.queueDevelopmentName, resource.queueStagingName, resource.queueProductionName)}"`,
|
|
7530
|
-
"",
|
|
7531
|
-
`[[${prefix}queues.consumers]]`,
|
|
7532
|
-
`queue = "${pick(resource.queueDevelopmentName, resource.queueStagingName, resource.queueProductionName)}"`,
|
|
7533
|
-
""
|
|
7534
|
-
);
|
|
7535
|
-
}
|
|
7536
|
-
if (resources.workflow) {
|
|
7537
|
-
lines.push(
|
|
7538
|
-
`[[${prefix}workflows]]`,
|
|
7539
|
-
`binding = "${resource.workflowBinding}"`,
|
|
7540
|
-
`name = "${pick(resource.workflowDevelopmentName, resource.workflowStagingName, resource.workflowProductionName)}"`,
|
|
7541
|
-
`class_name = "${resource.workflowClassName}"`,
|
|
7542
|
-
""
|
|
7543
|
-
);
|
|
7544
|
-
}
|
|
7545
|
-
if (resources.containerProcessing) {
|
|
7546
|
-
lines.push(`# ${envName} container metadata is declared in topolo.app.json and topolo.cloudcontrol.json.`);
|
|
7547
|
-
lines.push("");
|
|
7548
|
-
}
|
|
7549
|
-
}
|
|
7550
7602
|
function wranglerToml(application, target, resources, profile) {
|
|
7551
7603
|
const slug = toSlug(application.id);
|
|
7552
7604
|
const workerName = target === "platform" ? `topolo-${slug}` : application.packageName;
|
|
@@ -7707,9 +7759,15 @@ function platformWorkerTs(application, options) {
|
|
|
7707
7759
|
];
|
|
7708
7760
|
const workflowExport = options.resources.workflow ? [
|
|
7709
7761
|
"",
|
|
7710
|
-
`export class ${resource.workflowClassName} extends WorkflowEntrypoint<Env,
|
|
7711
|
-
" async run(
|
|
7712
|
-
"
|
|
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
|
+
" }));",
|
|
7713
7771
|
" }",
|
|
7714
7772
|
"}"
|
|
7715
7773
|
] : [];
|
|
@@ -7744,6 +7802,21 @@ function platformWorkerTs(application, options) {
|
|
|
7744
7802
|
"export interface Env {",
|
|
7745
7803
|
...envBindings,
|
|
7746
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
|
+
] : [],
|
|
7747
7820
|
"",
|
|
7748
7821
|
"function buildWidgets(): TopoloWidgetDraft[] {",
|
|
7749
7822
|
" return [",
|
|
@@ -7766,7 +7839,10 @@ function platformWorkerTs(application, options) {
|
|
|
7766
7839
|
" // Auth verification base URL is environment-driven (staging/prod/dev),",
|
|
7767
7840
|
" // so rt.verifyAuth() targets the correct Auth service per deploy.",
|
|
7768
7841
|
" authBaseUrl: (env) => env.AUTH_BASE_URL,",
|
|
7769
|
-
"
|
|
7842
|
+
" // The runtime's /api/auth/* gateway serves the app-switcher catalog,",
|
|
7843
|
+
" // preferences, and theme for the shared app shell \u2014 it must run before",
|
|
7844
|
+
" // the app's own auth so unentitled users still get the switcher.",
|
|
7845
|
+
" authGateway: { enabled: true },",
|
|
7770
7846
|
" actionContracts: topoloActionContracts,",
|
|
7771
7847
|
"});",
|
|
7772
7848
|
"",
|
|
@@ -7837,6 +7913,14 @@ function platformWorkerTs(application, options) {
|
|
|
7837
7913
|
" bindings.__rt = rt;",
|
|
7838
7914
|
" return securedFetch(request, bindings, ctx as ExecutionContext);",
|
|
7839
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
|
+
] : [],
|
|
7840
7924
|
"};",
|
|
7841
7925
|
"",
|
|
7842
7926
|
"function errorMessage(error: unknown): string {",
|
|
@@ -7919,8 +8003,13 @@ function developerWorkerTs(application) {
|
|
|
7919
8003
|
" const token = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : null;",
|
|
7920
8004
|
" if (!token) return { ok: false, status: 401, error: 'Bearer token required' };",
|
|
7921
8005
|
"",
|
|
8006
|
+
" // Fail loudly: defaulting to the production Auth host here would let a",
|
|
8007
|
+
" // misconfigured staging/dev worker validate tokens against prod. The",
|
|
8008
|
+
" // manifest declares AUTH_BASE_URL for every environment.",
|
|
8009
|
+
" if (!env.AUTH_BASE_URL) return { ok: false, status: 500, error: 'AUTH_BASE_URL not configured' };",
|
|
8010
|
+
"",
|
|
7922
8011
|
" try {",
|
|
7923
|
-
" const authBaseUrl = env.AUTH_BASE_URL
|
|
8012
|
+
" const authBaseUrl = env.AUTH_BASE_URL;",
|
|
7924
8013
|
" const response = await fetch(`${authBaseUrl}/validate`, {",
|
|
7925
8014
|
" method: 'POST',",
|
|
7926
8015
|
" headers: {",
|
|
@@ -7950,6 +8039,72 @@ function developerWorkerTs(application) {
|
|
|
7950
8039
|
].join("\n");
|
|
7951
8040
|
}
|
|
7952
8041
|
|
|
8042
|
+
// src/commands/app-scaffold-generate.ts
|
|
8043
|
+
import { existsSync as existsSync3, mkdirSync, mkdtempSync, readFileSync as readFileSync4, rmSync, writeFileSync } from "fs";
|
|
8044
|
+
import { spawnSync } from "child_process";
|
|
8045
|
+
import { tmpdir } from "os";
|
|
8046
|
+
import path2 from "path";
|
|
8047
|
+
function resolveGeneratorPath() {
|
|
8048
|
+
const candidates = [
|
|
8049
|
+
// Published CLI: vendored alongside dist/index.js at build time.
|
|
8050
|
+
new URL("./generate-wrangler.mjs", import.meta.url),
|
|
8051
|
+
// In-repo (tests, `pnpm dev`): the platform's canonical copy, which is the
|
|
8052
|
+
// very file the vendor step copies.
|
|
8053
|
+
new URL("../../../../tooling/generate-wrangler.mjs", import.meta.url)
|
|
8054
|
+
];
|
|
8055
|
+
for (const candidate of candidates) {
|
|
8056
|
+
const p = decodeURIComponent(candidate.pathname);
|
|
8057
|
+
if (existsSync3(p)) return p;
|
|
8058
|
+
}
|
|
8059
|
+
throw new Error(
|
|
8060
|
+
`topolo: could not locate generate-wrangler.mjs. A published CLI vendors it into dist/ at build time; in-repo it is read from topolo-platform/tooling/. Looked in:
|
|
8061
|
+
${candidates.map((c) => ` ${decodeURIComponent(c.pathname)}`).join("\n")}`
|
|
8062
|
+
);
|
|
8063
|
+
}
|
|
8064
|
+
var GENERATED = {
|
|
8065
|
+
purpose: "GENERATED from topolo.app.json by topolo-platform/tooling/generate-wrangler.mjs \u2014 never hand-edit.",
|
|
8066
|
+
requiredSignals: [
|
|
8067
|
+
"byte-identical to what generate-wrangler emits for this manifest",
|
|
8068
|
+
"every change is made in topolo.app.json, never here"
|
|
8069
|
+
]
|
|
8070
|
+
};
|
|
8071
|
+
function deriveGeneratedConfigs(files, base) {
|
|
8072
|
+
const generator = resolveGeneratorPath();
|
|
8073
|
+
const root = mkdtempSync(path2.join(tmpdir(), "topolo-scaffold-generate-"));
|
|
8074
|
+
try {
|
|
8075
|
+
const prefix = `${base}/`;
|
|
8076
|
+
for (const file of files) {
|
|
8077
|
+
if (!file.path.startsWith(prefix)) continue;
|
|
8078
|
+
const abs = path2.join(root, file.path.slice(prefix.length));
|
|
8079
|
+
mkdirSync(path2.dirname(abs), { recursive: true });
|
|
8080
|
+
writeFileSync(abs, file.content, "utf8");
|
|
8081
|
+
}
|
|
8082
|
+
const result = spawnSync(
|
|
8083
|
+
process.execPath,
|
|
8084
|
+
[generator, `--manifest=${path2.join(root, "topolo.app.json")}`],
|
|
8085
|
+
{ cwd: root, encoding: "utf8" }
|
|
8086
|
+
);
|
|
8087
|
+
if (result.status !== 0) {
|
|
8088
|
+
throw new Error(
|
|
8089
|
+
`topolo: generate-wrangler failed for the scaffolded manifest (exit ${result.status}).
|
|
8090
|
+
${result.stdout ?? ""}${result.stderr ?? ""}`
|
|
8091
|
+
);
|
|
8092
|
+
}
|
|
8093
|
+
const written = [...(result.stdout ?? "").matchAll(/^\[generate\] wrote (.+)$/gm)].map((m) => (m[1] ?? "").trim()).filter(Boolean);
|
|
8094
|
+
if (!written.length) {
|
|
8095
|
+
throw new Error(`topolo: generate-wrangler wrote no files.
|
|
8096
|
+
${result.stdout ?? ""}`);
|
|
8097
|
+
}
|
|
8098
|
+
return written.map((rel) => ({
|
|
8099
|
+
path: `${base}/${rel}`,
|
|
8100
|
+
...GENERATED,
|
|
8101
|
+
content: readFileSync4(path2.join(root, rel), "utf8")
|
|
8102
|
+
}));
|
|
8103
|
+
} finally {
|
|
8104
|
+
rmSync(root, { recursive: true, force: true });
|
|
8105
|
+
}
|
|
8106
|
+
}
|
|
8107
|
+
|
|
7953
8108
|
// src/commands/app-scaffold-workflows.ts
|
|
7954
8109
|
function scaffoldCommands(options) {
|
|
7955
8110
|
const scaffoldId = toSlug(options.id);
|
|
@@ -8113,6 +8268,10 @@ function topoloAppManifestJson(application, options) {
|
|
|
8113
8268
|
bindings.d1 = [
|
|
8114
8269
|
{
|
|
8115
8270
|
binding: resource.d1Binding,
|
|
8271
|
+
development: {
|
|
8272
|
+
databaseName: resource.d1DevelopmentName,
|
|
8273
|
+
databaseId: "provision-after-scaffold"
|
|
8274
|
+
},
|
|
8116
8275
|
staging: {
|
|
8117
8276
|
databaseName: resource.d1StagingName,
|
|
8118
8277
|
databaseId: "provision-after-scaffold"
|
|
@@ -8129,26 +8288,29 @@ function topoloAppManifestJson(application, options) {
|
|
|
8129
8288
|
bindings.r2 = [
|
|
8130
8289
|
{
|
|
8131
8290
|
binding: resource.r2Binding,
|
|
8291
|
+
development: { bucketName: resource.r2DevelopmentName },
|
|
8132
8292
|
staging: { bucketName: resource.r2StagingName },
|
|
8133
8293
|
production: { bucketName: resource.r2ProductionName }
|
|
8134
8294
|
}
|
|
8135
8295
|
];
|
|
8136
8296
|
}
|
|
8137
8297
|
if (options.resources.queue) {
|
|
8138
|
-
|
|
8139
|
-
{
|
|
8140
|
-
|
|
8141
|
-
|
|
8142
|
-
|
|
8143
|
-
|
|
8144
|
-
}
|
|
8145
|
-
|
|
8298
|
+
const queue = {
|
|
8299
|
+
development: { queue: resource.queueDevelopmentName },
|
|
8300
|
+
staging: { queue: resource.queueStagingName },
|
|
8301
|
+
production: { queue: resource.queueProductionName }
|
|
8302
|
+
};
|
|
8303
|
+
bindings.queues = {
|
|
8304
|
+
producers: [{ binding: resource.queueBinding, ...queue }],
|
|
8305
|
+
consumers: [{ ...queue }]
|
|
8306
|
+
};
|
|
8146
8307
|
}
|
|
8147
8308
|
if (options.resources.workflow) {
|
|
8148
8309
|
bindings.workflows = [
|
|
8149
8310
|
{
|
|
8150
8311
|
binding: resource.workflowBinding,
|
|
8151
8312
|
className: resource.workflowClassName,
|
|
8313
|
+
development: { name: resource.workflowDevelopmentName },
|
|
8152
8314
|
staging: { name: resource.workflowStagingName },
|
|
8153
8315
|
production: { name: resource.workflowProductionName }
|
|
8154
8316
|
}
|
|
@@ -8162,53 +8324,77 @@ function topoloAppManifestJson(application, options) {
|
|
|
8162
8324
|
compatibilityFlags: ["nodejs_compat"],
|
|
8163
8325
|
healthcheckPath: "/api/health",
|
|
8164
8326
|
vars: {
|
|
8327
|
+
// Every environment the manifest declares must be listed. A per-env map
|
|
8328
|
+
// that omits one leaves that env with no value, and both of these fall
|
|
8329
|
+
// back to PRODUCTION when unset (@topolo-io/nexus-client defaults
|
|
8330
|
+
// gatewayUrl to https://nexus.topolo.app) — a staging or dev worker
|
|
8331
|
+
// silently talking to prod.
|
|
8165
8332
|
AUTH_BASE_URL: {
|
|
8333
|
+
development: "https://auth.topolo.dev",
|
|
8166
8334
|
staging: "https://auth.stg.topolo.us",
|
|
8167
8335
|
production: "https://auth.topolo.app"
|
|
8168
8336
|
},
|
|
8169
8337
|
NEXUS_GATEWAY_URL: {
|
|
8338
|
+
development: "https://nexus.topolo.dev",
|
|
8170
8339
|
staging: "https://nexus.stg.topolo.us",
|
|
8171
8340
|
production: "https://nexus.topolo.app"
|
|
8172
8341
|
},
|
|
8173
|
-
|
|
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
|
+
} : {}
|
|
8349
|
+
// No NEXUS_APP_ID: generate-wrangler binds APP_ID (the app_id) into every
|
|
8350
|
+
// env, and worker-runtime reads env.APP_ID for Nexus usage attribution,
|
|
8351
|
+
// so a second copy of the same identity would only be a chance to drift.
|
|
8352
|
+
// It previously carried the SLUG, which reads as a working identity while
|
|
8353
|
+
// Nexus attributes by app_id.
|
|
8174
8354
|
},
|
|
8175
|
-
secrets: [
|
|
8355
|
+
secrets: [
|
|
8356
|
+
"TOPOLO_SERVICE_CLIENT_SECRET",
|
|
8357
|
+
...options.payload.capabilities.includes("webhooks") ? ["TOPOLO_WEBHOOK_SECRET"] : []
|
|
8358
|
+
],
|
|
8176
8359
|
...Object.keys(bindings).length ? { bindings } : {}
|
|
8177
8360
|
};
|
|
8178
8361
|
}
|
|
8179
|
-
|
|
8180
|
-
|
|
8181
|
-
|
|
8182
|
-
|
|
8183
|
-
|
|
8184
|
-
|
|
8185
|
-
|
|
8186
|
-
|
|
8187
|
-
|
|
8188
|
-
|
|
8189
|
-
|
|
8190
|
-
|
|
8191
|
-
|
|
8192
|
-
|
|
8193
|
-
|
|
8194
|
-
|
|
8195
|
-
|
|
8196
|
-
|
|
8197
|
-
|
|
8198
|
-
|
|
8199
|
-
|
|
8200
|
-
|
|
8201
|
-
|
|
8202
|
-
|
|
8203
|
-
|
|
8204
|
-
|
|
8205
|
-
|
|
8362
|
+
if (options.browser) {
|
|
8363
|
+
targets.web = {
|
|
8364
|
+
kind: "worker",
|
|
8365
|
+
dir: "apps/web",
|
|
8366
|
+
wranglerName: `topolo-${slug}`,
|
|
8367
|
+
subdomain: slug,
|
|
8368
|
+
main: "src/worker.ts",
|
|
8369
|
+
workersDev: false,
|
|
8370
|
+
assets: {
|
|
8371
|
+
directory: "./dist",
|
|
8372
|
+
binding: "ASSETS",
|
|
8373
|
+
notFoundHandling: "single-page-application",
|
|
8374
|
+
// Only meaningful when there is an API Worker to run first.
|
|
8375
|
+
...options.api ? { runWorkerFirst: ["/api/*"] } : {}
|
|
8376
|
+
},
|
|
8377
|
+
compatibilityFlags: ["nodejs_compat"],
|
|
8378
|
+
buildCommand: {
|
|
8379
|
+
staging: "cd ../.. && DEPLOY_ENVIRONMENT=staging VITE_TOPOLO_AUTH_URL=https://auth.stg.topolo.us pnpm run build",
|
|
8380
|
+
production: "cd ../.. && DEPLOY_ENVIRONMENT=production VITE_TOPOLO_AUTH_URL=https://auth.topolo.app pnpm run build"
|
|
8381
|
+
},
|
|
8382
|
+
healthcheckPath: options.api ? "/api/health" : "/",
|
|
8383
|
+
...options.api ? {
|
|
8384
|
+
bindings: {
|
|
8385
|
+
services: [
|
|
8386
|
+
{
|
|
8387
|
+
binding: "API",
|
|
8388
|
+
service: {
|
|
8389
|
+
staging: `topolo-${slug}-api-staging`,
|
|
8390
|
+
production: `topolo-${slug}-api-production`
|
|
8391
|
+
}
|
|
8206
8392
|
}
|
|
8207
|
-
|
|
8208
|
-
|
|
8209
|
-
}
|
|
8210
|
-
}
|
|
8211
|
-
}
|
|
8393
|
+
]
|
|
8394
|
+
}
|
|
8395
|
+
} : {}
|
|
8396
|
+
};
|
|
8397
|
+
}
|
|
8212
8398
|
const actions = options.api ? baselineApiActions() : [];
|
|
8213
8399
|
return `${json({
|
|
8214
8400
|
$schema: "./../../topolo-platform/tooling/app-manifest-schema.json",
|
|
@@ -8230,6 +8416,12 @@ function topoloAppManifestJson(application, options) {
|
|
|
8230
8416
|
}
|
|
8231
8417
|
},
|
|
8232
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
|
+
},
|
|
8233
8425
|
registration: {
|
|
8234
8426
|
ownerType: options.payload.ownerType,
|
|
8235
8427
|
portfolio: options.payload.portfolio,
|
|
@@ -8248,7 +8440,14 @@ function topoloAppManifestJson(application, options) {
|
|
|
8248
8440
|
supportEmail: "support@topolo.io",
|
|
8249
8441
|
auth: {
|
|
8250
8442
|
callbackPath: "/auth/callback",
|
|
8251
|
-
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
|
+
])
|
|
8252
8451
|
},
|
|
8253
8452
|
capabilities: options.payload.capabilities,
|
|
8254
8453
|
marketplace: {
|
|
@@ -8260,7 +8459,7 @@ function topoloAppManifestJson(application, options) {
|
|
|
8260
8459
|
docsUrl: `https://docs.topolo.app/applications/${slug}`
|
|
8261
8460
|
}
|
|
8262
8461
|
},
|
|
8263
|
-
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" },
|
|
8264
8463
|
...actions.length ? { actions } : {}
|
|
8265
8464
|
})}
|
|
8266
8465
|
`;
|
|
@@ -8352,263 +8551,6 @@ function baselineApiActions() {
|
|
|
8352
8551
|
}
|
|
8353
8552
|
];
|
|
8354
8553
|
}
|
|
8355
|
-
function cloudcontrolJson(application, options) {
|
|
8356
|
-
const slug = toSlug(application.id);
|
|
8357
|
-
const appPath = firstPartyAppPath(application.appDir, options.audience);
|
|
8358
|
-
const host = application.productionUrl ? new URL(application.productionUrl).host : `${slug}.topolo.app`;
|
|
8359
|
-
const stagingHost = `${slug}.stg.topolo.us`;
|
|
8360
|
-
const serviceSlug = application.serviceSlug ?? `topolo-${slug}`;
|
|
8361
|
-
const resources = resourceConfig(application, options.profile);
|
|
8362
|
-
const resourceBindings = [];
|
|
8363
|
-
if (options.resources.d1) {
|
|
8364
|
-
resourceBindings.push(
|
|
8365
|
-
{
|
|
8366
|
-
resource_type: "d1",
|
|
8367
|
-
binding: resources.d1Binding,
|
|
8368
|
-
environment: "production",
|
|
8369
|
-
account_id: CLOUDFLARE_ACCOUNT_ID,
|
|
8370
|
-
resource_name: resources.d1ProductionName,
|
|
8371
|
-
resource_id: "provision-after-scaffold",
|
|
8372
|
-
migrations_dir: "migrations"
|
|
8373
|
-
},
|
|
8374
|
-
{
|
|
8375
|
-
resource_type: "d1",
|
|
8376
|
-
binding: resources.d1Binding,
|
|
8377
|
-
environment: "staging",
|
|
8378
|
-
account_id: STAGING_CLOUDFLARE_ACCOUNT_ID,
|
|
8379
|
-
resource_name: resources.d1StagingName,
|
|
8380
|
-
resource_id: "provision-after-scaffold",
|
|
8381
|
-
migrations_dir: "migrations"
|
|
8382
|
-
},
|
|
8383
|
-
{
|
|
8384
|
-
resource_type: "d1",
|
|
8385
|
-
binding: resources.d1Binding,
|
|
8386
|
-
environment: "development",
|
|
8387
|
-
account_id: DEVELOPMENT_CLOUDFLARE_ACCOUNT_ID,
|
|
8388
|
-
resource_name: resources.d1DevelopmentName,
|
|
8389
|
-
resource_id: "provision-after-scaffold",
|
|
8390
|
-
migrations_dir: "migrations"
|
|
8391
|
-
}
|
|
8392
|
-
);
|
|
8393
|
-
}
|
|
8394
|
-
if (options.resources.r2) {
|
|
8395
|
-
resourceBindings.push(
|
|
8396
|
-
{
|
|
8397
|
-
resource_type: "r2",
|
|
8398
|
-
binding: resources.r2Binding,
|
|
8399
|
-
environment: "production",
|
|
8400
|
-
account_id: CLOUDFLARE_ACCOUNT_ID,
|
|
8401
|
-
resource_name: resources.r2ProductionName
|
|
8402
|
-
},
|
|
8403
|
-
{
|
|
8404
|
-
resource_type: "r2",
|
|
8405
|
-
binding: resources.r2Binding,
|
|
8406
|
-
environment: "staging",
|
|
8407
|
-
account_id: STAGING_CLOUDFLARE_ACCOUNT_ID,
|
|
8408
|
-
resource_name: resources.r2StagingName
|
|
8409
|
-
}
|
|
8410
|
-
);
|
|
8411
|
-
}
|
|
8412
|
-
if (options.resources.queue) {
|
|
8413
|
-
resourceBindings.push(
|
|
8414
|
-
{
|
|
8415
|
-
resource_type: "queue",
|
|
8416
|
-
binding: resources.queueBinding,
|
|
8417
|
-
environment: "production",
|
|
8418
|
-
account_id: CLOUDFLARE_ACCOUNT_ID,
|
|
8419
|
-
resource_name: resources.queueProductionName
|
|
8420
|
-
},
|
|
8421
|
-
{
|
|
8422
|
-
resource_type: "queue",
|
|
8423
|
-
binding: resources.queueBinding,
|
|
8424
|
-
environment: "staging",
|
|
8425
|
-
account_id: STAGING_CLOUDFLARE_ACCOUNT_ID,
|
|
8426
|
-
resource_name: resources.queueStagingName
|
|
8427
|
-
}
|
|
8428
|
-
);
|
|
8429
|
-
}
|
|
8430
|
-
if (options.resources.workflow) {
|
|
8431
|
-
resourceBindings.push(
|
|
8432
|
-
{
|
|
8433
|
-
resource_type: "workflow",
|
|
8434
|
-
binding: resources.workflowBinding,
|
|
8435
|
-
environment: "production",
|
|
8436
|
-
account_id: CLOUDFLARE_ACCOUNT_ID,
|
|
8437
|
-
resource_name: resources.workflowProductionName,
|
|
8438
|
-
class_name: resources.workflowClassName
|
|
8439
|
-
},
|
|
8440
|
-
{
|
|
8441
|
-
resource_type: "workflow",
|
|
8442
|
-
binding: resources.workflowBinding,
|
|
8443
|
-
environment: "staging",
|
|
8444
|
-
account_id: STAGING_CLOUDFLARE_ACCOUNT_ID,
|
|
8445
|
-
resource_name: resources.workflowStagingName,
|
|
8446
|
-
class_name: resources.workflowClassName
|
|
8447
|
-
}
|
|
8448
|
-
);
|
|
8449
|
-
}
|
|
8450
|
-
if (options.resources.containerProcessing) {
|
|
8451
|
-
resourceBindings.push(
|
|
8452
|
-
{
|
|
8453
|
-
resource_type: "container",
|
|
8454
|
-
environment: "production",
|
|
8455
|
-
account_id: CLOUDFLARE_ACCOUNT_ID,
|
|
8456
|
-
resource_name: resources.containerProductionName,
|
|
8457
|
-
purpose: "processing"
|
|
8458
|
-
},
|
|
8459
|
-
{
|
|
8460
|
-
resource_type: "container",
|
|
8461
|
-
environment: "staging",
|
|
8462
|
-
account_id: STAGING_CLOUDFLARE_ACCOUNT_ID,
|
|
8463
|
-
resource_name: resources.containerStagingName,
|
|
8464
|
-
purpose: "processing"
|
|
8465
|
-
}
|
|
8466
|
-
);
|
|
8467
|
-
}
|
|
8468
|
-
const deployTargets = options.api ? [
|
|
8469
|
-
{
|
|
8470
|
-
kind: "worker",
|
|
8471
|
-
environment: "production",
|
|
8472
|
-
target_name: `topolo-${slug}-api-production`,
|
|
8473
|
-
cloudflare_account_id: CLOUDFLARE_ACCOUNT_ID,
|
|
8474
|
-
config_path: "apps/api/wrangler.toml",
|
|
8475
|
-
workdir: "apps/api",
|
|
8476
|
-
build_command: null,
|
|
8477
|
-
output_dir: null,
|
|
8478
|
-
production_branch: "main",
|
|
8479
|
-
healthcheck_path: "/api/health",
|
|
8480
|
-
domains: [],
|
|
8481
|
-
routes: [],
|
|
8482
|
-
service_slug: serviceSlug
|
|
8483
|
-
},
|
|
8484
|
-
{
|
|
8485
|
-
kind: "worker",
|
|
8486
|
-
environment: "staging",
|
|
8487
|
-
target_name: `topolo-${slug}-staging`,
|
|
8488
|
-
cloudflare_account_id: STAGING_CLOUDFLARE_ACCOUNT_ID,
|
|
8489
|
-
config_path: "apps/web/wrangler.toml",
|
|
8490
|
-
workdir: "apps/web",
|
|
8491
|
-
build_command: "cd ../.. && DEPLOY_ENVIRONMENT=staging VITE_TOPOLO_AUTH_URL=https://auth.stg.topolo.us pnpm run build",
|
|
8492
|
-
output_dir: null,
|
|
8493
|
-
production_branch: "staging",
|
|
8494
|
-
healthcheck_path: "/api/health",
|
|
8495
|
-
domains: [stagingHost],
|
|
8496
|
-
routes: [stagingHost],
|
|
8497
|
-
api_url: `https://${stagingHost}`,
|
|
8498
|
-
service_slug: serviceSlug,
|
|
8499
|
-
sdk_app_id: application.id,
|
|
8500
|
-
agent_callable: true
|
|
8501
|
-
},
|
|
8502
|
-
{
|
|
8503
|
-
kind: "worker",
|
|
8504
|
-
environment: "staging",
|
|
8505
|
-
target_name: `topolo-${slug}-api-staging`,
|
|
8506
|
-
cloudflare_account_id: STAGING_CLOUDFLARE_ACCOUNT_ID,
|
|
8507
|
-
config_path: "apps/api/wrangler.toml",
|
|
8508
|
-
workdir: "apps/api",
|
|
8509
|
-
build_command: null,
|
|
8510
|
-
output_dir: null,
|
|
8511
|
-
production_branch: "staging",
|
|
8512
|
-
healthcheck_path: "/api/health",
|
|
8513
|
-
domains: [],
|
|
8514
|
-
routes: [],
|
|
8515
|
-
service_slug: serviceSlug
|
|
8516
|
-
},
|
|
8517
|
-
{
|
|
8518
|
-
kind: "worker",
|
|
8519
|
-
environment: "production",
|
|
8520
|
-
target_name: `topolo-${slug}-production`,
|
|
8521
|
-
cloudflare_account_id: CLOUDFLARE_ACCOUNT_ID,
|
|
8522
|
-
config_path: "apps/web/wrangler.toml",
|
|
8523
|
-
workdir: "apps/web",
|
|
8524
|
-
build_command: "cd ../.. && DEPLOY_ENVIRONMENT=production VITE_TOPOLO_AUTH_URL=https://auth.topolo.app pnpm run build",
|
|
8525
|
-
output_dir: null,
|
|
8526
|
-
production_branch: "main",
|
|
8527
|
-
healthcheck_path: "/api/health",
|
|
8528
|
-
domains: [host],
|
|
8529
|
-
routes: [host],
|
|
8530
|
-
api_url: application.productionUrl,
|
|
8531
|
-
service_slug: serviceSlug,
|
|
8532
|
-
sdk_app_id: application.id,
|
|
8533
|
-
agent_callable: true
|
|
8534
|
-
}
|
|
8535
|
-
] : [
|
|
8536
|
-
{
|
|
8537
|
-
kind: "worker",
|
|
8538
|
-
environment: "production",
|
|
8539
|
-
target_name: `topolo-${slug}`,
|
|
8540
|
-
cloudflare_account_id: CLOUDFLARE_ACCOUNT_ID,
|
|
8541
|
-
config_path: "wrangler.toml",
|
|
8542
|
-
build_command: "npm run build",
|
|
8543
|
-
output_dir: "dist",
|
|
8544
|
-
production_branch: "main",
|
|
8545
|
-
healthcheck_path: "/",
|
|
8546
|
-
domains: [host],
|
|
8547
|
-
routes: [host],
|
|
8548
|
-
api_url: application.productionUrl,
|
|
8549
|
-
agent_callable: false
|
|
8550
|
-
}
|
|
8551
|
-
];
|
|
8552
|
-
return `${json({
|
|
8553
|
-
name: `topolo-${slug}`,
|
|
8554
|
-
description: `Canonical documentation for ${toTitle(application.name)} lives in system-apps/TopoloDocs.`,
|
|
8555
|
-
deploy_method: "wrangler",
|
|
8556
|
-
ci_provider: "github_actions",
|
|
8557
|
-
repo_identity: {
|
|
8558
|
-
provider: "github",
|
|
8559
|
-
owner: "Topolo-io",
|
|
8560
|
-
repo: application.appDir,
|
|
8561
|
-
remote_url: `git@github.com:Topolo-io/${application.appDir}.git`,
|
|
8562
|
-
default_branch: "main"
|
|
8563
|
-
},
|
|
8564
|
-
environments: application.productionUrl ? [
|
|
8565
|
-
{
|
|
8566
|
-
name: "production",
|
|
8567
|
-
url: application.productionUrl,
|
|
8568
|
-
branch: "main",
|
|
8569
|
-
status: "unknown"
|
|
8570
|
-
},
|
|
8571
|
-
{
|
|
8572
|
-
name: "staging",
|
|
8573
|
-
url: `https://${stagingHost}`,
|
|
8574
|
-
branch: "main",
|
|
8575
|
-
status: "unknown"
|
|
8576
|
-
}
|
|
8577
|
-
] : [],
|
|
8578
|
-
deploy_targets: deployTargets,
|
|
8579
|
-
resource_bindings: resourceBindings,
|
|
8580
|
-
ci_workflows: [
|
|
8581
|
-
{
|
|
8582
|
-
provider: "github_actions",
|
|
8583
|
-
workflow_name: "topolo-conformance.yml",
|
|
8584
|
-
path: ".github/workflows/topolo-conformance.yml",
|
|
8585
|
-
branch: null,
|
|
8586
|
-
required: true
|
|
8587
|
-
}
|
|
8588
|
-
],
|
|
8589
|
-
ci_targets: [
|
|
8590
|
-
{
|
|
8591
|
-
name: "typecheck",
|
|
8592
|
-
command: "pnpm run typecheck",
|
|
8593
|
-
required: true
|
|
8594
|
-
},
|
|
8595
|
-
{
|
|
8596
|
-
name: "test",
|
|
8597
|
-
command: "pnpm run test",
|
|
8598
|
-
required: true
|
|
8599
|
-
},
|
|
8600
|
-
{
|
|
8601
|
-
name: "build",
|
|
8602
|
-
command: "pnpm run build",
|
|
8603
|
-
required: true
|
|
8604
|
-
}
|
|
8605
|
-
],
|
|
8606
|
-
tags: ["application", "cloudflare", "typescript", ...hasScaffoldResources(options.resources) ? ["resource-backed"] : []],
|
|
8607
|
-
source_paths: [appPath],
|
|
8608
|
-
cloudflare_account_id: CLOUDFLARE_ACCOUNT_ID
|
|
8609
|
-
})}
|
|
8610
|
-
`;
|
|
8611
|
-
}
|
|
8612
8554
|
function systemJson(application, options) {
|
|
8613
8555
|
const slug = toSlug(application.id);
|
|
8614
8556
|
const suiteLabel = options.audience === "personal" ? "personal" : "platform";
|
|
@@ -8893,12 +8835,18 @@ function serviceManifestJson(application, options) {
|
|
|
8893
8835
|
description: `Manage ${title} ${resourceLabel} data`,
|
|
8894
8836
|
resourcePattern: null
|
|
8895
8837
|
},
|
|
8896
|
-
{
|
|
8838
|
+
...options.servicePermissions.includes("usage:write") ? [{
|
|
8897
8839
|
id: `perm_${permissionPrefix}_usage_write`,
|
|
8898
8840
|
name: "usage:write",
|
|
8899
8841
|
description: `Record Nexus usage events for ${title}`,
|
|
8900
8842
|
resourcePattern: null
|
|
8901
|
-
}
|
|
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
|
+
}] : []
|
|
8902
8850
|
],
|
|
8903
8851
|
apiKeyScopes: [
|
|
8904
8852
|
{
|
|
@@ -8913,12 +8861,18 @@ function serviceManifestJson(application, options) {
|
|
|
8913
8861
|
description: `Manage ${title} ${resourceLabel} data`,
|
|
8914
8862
|
resourcePattern: null
|
|
8915
8863
|
},
|
|
8916
|
-
{
|
|
8864
|
+
...options.servicePermissions.includes("usage:write") ? [{
|
|
8917
8865
|
id: `aks_${permissionPrefix}_usage_write`,
|
|
8918
8866
|
name: "usage.write",
|
|
8919
8867
|
description: `Record Nexus usage events for ${title}`,
|
|
8920
8868
|
resourcePattern: null
|
|
8921
|
-
}
|
|
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
|
+
}] : []
|
|
8922
8876
|
],
|
|
8923
8877
|
roleBundles: {
|
|
8924
8878
|
admin: ["workspace:read", "workspace:write"],
|
|
@@ -8933,7 +8887,14 @@ function developerManifest(application, options) {
|
|
|
8933
8887
|
const callbackPath = "/auth/callback";
|
|
8934
8888
|
const localCallbackUrl = "http://localhost:5173/auth/callback";
|
|
8935
8889
|
const productionCallbackUrl = application.productionUrl ? `${application.productionUrl.replace(/\/$/, "")}${callbackPath}` : null;
|
|
8936
|
-
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
|
+
]);
|
|
8937
8898
|
return `${json({
|
|
8938
8899
|
schema: "https://topolo.app/schemas/topolo-app-manifest.v1.json",
|
|
8939
8900
|
id: application.id,
|
|
@@ -8948,17 +8909,9 @@ function developerManifest(application, options) {
|
|
|
8948
8909
|
app_id: application.appId,
|
|
8949
8910
|
capabilities: {
|
|
8950
8911
|
selected: options.capabilities,
|
|
8951
|
-
|
|
8952
|
-
|
|
8953
|
-
|
|
8954
|
-
"marketplace",
|
|
8955
|
-
"api",
|
|
8956
|
-
"widget",
|
|
8957
|
-
"billing",
|
|
8958
|
-
"nexus_ai",
|
|
8959
|
-
"agent_access",
|
|
8960
|
-
"hosting"
|
|
8961
|
-
]
|
|
8912
|
+
registry_version: options.capabilityComposition.registryVersion,
|
|
8913
|
+
declarations: options.capabilityComposition.declarations,
|
|
8914
|
+
supported_v1: TOPOLO_APP_CAPABILITY_IDS
|
|
8962
8915
|
},
|
|
8963
8916
|
auth: {
|
|
8964
8917
|
provider: "topolo",
|
|
@@ -9020,10 +8973,13 @@ function developerManifest(application, options) {
|
|
|
9020
8973
|
docsUrl: application.productionUrl || "https://developers.topolo.app"
|
|
9021
8974
|
}
|
|
9022
8975
|
},
|
|
9023
|
-
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" }
|
|
9024
8977
|
})}
|
|
9025
8978
|
`;
|
|
9026
8979
|
}
|
|
8980
|
+
function unique3(values) {
|
|
8981
|
+
return Array.from(new Set(values));
|
|
8982
|
+
}
|
|
9027
8983
|
function envExample(application) {
|
|
9028
8984
|
return [
|
|
9029
8985
|
"VITE_TOPOLO_AUTH_URL=https://auth.topolo.app",
|
|
@@ -9287,12 +9243,24 @@ function applicationScaffoldPayload(application, options = {}) {
|
|
|
9287
9243
|
const surfaceModel = normalizeSurfaceModel(options.surfaceModel);
|
|
9288
9244
|
const containerAppId = normalizeContainerAppId(options.containerAppId, surfaceModel);
|
|
9289
9245
|
const profile = normalizeScaffoldProfile(options.profile);
|
|
9290
|
-
const resources = resolveScaffoldResources(profile, options);
|
|
9291
|
-
if (target === "platform" && api && !profile) {
|
|
9292
|
-
resources.d1 = true;
|
|
9293
|
-
}
|
|
9294
9246
|
const ownerType = target === "platform" ? "topolo_first_party" : "third_party";
|
|
9295
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
|
+
};
|
|
9296
9264
|
const requestedAppDir = options.appDir ?? catalogApplication?.appDir ?? (target === "platform" ? `Topolo${toPascalCase(id)}` : `topolo-${slug}`);
|
|
9297
9265
|
const appDir = target === "platform" ? normalizeFirstPartyAppDir(requestedAppDir) : requestedAppDir;
|
|
9298
9266
|
const name = options.name ?? catalogApplication?.name ?? `topolo-${slug}`;
|
|
@@ -9312,7 +9280,7 @@ function applicationScaffoldPayload(application, options = {}) {
|
|
|
9312
9280
|
const appId = options.appId ?? defaultAppId;
|
|
9313
9281
|
const productionUrl = browser ? options.productionUrl ?? catalogApplication?.productionUrl ?? `https://${slug}.topolo.app` : null;
|
|
9314
9282
|
const scopes = scaffoldScopes({ browser, api });
|
|
9315
|
-
const capabilities =
|
|
9283
|
+
const capabilities = capabilityComposition.selected;
|
|
9316
9284
|
const ciWorkflowPath = target === "platform" ? ".github/workflows/topolo-conformance.yml" : null;
|
|
9317
9285
|
const ciWorkflow = target === "platform" ? topoloConformanceWorkflow({ id, appDir, audience }) : null;
|
|
9318
9286
|
const applicationPayload = {
|
|
@@ -9338,12 +9306,13 @@ function applicationScaffoldPayload(application, options = {}) {
|
|
|
9338
9306
|
experience,
|
|
9339
9307
|
distributionModel,
|
|
9340
9308
|
capabilities,
|
|
9309
|
+
capabilityComposition,
|
|
9341
9310
|
application: applicationPayload,
|
|
9342
9311
|
scopes,
|
|
9343
9312
|
requirements: APPLICATION_REQUIREMENTS.filter(
|
|
9344
9313
|
(requirement) => requirement.appliesTo.some((scope) => scopes.includes(scope))
|
|
9345
9314
|
),
|
|
9346
|
-
packages:
|
|
9315
|
+
packages: capabilityComposition.packages,
|
|
9347
9316
|
files: scaffoldFiles({
|
|
9348
9317
|
payload: {
|
|
9349
9318
|
ownerType,
|
|
@@ -9356,7 +9325,8 @@ function applicationScaffoldPayload(application, options = {}) {
|
|
|
9356
9325
|
resources,
|
|
9357
9326
|
experience,
|
|
9358
9327
|
distributionModel,
|
|
9359
|
-
capabilities
|
|
9328
|
+
capabilities,
|
|
9329
|
+
capabilityComposition
|
|
9360
9330
|
},
|
|
9361
9331
|
application: applicationPayload,
|
|
9362
9332
|
target,
|
|
@@ -9417,28 +9387,6 @@ function scaffoldScopes(options) {
|
|
|
9417
9387
|
}
|
|
9418
9388
|
return scopes;
|
|
9419
9389
|
}
|
|
9420
|
-
function scaffoldPackages(options) {
|
|
9421
|
-
if (options.target === "developer") {
|
|
9422
|
-
return options.browser ? ["@topolo/login"] : [];
|
|
9423
|
-
}
|
|
9424
|
-
const packages = /* @__PURE__ */ new Set();
|
|
9425
|
-
if (options.browser) {
|
|
9426
|
-
for (const packageName of PLATFORM_BROWSER_TOPOLO_PACKAGES) packages.add(packageName);
|
|
9427
|
-
}
|
|
9428
|
-
if (options.api) {
|
|
9429
|
-
for (const packageName of PLATFORM_API_TOPOLO_PACKAGES) packages.add(packageName);
|
|
9430
|
-
}
|
|
9431
|
-
return Array.from(packages);
|
|
9432
|
-
}
|
|
9433
|
-
function scaffoldCapabilities(options) {
|
|
9434
|
-
const capabilities = [];
|
|
9435
|
-
if (options.browser) {
|
|
9436
|
-
capabilities.push("login", "launcher");
|
|
9437
|
-
}
|
|
9438
|
-
if (options.distributionModel === "marketplace") capabilities.push("marketplace");
|
|
9439
|
-
if (options.api) capabilities.push("api", "widget");
|
|
9440
|
-
return capabilities;
|
|
9441
|
-
}
|
|
9442
9390
|
function scaffoldFiles(options) {
|
|
9443
9391
|
const { application, target } = options;
|
|
9444
9392
|
const slug = toSlug(application.id);
|
|
@@ -9478,9 +9426,31 @@ function scaffoldFiles(options) {
|
|
|
9478
9426
|
content: packageJson(application, target, {
|
|
9479
9427
|
browser: options.browser,
|
|
9480
9428
|
api: options.api,
|
|
9481
|
-
resources: options.payload.resources
|
|
9429
|
+
resources: options.payload.resources,
|
|
9430
|
+
packages: options.payload.capabilityComposition.packages
|
|
9482
9431
|
})
|
|
9483
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
|
+
}] : [],
|
|
9484
9454
|
{
|
|
9485
9455
|
path: `${base}/README.md`,
|
|
9486
9456
|
purpose: "Starter README with install, run, and verification commands.",
|
|
@@ -9686,6 +9656,7 @@ function scaffoldFiles(options) {
|
|
|
9686
9656
|
],
|
|
9687
9657
|
content: topoloAppManifestJson(application, {
|
|
9688
9658
|
api: options.api,
|
|
9659
|
+
browser: options.browser,
|
|
9689
9660
|
resources: options.payload.resources,
|
|
9690
9661
|
profile: options.payload.profile,
|
|
9691
9662
|
payload: options.payload
|
|
@@ -9711,20 +9682,6 @@ function scaffoldFiles(options) {
|
|
|
9711
9682
|
content: topoloActionContractsDmts()
|
|
9712
9683
|
}
|
|
9713
9684
|
] : [],
|
|
9714
|
-
{
|
|
9715
|
-
path: `${base}/topolo.cloudcontrol.json`,
|
|
9716
|
-
purpose: "CloudControl source of truth for deploy targets, routes, bindings, and CI metadata.",
|
|
9717
|
-
requiredSignals: [
|
|
9718
|
-
"production URLs and Cloudflare targets are explicit",
|
|
9719
|
-
"resource bindings and required secrets are declared without secret values"
|
|
9720
|
-
],
|
|
9721
|
-
content: cloudcontrolJson(application, {
|
|
9722
|
-
api: options.api,
|
|
9723
|
-
audience: options.payload.audience,
|
|
9724
|
-
profile: options.payload.profile,
|
|
9725
|
-
resources: options.payload.resources
|
|
9726
|
-
})
|
|
9727
|
-
},
|
|
9728
9685
|
{
|
|
9729
9686
|
path: `${base}/.github/workflows/topolo-conformance.yml`,
|
|
9730
9687
|
purpose: "PR/push gate that runs the local Topolo app audit with docs evidence.",
|
|
@@ -9825,7 +9782,8 @@ function scaffoldFiles(options) {
|
|
|
9825
9782
|
],
|
|
9826
9783
|
content: serviceManifestJson(application, {
|
|
9827
9784
|
audience: options.payload.audience,
|
|
9828
|
-
tenancy: options.payload.tenancy
|
|
9785
|
+
tenancy: options.payload.tenancy,
|
|
9786
|
+
servicePermissions: options.payload.capabilityComposition.servicePermissions
|
|
9829
9787
|
})
|
|
9830
9788
|
}
|
|
9831
9789
|
);
|
|
@@ -9846,7 +9804,8 @@ function scaffoldFiles(options) {
|
|
|
9846
9804
|
surfaceModel: options.payload.surfaceModel,
|
|
9847
9805
|
containerAppId: options.payload.containerAppId,
|
|
9848
9806
|
distributionModel: options.payload.distributionModel,
|
|
9849
|
-
capabilities: options.payload.capabilities
|
|
9807
|
+
capabilities: options.payload.capabilities,
|
|
9808
|
+
capabilityComposition: options.payload.capabilityComposition
|
|
9850
9809
|
})
|
|
9851
9810
|
},
|
|
9852
9811
|
{
|
|
@@ -9933,31 +9892,23 @@ export default defineConfig({
|
|
|
9933
9892
|
"only declares an API service binding for API-backed scaffolds"
|
|
9934
9893
|
],
|
|
9935
9894
|
content: webWorkerTs(application, options.api)
|
|
9936
|
-
},
|
|
9937
|
-
{
|
|
9938
|
-
path: `${browserBase}/wrangler.toml`,
|
|
9939
|
-
purpose: "Cloudflare Worker static-assets configuration for the browser surface.",
|
|
9940
|
-
requiredSignals: [
|
|
9941
|
-
"serves Vite assets through a Worker with assets",
|
|
9942
|
-
"only binds an API Worker when the scaffold includes one"
|
|
9943
|
-
],
|
|
9944
|
-
content: platformWebWranglerToml(application, options.api)
|
|
9945
9895
|
}
|
|
9946
9896
|
);
|
|
9947
9897
|
}
|
|
9948
9898
|
}
|
|
9949
9899
|
}
|
|
9950
9900
|
if (options.api) {
|
|
9901
|
+
const capabilityAdapter = target === "platform" ? managedCapabilityAdapter(options.payload.capabilities) : null;
|
|
9951
9902
|
files.push(
|
|
9952
|
-
{
|
|
9903
|
+
...target === "platform" ? [] : [{
|
|
9953
9904
|
path: `${apiBase}/wrangler.toml`,
|
|
9954
9905
|
purpose: "Cloudflare Worker runtime configuration.",
|
|
9955
9906
|
requiredSignals: [
|
|
9956
|
-
"bindings match
|
|
9907
|
+
"bindings match the declared resources",
|
|
9957
9908
|
"secrets are referenced by name only"
|
|
9958
9909
|
],
|
|
9959
|
-
content:
|
|
9960
|
-
},
|
|
9910
|
+
content: wranglerToml(application, target, options.payload.resources, options.payload.profile)
|
|
9911
|
+
}],
|
|
9961
9912
|
{
|
|
9962
9913
|
path: `${apiBase}/src/worker.ts`,
|
|
9963
9914
|
purpose: "Protected service boundary.",
|
|
@@ -9972,7 +9923,13 @@ export default defineConfig({
|
|
|
9972
9923
|
profile: options.payload.profile,
|
|
9973
9924
|
resources: options.payload.resources
|
|
9974
9925
|
})
|
|
9975
|
-
}
|
|
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
|
+
}] : []
|
|
9976
9933
|
);
|
|
9977
9934
|
if (target === "platform") {
|
|
9978
9935
|
files.push(
|
|
@@ -10025,6 +9982,9 @@ export default defineConfig({
|
|
|
10025
9982
|
}
|
|
10026
9983
|
}
|
|
10027
9984
|
}
|
|
9985
|
+
if (target === "platform") {
|
|
9986
|
+
files.push(...deriveGeneratedConfigs(files, base));
|
|
9987
|
+
}
|
|
10028
9988
|
return files;
|
|
10029
9989
|
}
|
|
10030
9990
|
|
|
@@ -10173,7 +10133,14 @@ function buildProvisioningRequest(payload, supportEmail) {
|
|
|
10173
10133
|
"http://localhost:5173/auth/callback",
|
|
10174
10134
|
payload.application.productionUrl ? `${payload.application.productionUrl.replace(/\/$/, "")}/auth/callback` : null
|
|
10175
10135
|
].filter(Boolean),
|
|
10176
|
-
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
|
+
])),
|
|
10177
10144
|
marketplace: {
|
|
10178
10145
|
intent: payload.distributionModel === "marketplace" ? "submit_when_ready" : "not_requested",
|
|
10179
10146
|
status: "preview",
|
|
@@ -10182,7 +10149,7 @@ function buildProvisioningRequest(payload, supportEmail) {
|
|
|
10182
10149
|
description: payload.experience.tagline,
|
|
10183
10150
|
docsUrl: payload.application.productionUrl || "https://developers.topolo.app"
|
|
10184
10151
|
},
|
|
10185
|
-
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" },
|
|
10186
10153
|
applicationId: payload.application.id,
|
|
10187
10154
|
name: payload.application.name,
|
|
10188
10155
|
description: payload.experience.tagline,
|
|
@@ -10250,18 +10217,18 @@ async function applyProvisionedAppId(manifestPath, appId) {
|
|
|
10250
10217
|
} catch {
|
|
10251
10218
|
return { changed: false, previous: null };
|
|
10252
10219
|
}
|
|
10253
|
-
let
|
|
10220
|
+
let json2;
|
|
10254
10221
|
try {
|
|
10255
|
-
|
|
10222
|
+
json2 = JSON.parse(raw);
|
|
10256
10223
|
} catch {
|
|
10257
10224
|
return { changed: false, previous: null };
|
|
10258
10225
|
}
|
|
10259
|
-
const previous = typeof
|
|
10226
|
+
const previous = typeof json2.appId === "string" ? json2.appId : null;
|
|
10260
10227
|
if (previous === appId) {
|
|
10261
10228
|
return { changed: false, previous };
|
|
10262
10229
|
}
|
|
10263
|
-
|
|
10264
|
-
await writeFile3(manifestPath, `${JSON.stringify(
|
|
10230
|
+
json2.appId = appId;
|
|
10231
|
+
await writeFile3(manifestPath, `${JSON.stringify(json2, null, 2)}
|
|
10265
10232
|
`, "utf8");
|
|
10266
10233
|
return { changed: true, previous };
|
|
10267
10234
|
}
|
|
@@ -10270,13 +10237,280 @@ async function applyProvisionedAppId(manifestPath, appId) {
|
|
|
10270
10237
|
import { join as join3 } from "path";
|
|
10271
10238
|
|
|
10272
10239
|
// src/commands/app-reconcile.ts
|
|
10273
|
-
import { readFile as
|
|
10274
|
-
import
|
|
10240
|
+
import { readFile as readFile4, writeFile as writeFile5 } from "fs/promises";
|
|
10241
|
+
import path4 from "path";
|
|
10275
10242
|
import { createInterface as createInterface2 } from "readline/promises";
|
|
10276
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
|
+
}
|
|
10277
10432
|
function record(value) {
|
|
10278
10433
|
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
10279
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
|
+
}
|
|
10280
10514
|
function environmentValue(value, environment) {
|
|
10281
10515
|
if (typeof value === "string") return value;
|
|
10282
10516
|
if (!value) return null;
|
|
@@ -10301,7 +10535,7 @@ function resolveManifestApplicationUrl(manifest, environment) {
|
|
|
10301
10535
|
return `https://${target.subdomain}.${domain}`;
|
|
10302
10536
|
}
|
|
10303
10537
|
function assertRegistration(value) {
|
|
10304
|
-
const registration =
|
|
10538
|
+
const registration = record2(value);
|
|
10305
10539
|
const required = [
|
|
10306
10540
|
"ownerType",
|
|
10307
10541
|
"applicationId",
|
|
@@ -10330,9 +10564,9 @@ function titleFromSlug(value) {
|
|
|
10330
10564
|
return value.replace(/^topolo[-_]/, "").split(/[-_]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
|
|
10331
10565
|
}
|
|
10332
10566
|
async function completeApplicationRegistrationInteractively(options) {
|
|
10333
|
-
const manifestPath =
|
|
10334
|
-
const manifest = JSON.parse(await
|
|
10335
|
-
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) || {};
|
|
10336
10570
|
const appId = String(manifest.appId || manifest.app_id || "").trim();
|
|
10337
10571
|
const slug = String(existing.serviceSlug || manifest.name || "").trim();
|
|
10338
10572
|
const defaultName = titleFromSlug(slug);
|
|
@@ -10366,19 +10600,19 @@ async function completeApplicationRegistrationInteractively(options) {
|
|
|
10366
10600
|
containerAppId: existing.containerAppId,
|
|
10367
10601
|
distributionModel: existing.distributionModel || (options.expectedOwnerType === "topolo_first_party" ? "marketplace" : "organization_internal"),
|
|
10368
10602
|
version: String(existing.version || "1.0.0"),
|
|
10369
|
-
pricing:
|
|
10603
|
+
pricing: record2(existing.pricing) || { type: "free", monthlyPrice: null },
|
|
10370
10604
|
supportEmail,
|
|
10371
10605
|
productionUrl,
|
|
10372
10606
|
demoUrl: typeof existing.demoUrl === "string" ? existing.demoUrl : void 0,
|
|
10373
10607
|
repoUrl: typeof existing.repoUrl === "string" ? existing.repoUrl : void 0,
|
|
10374
10608
|
tags: Array.isArray(existing.tags) ? existing.tags : [],
|
|
10375
10609
|
screenshots: Array.isArray(existing.screenshots) ? existing.screenshots : [],
|
|
10376
|
-
auth:
|
|
10610
|
+
auth: record2(existing.auth) || {
|
|
10377
10611
|
callbackPath: "/auth/callback",
|
|
10378
10612
|
requestedScopes: ["openid", "profile", "email", "organization:read"]
|
|
10379
10613
|
},
|
|
10380
10614
|
capabilities: Array.isArray(existing.capabilities) ? existing.capabilities : ["login", "launcher"],
|
|
10381
|
-
marketplace:
|
|
10615
|
+
marketplace: record2(existing.marketplace) || {
|
|
10382
10616
|
intent: options.expectedOwnerType === "topolo_first_party" ? "submit_when_ready" : "not_requested",
|
|
10383
10617
|
status: "preview",
|
|
10384
10618
|
shortBlurb: description,
|
|
@@ -10402,7 +10636,7 @@ async function completeApplicationRegistrationInteractively(options) {
|
|
|
10402
10636
|
changed = true;
|
|
10403
10637
|
}
|
|
10404
10638
|
if (!appId) throw new Error("topolo.app.json must declare the immutable appId before reconciliation.");
|
|
10405
|
-
if (changed) await
|
|
10639
|
+
if (changed) await writeFile5(manifestPath, `${JSON.stringify(manifest, null, 2)}
|
|
10406
10640
|
`, "utf8");
|
|
10407
10641
|
return { changed, manifestPath };
|
|
10408
10642
|
} finally {
|
|
@@ -10410,8 +10644,8 @@ async function completeApplicationRegistrationInteractively(options) {
|
|
|
10410
10644
|
}
|
|
10411
10645
|
}
|
|
10412
10646
|
async function loadApplicationReconcileRequest(options) {
|
|
10413
|
-
const manifestPath =
|
|
10414
|
-
const raw = await
|
|
10647
|
+
const manifestPath = path4.resolve(options.manifestPath || "topolo.app.json");
|
|
10648
|
+
const raw = await readFile4(manifestPath, "utf8");
|
|
10415
10649
|
const manifest = JSON.parse(raw);
|
|
10416
10650
|
assertRegistration(manifest.registration);
|
|
10417
10651
|
const registration = manifest.registration;
|
|
@@ -10555,6 +10789,7 @@ function registerApplicationReconcileCommand(parent, ownerType, dependencies = {
|
|
|
10555
10789
|
const loadRequest = dependencies.loadRequest || loadApplicationReconcileRequest;
|
|
10556
10790
|
const reconcileRequest = dependencies.reconcileRequest || reconcileApplicationRequest;
|
|
10557
10791
|
const confirm = dependencies.confirm || confirmApplicationReconcile;
|
|
10792
|
+
const reconcileSource = dependencies.reconcileSource || reconcileApplicationCapabilitySource;
|
|
10558
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) => {
|
|
10559
10794
|
const outputMode = resolveOutputMode(options.json);
|
|
10560
10795
|
try {
|
|
@@ -10565,6 +10800,20 @@ function registerApplicationReconcileCommand(parent, ownerType, dependencies = {
|
|
|
10565
10800
|
expectedOwnerType: ownerType
|
|
10566
10801
|
});
|
|
10567
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
|
+
}
|
|
10568
10817
|
const loaded = await loadRequest({
|
|
10569
10818
|
manifestPath: options.manifest,
|
|
10570
10819
|
expectedOwnerType: ownerType,
|
|
@@ -10582,11 +10831,11 @@ function registerApplicationReconcileCommand(parent, ownerType, dependencies = {
|
|
|
10582
10831
|
}
|
|
10583
10832
|
}
|
|
10584
10833
|
emit(
|
|
10585
|
-
{ manifest: loaded.manifestPath, reconciliation: result },
|
|
10834
|
+
{ manifest: loaded.manifestPath, source, reconciliation: result },
|
|
10586
10835
|
outputMode,
|
|
10587
10836
|
() => formatApplicationReconcile(result, loaded.manifestPath)
|
|
10588
10837
|
);
|
|
10589
|
-
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;
|
|
10590
10839
|
} catch (error) {
|
|
10591
10840
|
fail(error, outputMode);
|
|
10592
10841
|
}
|
|
@@ -10624,6 +10873,7 @@ function registerAppsCommand(program2) {
|
|
|
10624
10873
|
}
|
|
10625
10874
|
});
|
|
10626
10875
|
registerApplicationReconcileCommand(apps, "third_party");
|
|
10876
|
+
registerApplicationCapabilityCommands(apps, "third_party");
|
|
10627
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) => {
|
|
10628
10878
|
const mode = resolveOutputMode(opts.json);
|
|
10629
10879
|
try {
|
|
@@ -10693,7 +10943,7 @@ function registerAppsCommand(program2) {
|
|
|
10693
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(
|
|
10694
10944
|
"--surface-model <surfaceModel>",
|
|
10695
10945
|
"Developer app surface model: web_app, mobile_container, mobile_module, or messaging_surface"
|
|
10696
|
-
).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(
|
|
10697
10947
|
"--provision",
|
|
10698
10948
|
"Register the app in the current Topolo developer workspace using the authenticated session"
|
|
10699
10949
|
).action(async (application, opts) => {
|
|
@@ -10712,7 +10962,8 @@ function registerAppsCommand(program2) {
|
|
|
10712
10962
|
audience: resolveScaffoldAudience(opts.audience),
|
|
10713
10963
|
tenancy: resolveScaffoldTenancy(opts.tenancy),
|
|
10714
10964
|
surfaceModel: resolveScaffoldSurfaceModel(opts.surfaceModel),
|
|
10715
|
-
containerAppId: opts.containerAppId
|
|
10965
|
+
containerAppId: opts.containerAppId,
|
|
10966
|
+
capabilities: opts.capability
|
|
10716
10967
|
});
|
|
10717
10968
|
const writeResult = opts.write ? await writeApplicationScaffold(payload, { repoRoot: opts.repoRoot, force: opts.force }) : null;
|
|
10718
10969
|
const provisionResult = opts.provision ? await provisionApplicationScaffold(payload) : null;
|
|
@@ -10796,8 +11047,8 @@ function applicationAuditPayload(application, options = {}) {
|
|
|
10796
11047
|
}
|
|
10797
11048
|
function recordApplicationEvidence(application, options = {}) {
|
|
10798
11049
|
const appId = resolveApplication(application).id;
|
|
10799
|
-
const root =
|
|
10800
|
-
if (!
|
|
11050
|
+
const root = path5.resolve(options.appRoot ?? process.cwd());
|
|
11051
|
+
if (!existsSync4(root)) {
|
|
10801
11052
|
throw new Error(`Application root does not exist: ${root}`);
|
|
10802
11053
|
}
|
|
10803
11054
|
let commit = options.commit;
|
|
@@ -10808,12 +11059,12 @@ function recordApplicationEvidence(application, options = {}) {
|
|
|
10808
11059
|
commit = void 0;
|
|
10809
11060
|
}
|
|
10810
11061
|
}
|
|
10811
|
-
const evidenceDir =
|
|
10812
|
-
const evidencePath =
|
|
11062
|
+
const evidenceDir = path5.join(root, ".topolo");
|
|
11063
|
+
const evidencePath = path5.join(evidenceDir, "evidence.json");
|
|
10813
11064
|
let before = null;
|
|
10814
|
-
if (
|
|
11065
|
+
if (existsSync4(evidencePath)) {
|
|
10815
11066
|
try {
|
|
10816
|
-
const raw = JSON.parse(
|
|
11067
|
+
const raw = JSON.parse(readFileSync5(evidencePath, "utf8"));
|
|
10817
11068
|
if (raw && typeof raw === "object" && raw.version === 1) {
|
|
10818
11069
|
before = raw;
|
|
10819
11070
|
}
|
|
@@ -10851,8 +11102,8 @@ function recordApplicationEvidence(application, options = {}) {
|
|
|
10851
11102
|
"record-evidence: pass at least one of --build, --typecheck, --test, --docs-validate, --smoke <url>"
|
|
10852
11103
|
);
|
|
10853
11104
|
}
|
|
10854
|
-
|
|
10855
|
-
|
|
11105
|
+
mkdirSync2(evidenceDir, { recursive: true });
|
|
11106
|
+
writeFileSync2(evidencePath, JSON.stringify(after, null, 2) + "\n");
|
|
10856
11107
|
return { appId, evidencePath, before, after, updatedFields };
|
|
10857
11108
|
}
|
|
10858
11109
|
function formatEvidence(result) {
|
|
@@ -11108,18 +11359,182 @@ function statusesAtOrWorse(threshold) {
|
|
|
11108
11359
|
}
|
|
11109
11360
|
|
|
11110
11361
|
// src/commands/platform.ts
|
|
11111
|
-
import { readdirSync as readdirSync2, readFileSync as
|
|
11112
|
-
import
|
|
11362
|
+
import { readdirSync as readdirSync2, readFileSync as readFileSync6, statSync as statSync2 } from "fs";
|
|
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
|
|
11113
11526
|
function registerPlatformCommand(program2) {
|
|
11114
11527
|
const platform3 = program2.command("platform").description("Internal Topolo first-party platform and personal app commands");
|
|
11115
11528
|
const platformApps = platform3.command("apps").description("Scaffold and operate first-party Topolo platform and personal applications");
|
|
11116
11529
|
registerApplicationReconcileCommand(platformApps, "topolo_first_party");
|
|
11530
|
+
registerApplicationCapabilityCommands(platformApps, "topolo_first_party");
|
|
11531
|
+
registerApplicationResourceProvisionCommand(platformApps);
|
|
11117
11532
|
platformApps.command("scaffold <application>").description(
|
|
11118
11533
|
"Generate a first-party Topolo scaffold for the platform or personal portfolio."
|
|
11119
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(
|
|
11120
11535
|
"--surface-model <surfaceModel>",
|
|
11121
11536
|
"First-party surface model: web_app, mobile_container, mobile_module, or messaging_surface"
|
|
11122
|
-
).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(
|
|
11123
11538
|
"--provision",
|
|
11124
11539
|
"Provision the generated first-party app into Topolo Developers and Auth using the authenticated Topolo admin session"
|
|
11125
11540
|
).option(
|
|
@@ -11150,11 +11565,7 @@ function registerPlatformCommand(program2) {
|
|
|
11150
11565
|
surfaceModel: resolvePlatformSurfaceModel(opts.surfaceModel),
|
|
11151
11566
|
containerAppId: opts.containerAppId,
|
|
11152
11567
|
profile: opts.profile,
|
|
11153
|
-
|
|
11154
|
-
withR2: opts.withR2,
|
|
11155
|
-
withQueue: opts.withQueue,
|
|
11156
|
-
withWorkflow: opts.withWorkflow,
|
|
11157
|
-
withContainerProcessing: opts.withContainerProcessing,
|
|
11568
|
+
capabilities: opts.capability,
|
|
11158
11569
|
homePath: opts.homePath,
|
|
11159
11570
|
homeLabel: opts.homeLabel,
|
|
11160
11571
|
navigationMode: opts.navigationMode,
|
|
@@ -11227,12 +11638,12 @@ var SKIPPED_SCAN_DIRS = /* @__PURE__ */ new Set([
|
|
|
11227
11638
|
"output"
|
|
11228
11639
|
]);
|
|
11229
11640
|
function scanStagingProductionUrls(rootInput, allowPatterns) {
|
|
11230
|
-
const root =
|
|
11641
|
+
const root = path7.resolve(rootInput);
|
|
11231
11642
|
const allowlist = allowPatterns.map((pattern) => new RegExp(pattern));
|
|
11232
11643
|
const violations = [];
|
|
11233
11644
|
function shouldSkipFile(filePath) {
|
|
11234
|
-
const basename =
|
|
11235
|
-
const
|
|
11645
|
+
const basename = path7.basename(filePath);
|
|
11646
|
+
const relativePath2 = path7.relative(root, filePath);
|
|
11236
11647
|
if (basename === "package-lock.json" || basename === "pnpm-lock.yaml" || basename === "yarn.lock") return true;
|
|
11237
11648
|
if (basename === "topolo.cloudcontrol.json") return true;
|
|
11238
11649
|
if (basename === "wrangler.toml" || basename === "wrangler.jsonc") return true;
|
|
@@ -11241,29 +11652,29 @@ function scanStagingProductionUrls(rootInput, allowPatterns) {
|
|
|
11241
11652
|
if (basename.endsWith(".generated.ts")) return true;
|
|
11242
11653
|
if (basename.endsWith(".test.ts") || basename.endsWith(".test.tsx") || basename.endsWith(".spec.ts")) return true;
|
|
11243
11654
|
if (basename === ".env.production") return true;
|
|
11244
|
-
if (
|
|
11245
|
-
if (
|
|
11246
|
-
if (
|
|
11247
|
-
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));
|
|
11248
11659
|
}
|
|
11249
11660
|
function scanFile(filePath) {
|
|
11250
11661
|
if (shouldSkipFile(filePath)) return;
|
|
11251
|
-
const
|
|
11252
|
-
const content =
|
|
11662
|
+
const relativePath2 = path7.relative(root, filePath);
|
|
11663
|
+
const content = readFileSync6(filePath, "utf8");
|
|
11253
11664
|
const lines = content.split(/\r?\n/);
|
|
11254
11665
|
lines.forEach((line, index) => {
|
|
11255
11666
|
const matches = [...line.matchAll(PRODUCTION_TOPOLO_URL_PATTERN)].map((match) => match[0]);
|
|
11256
11667
|
for (const url of new Set(matches)) {
|
|
11257
|
-
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}`));
|
|
11258
11669
|
if (!allowed) {
|
|
11259
|
-
violations.push({ file:
|
|
11670
|
+
violations.push({ file: relativePath2, line: index + 1, url, text: line.trim() });
|
|
11260
11671
|
}
|
|
11261
11672
|
}
|
|
11262
11673
|
});
|
|
11263
11674
|
}
|
|
11264
11675
|
function walk(dir) {
|
|
11265
11676
|
for (const entry of readdirSync2(dir, { withFileTypes: true })) {
|
|
11266
|
-
const fullPath =
|
|
11677
|
+
const fullPath = path7.join(dir, entry.name);
|
|
11267
11678
|
if (entry.isDirectory()) {
|
|
11268
11679
|
if (!SKIPPED_SCAN_DIRS.has(entry.name)) walk(fullPath);
|
|
11269
11680
|
continue;
|
|
@@ -11431,7 +11842,7 @@ var DEFAULT_SEED_BASE_URL = "https://seed.stg.topolo.us";
|
|
|
11431
11842
|
function seedBaseUrl() {
|
|
11432
11843
|
return process.env.TOPOLO_SEED_URL || DEFAULT_SEED_BASE_URL;
|
|
11433
11844
|
}
|
|
11434
|
-
async function authedFetch(
|
|
11845
|
+
async function authedFetch(path8, init = {}) {
|
|
11435
11846
|
const credential = await refreshCredentialIfNeeded("staging");
|
|
11436
11847
|
if (!credential) {
|
|
11437
11848
|
throw new Error("No staging credential configured. Run `topolo auth login --env staging` or set TOPOLO_STAGING_API_KEY.");
|
|
@@ -11441,7 +11852,7 @@ async function authedFetch(path5, init = {}) {
|
|
|
11441
11852
|
else headers.set("Authorization", `Bearer ${credential.accessToken}`);
|
|
11442
11853
|
headers.set("X-Topolo-Client", `topolo-cli/${CLI_VERSION}`);
|
|
11443
11854
|
if (init.body && !headers.has("Content-Type")) headers.set("Content-Type", "application/json");
|
|
11444
|
-
const response = await fetch(`${seedBaseUrl()}${
|
|
11855
|
+
const response = await fetch(`${seedBaseUrl()}${path8}`, { ...init, headers });
|
|
11445
11856
|
const text = await response.text();
|
|
11446
11857
|
let body;
|
|
11447
11858
|
try {
|
|
@@ -11718,7 +12129,7 @@ function createOAuthClientPayload(options) {
|
|
|
11718
12129
|
logo_url: options.logoUrl
|
|
11719
12130
|
};
|
|
11720
12131
|
}
|
|
11721
|
-
async function authFetchJson(
|
|
12132
|
+
async function authFetchJson(path8, init = {}) {
|
|
11722
12133
|
const environment = selectedEnvironment();
|
|
11723
12134
|
const credential = await refreshCredentialIfNeeded(environment);
|
|
11724
12135
|
if (!credential) {
|
|
@@ -11740,7 +12151,7 @@ async function authFetchJson(path5, init = {}) {
|
|
|
11740
12151
|
throw err;
|
|
11741
12152
|
}
|
|
11742
12153
|
headers.set("Authorization", `Bearer ${credential.accessToken}`);
|
|
11743
|
-
const response = await fetch(`${authBaseUrl}${
|
|
12154
|
+
const response = await fetch(`${authBaseUrl}${path8}`, { ...init, headers });
|
|
11744
12155
|
const text = await response.text();
|
|
11745
12156
|
const payload = text ? JSON.parse(text) : {};
|
|
11746
12157
|
if (!response.ok || payload.success === false) {
|
|
@@ -11756,7 +12167,7 @@ function normalizeList(values, fallback = []) {
|
|
|
11756
12167
|
}
|
|
11757
12168
|
|
|
11758
12169
|
// src/commands/integrations.ts
|
|
11759
|
-
import { readdir, readFile as
|
|
12170
|
+
import { readdir, readFile as readFile6, stat } from "fs/promises";
|
|
11760
12171
|
import { join as join4, relative as relative3 } from "path";
|
|
11761
12172
|
function registerIntegrationsCommand(program2) {
|
|
11762
12173
|
const integrations = program2.command("integrations").description("Generate and validate Topolo integration handoffs.");
|
|
@@ -11900,7 +12311,7 @@ function detectFramework(packageJson2, haystack) {
|
|
|
11900
12311
|
}
|
|
11901
12312
|
async function readPackageJson2(repoRoot) {
|
|
11902
12313
|
try {
|
|
11903
|
-
return JSON.parse(await
|
|
12314
|
+
return JSON.parse(await readFile6(join4(repoRoot, "package.json"), "utf8"));
|
|
11904
12315
|
} catch {
|
|
11905
12316
|
return null;
|
|
11906
12317
|
}
|
|
@@ -11923,7 +12334,7 @@ async function collectFiles(repoRoot) {
|
|
|
11923
12334
|
if (info.size > 25e4) continue;
|
|
11924
12335
|
result.push({
|
|
11925
12336
|
path: relative3(root, fullPath),
|
|
11926
|
-
content: await
|
|
12337
|
+
content: await readFile6(fullPath, "utf8")
|
|
11927
12338
|
});
|
|
11928
12339
|
}
|
|
11929
12340
|
}
|
|
@@ -11939,9 +12350,9 @@ function formatCheckResult(result) {
|
|
|
11939
12350
|
}
|
|
11940
12351
|
|
|
11941
12352
|
// src/commands/skills.ts
|
|
11942
|
-
import { spawnSync } from "child_process";
|
|
11943
|
-
import { existsSync as
|
|
11944
|
-
import { mkdir as
|
|
12353
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
12354
|
+
import { existsSync as existsSync5 } from "fs";
|
|
12355
|
+
import { mkdir as mkdir4, copyFile, readFile as readFile7, writeFile as writeFile7, rm } from "fs/promises";
|
|
11945
12356
|
import { homedir as homedir2 } from "os";
|
|
11946
12357
|
import { dirname as dirname4, join as join5, resolve as resolve3 } from "path";
|
|
11947
12358
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
@@ -11952,19 +12363,19 @@ function skillsSourceDir() {
|
|
|
11952
12363
|
const distCandidate = resolve3(here, "../skills");
|
|
11953
12364
|
const srcCandidate = resolve3(here, "../../skills");
|
|
11954
12365
|
for (const candidate of [distCandidate, srcCandidate]) {
|
|
11955
|
-
if (
|
|
12366
|
+
if (existsSync5(join5(candidate, "topolo", "SKILL.md"))) return candidate;
|
|
11956
12367
|
}
|
|
11957
12368
|
return distCandidate;
|
|
11958
12369
|
}
|
|
11959
12370
|
function resolveMcpCommand() {
|
|
11960
|
-
const probe =
|
|
12371
|
+
const probe = spawnSync2(process.platform === "win32" ? "where" : "which", ["topolo-mcp"], {
|
|
11961
12372
|
stdio: "ignore"
|
|
11962
12373
|
});
|
|
11963
12374
|
if (probe.status === 0) return { command: "topolo-mcp", args: [] };
|
|
11964
12375
|
return { command: "npx", args: ["-y", "@topolo/mcp"] };
|
|
11965
12376
|
}
|
|
11966
12377
|
function agentBinaryExists(bin) {
|
|
11967
|
-
const probe =
|
|
12378
|
+
const probe = spawnSync2(process.platform === "win32" ? "where" : "which", [bin], {
|
|
11968
12379
|
stdio: "ignore"
|
|
11969
12380
|
});
|
|
11970
12381
|
return probe.status === 0;
|
|
@@ -11977,7 +12388,7 @@ function codexSkillDir() {
|
|
|
11977
12388
|
return join5(homedir2(), ".codex", "skills", "topolo");
|
|
11978
12389
|
}
|
|
11979
12390
|
async function installSkillFile(sourceDir, targetDir) {
|
|
11980
|
-
await
|
|
12391
|
+
await mkdir4(targetDir, { recursive: true });
|
|
11981
12392
|
const target = join5(targetDir, "SKILL.md");
|
|
11982
12393
|
await copyFile(join5(sourceDir, "topolo", "SKILL.md"), target);
|
|
11983
12394
|
return target;
|
|
@@ -11985,9 +12396,9 @@ async function installSkillFile(sourceDir, targetDir) {
|
|
|
11985
12396
|
function registerViaOfficialCli(cli, mcp, agentName) {
|
|
11986
12397
|
if (!agentBinaryExists(cli)) return false;
|
|
11987
12398
|
const scopeArgs = cli === "claude" ? ["-s", "user"] : [];
|
|
11988
|
-
|
|
12399
|
+
spawnSync2(cli, ["mcp", "remove", "topolo", ...scopeArgs], { stdio: "ignore" });
|
|
11989
12400
|
const envArgs = cli === "claude" ? ["-e", `TOPOLO_AGENT_NAME=${agentName}`] : ["--env", `TOPOLO_AGENT_NAME=${agentName}`];
|
|
11990
|
-
const result =
|
|
12401
|
+
const result = spawnSync2(
|
|
11991
12402
|
cli,
|
|
11992
12403
|
["mcp", "add", "topolo", ...scopeArgs, ...envArgs, "--", mcp.command, ...mcp.args],
|
|
11993
12404
|
{ stdio: "ignore" }
|
|
@@ -11995,11 +12406,11 @@ function registerViaOfficialCli(cli, mcp, agentName) {
|
|
|
11995
12406
|
return result.status === 0;
|
|
11996
12407
|
}
|
|
11997
12408
|
async function registerClaudeViaConfig(mcp) {
|
|
11998
|
-
const
|
|
12409
|
+
const path8 = join5(homedir2(), ".claude.json");
|
|
11999
12410
|
let config = {};
|
|
12000
|
-
if (
|
|
12411
|
+
if (existsSync5(path8)) {
|
|
12001
12412
|
try {
|
|
12002
|
-
config = JSON.parse(await
|
|
12413
|
+
config = JSON.parse(await readFile7(path8, "utf8"));
|
|
12003
12414
|
} catch {
|
|
12004
12415
|
config = {};
|
|
12005
12416
|
}
|
|
@@ -12012,13 +12423,13 @@ async function registerClaudeViaConfig(mcp) {
|
|
|
12012
12423
|
env: { TOPOLO_AGENT_NAME: AGENT_NAME_FOR.claude }
|
|
12013
12424
|
};
|
|
12014
12425
|
config.mcpServers = servers;
|
|
12015
|
-
await
|
|
12426
|
+
await writeFile7(path8, `${JSON.stringify(config, null, 2)}
|
|
12016
12427
|
`, "utf8");
|
|
12017
12428
|
}
|
|
12018
12429
|
async function registerCodexViaConfig(mcp) {
|
|
12019
|
-
const
|
|
12430
|
+
const path8 = join5(homedir2(), ".codex", "config.toml");
|
|
12020
12431
|
let text = "";
|
|
12021
|
-
if (
|
|
12432
|
+
if (existsSync5(path8)) text = await readFile7(path8, "utf8");
|
|
12022
12433
|
text = text.replace(
|
|
12023
12434
|
/(^|\n)\[mcp_servers\.topolo(?:\.[a-z]+)?\][^\[]*/g,
|
|
12024
12435
|
(match) => match.startsWith("\n") ? "\n" : ""
|
|
@@ -12034,8 +12445,8 @@ command = "${mcp.command}"${argsToml}
|
|
|
12034
12445
|
[mcp_servers.topolo.env]
|
|
12035
12446
|
TOPOLO_AGENT_NAME = "${AGENT_NAME_FOR.codex}"
|
|
12036
12447
|
`;
|
|
12037
|
-
await
|
|
12038
|
-
await
|
|
12448
|
+
await mkdir4(dirname4(path8), { recursive: true });
|
|
12449
|
+
await writeFile7(path8, `${text}${block}`, "utf8");
|
|
12039
12450
|
}
|
|
12040
12451
|
async function installForAgent(agent, sourceDir, mcp, opts) {
|
|
12041
12452
|
const skillDir = agent === "claude" ? claudeSkillDir(opts.projectDir) : codexSkillDir();
|
|
@@ -12073,7 +12484,7 @@ function registerSkillsCommand(program2) {
|
|
|
12073
12484
|
const mode = resolveOutputMode(opts.json);
|
|
12074
12485
|
try {
|
|
12075
12486
|
const sourceDir = skillsSourceDir();
|
|
12076
|
-
if (!
|
|
12487
|
+
if (!existsSync5(join5(sourceDir, "topolo", "SKILL.md"))) {
|
|
12077
12488
|
throw new Error(
|
|
12078
12489
|
"Bundled skill file not found. Reinstall the CLI: npm install -g @topolo/cli"
|
|
12079
12490
|
);
|
|
@@ -12102,7 +12513,7 @@ function registerSkillsCommand(program2) {
|
|
|
12102
12513
|
const skillDir = agent === "claude" ? claudeSkillDir() : codexSkillDir();
|
|
12103
12514
|
return {
|
|
12104
12515
|
agent,
|
|
12105
|
-
skillInstalled:
|
|
12516
|
+
skillInstalled: existsSync5(join5(skillDir, "SKILL.md")),
|
|
12106
12517
|
skillPath: join5(skillDir, "SKILL.md"),
|
|
12107
12518
|
cliAvailable: agentBinaryExists(agent)
|
|
12108
12519
|
};
|
|
@@ -12119,10 +12530,10 @@ function registerSkillsCommand(program2) {
|
|
|
12119
12530
|
for (const agent of resolveTargets(opts)) {
|
|
12120
12531
|
const skillDir = agent === "claude" ? claudeSkillDir() : codexSkillDir();
|
|
12121
12532
|
const skillFile = join5(skillDir, "SKILL.md");
|
|
12122
|
-
const skillRemoved =
|
|
12533
|
+
const skillRemoved = existsSync5(skillFile);
|
|
12123
12534
|
if (skillRemoved) await rm(skillFile, { force: true });
|
|
12124
12535
|
if (agentBinaryExists(agent)) {
|
|
12125
|
-
|
|
12536
|
+
spawnSync2(agent, ["mcp", "remove", "topolo"], { stdio: "ignore" });
|
|
12126
12537
|
}
|
|
12127
12538
|
removed.push({ agent, skillRemoved });
|
|
12128
12539
|
}
|