@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/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
- // ../topolo-sdk/dist/index.js
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, path5, status, body, message) {
65
- super("http_error", message ?? `HTTP ${status} from ${service}${path5}`);
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 = path5;
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, path5, root, depth) {
2182
- if (depth > 128) return { path: path5, code: "schema_depth", message: "Schema recursion exceeded 128 levels." };
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: path5, code: "unsupported_schema_keyword", message: `Action schema uses unsupported keyword ${unsupportedKeyword}.` };
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: path5, code: "unsupported_schema_format", message: `Action schema uses unsupported format ${schema.format}.` };
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: path5, code: "unsupported_schema_ref", message: `Cannot resolve schema reference ${schema.$ref}.` };
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, `${path5}.${key}`, root, depth + 1);
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, path5, root, errors, depth) {
2215
+ function validateNode(schema, value, path8, root, errors, depth) {
2216
2216
  if (depth > 128) {
2217
- errors.push({ path: path5, code: "schema_depth", message: "Schema recursion exceeded 128 levels." });
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: path5, code: "unsupported_schema_ref", message: `Cannot resolve schema reference ${schema.$ref}.` });
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, path5, root, errors, depth + 1);
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, path5, root, errors, depth + 1);
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: path5, code: "any_of", message: "Value does not match any allowed schema." });
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: path5, code: "one_of", message: "Value must match exactly one allowed schema." });
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: path5, code: "const", message: `Value must equal ${JSON.stringify(schema.const)}.` });
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: path5, code: "enum", message: `Value must be one of ${schema.enum.map((item) => JSON.stringify(item)).join(", ")}.` });
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: path5, code: "type", message: `Expected ${String(schema.type)}.` });
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, path5, errors);
2258
- if (typeof value === "number") validateNumber(schema, value, path5, errors);
2259
- if (Array.isArray(value)) validateArray(schema, value, path5, root, errors, depth);
2260
- if (isRecord(value)) validateObject(schema, value, path5, root, errors, depth);
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, path5, root, errors, depth) {
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: path5, code: "min_properties", message: `Expected at least ${schema.minProperties} properties.` });
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: path5, code: "max_properties", message: `Expected at most ${schema.maxProperties} properties.` });
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: `${path5}.${key}`, code: "required", message: "Required property is missing." });
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, `${path5}.${key}`, root, errors, depth + 1);
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, `${path5}.${key}`, root, errors, depth + 1);
2284
+ validateNode(childSchema, child, `${path8}.${key}`, root, errors, depth + 1);
2285
2285
  } else if (schema.additionalProperties === false) {
2286
- errors.push({ path: `${path5}.${key}`, code: "additional_property", message: "Property is not declared by the action schema." });
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, `${path5}.${key}`, root, errors, depth + 1);
2288
+ validateNode(schema.additionalProperties, child, `${path8}.${key}`, root, errors, depth + 1);
2289
2289
  }
2290
2290
  }
2291
2291
  }
2292
- function validateArray(schema, value, path5, root, errors, depth) {
2292
+ function validateArray(schema, value, path8, root, errors, depth) {
2293
2293
  if (typeof schema.minItems === "number" && value.length < schema.minItems) {
2294
- errors.push({ path: path5, code: "min_items", message: `Expected at least ${schema.minItems} items.` });
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: path5, code: "max_items", message: `Expected at most ${schema.maxItems} items.` });
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 unique2 = new Set(value.map((item) => JSON.stringify(item)));
2301
- if (unique2.size !== value.length) errors.push({ path: path5, code: "unique_items", message: "Expected every item to be unique." });
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, `${path5}[${index}]`, root, errors, depth + 1));
2304
+ value.forEach((item, index) => validateNode(schema.items, item, `${path8}[${index}]`, root, errors, depth + 1));
2305
2305
  }
2306
2306
  }
2307
- function validateString(schema, value, path5, errors) {
2307
+ function validateString(schema, value, path8, errors) {
2308
2308
  if (typeof schema.minLength === "number" && value.length < schema.minLength) {
2309
- errors.push({ path: path5, code: "min_length", message: `Expected at least ${schema.minLength} characters.` });
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: path5, code: "max_length", message: `Expected at most ${schema.maxLength} characters.` });
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: path5, code: "pattern", message: "Value does not match the required pattern." });
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: path5, code: "invalid_schema_pattern", message: "Action schema contains an invalid pattern." });
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: path5, code: "format_uri", message: "Expected an absolute URL." });
2322
- if (schema.format === "email" && !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(value)) errors.push({ path: path5, code: "format_email", message: "Expected an email address." });
2323
- if (schema.format === "date-time" && !isDateTime(value)) errors.push({ path: path5, code: "format_date_time", message: "Expected an ISO-8601 date-time." });
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, path5, errors) {
2326
- if (typeof schema.minimum === "number" && value < schema.minimum) errors.push({ path: path5, code: "minimum", message: `Expected a value of at least ${schema.minimum}.` });
2327
- if (typeof schema.maximum === "number" && value > schema.maximum) errors.push({ path: path5, code: "maximum", message: `Expected a value of at most ${schema.maximum}.` });
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: path5, code: "exclusive_minimum", message: `Expected a value greater than ${exclusiveMinimum}.` });
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: path5, code: "exclusive_maximum", message: `Expected a value less than ${exclusiveMaximum}.` });
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: path5, code: "multiple_of", message: `Expected a multiple of ${schema.multipleOf}.` });
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 record2 = raw;
2849
- if (record2.algorithm !== "sha256-v1" || typeof record2.digest !== "string" || !/^[a-f0-9]{64}$/.test(record2.digest) || !Number.isSafeInteger(record2.actionCount) || Number(record2.actionCount) < 0) {
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: record2.digest,
2855
- actionCount: Number(record2.actionCount)
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 path5 = pickActionString(a.path);
3231
- if (!actionId || !name || !appId || !method || !path5 || !isHttpMethod(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: path5,
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 path5 = action.path.replace(/\{([a-zA-Z0-9_]+)\}/g, (_match, key) => {
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: path5,
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: path5,
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(path5, body) {
3477
- const url = new URL(path5, this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`);
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", path5, res.status, parsed);
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
- // ../topolo-sdk/dist/node/credential-store.js
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, path5, status, body, message) {
3532
- super("http_error", message ?? `HTTP ${status} from ${service}${path5}`);
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 = path5;
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(path5, body) {
3669
- const url = new URL(path5, this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`);
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", path5, res.status, parsed);
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 path5 = configPath();
3716
- if (!existsSync(path5)) return {};
3715
+ const path8 = configPath();
3716
+ if (!existsSync(path8)) return {};
3717
3717
  try {
3718
- const raw = await readFile(path5, "utf8");
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 path5 = configPath();
3726
- await mkdir(dirname(path5), { recursive: true });
3727
- await writeFile(path5, JSON.stringify(config, null, 2) + "\n", "utf8");
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(path5, 384);
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 path5 of candidates) {
4050
+ for (const path8 of candidates) {
4051
4051
  try {
4052
- const pkg = JSON.parse(readFileSync(path5, "utf8"));
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, path5, opts) => {
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: path5,
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 path5 = value.slice(1);
4615
- if (!path5) throw new Error("--data @file path must not be empty");
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(path5, "utf8");
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 ${path5}: ${detail}`);
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 existsSync3, mkdirSync, readFileSync as readFileSync4, writeFileSync } from "fs";
4695
- import path3 from "path";
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((record2) => {
5287
- const scripts = record2.packageJson.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 record2 of evidence.packageJsons) {
5554
- const scripts = record2.packageJson.scripts ?? {};
5555
- const isRootPackage = record2.path === evidence.packageJsonPath;
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(record2)}:${canonical}`);
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((record2) => [
5569
- ...Object.keys(record2.packageJson.dependencies ?? {}),
5570
- ...Object.keys(record2.packageJson.devDependencies ?? {}),
5571
- ...Object.keys(record2.packageJson.peerDependencies ?? {})
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((record2) => {
5577
+ evidence.packageJsons.flatMap((record4) => {
5578
5578
  const specs = [
5579
- record2.packageJson.dependencies?.[packageName],
5580
- record2.packageJson.devDependencies?.[packageName],
5581
- record2.packageJson.peerDependencies?.[packageName]
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
- function applicationSourcePath(appDir) {
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
- if (appDir === "TopoloDocs") return "system-apps/TopoloDocs";
5635
- if (appDir === "TopoloAuth") return "topolo-platform/core/TopoloAuth";
5636
- if (appDir === "TopoloCloudControl") return "topolo-platform/core/TopoloCloudControl";
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(record2) {
5708
- return record2.packageJson.name ?? relativeTopoloPath(path.dirname(record2.path));
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 path5 = String(value || "/dashboard").trim();
5798
- if (!path5.startsWith("/") || path5.startsWith("//") || /[?#]/.test(path5)) {
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(path5)) {
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 path5.replace(/\/$/, "") || "/dashboard";
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(path5) {
5943
+ async function pathExists(path8) {
5946
5944
  try {
5947
- await access(path5, constants.F_OK);
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 relativePath = relative(root, resolved);
5956
- if (relativePath.startsWith("..") || relativePath === "" || relativePath.startsWith("/")) {
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" && hasScaffoldResources(options.resources)) {
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
- " const defaultAuthUrl =",
6484
- " typeof window !== 'undefined' && window.location.hostname.endsWith('.stg.topolo.us')",
6485
- " ? 'https://auth.stg.topolo.us'",
6486
- " : 'https://auth.topolo.app';",
6487
- " return (import.meta.env.VITE_TOPOLO_AUTH_URL || defaultAuthUrl).replace(/\\/+$/, '');",
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
- const AUTH_URL = import.meta.env.VITE_TOPOLO_AUTH_URL ?? import.meta.env.VITE_TOPOLO_AUTH_BASE_URL ?? 'https://auth.topolo.app';
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, unknown> {`,
7711
- " async run(_event: Readonly<WorkflowEvent<unknown>>, step: WorkflowStep): Promise<unknown> {",
7712
- " return step.do('scaffold no-op', async () => ({ status: 'pending_implementation' }));",
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
- " authGateway: { enabled: false },",
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 || 'https://auth.topolo.app';",
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
- bindings.queues = [
8139
- {
8140
- binding: resource.queueBinding,
8141
- staging: { queueName: resource.queueStagingName },
8142
- production: { queueName: resource.queueProductionName },
8143
- consumer: true
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
- NEXUS_APP_ID: slug
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: ["TOPOLO_SERVICE_CLIENT_SECRET"],
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
- targets.web = {
8180
- kind: "worker",
8181
- dir: "apps/web",
8182
- wranglerName: `topolo-${slug}`,
8183
- subdomain: slug,
8184
- main: "src/worker.ts",
8185
- workersDev: false,
8186
- assets: {
8187
- directory: "./dist",
8188
- binding: "ASSETS",
8189
- notFoundHandling: "single-page-application",
8190
- runWorkerFirst: ["/api/*"]
8191
- },
8192
- compatibilityFlags: ["nodejs_compat"],
8193
- buildCommand: {
8194
- staging: "cd ../.. && DEPLOY_ENVIRONMENT=staging VITE_TOPOLO_AUTH_URL=https://auth.stg.topolo.us pnpm run build",
8195
- production: "cd ../.. && DEPLOY_ENVIRONMENT=production VITE_TOPOLO_AUTH_URL=https://auth.topolo.app pnpm run build"
8196
- },
8197
- healthcheckPath: options.api ? "/api/health" : "/",
8198
- ...options.api ? {
8199
- bindings: {
8200
- services: [
8201
- {
8202
- binding: "API",
8203
- service: {
8204
- staging: `topolo-${slug}-api-staging`,
8205
- production: `topolo-${slug}-api-production`
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: options.api ? ["openid", "profile", "email", "organization:read", "entitlements:read", `${application.id}:read`] : ["openid", "profile", "email", "organization:read"]
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 = options.api ? ["openid", "profile", "email", "organization:read", "entitlements:read", `${application.id}:read`] : ["openid", "profile", "email", "organization:read"];
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
- supported_v1: [
8952
- "login",
8953
- "launcher",
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 = scaffoldCapabilities({ browser, api, distributionModel });
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: scaffoldPackages({ browser, api, target }),
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 topolo.cloudcontrol.json or topolo.app.json",
9907
+ "bindings match the declared resources",
9957
9908
  "secrets are referenced by name only"
9958
9909
  ],
9959
- content: target === "platform" ? platformApiWranglerToml(application, options.payload.resources, options.payload.profile) : wranglerToml(application, target, options.payload.resources, options.payload.profile)
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: payload.application.appId ? ["openid", "profile", "email", "organization:read", "entitlements:read", `${payload.application.id}:read`] : ["openid", "profile", "email", "organization:read"],
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 json3;
10220
+ let json2;
10254
10221
  try {
10255
- json3 = JSON.parse(raw);
10222
+ json2 = JSON.parse(raw);
10256
10223
  } catch {
10257
10224
  return { changed: false, previous: null };
10258
10225
  }
10259
- const previous = typeof json3.appId === "string" ? json3.appId : null;
10226
+ const previous = typeof json2.appId === "string" ? json2.appId : null;
10260
10227
  if (previous === appId) {
10261
10228
  return { changed: false, previous };
10262
10229
  }
10263
- json3.appId = appId;
10264
- await writeFile3(manifestPath, `${JSON.stringify(json3, null, 2)}
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 readFile3, writeFile as writeFile4 } from "fs/promises";
10274
- import path2 from "path";
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 = record(value);
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 = path2.resolve(options.manifestPath || "topolo.app.json");
10334
- const manifest = JSON.parse(await readFile3(manifestPath, "utf8"));
10335
- const existing = record(manifest.registration) || {};
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: record(existing.pricing) || { type: "free", monthlyPrice: null },
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: record(existing.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: record(existing.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 writeFile4(manifestPath, `${JSON.stringify(manifest, null, 2)}
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 = path2.resolve(options.manifestPath || "topolo.app.json");
10414
- const raw = await readFile3(manifestPath, "utf8");
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 = path3.resolve(options.appRoot ?? process.cwd());
10800
- if (!existsSync3(root)) {
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 = path3.join(root, ".topolo");
10812
- const evidencePath = path3.join(evidenceDir, "evidence.json");
11062
+ const evidenceDir = path5.join(root, ".topolo");
11063
+ const evidencePath = path5.join(evidenceDir, "evidence.json");
10813
11064
  let before = null;
10814
- if (existsSync3(evidencePath)) {
11065
+ if (existsSync4(evidencePath)) {
10815
11066
  try {
10816
- const raw = JSON.parse(readFileSync4(evidencePath, "utf8"));
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
- mkdirSync(evidenceDir, { recursive: true });
10855
- writeFileSync(evidencePath, JSON.stringify(after, null, 2) + "\n");
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 readFileSync5, statSync as statSync2 } from "fs";
11112
- import path4 from "path";
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("--with-d1", "Declare a D1 binding in the first-party scaffold").option("--with-r2", "Declare an R2 binding in the first-party scaffold").option("--with-queue", "Declare a Queue binding in the first-party scaffold").option("--with-workflow", "Declare a Workflow binding in the first-party scaffold").option("--with-container-processing", "Declare container-processing metadata in CloudControl").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(
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
- withD1: opts.withD1,
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 = path4.resolve(rootInput);
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 = path4.basename(filePath);
11235
- const relativePath = path4.relative(root, filePath);
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 (relativePath === path4.join("src", "environment.ts") || relativePath === path4.join("src", "environmentUrls.ts") || relativePath.endsWith(path4.join("src", "environment.ts")) || relativePath.endsWith(path4.join("src", "lib", "environment.ts")) || relativePath === path4.join("packages", "topolo-ui-kit", "src", "environmentUrls.ts")) return true;
11245
- if (relativePath.includes(`${path4.sep}test${path4.sep}`) || relativePath.includes(`${path4.sep}tests${path4.sep}`) || relativePath.includes(`${path4.sep}e2e${path4.sep}`)) return true;
11246
- if (relativePath.startsWith(`scripts${path4.sep}`) || relativePath.includes(`${path4.sep}scripts${path4.sep}`)) return true;
11247
- return !RUNTIME_EXTENSIONS.has(path4.extname(filePath));
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 relativePath = path4.relative(root, filePath);
11252
- const content = readFileSync5(filePath, "utf8");
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(`${relativePath}:${index + 1}:${line}`));
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: relativePath, line: index + 1, url, text: line.trim() });
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 = path4.join(dir, entry.name);
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(path5, init = {}) {
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()}${path5}`, { ...init, headers });
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(path5, init = {}) {
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}${path5}`, { ...init, headers });
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 readFile4, stat } from "fs/promises";
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 readFile4(join4(repoRoot, "package.json"), "utf8"));
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 readFile4(fullPath, "utf8")
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 existsSync4 } from "fs";
11944
- import { mkdir as mkdir3, copyFile, readFile as readFile5, writeFile as writeFile5, rm } from "fs/promises";
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 (existsSync4(join5(candidate, "topolo", "SKILL.md"))) return candidate;
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 = spawnSync(process.platform === "win32" ? "where" : "which", ["topolo-mcp"], {
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 = spawnSync(process.platform === "win32" ? "where" : "which", [bin], {
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 mkdir3(targetDir, { recursive: true });
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
- spawnSync(cli, ["mcp", "remove", "topolo", ...scopeArgs], { stdio: "ignore" });
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 = spawnSync(
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 path5 = join5(homedir2(), ".claude.json");
12409
+ const path8 = join5(homedir2(), ".claude.json");
11999
12410
  let config = {};
12000
- if (existsSync4(path5)) {
12411
+ if (existsSync5(path8)) {
12001
12412
  try {
12002
- config = JSON.parse(await readFile5(path5, "utf8"));
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 writeFile5(path5, `${JSON.stringify(config, null, 2)}
12426
+ await writeFile7(path8, `${JSON.stringify(config, null, 2)}
12016
12427
  `, "utf8");
12017
12428
  }
12018
12429
  async function registerCodexViaConfig(mcp) {
12019
- const path5 = join5(homedir2(), ".codex", "config.toml");
12430
+ const path8 = join5(homedir2(), ".codex", "config.toml");
12020
12431
  let text = "";
12021
- if (existsSync4(path5)) text = await readFile5(path5, "utf8");
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 mkdir3(dirname4(path5), { recursive: true });
12038
- await writeFile5(path5, `${text}${block}`, "utf8");
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 (!existsSync4(join5(sourceDir, "topolo", "SKILL.md"))) {
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: existsSync4(join5(skillDir, "SKILL.md")),
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 = existsSync4(skillFile);
12533
+ const skillRemoved = existsSync5(skillFile);
12123
12534
  if (skillRemoved) await rm(skillFile, { force: true });
12124
12535
  if (agentBinaryExists(agent)) {
12125
- spawnSync(agent, ["mcp", "remove", "topolo"], { stdio: "ignore" });
12536
+ spawnSync2(agent, ["mcp", "remove", "topolo"], { stdio: "ignore" });
12126
12537
  }
12127
12538
  removed.push({ agent, skillRemoved });
12128
12539
  }