@topolo/cli 0.5.2 → 0.6.0

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