@topolo/cli 0.5.2 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/generate-wrangler.mjs +10 -2
- package/dist/index.js +1215 -331
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -61,13 +61,13 @@ var TopoloHttpError = class extends TopoloSdkError {
|
|
|
61
61
|
body;
|
|
62
62
|
service;
|
|
63
63
|
path;
|
|
64
|
-
constructor(service,
|
|
65
|
-
super("http_error", message ?? `HTTP ${status} from ${service}${
|
|
64
|
+
constructor(service, path8, status, body, message) {
|
|
65
|
+
super("http_error", message ?? `HTTP ${status} from ${service}${path8}`);
|
|
66
66
|
this.name = "TopoloHttpError";
|
|
67
67
|
this.status = status;
|
|
68
68
|
this.body = body;
|
|
69
69
|
this.service = service;
|
|
70
|
-
this.path =
|
|
70
|
+
this.path = path8;
|
|
71
71
|
}
|
|
72
72
|
};
|
|
73
73
|
var APP_API_URLS = {
|
|
@@ -137,7 +137,9 @@ var APPLICATIONS = {
|
|
|
137
137
|
"domains": [
|
|
138
138
|
"admin.topolo.app"
|
|
139
139
|
],
|
|
140
|
-
"routes": [
|
|
140
|
+
"routes": [
|
|
141
|
+
"admin.topolo.app"
|
|
142
|
+
],
|
|
141
143
|
"appId": null
|
|
142
144
|
},
|
|
143
145
|
{
|
|
@@ -221,7 +223,9 @@ var APPLICATIONS = {
|
|
|
221
223
|
"domains": [
|
|
222
224
|
"backup.topolo.app"
|
|
223
225
|
],
|
|
224
|
-
"routes": [
|
|
226
|
+
"routes": [
|
|
227
|
+
"backup.topolo.app"
|
|
228
|
+
],
|
|
225
229
|
"appId": "backup"
|
|
226
230
|
}
|
|
227
231
|
]
|
|
@@ -325,7 +329,9 @@ var APPLICATIONS = {
|
|
|
325
329
|
"domains": [
|
|
326
330
|
"bytes.topolo.app"
|
|
327
331
|
],
|
|
328
|
-
"routes": [
|
|
332
|
+
"routes": [
|
|
333
|
+
"bytes.topolo.app"
|
|
334
|
+
],
|
|
329
335
|
"appId": "bytes"
|
|
330
336
|
},
|
|
331
337
|
{
|
|
@@ -475,7 +481,9 @@ var APPLICATIONS = {
|
|
|
475
481
|
"domains": [
|
|
476
482
|
"guest.topolo.app"
|
|
477
483
|
],
|
|
478
|
-
"routes": [
|
|
484
|
+
"routes": [
|
|
485
|
+
"guest.topolo.app"
|
|
486
|
+
],
|
|
479
487
|
"appId": null
|
|
480
488
|
},
|
|
481
489
|
{
|
|
@@ -485,7 +493,9 @@ var APPLICATIONS = {
|
|
|
485
493
|
"domains": [
|
|
486
494
|
"commerce.topolo.app"
|
|
487
495
|
],
|
|
488
|
-
"routes": [
|
|
496
|
+
"routes": [
|
|
497
|
+
"commerce.topolo.app"
|
|
498
|
+
],
|
|
489
499
|
"appId": "commerce"
|
|
490
500
|
},
|
|
491
501
|
{
|
|
@@ -495,7 +505,9 @@ var APPLICATIONS = {
|
|
|
495
505
|
"domains": [
|
|
496
506
|
"station-commerce.topolo.app"
|
|
497
507
|
],
|
|
498
|
-
"routes": [
|
|
508
|
+
"routes": [
|
|
509
|
+
"station-commerce.topolo.app"
|
|
510
|
+
],
|
|
499
511
|
"appId": null
|
|
500
512
|
}
|
|
501
513
|
]
|
|
@@ -673,7 +685,9 @@ var APPLICATIONS = {
|
|
|
673
685
|
"domains": [
|
|
674
686
|
"docs.topolo.app"
|
|
675
687
|
],
|
|
676
|
-
"routes": [
|
|
688
|
+
"routes": [
|
|
689
|
+
"docs.topolo.app"
|
|
690
|
+
],
|
|
677
691
|
"appId": null
|
|
678
692
|
},
|
|
679
693
|
{
|
|
@@ -703,7 +717,9 @@ var APPLICATIONS = {
|
|
|
703
717
|
"domains": [
|
|
704
718
|
"feed-analytics-api.topolo.app"
|
|
705
719
|
],
|
|
706
|
-
"routes": [
|
|
720
|
+
"routes": [
|
|
721
|
+
"feed-analytics-api.topolo.app"
|
|
722
|
+
],
|
|
707
723
|
"appId": null
|
|
708
724
|
},
|
|
709
725
|
{
|
|
@@ -713,7 +729,9 @@ var APPLICATIONS = {
|
|
|
713
729
|
"domains": [
|
|
714
730
|
"feed-api.topolo.app"
|
|
715
731
|
],
|
|
716
|
-
"routes": [
|
|
732
|
+
"routes": [
|
|
733
|
+
"feed-api.topolo.app"
|
|
734
|
+
],
|
|
717
735
|
"appId": null
|
|
718
736
|
},
|
|
719
737
|
{
|
|
@@ -723,7 +741,9 @@ var APPLICATIONS = {
|
|
|
723
741
|
"domains": [
|
|
724
742
|
"feed.topolo.app"
|
|
725
743
|
],
|
|
726
|
-
"routes": [
|
|
744
|
+
"routes": [
|
|
745
|
+
"feed.topolo.app"
|
|
746
|
+
],
|
|
727
747
|
"appId": "feed"
|
|
728
748
|
}
|
|
729
749
|
]
|
|
@@ -835,7 +855,9 @@ var APPLICATIONS = {
|
|
|
835
855
|
"domains": [
|
|
836
856
|
"home.topolo.app"
|
|
837
857
|
],
|
|
838
|
-
"routes": [
|
|
858
|
+
"routes": [
|
|
859
|
+
"home.topolo.app"
|
|
860
|
+
],
|
|
839
861
|
"appId": "home"
|
|
840
862
|
},
|
|
841
863
|
{
|
|
@@ -925,7 +947,9 @@ var APPLICATIONS = {
|
|
|
925
947
|
"domains": [
|
|
926
948
|
"learn.topolo.app"
|
|
927
949
|
],
|
|
928
|
-
"routes": [
|
|
950
|
+
"routes": [
|
|
951
|
+
"learn.topolo.app"
|
|
952
|
+
],
|
|
929
953
|
"appId": "learn"
|
|
930
954
|
},
|
|
931
955
|
{
|
|
@@ -1017,7 +1041,9 @@ var APPLICATIONS = {
|
|
|
1017
1041
|
"domains": [
|
|
1018
1042
|
"mdm.topolo.app"
|
|
1019
1043
|
],
|
|
1020
|
-
"routes": [
|
|
1044
|
+
"routes": [
|
|
1045
|
+
"mdm.topolo.app"
|
|
1046
|
+
],
|
|
1021
1047
|
"appId": "mdm"
|
|
1022
1048
|
}
|
|
1023
1049
|
]
|
|
@@ -1039,7 +1065,9 @@ var APPLICATIONS = {
|
|
|
1039
1065
|
"domains": [
|
|
1040
1066
|
"messages.topolo.app"
|
|
1041
1067
|
],
|
|
1042
|
-
"routes": [
|
|
1068
|
+
"routes": [
|
|
1069
|
+
"messages.topolo.app"
|
|
1070
|
+
],
|
|
1043
1071
|
"appId": "messages"
|
|
1044
1072
|
},
|
|
1045
1073
|
{
|
|
@@ -1207,7 +1235,9 @@ var APPLICATIONS = {
|
|
|
1207
1235
|
"domains": [
|
|
1208
1236
|
"p2p.topolo.app"
|
|
1209
1237
|
],
|
|
1210
|
-
"routes": [
|
|
1238
|
+
"routes": [
|
|
1239
|
+
"p2p.topolo.app"
|
|
1240
|
+
],
|
|
1211
1241
|
"appId": "p2p"
|
|
1212
1242
|
},
|
|
1213
1243
|
{
|
|
@@ -1404,7 +1434,9 @@ var APPLICATIONS = {
|
|
|
1404
1434
|
"domains": [
|
|
1405
1435
|
"studio.topolo.app"
|
|
1406
1436
|
],
|
|
1407
|
-
"routes": [
|
|
1437
|
+
"routes": [
|
|
1438
|
+
"studio.topolo.app"
|
|
1439
|
+
],
|
|
1408
1440
|
"appId": "social_studio"
|
|
1409
1441
|
},
|
|
1410
1442
|
{
|
|
@@ -1442,7 +1474,9 @@ var APPLICATIONS = {
|
|
|
1442
1474
|
"domains": [
|
|
1443
1475
|
"socialize.topolo.app"
|
|
1444
1476
|
],
|
|
1445
|
-
"routes": [
|
|
1477
|
+
"routes": [
|
|
1478
|
+
"socialize.topolo.app"
|
|
1479
|
+
],
|
|
1446
1480
|
"appId": "socialize"
|
|
1447
1481
|
},
|
|
1448
1482
|
{
|
|
@@ -1452,7 +1486,9 @@ var APPLICATIONS = {
|
|
|
1452
1486
|
"domains": [
|
|
1453
1487
|
"socialize-api.topolo.app"
|
|
1454
1488
|
],
|
|
1455
|
-
"routes": [
|
|
1489
|
+
"routes": [
|
|
1490
|
+
"socialize-api.topolo.app"
|
|
1491
|
+
],
|
|
1456
1492
|
"appId": null
|
|
1457
1493
|
},
|
|
1458
1494
|
{
|
|
@@ -1512,7 +1548,9 @@ var APPLICATIONS = {
|
|
|
1512
1548
|
"domains": [
|
|
1513
1549
|
"status.topolo.app"
|
|
1514
1550
|
],
|
|
1515
|
-
"routes": [
|
|
1551
|
+
"routes": [
|
|
1552
|
+
"status.topolo.app"
|
|
1553
|
+
],
|
|
1516
1554
|
"appId": "status"
|
|
1517
1555
|
}
|
|
1518
1556
|
]
|
|
@@ -1628,7 +1666,9 @@ var APPLICATIONS = {
|
|
|
1628
1666
|
"domains": [
|
|
1629
1667
|
"voice.topolo.app"
|
|
1630
1668
|
],
|
|
1631
|
-
"routes": [
|
|
1669
|
+
"routes": [
|
|
1670
|
+
"voice.topolo.app"
|
|
1671
|
+
],
|
|
1632
1672
|
"appId": "voice"
|
|
1633
1673
|
},
|
|
1634
1674
|
{
|
|
@@ -1679,7 +1719,9 @@ var APPLICATIONS = {
|
|
|
1679
1719
|
"domains": [
|
|
1680
1720
|
"web.topolo.app"
|
|
1681
1721
|
],
|
|
1682
|
-
"routes": [
|
|
1722
|
+
"routes": [
|
|
1723
|
+
"web.topolo.app"
|
|
1724
|
+
],
|
|
1683
1725
|
"appId": "web"
|
|
1684
1726
|
}
|
|
1685
1727
|
]
|
|
@@ -2176,17 +2218,17 @@ function validateTopoloActionInput(schema, input3) {
|
|
|
2176
2218
|
validateNode(schema, input3, "$", schema, errors, 0);
|
|
2177
2219
|
return { valid: errors.length === 0, errors };
|
|
2178
2220
|
}
|
|
2179
|
-
function inspectSchema(schema,
|
|
2180
|
-
if (depth > 128) return { path:
|
|
2221
|
+
function inspectSchema(schema, path8, root, depth) {
|
|
2222
|
+
if (depth > 128) return { path: path8, code: "schema_depth", message: "Schema recursion exceeded 128 levels." };
|
|
2181
2223
|
const unsupportedKeyword = Object.keys(schema).find((keyword) => !SUPPORTED_SCHEMA_KEYWORDS.has(keyword));
|
|
2182
2224
|
if (unsupportedKeyword) {
|
|
2183
|
-
return { path:
|
|
2225
|
+
return { path: path8, code: "unsupported_schema_keyword", message: `Action schema uses unsupported keyword ${unsupportedKeyword}.` };
|
|
2184
2226
|
}
|
|
2185
2227
|
if (typeof schema.format === "string" && !["uri", "email", "date-time"].includes(schema.format)) {
|
|
2186
|
-
return { path:
|
|
2228
|
+
return { path: path8, code: "unsupported_schema_format", message: `Action schema uses unsupported format ${schema.format}.` };
|
|
2187
2229
|
}
|
|
2188
2230
|
if (typeof schema.$ref === "string" && !resolveLocalReference(root, schema.$ref)) {
|
|
2189
|
-
return { path:
|
|
2231
|
+
return { path: path8, code: "unsupported_schema_ref", message: `Cannot resolve schema reference ${schema.$ref}.` };
|
|
2190
2232
|
}
|
|
2191
2233
|
const children = [];
|
|
2192
2234
|
if (isRecord(schema.properties)) children.push(...Object.entries(schema.properties));
|
|
@@ -2202,7 +2244,7 @@ function inspectSchema(schema, path6, root, depth) {
|
|
|
2202
2244
|
);
|
|
2203
2245
|
for (const [key, child] of children) {
|
|
2204
2246
|
if (!isRecord(child)) continue;
|
|
2205
|
-
const issue = inspectSchema(child, `${
|
|
2247
|
+
const issue = inspectSchema(child, `${path8}.${key}`, root, depth + 1);
|
|
2206
2248
|
if (issue) return issue;
|
|
2207
2249
|
}
|
|
2208
2250
|
return null;
|
|
@@ -2210,129 +2252,129 @@ function inspectSchema(schema, path6, root, depth) {
|
|
|
2210
2252
|
function arraySchemaEntries(keyword, value) {
|
|
2211
2253
|
return Array.isArray(value) ? value.map((candidate, index) => [`${keyword}[${index}]`, candidate]) : [];
|
|
2212
2254
|
}
|
|
2213
|
-
function validateNode(schema, value,
|
|
2255
|
+
function validateNode(schema, value, path8, root, errors, depth) {
|
|
2214
2256
|
if (depth > 128) {
|
|
2215
|
-
errors.push({ path:
|
|
2257
|
+
errors.push({ path: path8, code: "schema_depth", message: "Schema recursion exceeded 128 levels." });
|
|
2216
2258
|
return;
|
|
2217
2259
|
}
|
|
2218
2260
|
if (typeof schema.$ref === "string") {
|
|
2219
2261
|
const resolved = resolveLocalReference(root, schema.$ref);
|
|
2220
2262
|
if (!resolved) {
|
|
2221
|
-
errors.push({ path:
|
|
2263
|
+
errors.push({ path: path8, code: "unsupported_schema_ref", message: `Cannot resolve schema reference ${schema.$ref}.` });
|
|
2222
2264
|
return;
|
|
2223
2265
|
}
|
|
2224
|
-
validateNode(resolved, value,
|
|
2266
|
+
validateNode(resolved, value, path8, root, errors, depth + 1);
|
|
2225
2267
|
}
|
|
2226
2268
|
if (Array.isArray(schema.allOf)) {
|
|
2227
2269
|
for (const candidate of schema.allOf) {
|
|
2228
|
-
if (isRecord(candidate)) validateNode(candidate, value,
|
|
2270
|
+
if (isRecord(candidate)) validateNode(candidate, value, path8, root, errors, depth + 1);
|
|
2229
2271
|
}
|
|
2230
2272
|
}
|
|
2231
2273
|
if (Array.isArray(schema.anyOf) && !schema.anyOf.some((candidate) => candidateMatches(candidate, value, root, depth))) {
|
|
2232
|
-
errors.push({ path:
|
|
2274
|
+
errors.push({ path: path8, code: "any_of", message: "Value does not match any allowed schema." });
|
|
2233
2275
|
return;
|
|
2234
2276
|
}
|
|
2235
2277
|
if (Array.isArray(schema.oneOf)) {
|
|
2236
2278
|
const matches = schema.oneOf.filter((candidate) => candidateMatches(candidate, value, root, depth)).length;
|
|
2237
2279
|
if (matches !== 1) {
|
|
2238
|
-
errors.push({ path:
|
|
2280
|
+
errors.push({ path: path8, code: "one_of", message: "Value must match exactly one allowed schema." });
|
|
2239
2281
|
return;
|
|
2240
2282
|
}
|
|
2241
2283
|
}
|
|
2242
2284
|
if (value === null && schema.nullable === true) return;
|
|
2243
2285
|
if (Object.prototype.hasOwnProperty.call(schema, "const") && !sameJsonValue(value, schema.const)) {
|
|
2244
|
-
errors.push({ path:
|
|
2286
|
+
errors.push({ path: path8, code: "const", message: `Value must equal ${JSON.stringify(schema.const)}.` });
|
|
2245
2287
|
return;
|
|
2246
2288
|
}
|
|
2247
2289
|
if (Array.isArray(schema.enum) && !schema.enum.some((candidate) => sameJsonValue(value, candidate))) {
|
|
2248
|
-
errors.push({ path:
|
|
2290
|
+
errors.push({ path: path8, code: "enum", message: `Value must be one of ${schema.enum.map((item) => JSON.stringify(item)).join(", ")}.` });
|
|
2249
2291
|
return;
|
|
2250
2292
|
}
|
|
2251
2293
|
if (!matchesType(schema.type, value)) {
|
|
2252
|
-
errors.push({ path:
|
|
2294
|
+
errors.push({ path: path8, code: "type", message: `Expected ${String(schema.type)}.` });
|
|
2253
2295
|
return;
|
|
2254
2296
|
}
|
|
2255
|
-
if (typeof value === "string") validateString(schema, value,
|
|
2256
|
-
if (typeof value === "number") validateNumber(schema, value,
|
|
2257
|
-
if (Array.isArray(value)) validateArray(schema, value,
|
|
2258
|
-
if (isRecord(value)) validateObject(schema, value,
|
|
2297
|
+
if (typeof value === "string") validateString(schema, value, path8, errors);
|
|
2298
|
+
if (typeof value === "number") validateNumber(schema, value, path8, errors);
|
|
2299
|
+
if (Array.isArray(value)) validateArray(schema, value, path8, root, errors, depth);
|
|
2300
|
+
if (isRecord(value)) validateObject(schema, value, path8, root, errors, depth);
|
|
2259
2301
|
}
|
|
2260
|
-
function validateObject(schema, value,
|
|
2302
|
+
function validateObject(schema, value, path8, root, errors, depth) {
|
|
2261
2303
|
if (typeof schema.minProperties === "number" && Object.keys(value).length < schema.minProperties) {
|
|
2262
|
-
errors.push({ path:
|
|
2304
|
+
errors.push({ path: path8, code: "min_properties", message: `Expected at least ${schema.minProperties} properties.` });
|
|
2263
2305
|
}
|
|
2264
2306
|
if (typeof schema.maxProperties === "number" && Object.keys(value).length > schema.maxProperties) {
|
|
2265
|
-
errors.push({ path:
|
|
2307
|
+
errors.push({ path: path8, code: "max_properties", message: `Expected at most ${schema.maxProperties} properties.` });
|
|
2266
2308
|
}
|
|
2267
2309
|
const properties = isRecord(schema.properties) ? schema.properties : {};
|
|
2268
2310
|
const required = Array.isArray(schema.required) ? schema.required.map(String) : [];
|
|
2269
2311
|
for (const key of required) {
|
|
2270
2312
|
if (!Object.prototype.hasOwnProperty.call(value, key)) {
|
|
2271
|
-
errors.push({ path: `${
|
|
2313
|
+
errors.push({ path: `${path8}.${key}`, code: "required", message: "Required property is missing." });
|
|
2272
2314
|
}
|
|
2273
2315
|
}
|
|
2274
2316
|
if (isRecord(schema.propertyNames)) {
|
|
2275
2317
|
for (const key of Object.keys(value)) {
|
|
2276
|
-
validateNode(schema.propertyNames, key, `${
|
|
2318
|
+
validateNode(schema.propertyNames, key, `${path8}.${key}`, root, errors, depth + 1);
|
|
2277
2319
|
}
|
|
2278
2320
|
}
|
|
2279
2321
|
for (const [key, child] of Object.entries(value)) {
|
|
2280
2322
|
const childSchema = properties[key];
|
|
2281
2323
|
if (isRecord(childSchema)) {
|
|
2282
|
-
validateNode(childSchema, child, `${
|
|
2324
|
+
validateNode(childSchema, child, `${path8}.${key}`, root, errors, depth + 1);
|
|
2283
2325
|
} else if (schema.additionalProperties === false) {
|
|
2284
|
-
errors.push({ path: `${
|
|
2326
|
+
errors.push({ path: `${path8}.${key}`, code: "additional_property", message: "Property is not declared by the action schema." });
|
|
2285
2327
|
} else if (isRecord(schema.additionalProperties)) {
|
|
2286
|
-
validateNode(schema.additionalProperties, child, `${
|
|
2328
|
+
validateNode(schema.additionalProperties, child, `${path8}.${key}`, root, errors, depth + 1);
|
|
2287
2329
|
}
|
|
2288
2330
|
}
|
|
2289
2331
|
}
|
|
2290
|
-
function validateArray(schema, value,
|
|
2332
|
+
function validateArray(schema, value, path8, root, errors, depth) {
|
|
2291
2333
|
if (typeof schema.minItems === "number" && value.length < schema.minItems) {
|
|
2292
|
-
errors.push({ path:
|
|
2334
|
+
errors.push({ path: path8, code: "min_items", message: `Expected at least ${schema.minItems} items.` });
|
|
2293
2335
|
}
|
|
2294
2336
|
if (typeof schema.maxItems === "number" && value.length > schema.maxItems) {
|
|
2295
|
-
errors.push({ path:
|
|
2337
|
+
errors.push({ path: path8, code: "max_items", message: `Expected at most ${schema.maxItems} items.` });
|
|
2296
2338
|
}
|
|
2297
2339
|
if (schema.uniqueItems === true) {
|
|
2298
|
-
const
|
|
2299
|
-
if (
|
|
2340
|
+
const unique4 = new Set(value.map((item) => JSON.stringify(item)));
|
|
2341
|
+
if (unique4.size !== value.length) errors.push({ path: path8, code: "unique_items", message: "Expected every item to be unique." });
|
|
2300
2342
|
}
|
|
2301
2343
|
if (isRecord(schema.items)) {
|
|
2302
|
-
value.forEach((item, index) => validateNode(schema.items, item, `${
|
|
2344
|
+
value.forEach((item, index) => validateNode(schema.items, item, `${path8}[${index}]`, root, errors, depth + 1));
|
|
2303
2345
|
}
|
|
2304
2346
|
}
|
|
2305
|
-
function validateString(schema, value,
|
|
2347
|
+
function validateString(schema, value, path8, errors) {
|
|
2306
2348
|
if (typeof schema.minLength === "number" && value.length < schema.minLength) {
|
|
2307
|
-
errors.push({ path:
|
|
2349
|
+
errors.push({ path: path8, code: "min_length", message: `Expected at least ${schema.minLength} characters.` });
|
|
2308
2350
|
}
|
|
2309
2351
|
if (typeof schema.maxLength === "number" && value.length > schema.maxLength) {
|
|
2310
|
-
errors.push({ path:
|
|
2352
|
+
errors.push({ path: path8, code: "max_length", message: `Expected at most ${schema.maxLength} characters.` });
|
|
2311
2353
|
}
|
|
2312
2354
|
if (typeof schema.pattern === "string") {
|
|
2313
2355
|
try {
|
|
2314
|
-
if (!new RegExp(schema.pattern).test(value)) errors.push({ path:
|
|
2356
|
+
if (!new RegExp(schema.pattern).test(value)) errors.push({ path: path8, code: "pattern", message: "Value does not match the required pattern." });
|
|
2315
2357
|
} catch {
|
|
2316
|
-
errors.push({ path:
|
|
2358
|
+
errors.push({ path: path8, code: "invalid_schema_pattern", message: "Action schema contains an invalid pattern." });
|
|
2317
2359
|
}
|
|
2318
2360
|
}
|
|
2319
|
-
if (schema.format === "uri" && !isUrl(value)) errors.push({ path:
|
|
2320
|
-
if (schema.format === "email" && !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(value)) errors.push({ path:
|
|
2321
|
-
if (schema.format === "date-time" && !isDateTime(value)) errors.push({ path:
|
|
2361
|
+
if (schema.format === "uri" && !isUrl(value)) errors.push({ path: path8, code: "format_uri", message: "Expected an absolute URL." });
|
|
2362
|
+
if (schema.format === "email" && !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(value)) errors.push({ path: path8, code: "format_email", message: "Expected an email address." });
|
|
2363
|
+
if (schema.format === "date-time" && !isDateTime(value)) errors.push({ path: path8, code: "format_date_time", message: "Expected an ISO-8601 date-time." });
|
|
2322
2364
|
}
|
|
2323
|
-
function validateNumber(schema, value,
|
|
2324
|
-
if (typeof schema.minimum === "number" && value < schema.minimum) errors.push({ path:
|
|
2325
|
-
if (typeof schema.maximum === "number" && value > schema.maximum) errors.push({ path:
|
|
2365
|
+
function validateNumber(schema, value, path8, errors) {
|
|
2366
|
+
if (typeof schema.minimum === "number" && value < schema.minimum) errors.push({ path: path8, code: "minimum", message: `Expected a value of at least ${schema.minimum}.` });
|
|
2367
|
+
if (typeof schema.maximum === "number" && value > schema.maximum) errors.push({ path: path8, code: "maximum", message: `Expected a value of at most ${schema.maximum}.` });
|
|
2326
2368
|
const exclusiveMinimum = typeof schema.exclusiveMinimum === "number" ? schema.exclusiveMinimum : schema.exclusiveMinimum === true && typeof schema.minimum === "number" ? schema.minimum : null;
|
|
2327
2369
|
const exclusiveMaximum = typeof schema.exclusiveMaximum === "number" ? schema.exclusiveMaximum : schema.exclusiveMaximum === true && typeof schema.maximum === "number" ? schema.maximum : null;
|
|
2328
2370
|
if (exclusiveMinimum !== null && value <= exclusiveMinimum) {
|
|
2329
|
-
errors.push({ path:
|
|
2371
|
+
errors.push({ path: path8, code: "exclusive_minimum", message: `Expected a value greater than ${exclusiveMinimum}.` });
|
|
2330
2372
|
}
|
|
2331
2373
|
if (exclusiveMaximum !== null && value >= exclusiveMaximum) {
|
|
2332
|
-
errors.push({ path:
|
|
2374
|
+
errors.push({ path: path8, code: "exclusive_maximum", message: `Expected a value less than ${exclusiveMaximum}.` });
|
|
2333
2375
|
}
|
|
2334
2376
|
if (typeof schema.multipleOf === "number" && !isMultipleOf(value, schema.multipleOf)) {
|
|
2335
|
-
errors.push({ path:
|
|
2377
|
+
errors.push({ path: path8, code: "multiple_of", message: `Expected a multiple of ${schema.multipleOf}.` });
|
|
2336
2378
|
}
|
|
2337
2379
|
}
|
|
2338
2380
|
function candidateMatches(candidate, value, root, depth) {
|
|
@@ -2843,14 +2885,14 @@ var TopoloClient = class _TopoloClient {
|
|
|
2843
2885
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
2844
2886
|
throw new TopoloSdkError("action_catalog_summary_unavailable", "Action catalog summary is unavailable.");
|
|
2845
2887
|
}
|
|
2846
|
-
const
|
|
2847
|
-
if (
|
|
2888
|
+
const record4 = raw;
|
|
2889
|
+
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
2890
|
throw new TopoloSdkError("action_catalog_summary_unavailable", "Action catalog summary is invalid.");
|
|
2849
2891
|
}
|
|
2850
2892
|
return {
|
|
2851
2893
|
algorithm: "sha256-v1",
|
|
2852
|
-
digest:
|
|
2853
|
-
actionCount: Number(
|
|
2894
|
+
digest: record4.digest,
|
|
2895
|
+
actionCount: Number(record4.actionCount)
|
|
2854
2896
|
};
|
|
2855
2897
|
}
|
|
2856
2898
|
/** @deprecated Prefer searchActions() so callers retain the continuation cursor. */
|
|
@@ -3225,8 +3267,8 @@ function normalizeActionCatalogEntry(item) {
|
|
|
3225
3267
|
const name = pickActionString(a.name);
|
|
3226
3268
|
const appId = pickActionString(a.app_id) ?? pickActionString(a.appId);
|
|
3227
3269
|
const method = pickActionString(a.method)?.toUpperCase();
|
|
3228
|
-
const
|
|
3229
|
-
if (!actionId || !name || !appId || !method || !
|
|
3270
|
+
const path8 = pickActionString(a.path);
|
|
3271
|
+
if (!actionId || !name || !appId || !method || !path8 || !isHttpMethod(method)) {
|
|
3230
3272
|
return null;
|
|
3231
3273
|
}
|
|
3232
3274
|
return {
|
|
@@ -3241,7 +3283,7 @@ function normalizeActionCatalogEntry(item) {
|
|
|
3241
3283
|
appSlug: pickActionString(a.app_slug) ?? pickActionString(a.appSlug) ?? null,
|
|
3242
3284
|
appName: pickActionString(a.app_name) ?? pickActionString(a.appName) ?? null,
|
|
3243
3285
|
method,
|
|
3244
|
-
path:
|
|
3286
|
+
path: path8,
|
|
3245
3287
|
permissionName: pickActionString(a.permission_name) ?? pickActionString(a.permissionName) ?? "",
|
|
3246
3288
|
requiredPermission: pickActionString(a.required_permission) ?? pickActionString(a.requiredPermission) ?? "",
|
|
3247
3289
|
authorizationMode: pickAuthorizationMode(a.authorization_mode ?? a.authorizationMode),
|
|
@@ -3277,7 +3319,7 @@ function stringArray(value) {
|
|
|
3277
3319
|
}
|
|
3278
3320
|
function mapActionInput(action, input3) {
|
|
3279
3321
|
const remaining = { ...input3 || {} };
|
|
3280
|
-
const
|
|
3322
|
+
const path8 = action.path.replace(/\{([a-zA-Z0-9_]+)\}/g, (_match, key) => {
|
|
3281
3323
|
const value = remaining[key];
|
|
3282
3324
|
if (value === void 0 || value === null || value === "") {
|
|
3283
3325
|
throw new TopoloSdkError(
|
|
@@ -3294,14 +3336,14 @@ function mapActionInput(action, input3) {
|
|
|
3294
3336
|
delete remaining.body;
|
|
3295
3337
|
if (action.method === "GET") {
|
|
3296
3338
|
return {
|
|
3297
|
-
path:
|
|
3339
|
+
path: path8,
|
|
3298
3340
|
query: objectToQuery({ ...remaining, ...explicitQuery })
|
|
3299
3341
|
};
|
|
3300
3342
|
}
|
|
3301
3343
|
const query = objectToQuery(explicitQuery);
|
|
3302
3344
|
const body = explicitBody !== void 0 ? explicitBody : remaining;
|
|
3303
3345
|
return {
|
|
3304
|
-
path:
|
|
3346
|
+
path: path8,
|
|
3305
3347
|
...query ? { query } : {},
|
|
3306
3348
|
body
|
|
3307
3349
|
};
|
|
@@ -3361,7 +3403,6 @@ var CrmModule = class {
|
|
|
3361
3403
|
constructor(client) {
|
|
3362
3404
|
this.client = client;
|
|
3363
3405
|
}
|
|
3364
|
-
client;
|
|
3365
3406
|
async listContacts(options = {}) {
|
|
3366
3407
|
const res = await this.client.request({
|
|
3367
3408
|
service: "crm",
|
|
@@ -3407,7 +3448,6 @@ var IdentityModule = class {
|
|
|
3407
3448
|
constructor(client) {
|
|
3408
3449
|
this.client = client;
|
|
3409
3450
|
}
|
|
3410
|
-
client;
|
|
3411
3451
|
/** Returns the resolved user, organization, and permission set for the
|
|
3412
3452
|
* current credential. Equivalent to `TopoloClient.introspect()` — exposed
|
|
3413
3453
|
* on this module for symmetry with the other domain modules. */
|
|
@@ -3471,8 +3511,8 @@ var TopoloOAuth = class {
|
|
|
3471
3511
|
if (params.clientSecret) body.set("client_secret", params.clientSecret);
|
|
3472
3512
|
return this.#postForm("/api/developer-oauth/token", body);
|
|
3473
3513
|
}
|
|
3474
|
-
async #postForm(
|
|
3475
|
-
const url = new URL(
|
|
3514
|
+
async #postForm(path8, body) {
|
|
3515
|
+
const url = new URL(path8, this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`);
|
|
3476
3516
|
const res = await this.fetchImpl(url.toString(), {
|
|
3477
3517
|
method: "POST",
|
|
3478
3518
|
headers: {
|
|
@@ -3488,7 +3528,7 @@ var TopoloOAuth = class {
|
|
|
3488
3528
|
const err = parsed;
|
|
3489
3529
|
throw new TopoloAuthError(err.error_description || err.error, err.error);
|
|
3490
3530
|
}
|
|
3491
|
-
throw new TopoloHttpError("auth",
|
|
3531
|
+
throw new TopoloHttpError("auth", path8, res.status, parsed);
|
|
3492
3532
|
}
|
|
3493
3533
|
return parsed;
|
|
3494
3534
|
}
|
|
@@ -3526,13 +3566,13 @@ var TopoloHttpError2 = class extends TopoloSdkError2 {
|
|
|
3526
3566
|
body;
|
|
3527
3567
|
service;
|
|
3528
3568
|
path;
|
|
3529
|
-
constructor(service,
|
|
3530
|
-
super("http_error", message ?? `HTTP ${status} from ${service}${
|
|
3569
|
+
constructor(service, path8, status, body, message) {
|
|
3570
|
+
super("http_error", message ?? `HTTP ${status} from ${service}${path8}`);
|
|
3531
3571
|
this.name = "TopoloHttpError";
|
|
3532
3572
|
this.status = status;
|
|
3533
3573
|
this.body = body;
|
|
3534
3574
|
this.service = service;
|
|
3535
|
-
this.path =
|
|
3575
|
+
this.path = path8;
|
|
3536
3576
|
}
|
|
3537
3577
|
};
|
|
3538
3578
|
var APP_API_URLS2 = {
|
|
@@ -3663,8 +3703,8 @@ var TopoloOAuth2 = class {
|
|
|
3663
3703
|
if (params.clientSecret) body.set("client_secret", params.clientSecret);
|
|
3664
3704
|
return this.#postForm("/api/developer-oauth/token", body);
|
|
3665
3705
|
}
|
|
3666
|
-
async #postForm(
|
|
3667
|
-
const url = new URL(
|
|
3706
|
+
async #postForm(path8, body) {
|
|
3707
|
+
const url = new URL(path8, this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`);
|
|
3668
3708
|
const res = await this.fetchImpl(url.toString(), {
|
|
3669
3709
|
method: "POST",
|
|
3670
3710
|
headers: {
|
|
@@ -3680,7 +3720,7 @@ var TopoloOAuth2 = class {
|
|
|
3680
3720
|
const err = parsed;
|
|
3681
3721
|
throw new TopoloAuthError2(err.error_description || err.error, err.error);
|
|
3682
3722
|
}
|
|
3683
|
-
throw new TopoloHttpError2("auth",
|
|
3723
|
+
throw new TopoloHttpError2("auth", path8, res.status, parsed);
|
|
3684
3724
|
}
|
|
3685
3725
|
return parsed;
|
|
3686
3726
|
}
|
|
@@ -3710,22 +3750,22 @@ function configPath() {
|
|
|
3710
3750
|
return join(base, "topolo", "config.json");
|
|
3711
3751
|
}
|
|
3712
3752
|
async function readConfig() {
|
|
3713
|
-
const
|
|
3714
|
-
if (!existsSync(
|
|
3753
|
+
const path8 = configPath();
|
|
3754
|
+
if (!existsSync(path8)) return {};
|
|
3715
3755
|
try {
|
|
3716
|
-
const raw = await readFile(
|
|
3756
|
+
const raw = await readFile(path8, "utf8");
|
|
3717
3757
|
return JSON.parse(raw);
|
|
3718
3758
|
} catch {
|
|
3719
3759
|
return {};
|
|
3720
3760
|
}
|
|
3721
3761
|
}
|
|
3722
3762
|
async function writeConfig(config) {
|
|
3723
|
-
const
|
|
3724
|
-
await mkdir(dirname(
|
|
3725
|
-
await writeFile(
|
|
3763
|
+
const path8 = configPath();
|
|
3764
|
+
await mkdir(dirname(path8), { recursive: true });
|
|
3765
|
+
await writeFile(path8, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
3726
3766
|
if (platform() !== "win32") {
|
|
3727
3767
|
try {
|
|
3728
|
-
await chmod(
|
|
3768
|
+
await chmod(path8, 384);
|
|
3729
3769
|
} catch {
|
|
3730
3770
|
}
|
|
3731
3771
|
}
|
|
@@ -4045,9 +4085,9 @@ function readCliVersion() {
|
|
|
4045
4085
|
join2(here, "package.json"),
|
|
4046
4086
|
join2(here, "..", "package.json")
|
|
4047
4087
|
];
|
|
4048
|
-
for (const
|
|
4088
|
+
for (const path8 of candidates) {
|
|
4049
4089
|
try {
|
|
4050
|
-
const pkg = JSON.parse(readFileSync(
|
|
4090
|
+
const pkg = JSON.parse(readFileSync(path8, "utf8"));
|
|
4051
4091
|
if (typeof pkg.version === "string" && pkg.version) return pkg.version;
|
|
4052
4092
|
} catch {
|
|
4053
4093
|
}
|
|
@@ -4406,7 +4446,7 @@ function registerApiCommand(program2) {
|
|
|
4406
4446
|
"-q, --query <kv...>",
|
|
4407
4447
|
"Query params as key=value (repeatable), e.g. --query source=cli page=2"
|
|
4408
4448
|
).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,
|
|
4449
|
+
async (service, method, path8, opts) => {
|
|
4410
4450
|
const mode = resolveOutputMode(opts.json);
|
|
4411
4451
|
try {
|
|
4412
4452
|
const upperMethod = method.toUpperCase();
|
|
@@ -4445,7 +4485,7 @@ function registerApiCommand(program2) {
|
|
|
4445
4485
|
const result = await topolo.client.request({
|
|
4446
4486
|
service,
|
|
4447
4487
|
method: httpMethod,
|
|
4448
|
-
path:
|
|
4488
|
+
path: path8,
|
|
4449
4489
|
body,
|
|
4450
4490
|
...query ? { query } : {},
|
|
4451
4491
|
...resource ? { resource } : {},
|
|
@@ -4609,13 +4649,13 @@ async function requiredClient() {
|
|
|
4609
4649
|
function parseJsonObject(value) {
|
|
4610
4650
|
let source = value;
|
|
4611
4651
|
if (value.startsWith("@")) {
|
|
4612
|
-
const
|
|
4613
|
-
if (!
|
|
4652
|
+
const path8 = value.slice(1);
|
|
4653
|
+
if (!path8) throw new Error("--data @file path must not be empty");
|
|
4614
4654
|
try {
|
|
4615
|
-
source = readFileSync2(
|
|
4655
|
+
source = readFileSync2(path8, "utf8");
|
|
4616
4656
|
} catch (err) {
|
|
4617
4657
|
const detail = err instanceof Error ? err.message : String(err);
|
|
4618
|
-
throw new Error(`Could not read --data file ${
|
|
4658
|
+
throw new Error(`Could not read --data file ${path8}: ${detail}`);
|
|
4619
4659
|
}
|
|
4620
4660
|
}
|
|
4621
4661
|
let parsed;
|
|
@@ -4690,7 +4730,7 @@ function formatActionPlan(plan) {
|
|
|
4690
4730
|
// src/commands/apps.ts
|
|
4691
4731
|
import { execSync } from "child_process";
|
|
4692
4732
|
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "fs";
|
|
4693
|
-
import
|
|
4733
|
+
import path5 from "path";
|
|
4694
4734
|
|
|
4695
4735
|
// src/commands/app-audit-local.ts
|
|
4696
4736
|
import {
|
|
@@ -4722,14 +4762,26 @@ function isEvidenceStampFresh(stamp) {
|
|
|
4722
4762
|
return Date.now() - recordedAt <= EVIDENCE_FRESHNESS_MS;
|
|
4723
4763
|
}
|
|
4724
4764
|
var SKIPPED_DIRS = /* @__PURE__ */ new Set([
|
|
4765
|
+
".action-normalize",
|
|
4766
|
+
".claude",
|
|
4767
|
+
".codex-worktrees",
|
|
4768
|
+
".deploy",
|
|
4725
4769
|
".git",
|
|
4726
4770
|
".next",
|
|
4771
|
+
".playwright",
|
|
4772
|
+
".topolo-tmp",
|
|
4727
4773
|
".turbo",
|
|
4774
|
+
".worktrees",
|
|
4728
4775
|
".wrangler",
|
|
4776
|
+
"_tmp",
|
|
4777
|
+
"_worktrees",
|
|
4778
|
+
"artifacts",
|
|
4729
4779
|
"build",
|
|
4730
4780
|
"coverage",
|
|
4731
4781
|
"dist",
|
|
4732
|
-
"node_modules"
|
|
4782
|
+
"node_modules",
|
|
4783
|
+
"output",
|
|
4784
|
+
"tmp"
|
|
4733
4785
|
]);
|
|
4734
4786
|
var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
4735
4787
|
".cjs",
|
|
@@ -5281,8 +5333,8 @@ function enrichFinding(application, finding2, evidence) {
|
|
|
5281
5333
|
evidence: verificationEvidence
|
|
5282
5334
|
});
|
|
5283
5335
|
}
|
|
5284
|
-
const usesNodeCheck = evidence.packageJsons.some((
|
|
5285
|
-
const scripts =
|
|
5336
|
+
const usesNodeCheck = evidence.packageJsons.some((record4) => {
|
|
5337
|
+
const scripts = record4.packageJson.scripts ?? {};
|
|
5286
5338
|
return Object.values(scripts).some((cmd) => typeof cmd === "string" && /node\s+--check/.test(cmd));
|
|
5287
5339
|
});
|
|
5288
5340
|
if (hasBuild && usesNodeCheck) {
|
|
@@ -5362,7 +5414,7 @@ function nativeDashboardWidgetEvidence(evidence) {
|
|
|
5362
5414
|
return unique(items);
|
|
5363
5415
|
}
|
|
5364
5416
|
function collectEvidence(repoRoot, application) {
|
|
5365
|
-
const appRepoPath = applicationSourcePath(application.appDir);
|
|
5417
|
+
const appRepoPath = applicationSourcePath(repoRoot, application.appDir);
|
|
5366
5418
|
const appRoot = path.join(repoRoot, appRepoPath);
|
|
5367
5419
|
const packageJsonPath = path.join(appRoot, "package.json");
|
|
5368
5420
|
const packageJson2 = readPackageJson(packageJsonPath);
|
|
@@ -5414,8 +5466,11 @@ function sharedAuthEvidence(evidence) {
|
|
|
5414
5466
|
if (text.includes("TopoloAuthMiddleware")) items.push("source=TopoloAuthMiddleware");
|
|
5415
5467
|
if (text.includes("TopoloClient")) items.push("source=TopoloClient");
|
|
5416
5468
|
if (text.includes("TopoloOAuth")) items.push("source=TopoloOAuth");
|
|
5417
|
-
|
|
5418
|
-
|
|
5469
|
+
const browserRecords = productionBrowserSourceRecords(evidence);
|
|
5470
|
+
const blockingBoot = browserRecords.find(({ text: source }) => /\b(?:auth|authClient|topoloAuth)\.initialize(?:Session)?\(\s*\)/i.test(source));
|
|
5471
|
+
const backgroundBoot = browserRecords.find(({ text: source }) => /cookieRefresh:\s*['"]background['"]/.test(source));
|
|
5472
|
+
if (blockingBoot) items.push("auth-boot=blocking-cookie-refresh", `auth-boot-file=${blockingBoot.path}`);
|
|
5473
|
+
if (backgroundBoot) items.push("auth-boot=background-cookie-refresh", `auth-background-file=${backgroundBoot.path}`);
|
|
5419
5474
|
if (/Authorization.*Bearer/i.test(text) && (text.includes("/validate") || text.includes("TOPOLO_AUTH_URL") || text.includes("AUTH_BASE_URL") || text.includes("auth.topolo.app"))) {
|
|
5420
5475
|
items.push("source=auth-validate-fetch");
|
|
5421
5476
|
}
|
|
@@ -5434,7 +5489,8 @@ function documentedOperatorLocalAuthBoundaryEvidence(evidence) {
|
|
|
5434
5489
|
}
|
|
5435
5490
|
function sharedShellEvidence(evidence) {
|
|
5436
5491
|
const deps = dependencyNames(evidence);
|
|
5437
|
-
const
|
|
5492
|
+
const browserRecords = productionBrowserSourceRecords(evidence);
|
|
5493
|
+
const text = browserRecords.map((record4) => record4.text).join("\n");
|
|
5438
5494
|
const items = [];
|
|
5439
5495
|
if (deps.has("@topolo-io/app-shell")) items.push("dependency=@topolo-io/app-shell");
|
|
5440
5496
|
if (deps.has("@topolo-io/ui-kit")) items.push("dependency=@topolo-io/ui-kit");
|
|
@@ -5454,11 +5510,9 @@ function sharedShellEvidence(evidence) {
|
|
|
5454
5510
|
const hasVisualQaFixtures = hasVisualQaMode && /fixture|visualQaResponse|visualQaApi|mock[A-Z]\w*|devpreview/i.test(text) && /fetch|useQuery|api/i.test(text);
|
|
5455
5511
|
if (hasVisualQaFixtures) items.push("source=visual-qa-fixtures");
|
|
5456
5512
|
if (/from\s+['"]lucide-react['"]/.test(text)) items.push("local=direct-lucide-react");
|
|
5457
|
-
|
|
5458
|
-
|
|
5459
|
-
|
|
5460
|
-
if (/(?:<span[^>]*>|<div[^>]*>)[^<]{0,16}[◈◇⬡⬢▣◉✉▷✦○◆✕⚙🔑💡⚠✓♫]/.test(text)) {
|
|
5461
|
-
items.push("local=raw-symbol-icons");
|
|
5513
|
+
const rawSymbolRecord = browserRecords.find(({ text: source }) => /(?:icon|Icon)\s*:\s*['"][◈◇⬡⬢▣◉✉▷✦○◆✕⚙🔑💡⚠✓♫]/.test(source) || /(?:<span[^>]*>|<div[^>]*>)[^<]{0,16}[◈◇⬡⬢▣◉✉▷✦○◆✕⚙🔑💡⚠✓♫]/.test(source));
|
|
5514
|
+
if (rawSymbolRecord) {
|
|
5515
|
+
items.push("local=raw-symbol-icons", `raw-symbol-file=${rawSymbolRecord.path}`);
|
|
5462
5516
|
}
|
|
5463
5517
|
if (text.includes(["Topolo", "Workspace", "Shell"].join(""))) items.push("forbidden=workspace-shell");
|
|
5464
5518
|
if (/\bTopoloShell\b/.test(text)) items.push("legacy=TopoloShellFrame");
|
|
@@ -5488,6 +5542,16 @@ function hasLocalBrowserSurface(evidence) {
|
|
|
5488
5542
|
(file) => file.endsWith(".tsx") || file.endsWith(".jsx") || file.includes("/src/client/") || file.includes("/client/")
|
|
5489
5543
|
);
|
|
5490
5544
|
}
|
|
5545
|
+
function productionBrowserSourceRecords(evidence) {
|
|
5546
|
+
return evidence.source.records.filter((record4) => {
|
|
5547
|
+
const sourcePath = record4.path.toLowerCase();
|
|
5548
|
+
if (/(^|\/)(?:__tests__|test|tests|scripts|fixtures|migrations)(\/|$)/.test(sourcePath)) return false;
|
|
5549
|
+
if (/\.(?:test|spec)\.[^.]+$/.test(sourcePath)) return false;
|
|
5550
|
+
if (/\/apps\/(?:api|analytics|gateway|jobs|queue|runtime|worker)\//.test(sourcePath)) return false;
|
|
5551
|
+
if (/\.(?:tsx|jsx)$/.test(sourcePath)) return true;
|
|
5552
|
+
return /\/(?:web|marketing|desktop)\/src\//.test(sourcePath) && /\.(?:ts|js)$/.test(sourcePath);
|
|
5553
|
+
});
|
|
5554
|
+
}
|
|
5491
5555
|
function organizationScopeEvidence(evidence) {
|
|
5492
5556
|
const text = evidence.source.text;
|
|
5493
5557
|
const items = [];
|
|
@@ -5548,35 +5612,35 @@ function verificationEvidenceFor(evidence) {
|
|
|
5548
5612
|
"test:auth-contract": "test",
|
|
5549
5613
|
validate: "validate"
|
|
5550
5614
|
};
|
|
5551
|
-
for (const
|
|
5552
|
-
const scripts =
|
|
5553
|
-
const isRootPackage =
|
|
5615
|
+
for (const record4 of evidence.packageJsons) {
|
|
5616
|
+
const scripts = record4.packageJson.scripts ?? {};
|
|
5617
|
+
const isRootPackage = record4.path === evidence.packageJsonPath;
|
|
5554
5618
|
const seenCanonical = /* @__PURE__ */ new Set();
|
|
5555
5619
|
for (const [scriptName, canonical] of Object.entries(scriptAliases)) {
|
|
5556
5620
|
if (!scripts[scriptName]) continue;
|
|
5557
5621
|
if (seenCanonical.has(canonical)) continue;
|
|
5558
5622
|
seenCanonical.add(canonical);
|
|
5559
|
-
items.push(isRootPackage ? `script=${canonical}` : `script=${packageLabel(
|
|
5623
|
+
items.push(isRootPackage ? `script=${canonical}` : `script=${packageLabel(record4)}:${canonical}`);
|
|
5560
5624
|
}
|
|
5561
5625
|
}
|
|
5562
5626
|
return unique(items);
|
|
5563
5627
|
}
|
|
5564
5628
|
function dependencyNames(evidence) {
|
|
5565
5629
|
return new Set(
|
|
5566
|
-
evidence.packageJsons.flatMap((
|
|
5567
|
-
...Object.keys(
|
|
5568
|
-
...Object.keys(
|
|
5569
|
-
...Object.keys(
|
|
5630
|
+
evidence.packageJsons.flatMap((record4) => [
|
|
5631
|
+
...Object.keys(record4.packageJson.dependencies ?? {}),
|
|
5632
|
+
...Object.keys(record4.packageJson.devDependencies ?? {}),
|
|
5633
|
+
...Object.keys(record4.packageJson.peerDependencies ?? {})
|
|
5570
5634
|
])
|
|
5571
5635
|
);
|
|
5572
5636
|
}
|
|
5573
5637
|
function packageDependencyVersions(evidence, packageName) {
|
|
5574
5638
|
return unique(
|
|
5575
|
-
evidence.packageJsons.flatMap((
|
|
5639
|
+
evidence.packageJsons.flatMap((record4) => {
|
|
5576
5640
|
const specs = [
|
|
5577
|
-
|
|
5578
|
-
|
|
5579
|
-
|
|
5641
|
+
record4.packageJson.dependencies?.[packageName],
|
|
5642
|
+
record4.packageJson.devDependencies?.[packageName],
|
|
5643
|
+
record4.packageJson.peerDependencies?.[packageName]
|
|
5580
5644
|
];
|
|
5581
5645
|
return specs.filter((spec) => typeof spec === "string");
|
|
5582
5646
|
})
|
|
@@ -5604,7 +5668,7 @@ function matchingSystems(repoRoot, application) {
|
|
|
5604
5668
|
const docsRoot = topoloDocsRoot(repoRoot);
|
|
5605
5669
|
const systemsRoot = path.join(docsRoot, "src", "content", "systems");
|
|
5606
5670
|
if (!isDirectory(systemsRoot)) return [];
|
|
5607
|
-
const appRepoPath = applicationSourcePath(application.appDir);
|
|
5671
|
+
const appRepoPath = applicationSourcePath(repoRoot, application.appDir);
|
|
5608
5672
|
return readdirSync(systemsRoot).filter((entry) => entry.endsWith(".json")).map((entry) => readJson(path.join(systemsRoot, entry))).filter((system) => Boolean(system)).filter((system) => {
|
|
5609
5673
|
const repoPaths = [...system.repo_paths ?? [], ...system.repoPaths ?? []];
|
|
5610
5674
|
return repoPaths.some(
|
|
@@ -5627,11 +5691,19 @@ function existingDocs(repoRoot, systems) {
|
|
|
5627
5691
|
function topoloDocsRoot(repoRoot) {
|
|
5628
5692
|
return path.join(repoRoot, "system-apps", "TopoloDocs", "apps", "web");
|
|
5629
5693
|
}
|
|
5630
|
-
|
|
5694
|
+
var APPLICATION_SOURCE_ROOTS = [
|
|
5695
|
+
"apps",
|
|
5696
|
+
"system-apps",
|
|
5697
|
+
"topolo-platform/services",
|
|
5698
|
+
"topolo-platform/core",
|
|
5699
|
+
"topolo-platform/packages"
|
|
5700
|
+
];
|
|
5701
|
+
function applicationSourcePath(repoRoot, appDir) {
|
|
5631
5702
|
if (appDir === "TopoloCli" || appDir === "TopoloCLI") return "topolo-platform/packages/topolo-cli";
|
|
5632
|
-
|
|
5633
|
-
|
|
5634
|
-
|
|
5703
|
+
for (const root of APPLICATION_SOURCE_ROOTS) {
|
|
5704
|
+
const relative4 = `${root}/${appDir}`;
|
|
5705
|
+
if (isDirectory(path.join(repoRoot, relative4))) return relative4;
|
|
5706
|
+
}
|
|
5635
5707
|
return `apps/${appDir}`;
|
|
5636
5708
|
}
|
|
5637
5709
|
function docExists(contentRoot, docPath) {
|
|
@@ -5647,18 +5719,21 @@ function findWranglerFiles(appRoot) {
|
|
|
5647
5719
|
return ["wrangler.json", "wrangler.jsonc", "wrangler.toml"].filter((file) => isFile(path.join(appRoot, file))).map((file) => relativeTopoloPath(path.join(appRoot, file)));
|
|
5648
5720
|
}
|
|
5649
5721
|
function readSourceIndex(root) {
|
|
5650
|
-
if (!isDirectory(root)) return { files: [], text: "" };
|
|
5722
|
+
if (!isDirectory(root)) return { files: [], text: "", records: [] };
|
|
5651
5723
|
const files = collectSourceFiles(root, 4e3);
|
|
5652
|
-
const
|
|
5724
|
+
const records = files.map((file) => {
|
|
5725
|
+
let text = "";
|
|
5653
5726
|
try {
|
|
5654
|
-
|
|
5727
|
+
text = readFileSync3(file, "utf8");
|
|
5655
5728
|
} catch {
|
|
5656
|
-
|
|
5729
|
+
text = "";
|
|
5657
5730
|
}
|
|
5731
|
+
return { path: relativeTopoloPath(file), text };
|
|
5658
5732
|
});
|
|
5659
5733
|
return {
|
|
5660
|
-
files:
|
|
5661
|
-
text:
|
|
5734
|
+
files: records.map((record4) => record4.path),
|
|
5735
|
+
text: records.map((record4) => record4.text).join("\n"),
|
|
5736
|
+
records
|
|
5662
5737
|
};
|
|
5663
5738
|
}
|
|
5664
5739
|
function collectSourceFiles(root, limit) {
|
|
@@ -5702,8 +5777,8 @@ function collectPackageJsons(root) {
|
|
|
5702
5777
|
function readPackageJson(filePath) {
|
|
5703
5778
|
return readJson(filePath);
|
|
5704
5779
|
}
|
|
5705
|
-
function packageLabel(
|
|
5706
|
-
return
|
|
5780
|
+
function packageLabel(record4) {
|
|
5781
|
+
return record4.packageJson.name ?? relativeTopoloPath(path.dirname(record4.path));
|
|
5707
5782
|
}
|
|
5708
5783
|
function sortedDirEntries(dir) {
|
|
5709
5784
|
return readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
@@ -5792,14 +5867,14 @@ import { constants } from "fs";
|
|
|
5792
5867
|
import { access } from "fs/promises";
|
|
5793
5868
|
import { relative, resolve } from "path";
|
|
5794
5869
|
function normalizeHomePath(value) {
|
|
5795
|
-
const
|
|
5796
|
-
if (!
|
|
5870
|
+
const path8 = String(value || "/dashboard").trim();
|
|
5871
|
+
if (!path8.startsWith("/") || path8.startsWith("//") || /[?#]/.test(path8)) {
|
|
5797
5872
|
throw new Error("Invalid home path. Use an absolute application path such as /dashboard.");
|
|
5798
5873
|
}
|
|
5799
|
-
if (["/", "/login", "/auth/callback", "/getting-started"].includes(
|
|
5874
|
+
if (["/", "/login", "/auth/callback", "/getting-started"].includes(path8)) {
|
|
5800
5875
|
throw new Error("Home path must be a protected product route, not a reserved entry route.");
|
|
5801
5876
|
}
|
|
5802
|
-
return
|
|
5877
|
+
return path8.replace(/\/$/, "") || "/dashboard";
|
|
5803
5878
|
}
|
|
5804
5879
|
function normalizeHomeLabel(value) {
|
|
5805
5880
|
const label = String(value || "Dashboard").trim();
|
|
@@ -5880,16 +5955,6 @@ function normalizeScaffoldProfile(value) {
|
|
|
5880
5955
|
if (value === "spatial-assets") return value;
|
|
5881
5956
|
throw new Error("Invalid scaffold profile. Use spatial-assets.");
|
|
5882
5957
|
}
|
|
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
5958
|
function normalizeContainerAppId(value, surfaceModel) {
|
|
5894
5959
|
const trimmed = String(value || "").trim();
|
|
5895
5960
|
if (surfaceModel === "mobile_module") {
|
|
@@ -5940,9 +6005,9 @@ function toTitle(value) {
|
|
|
5940
6005
|
function json(value) {
|
|
5941
6006
|
return JSON.stringify(value, null, 2);
|
|
5942
6007
|
}
|
|
5943
|
-
async function pathExists(
|
|
6008
|
+
async function pathExists(path8) {
|
|
5944
6009
|
try {
|
|
5945
|
-
await access(
|
|
6010
|
+
await access(path8, constants.F_OK);
|
|
5946
6011
|
return true;
|
|
5947
6012
|
} catch {
|
|
5948
6013
|
return false;
|
|
@@ -5950,8 +6015,8 @@ async function pathExists(path6) {
|
|
|
5950
6015
|
}
|
|
5951
6016
|
function resolveInside(root, filePath) {
|
|
5952
6017
|
const resolved = resolve(root, filePath);
|
|
5953
|
-
const
|
|
5954
|
-
if (
|
|
6018
|
+
const relativePath2 = relative(root, resolved);
|
|
6019
|
+
if (relativePath2.startsWith("..") || relativePath2 === "" || relativePath2.startsWith("/")) {
|
|
5955
6020
|
throw new Error(`Scaffold file path escapes the target root: ${filePath}`);
|
|
5956
6021
|
}
|
|
5957
6022
|
return resolved;
|
|
@@ -5978,48 +6043,6 @@ var STAGING_CLOUDFLARE_ACCOUNT_ID = "4f4e1c69a3830946f9fea7b1eb7531ac";
|
|
|
5978
6043
|
var DEVELOPMENT_CLOUDFLARE_ACCOUNT_ID = "0ebdeec262a245481a721fc4f9d6d798";
|
|
5979
6044
|
var CLOUDFLARE_COMPATIBILITY_DATE = "2026-05-05";
|
|
5980
6045
|
|
|
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
6046
|
// src/commands/app-scaffold-package-template.ts
|
|
6024
6047
|
function packageJson(application, target, options) {
|
|
6025
6048
|
const dependencies = {};
|
|
@@ -6044,9 +6067,6 @@ function packageJson(application, target, options) {
|
|
|
6044
6067
|
devDependencies.autoprefixer = "10.5.0";
|
|
6045
6068
|
devDependencies.postcss = "8.5.6";
|
|
6046
6069
|
devDependencies.tailwindcss = "3.4.19";
|
|
6047
|
-
for (const packageName of PLATFORM_BROWSER_TOPOLO_PACKAGES) {
|
|
6048
|
-
dependencies[packageName] = topoloPackageVersion(packageName, target);
|
|
6049
|
-
}
|
|
6050
6070
|
dependencies["react-router-dom"] = "^7.6.2";
|
|
6051
6071
|
} else {
|
|
6052
6072
|
dependencies["@topolo/login"] = topoloPackageVersion("@topolo/login", target);
|
|
@@ -6055,9 +6075,6 @@ function packageJson(application, target, options) {
|
|
|
6055
6075
|
if (options.api) {
|
|
6056
6076
|
devDependencies["@cloudflare/workers-types"] = "^5.20260716.1";
|
|
6057
6077
|
if (target === "platform") {
|
|
6058
|
-
for (const packageName of PLATFORM_API_TOPOLO_PACKAGES) {
|
|
6059
|
-
dependencies[packageName] = topoloPackageVersion(packageName, target);
|
|
6060
|
-
}
|
|
6061
6078
|
dependencies.hono = "^4";
|
|
6062
6079
|
dependencies.zod = "^4.4.3";
|
|
6063
6080
|
scripts.deploy = `node -e 'console.error("\\nX Bare deploy is disabled. Use deploy:staging or deploy:production.\\n"); process.exit(1)'`;
|
|
@@ -6069,6 +6086,9 @@ function packageJson(application, target, options) {
|
|
|
6069
6086
|
}
|
|
6070
6087
|
devDependencies.wrangler = "^4.91.0";
|
|
6071
6088
|
}
|
|
6089
|
+
for (const packageName of options.packages) {
|
|
6090
|
+
dependencies[packageName] = topoloPackageVersion(packageName, target);
|
|
6091
|
+
}
|
|
6072
6092
|
if (target === "platform") {
|
|
6073
6093
|
const testDirs = [];
|
|
6074
6094
|
if (options.browser) testDirs.push("apps/web");
|
|
@@ -6090,7 +6110,7 @@ function packageJson(application, target, options) {
|
|
|
6090
6110
|
}
|
|
6091
6111
|
}
|
|
6092
6112
|
}
|
|
6093
|
-
if (target === "platform" &&
|
|
6113
|
+
if (target === "platform" && (options.api || options.browser)) {
|
|
6094
6114
|
const deployChecks = [
|
|
6095
6115
|
...options.api ? ["wrangler deploy --config apps/api/wrangler.toml --env staging --dry-run --outdir .wrangler/api-build"] : [],
|
|
6096
6116
|
...options.browser ? ["wrangler deploy --config apps/web/wrangler.toml --env staging --dry-run --outdir .wrangler/web-build"] : []
|
|
@@ -6174,6 +6194,320 @@ function resourceConfig(application, profile) {
|
|
|
6174
6194
|
};
|
|
6175
6195
|
}
|
|
6176
6196
|
|
|
6197
|
+
// src/commands/app-capability-registry.ts
|
|
6198
|
+
var APPLICATION_CAPABILITY_REGISTRY_VERSION = "1";
|
|
6199
|
+
var TOPOLO_APP_CAPABILITY_IDS = [
|
|
6200
|
+
"login",
|
|
6201
|
+
"launcher",
|
|
6202
|
+
"marketplace",
|
|
6203
|
+
"hosting",
|
|
6204
|
+
"api",
|
|
6205
|
+
"widget",
|
|
6206
|
+
"onboarding",
|
|
6207
|
+
"resource_management",
|
|
6208
|
+
"notifications",
|
|
6209
|
+
"notification_producer",
|
|
6210
|
+
"billing",
|
|
6211
|
+
"metering",
|
|
6212
|
+
"nexus_ai",
|
|
6213
|
+
"integrations",
|
|
6214
|
+
"webhooks",
|
|
6215
|
+
"automation",
|
|
6216
|
+
"agent_access",
|
|
6217
|
+
"data",
|
|
6218
|
+
"object_storage",
|
|
6219
|
+
"background_jobs",
|
|
6220
|
+
"durable_workflows",
|
|
6221
|
+
"privacy",
|
|
6222
|
+
"chat",
|
|
6223
|
+
"calendar",
|
|
6224
|
+
"mailbox",
|
|
6225
|
+
"mobile"
|
|
6226
|
+
];
|
|
6227
|
+
var bothTargets = ["platform", "developer"];
|
|
6228
|
+
var platformTarget = ["platform"];
|
|
6229
|
+
var APPLICATION_CAPABILITY_REGISTRY = {
|
|
6230
|
+
login: capability("login", "TopoloAuth", bothTargets, [], ["browser"], {
|
|
6231
|
+
platform: ["@topolo-io/auth-client", "@topolo-io/app-shell"],
|
|
6232
|
+
developer: ["@topolo/login"]
|
|
6233
|
+
}, ["openid", "profile", "email", "organization:read"], [], [], ["auth-client"], ["auth-login-smoke"]),
|
|
6234
|
+
launcher: capability("launcher", "TopoloDevelopers + TopoloAuth", bothTargets, ["login"], ["browser"], {
|
|
6235
|
+
platform: ["@topolo-io/app-funnel", "@topolo-io/app-launcher", "@topolo-io/app-shell", "@topolo-io/design-tokens", "@topolo-io/ui-kit"],
|
|
6236
|
+
developer: []
|
|
6237
|
+
}, [], [], [], ["launcher-registration"], ["launcher-catalog-smoke"]),
|
|
6238
|
+
marketplace: capability("marketplace", "TopoloDevelopers", bothTargets, ["launcher"], ["browser"], emptyPackages(), [], [], [], ["marketplace-registration"], ["marketplace-projection-check"]),
|
|
6239
|
+
hosting: capability("hosting", "TopoloCloudControl or external owner", bothTargets, [], [], emptyPackages(), [], [], [], ["environment-manifest", "health-contract"], ["environment-health-smoke"]),
|
|
6240
|
+
api: capability("api", "Application runtime + TopoloAuth", bothTargets, [], ["api"], {
|
|
6241
|
+
platform: ["@topolo-io/auth-middleware", "@topolo-io/error-middleware", "@topolo-io/security-headers", "@topolo-io/worker-runtime", "@topolo/sdk"],
|
|
6242
|
+
developer: ["@topolo/sdk"]
|
|
6243
|
+
}, ["entitlements:read"], ["workspace:read", "workspace:write"], [], ["protected-worker"], ["api-auth-allow-deny"]),
|
|
6244
|
+
widget: capability("widget", "Application runtime + TopoloDevelopers", bothTargets, ["api"], ["api"], {
|
|
6245
|
+
platform: ["@topolo-io/widget-server", "@topolo/sdk"],
|
|
6246
|
+
developer: ["@topolo/sdk"]
|
|
6247
|
+
}, [], ["workspace:read"], [], ["widget-route", "widget-action-contract"], ["widget-contract-smoke"]),
|
|
6248
|
+
onboarding: capability("onboarding", "TopoloAuth + shared onboarding packages", platformTarget, ["launcher"], ["browser"], {
|
|
6249
|
+
platform: ["@topolo-io/onboarding", "@topolo-io/tour"],
|
|
6250
|
+
developer: []
|
|
6251
|
+
}, [], [], [], ["getting-started-route"], ["onboarding-progress-smoke"]),
|
|
6252
|
+
resource_management: capability("resource_management", "TopoloAuth + application resource catalog", platformTarget, ["launcher"], ["browser"], {
|
|
6253
|
+
platform: ["@topolo-io/api-key-management", "@topolo-io/resource-context"],
|
|
6254
|
+
developer: []
|
|
6255
|
+
}, [], ["workspace:read"], [], ["resource-selector"], ["resource-binding-smoke"]),
|
|
6256
|
+
notifications: capability("notifications", "TopoloNotify + TopoloAuth", platformTarget, ["launcher"], ["browser"], {
|
|
6257
|
+
platform: ["@topolo-io/notifications"],
|
|
6258
|
+
developer: []
|
|
6259
|
+
}, [], [], [], ["notification-consumer"], ["notification-inbox-smoke"]),
|
|
6260
|
+
notification_producer: capability("notification_producer", "TopoloNotify + TopoloDevelopers", platformTarget, ["api"], ["api"], {
|
|
6261
|
+
platform: ["@topolo-io/notifications", "@topolo-io/auth-middleware"],
|
|
6262
|
+
developer: []
|
|
6263
|
+
}, [], ["events:write"], [], ["notification-producer-adapter", "notification-contracts"], ["notification-delivery-smoke"]),
|
|
6264
|
+
billing: capability("billing", "TopoloPay + TopoloAuth", bothTargets, ["launcher"], ["browser"], {
|
|
6265
|
+
platform: ["@topolo-io/billing-manifest", "@topolo-io/billing-ui"],
|
|
6266
|
+
developer: []
|
|
6267
|
+
}, ["entitlements:read"], [], [], ["pricing-manifest"], ["pricing-registration-smoke"]),
|
|
6268
|
+
metering: capability("metering", "TopoloNexus", platformTarget, ["api"], ["api"], {
|
|
6269
|
+
platform: ["@topolo-io/nexus-client", "@topolo-io/worker-runtime"],
|
|
6270
|
+
developer: []
|
|
6271
|
+
}, [], ["usage:write"], [], ["nexus-usage-adapter"], ["usage-attribution-smoke"]),
|
|
6272
|
+
nexus_ai: capability("nexus_ai", "TopoloNexus", platformTarget, ["metering"], ["api"], {
|
|
6273
|
+
platform: ["@topolo-io/nexus-client"],
|
|
6274
|
+
developer: []
|
|
6275
|
+
}, [], ["usage:write"], [], ["nexus-ai-adapter"], ["ai-budget-rejection-smoke"]),
|
|
6276
|
+
integrations: capability("integrations", "TopoloNexus", platformTarget, ["api"], ["api"], {
|
|
6277
|
+
platform: ["@topolo-io/nexus-client"],
|
|
6278
|
+
developer: []
|
|
6279
|
+
}, [], [], [], ["integration-connection-adapter"], ["integration-connection-smoke"]),
|
|
6280
|
+
webhooks: capability("webhooks", "TopoloNotify", bothTargets, ["api"], ["api"], {
|
|
6281
|
+
platform: ["@topolo-io/notifications"],
|
|
6282
|
+
developer: []
|
|
6283
|
+
}, [], [], [], ["webhook-receiver"], ["webhook-signature-smoke"]),
|
|
6284
|
+
automation: capability("automation", "TopoloFlow + application runtime", platformTarget, ["api"], ["api"], {
|
|
6285
|
+
platform: ["@topolo/sdk"],
|
|
6286
|
+
developer: []
|
|
6287
|
+
}, [], [], [], ["flow-adapter"], ["flow-run-smoke"]),
|
|
6288
|
+
agent_access: capability("agent_access", "TopoloDevelopers + TopoloAgent", bothTargets, ["widget"], ["api"], {
|
|
6289
|
+
platform: ["@topolo-io/worker-runtime", "@topolo/sdk"],
|
|
6290
|
+
developer: ["@topolo/sdk"]
|
|
6291
|
+
}, [], ["workspace:read"], [], ["action-contracts"], ["credential-scoped-action-smoke"]),
|
|
6292
|
+
data: capability("data", "Application runtime through @topolo-io/data", platformTarget, ["api"], ["api"], {
|
|
6293
|
+
platform: ["@topolo-io/data"],
|
|
6294
|
+
developer: []
|
|
6295
|
+
}, [], ["workspace:read", "workspace:write"], ["d1"], ["workspace-migration", "data-port"], ["tenant-isolation-smoke"]),
|
|
6296
|
+
object_storage: capability("object_storage", "Application runtime through @topolo-io/data", platformTarget, ["data"], ["api"], {
|
|
6297
|
+
platform: ["@topolo-io/data"],
|
|
6298
|
+
developer: []
|
|
6299
|
+
}, [], ["workspace:read", "workspace:write"], ["r2"], ["object-store-port"], ["object-lifecycle-smoke"]),
|
|
6300
|
+
background_jobs: capability("background_jobs", "Application runtime", platformTarget, ["api"], ["api"], emptyPackages(), [], ["workspace:write"], ["queue"], ["queue-producer", "queue-consumer"], ["queue-execution-smoke"]),
|
|
6301
|
+
durable_workflows: capability("durable_workflows", "Application runtime + TopoloFlow", platformTarget, ["background_jobs"], ["api"], emptyPackages(), [], ["workspace:write"], ["workflow"], ["workflow-entrypoint"], ["workflow-execution-smoke"]),
|
|
6302
|
+
privacy: capability("privacy", "TopoloConsent + application data owner", platformTarget, ["api"], ["api"], emptyPackages(), [], [], [], ["privacy-adapter", "retention-declaration"], ["dsr-export-delete-smoke"]),
|
|
6303
|
+
chat: capability("chat", "TopoloChat", platformTarget, ["launcher"], ["browser"], {
|
|
6304
|
+
platform: ["@topolo-io/chat-widget"],
|
|
6305
|
+
developer: []
|
|
6306
|
+
}, [], [], [], ["chat-surface"], ["chat-surface-smoke"]),
|
|
6307
|
+
calendar: capability("calendar", "TopoloCalendar", platformTarget, ["launcher"], ["browser"], {
|
|
6308
|
+
platform: ["@topolo-io/calendar"],
|
|
6309
|
+
developer: []
|
|
6310
|
+
}, [], [], [], ["calendar-surface"], ["calendar-surface-smoke"]),
|
|
6311
|
+
mailbox: capability("mailbox", "TopoloMail", platformTarget, ["launcher"], ["browser"], {
|
|
6312
|
+
platform: ["@topolo-io/mailbox"],
|
|
6313
|
+
developer: []
|
|
6314
|
+
}, [], [], [], ["mailbox-surface"], ["mailbox-surface-smoke"]),
|
|
6315
|
+
mobile: capability("mobile", "TopoloOne + application native runtime", bothTargets, [], ["mobile"], {
|
|
6316
|
+
platform: ["@topolo-io/bridge", "@topolo-io/mobile-error-middleware", "@topolo-io/mobile-experience-contracts"],
|
|
6317
|
+
developer: []
|
|
6318
|
+
}, [], [], [], ["mobile-experience-metadata"], ["native-experience-smoke"])
|
|
6319
|
+
};
|
|
6320
|
+
var PROFILE_CAPABILITIES = {
|
|
6321
|
+
"spatial-assets": ["object_storage", "background_jobs", "durable_workflows"]
|
|
6322
|
+
};
|
|
6323
|
+
function resolveApplicationCapabilities(options) {
|
|
6324
|
+
const requested = options.requested ?? [];
|
|
6325
|
+
const unknown = requested.filter((id) => !isTopoloAppCapability(id));
|
|
6326
|
+
if (unknown.length > 0) {
|
|
6327
|
+
throw new Error(`Unknown application capability: ${unknown.join(", ")}. Supported capabilities: ${TOPOLO_APP_CAPABILITY_IDS.join(", ")}.`);
|
|
6328
|
+
}
|
|
6329
|
+
if (options.profile && !PROFILE_CAPABILITIES[options.profile]) {
|
|
6330
|
+
throw new Error(`Unknown capability profile: ${options.profile}.`);
|
|
6331
|
+
}
|
|
6332
|
+
const selected = /* @__PURE__ */ new Set();
|
|
6333
|
+
const add = (id) => {
|
|
6334
|
+
const definition = APPLICATION_CAPABILITY_REGISTRY[id];
|
|
6335
|
+
if (!definition.targets.includes(options.target)) {
|
|
6336
|
+
throw new Error(`Capability ${id} is not available to ${options.target} applications.`);
|
|
6337
|
+
}
|
|
6338
|
+
for (const dependency of definition.requires) add(dependency);
|
|
6339
|
+
selected.add(id);
|
|
6340
|
+
};
|
|
6341
|
+
if (options.browser) {
|
|
6342
|
+
add("login");
|
|
6343
|
+
add("launcher");
|
|
6344
|
+
add("hosting");
|
|
6345
|
+
if (options.target === "platform") {
|
|
6346
|
+
add("onboarding");
|
|
6347
|
+
add("resource_management");
|
|
6348
|
+
add("notifications");
|
|
6349
|
+
}
|
|
6350
|
+
}
|
|
6351
|
+
if (options.distributionModel === "marketplace" && options.browser) add("marketplace");
|
|
6352
|
+
if (options.api) {
|
|
6353
|
+
add("api");
|
|
6354
|
+
add("widget");
|
|
6355
|
+
if (options.target === "platform") add("agent_access");
|
|
6356
|
+
}
|
|
6357
|
+
if (options.mobile) add("mobile");
|
|
6358
|
+
for (const id of options.profile ? PROFILE_CAPABILITIES[options.profile] ?? [] : []) add(id);
|
|
6359
|
+
for (const id of requested) add(id);
|
|
6360
|
+
for (const id of selected) {
|
|
6361
|
+
const definition = APPLICATION_CAPABILITY_REGISTRY[id];
|
|
6362
|
+
if (definition.surfaces.includes("browser") && !options.browser) {
|
|
6363
|
+
throw new Error(`Capability ${id} requires a browser surface.`);
|
|
6364
|
+
}
|
|
6365
|
+
if (definition.surfaces.includes("api") && !options.api) {
|
|
6366
|
+
throw new Error(`Capability ${id} requires an API surface.`);
|
|
6367
|
+
}
|
|
6368
|
+
if (definition.surfaces.includes("mobile") && !options.mobile) {
|
|
6369
|
+
throw new Error(`Capability ${id} requires a mobile surface model.`);
|
|
6370
|
+
}
|
|
6371
|
+
}
|
|
6372
|
+
const ordered = TOPOLO_APP_CAPABILITY_IDS.filter((id) => selected.has(id));
|
|
6373
|
+
const definitions = ordered.map((id) => APPLICATION_CAPABILITY_REGISTRY[id]);
|
|
6374
|
+
const targetPackages = options.target === "platform" ? "platform" : "developer";
|
|
6375
|
+
return {
|
|
6376
|
+
registryVersion: APPLICATION_CAPABILITY_REGISTRY_VERSION,
|
|
6377
|
+
selected: ordered,
|
|
6378
|
+
declarations: ordered.map((id) => ({ id, version: 1 })),
|
|
6379
|
+
packages: unique2(definitions.flatMap((definition) => definition.packages[targetPackages])),
|
|
6380
|
+
authScopes: unique2(definitions.flatMap((definition) => definition.authScopes)),
|
|
6381
|
+
servicePermissions: unique2(definitions.flatMap((definition) => definition.servicePermissions)),
|
|
6382
|
+
resources: unique2(definitions.flatMap((definition) => definition.resources)),
|
|
6383
|
+
artifacts: unique2(definitions.flatMap((definition) => definition.artifacts)),
|
|
6384
|
+
stagingEvidence: unique2(definitions.flatMap((definition) => definition.stagingEvidence))
|
|
6385
|
+
};
|
|
6386
|
+
}
|
|
6387
|
+
function isTopoloAppCapability(value) {
|
|
6388
|
+
return TOPOLO_APP_CAPABILITY_IDS.includes(value);
|
|
6389
|
+
}
|
|
6390
|
+
function capability(id, owner, targets, requires, surfaces, packages, authScopes, servicePermissions, resources, artifacts, stagingEvidence) {
|
|
6391
|
+
return { id, version: 1, owner, targets, requires, surfaces, packages, authScopes, servicePermissions, resources, artifacts, stagingEvidence };
|
|
6392
|
+
}
|
|
6393
|
+
function emptyPackages() {
|
|
6394
|
+
return { platform: [], developer: [] };
|
|
6395
|
+
}
|
|
6396
|
+
function unique2(values) {
|
|
6397
|
+
return Array.from(new Set(values));
|
|
6398
|
+
}
|
|
6399
|
+
|
|
6400
|
+
// src/commands/app-capability-artifacts.ts
|
|
6401
|
+
function capabilityCompositionProjection(capabilities) {
|
|
6402
|
+
return {
|
|
6403
|
+
registryVersion: capabilities.registryVersion,
|
|
6404
|
+
selected: capabilities.declarations,
|
|
6405
|
+
artifacts: capabilities.artifacts,
|
|
6406
|
+
stagingEvidence: capabilities.stagingEvidence
|
|
6407
|
+
};
|
|
6408
|
+
}
|
|
6409
|
+
function capabilityContractProjection(capabilities) {
|
|
6410
|
+
return {
|
|
6411
|
+
registryVersion: APPLICATION_CAPABILITY_REGISTRY_VERSION,
|
|
6412
|
+
selected: capabilities.selected.map((id) => {
|
|
6413
|
+
const definition = APPLICATION_CAPABILITY_REGISTRY[id];
|
|
6414
|
+
return {
|
|
6415
|
+
id,
|
|
6416
|
+
version: definition.version,
|
|
6417
|
+
owner: definition.owner,
|
|
6418
|
+
packages: definition.packages,
|
|
6419
|
+
authScopes: definition.authScopes,
|
|
6420
|
+
servicePermissions: definition.servicePermissions,
|
|
6421
|
+
resources: definition.resources,
|
|
6422
|
+
artifacts: definition.artifacts,
|
|
6423
|
+
stagingEvidence: definition.stagingEvidence
|
|
6424
|
+
};
|
|
6425
|
+
})
|
|
6426
|
+
};
|
|
6427
|
+
}
|
|
6428
|
+
function defaultPricingManifest(options) {
|
|
6429
|
+
if (!options.appId) throw new Error("Billing composition requires a canonical appId.");
|
|
6430
|
+
return {
|
|
6431
|
+
appId: options.appId,
|
|
6432
|
+
developerOrgId: null,
|
|
6433
|
+
schemaVersion: 1,
|
|
6434
|
+
marketingCopy: { tagline: options.description, marketplaceBadges: ["free"] },
|
|
6435
|
+
meters: [],
|
|
6436
|
+
plans: [{
|
|
6437
|
+
id: "free",
|
|
6438
|
+
tier: "free",
|
|
6439
|
+
displayName: "Free",
|
|
6440
|
+
summary: `Start using ${options.displayName}.`,
|
|
6441
|
+
pricing: { model: "free" },
|
|
6442
|
+
bullets: ["Shared Topolo identity and application access"],
|
|
6443
|
+
isDefault: true
|
|
6444
|
+
}]
|
|
6445
|
+
};
|
|
6446
|
+
}
|
|
6447
|
+
|
|
6448
|
+
// src/commands/app-capability-adapter-template.ts
|
|
6449
|
+
function managedCapabilityAdapter(capabilities) {
|
|
6450
|
+
const notifications = capabilities.includes("notification_producer");
|
|
6451
|
+
const nexus = capabilities.some((capability2) => capability2 === "metering" || capability2 === "nexus_ai" || capability2 === "integrations");
|
|
6452
|
+
const webhooks = capabilities.includes("webhooks");
|
|
6453
|
+
if (!notifications && !nexus && !webhooks) return null;
|
|
6454
|
+
return [
|
|
6455
|
+
...notifications || nexus ? ["import { createServiceClient } from '@topolo-io/auth-middleware';"] : [],
|
|
6456
|
+
...notifications ? ["import { TopoloEvents } from '@topolo-io/notifications';"] : [],
|
|
6457
|
+
...webhooks ? ["import { WEBHOOK_SIGNATURE_HEADER, verifyWebhookSignature } from '@topolo-io/notifications';"] : [],
|
|
6458
|
+
...nexus ? ["import { createNexusClient } from '@topolo-io/nexus-client';"] : [],
|
|
6459
|
+
"",
|
|
6460
|
+
"export interface TopoloCapabilityEnv {",
|
|
6461
|
+
" APP_ID?: string;",
|
|
6462
|
+
" TOPOLO_SERVICE_CLIENT_SECRET?: string;",
|
|
6463
|
+
...notifications ? [" TOPOLO_NOTIFY_URL?: string;"] : [],
|
|
6464
|
+
...nexus ? [" NEXUS_GATEWAY_URL?: string;"] : [],
|
|
6465
|
+
...webhooks ? [" TOPOLO_WEBHOOK_SECRET?: string;"] : [],
|
|
6466
|
+
"}",
|
|
6467
|
+
...notifications ? [
|
|
6468
|
+
"",
|
|
6469
|
+
"export function createTopoloEvents(env: TopoloCapabilityEnv): TopoloEvents {",
|
|
6470
|
+
" if (!env.APP_ID) throw new Error('APP_ID is required for notification attribution.');",
|
|
6471
|
+
" if (!env.TOPOLO_NOTIFY_URL) throw new Error('TOPOLO_NOTIFY_URL is required for notification delivery.');",
|
|
6472
|
+
" return new TopoloEvents({",
|
|
6473
|
+
" appId: env.APP_ID,",
|
|
6474
|
+
" notifyBaseUrl: env.TOPOLO_NOTIFY_URL,",
|
|
6475
|
+
" serviceClient: createServiceClient(env),",
|
|
6476
|
+
" });",
|
|
6477
|
+
"}"
|
|
6478
|
+
] : [],
|
|
6479
|
+
...nexus ? [
|
|
6480
|
+
"",
|
|
6481
|
+
"export function createTopoloNexus(env: TopoloCapabilityEnv, organizationId: string) {",
|
|
6482
|
+
" if (!env.APP_ID) throw new Error('APP_ID is required for Nexus attribution.');",
|
|
6483
|
+
" if (!env.NEXUS_GATEWAY_URL) throw new Error('NEXUS_GATEWAY_URL is required for Nexus requests.');",
|
|
6484
|
+
" if (!organizationId) throw new Error('Verified organizationId is required for Nexus requests.');",
|
|
6485
|
+
" const serviceClient = createServiceClient(env);",
|
|
6486
|
+
" return createNexusClient({",
|
|
6487
|
+
" appId: env.APP_ID,",
|
|
6488
|
+
" gatewayUrl: env.NEXUS_GATEWAY_URL,",
|
|
6489
|
+
" organizationId,",
|
|
6490
|
+
" serviceTokenProvider: (scope) => serviceClient.getToken(scope ?? 'usage:write'),",
|
|
6491
|
+
" });",
|
|
6492
|
+
"}"
|
|
6493
|
+
] : [],
|
|
6494
|
+
...webhooks ? [
|
|
6495
|
+
"",
|
|
6496
|
+
"export async function verifyTopoloWebhookRequest(",
|
|
6497
|
+
" request: Request,",
|
|
6498
|
+
" env: TopoloCapabilityEnv,",
|
|
6499
|
+
"): Promise<{ ok: true; body: string } | { ok: false; response: Response }> {",
|
|
6500
|
+
" if (!env.TOPOLO_WEBHOOK_SECRET) return { ok: false, response: Response.json({ error: 'webhook_secret_missing' }, { status: 503 }) };",
|
|
6501
|
+
" const body = await request.text();",
|
|
6502
|
+
" const valid = await verifyWebhookSignature(env.TOPOLO_WEBHOOK_SECRET, body, request.headers.get(WEBHOOK_SIGNATURE_HEADER));",
|
|
6503
|
+
" if (!valid) return { ok: false, response: Response.json({ error: 'invalid_webhook_signature' }, { status: 401 }) };",
|
|
6504
|
+
" return { ok: true, body };",
|
|
6505
|
+
"}"
|
|
6506
|
+
] : [],
|
|
6507
|
+
""
|
|
6508
|
+
].join("\n");
|
|
6509
|
+
}
|
|
6510
|
+
|
|
6177
6511
|
// src/commands/app-scaffold-browser-template.ts
|
|
6178
6512
|
function readme(application, target, options) {
|
|
6179
6513
|
const displayName = toTitle(application.name);
|
|
@@ -7490,9 +7824,15 @@ function platformWorkerTs(application, options) {
|
|
|
7490
7824
|
];
|
|
7491
7825
|
const workflowExport = options.resources.workflow ? [
|
|
7492
7826
|
"",
|
|
7493
|
-
`export class ${resource.workflowClassName} extends WorkflowEntrypoint<Env,
|
|
7494
|
-
" async run(
|
|
7495
|
-
"
|
|
7827
|
+
`export class ${resource.workflowClassName} extends WorkflowEntrypoint<Env, CapabilityWorkflowInput> {`,
|
|
7828
|
+
" async run(event: Readonly<WorkflowEvent<CapabilityWorkflowInput>>, step: WorkflowStep): Promise<unknown> {",
|
|
7829
|
+
" if (event.payload.type !== 'capability.probe' || !event.payload.workspaceId) {",
|
|
7830
|
+
" throw new Error('Unsupported workflow message. Implement a vertical workflow contract before dispatching it.');",
|
|
7831
|
+
" }",
|
|
7832
|
+
" return step.do('capability probe', async () => ({",
|
|
7833
|
+
" status: 'completed',",
|
|
7834
|
+
" workspaceId: event.payload.workspaceId,",
|
|
7835
|
+
" }));",
|
|
7496
7836
|
" }",
|
|
7497
7837
|
"}"
|
|
7498
7838
|
] : [];
|
|
@@ -7527,6 +7867,21 @@ function platformWorkerTs(application, options) {
|
|
|
7527
7867
|
"export interface Env {",
|
|
7528
7868
|
...envBindings,
|
|
7529
7869
|
"}",
|
|
7870
|
+
...options.resources.queue ? [
|
|
7871
|
+
"",
|
|
7872
|
+
"interface CapabilityJob {",
|
|
7873
|
+
" type: 'capability.probe';",
|
|
7874
|
+
" workspaceId: string;",
|
|
7875
|
+
" payload?: unknown;",
|
|
7876
|
+
"}"
|
|
7877
|
+
] : [],
|
|
7878
|
+
...options.resources.workflow ? [
|
|
7879
|
+
"",
|
|
7880
|
+
"interface CapabilityWorkflowInput {",
|
|
7881
|
+
" type: 'capability.probe';",
|
|
7882
|
+
" workspaceId: string;",
|
|
7883
|
+
"}"
|
|
7884
|
+
] : [],
|
|
7530
7885
|
"",
|
|
7531
7886
|
"function buildWidgets(): TopoloWidgetDraft[] {",
|
|
7532
7887
|
" return [",
|
|
@@ -7623,6 +7978,14 @@ function platformWorkerTs(application, options) {
|
|
|
7623
7978
|
" bindings.__rt = rt;",
|
|
7624
7979
|
" return securedFetch(request, bindings, ctx as ExecutionContext);",
|
|
7625
7980
|
" }),",
|
|
7981
|
+
...options.resources.queue ? [
|
|
7982
|
+
" async queue(batch: MessageBatch<CapabilityJob>): Promise<void> {",
|
|
7983
|
+
" for (const message of batch.messages) {",
|
|
7984
|
+
" if (message.body?.type === 'capability.probe' && message.body.workspaceId) message.ack();",
|
|
7985
|
+
" else message.retry();",
|
|
7986
|
+
" }",
|
|
7987
|
+
" },"
|
|
7988
|
+
] : [],
|
|
7626
7989
|
"};",
|
|
7627
7990
|
"",
|
|
7628
7991
|
"function errorMessage(error: unknown): string {",
|
|
@@ -8040,14 +8403,24 @@ function topoloAppManifestJson(application, options) {
|
|
|
8040
8403
|
development: "https://nexus.topolo.dev",
|
|
8041
8404
|
staging: "https://nexus.stg.topolo.us",
|
|
8042
8405
|
production: "https://nexus.topolo.app"
|
|
8043
|
-
}
|
|
8406
|
+
},
|
|
8407
|
+
...options.payload.capabilities.includes("notification_producer") ? {
|
|
8408
|
+
TOPOLO_NOTIFY_URL: {
|
|
8409
|
+
development: "https://notify.topolo.dev",
|
|
8410
|
+
staging: "https://notify.stg.topolo.us",
|
|
8411
|
+
production: "https://notify.topolo.app"
|
|
8412
|
+
}
|
|
8413
|
+
} : {}
|
|
8044
8414
|
// No NEXUS_APP_ID: generate-wrangler binds APP_ID (the app_id) into every
|
|
8045
8415
|
// env, and worker-runtime reads env.APP_ID for Nexus usage attribution,
|
|
8046
8416
|
// so a second copy of the same identity would only be a chance to drift.
|
|
8047
8417
|
// It previously carried the SLUG, which reads as a working identity while
|
|
8048
8418
|
// Nexus attributes by app_id.
|
|
8049
8419
|
},
|
|
8050
|
-
secrets: [
|
|
8420
|
+
secrets: [
|
|
8421
|
+
"TOPOLO_SERVICE_CLIENT_SECRET",
|
|
8422
|
+
...options.payload.capabilities.includes("webhooks") ? ["TOPOLO_WEBHOOK_SECRET"] : []
|
|
8423
|
+
],
|
|
8051
8424
|
...Object.keys(bindings).length ? { bindings } : {}
|
|
8052
8425
|
};
|
|
8053
8426
|
}
|
|
@@ -8108,6 +8481,12 @@ function topoloAppManifestJson(application, options) {
|
|
|
8108
8481
|
}
|
|
8109
8482
|
},
|
|
8110
8483
|
targets,
|
|
8484
|
+
capabilityComposition: {
|
|
8485
|
+
registryVersion: options.payload.capabilityComposition.registryVersion,
|
|
8486
|
+
selected: options.payload.capabilityComposition.declarations,
|
|
8487
|
+
artifacts: options.payload.capabilityComposition.artifacts,
|
|
8488
|
+
stagingEvidence: options.payload.capabilityComposition.stagingEvidence
|
|
8489
|
+
},
|
|
8111
8490
|
registration: {
|
|
8112
8491
|
ownerType: options.payload.ownerType,
|
|
8113
8492
|
portfolio: options.payload.portfolio,
|
|
@@ -8126,7 +8505,14 @@ function topoloAppManifestJson(application, options) {
|
|
|
8126
8505
|
supportEmail: "support@topolo.io",
|
|
8127
8506
|
auth: {
|
|
8128
8507
|
callbackPath: "/auth/callback",
|
|
8129
|
-
requestedScopes:
|
|
8508
|
+
requestedScopes: unique3([
|
|
8509
|
+
"openid",
|
|
8510
|
+
"profile",
|
|
8511
|
+
"email",
|
|
8512
|
+
"organization:read",
|
|
8513
|
+
...options.payload.capabilityComposition.authScopes,
|
|
8514
|
+
...options.api ? [`${application.id}:read`] : []
|
|
8515
|
+
])
|
|
8130
8516
|
},
|
|
8131
8517
|
capabilities: options.payload.capabilities,
|
|
8132
8518
|
marketplace: {
|
|
@@ -8138,7 +8524,7 @@ function topoloAppManifestJson(application, options) {
|
|
|
8138
8524
|
docsUrl: `https://docs.topolo.app/applications/${slug}`
|
|
8139
8525
|
}
|
|
8140
8526
|
},
|
|
8141
|
-
notifications: { role: "consumer_only" },
|
|
8527
|
+
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
8528
|
...actions.length ? { actions } : {}
|
|
8143
8529
|
})}
|
|
8144
8530
|
`;
|
|
@@ -8514,12 +8900,18 @@ function serviceManifestJson(application, options) {
|
|
|
8514
8900
|
description: `Manage ${title} ${resourceLabel} data`,
|
|
8515
8901
|
resourcePattern: null
|
|
8516
8902
|
},
|
|
8517
|
-
{
|
|
8903
|
+
...options.servicePermissions.includes("usage:write") ? [{
|
|
8518
8904
|
id: `perm_${permissionPrefix}_usage_write`,
|
|
8519
8905
|
name: "usage:write",
|
|
8520
8906
|
description: `Record Nexus usage events for ${title}`,
|
|
8521
8907
|
resourcePattern: null
|
|
8522
|
-
}
|
|
8908
|
+
}] : [],
|
|
8909
|
+
...options.servicePermissions.includes("events:write") ? [{
|
|
8910
|
+
id: `perm_${permissionPrefix}_events_write`,
|
|
8911
|
+
name: "events:write",
|
|
8912
|
+
description: `Publish governed notification events for ${title}`,
|
|
8913
|
+
resourcePattern: null
|
|
8914
|
+
}] : []
|
|
8523
8915
|
],
|
|
8524
8916
|
apiKeyScopes: [
|
|
8525
8917
|
{
|
|
@@ -8534,12 +8926,18 @@ function serviceManifestJson(application, options) {
|
|
|
8534
8926
|
description: `Manage ${title} ${resourceLabel} data`,
|
|
8535
8927
|
resourcePattern: null
|
|
8536
8928
|
},
|
|
8537
|
-
{
|
|
8929
|
+
...options.servicePermissions.includes("usage:write") ? [{
|
|
8538
8930
|
id: `aks_${permissionPrefix}_usage_write`,
|
|
8539
8931
|
name: "usage.write",
|
|
8540
8932
|
description: `Record Nexus usage events for ${title}`,
|
|
8541
8933
|
resourcePattern: null
|
|
8542
|
-
}
|
|
8934
|
+
}] : [],
|
|
8935
|
+
...options.servicePermissions.includes("events:write") ? [{
|
|
8936
|
+
id: `aks_${permissionPrefix}_events_write`,
|
|
8937
|
+
name: "events.write",
|
|
8938
|
+
description: `Publish governed notification events for ${title}`,
|
|
8939
|
+
resourcePattern: null
|
|
8940
|
+
}] : []
|
|
8543
8941
|
],
|
|
8544
8942
|
roleBundles: {
|
|
8545
8943
|
admin: ["workspace:read", "workspace:write"],
|
|
@@ -8554,7 +8952,14 @@ function developerManifest(application, options) {
|
|
|
8554
8952
|
const callbackPath = "/auth/callback";
|
|
8555
8953
|
const localCallbackUrl = "http://localhost:5173/auth/callback";
|
|
8556
8954
|
const productionCallbackUrl = application.productionUrl ? `${application.productionUrl.replace(/\/$/, "")}${callbackPath}` : null;
|
|
8557
|
-
const requestedScopes =
|
|
8955
|
+
const requestedScopes = unique3([
|
|
8956
|
+
"openid",
|
|
8957
|
+
"profile",
|
|
8958
|
+
"email",
|
|
8959
|
+
"organization:read",
|
|
8960
|
+
...options.capabilityComposition.authScopes,
|
|
8961
|
+
...options.api ? [`${application.id}:read`] : []
|
|
8962
|
+
]);
|
|
8558
8963
|
return `${json({
|
|
8559
8964
|
schema: "https://topolo.app/schemas/topolo-app-manifest.v1.json",
|
|
8560
8965
|
id: application.id,
|
|
@@ -8569,17 +8974,9 @@ function developerManifest(application, options) {
|
|
|
8569
8974
|
app_id: application.appId,
|
|
8570
8975
|
capabilities: {
|
|
8571
8976
|
selected: options.capabilities,
|
|
8572
|
-
|
|
8573
|
-
|
|
8574
|
-
|
|
8575
|
-
"marketplace",
|
|
8576
|
-
"api",
|
|
8577
|
-
"widget",
|
|
8578
|
-
"billing",
|
|
8579
|
-
"nexus_ai",
|
|
8580
|
-
"agent_access",
|
|
8581
|
-
"hosting"
|
|
8582
|
-
]
|
|
8977
|
+
registry_version: options.capabilityComposition.registryVersion,
|
|
8978
|
+
declarations: options.capabilityComposition.declarations,
|
|
8979
|
+
supported_v1: TOPOLO_APP_CAPABILITY_IDS
|
|
8583
8980
|
},
|
|
8584
8981
|
auth: {
|
|
8585
8982
|
provider: "topolo",
|
|
@@ -8641,10 +9038,13 @@ function developerManifest(application, options) {
|
|
|
8641
9038
|
docsUrl: application.productionUrl || "https://developers.topolo.app"
|
|
8642
9039
|
}
|
|
8643
9040
|
},
|
|
8644
|
-
notifications: { role: "consumer_only" }
|
|
9041
|
+
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
9042
|
})}
|
|
8646
9043
|
`;
|
|
8647
9044
|
}
|
|
9045
|
+
function unique3(values) {
|
|
9046
|
+
return Array.from(new Set(values));
|
|
9047
|
+
}
|
|
8648
9048
|
function envExample(application) {
|
|
8649
9049
|
return [
|
|
8650
9050
|
"VITE_TOPOLO_AUTH_URL=https://auth.topolo.app",
|
|
@@ -8908,12 +9308,24 @@ function applicationScaffoldPayload(application, options = {}) {
|
|
|
8908
9308
|
const surfaceModel = normalizeSurfaceModel(options.surfaceModel);
|
|
8909
9309
|
const containerAppId = normalizeContainerAppId(options.containerAppId, surfaceModel);
|
|
8910
9310
|
const profile = normalizeScaffoldProfile(options.profile);
|
|
8911
|
-
const resources = resolveScaffoldResources(profile, options);
|
|
8912
|
-
if (target === "platform" && api && !profile) {
|
|
8913
|
-
resources.d1 = true;
|
|
8914
|
-
}
|
|
8915
9311
|
const ownerType = target === "platform" ? "topolo_first_party" : "third_party";
|
|
8916
9312
|
const distributionModel = target === "platform" ? "marketplace" : "organization_internal";
|
|
9313
|
+
const capabilityComposition = resolveApplicationCapabilities({
|
|
9314
|
+
target,
|
|
9315
|
+
browser,
|
|
9316
|
+
api,
|
|
9317
|
+
mobile: surfaceModel === "mobile_container" || surfaceModel === "mobile_module",
|
|
9318
|
+
distributionModel,
|
|
9319
|
+
requested: options.capabilities,
|
|
9320
|
+
profile
|
|
9321
|
+
});
|
|
9322
|
+
const resources = {
|
|
9323
|
+
d1: capabilityComposition.resources.includes("d1"),
|
|
9324
|
+
r2: capabilityComposition.resources.includes("r2"),
|
|
9325
|
+
queue: capabilityComposition.resources.includes("queue"),
|
|
9326
|
+
workflow: capabilityComposition.resources.includes("workflow"),
|
|
9327
|
+
containerProcessing: profile === "spatial-assets"
|
|
9328
|
+
};
|
|
8917
9329
|
const requestedAppDir = options.appDir ?? catalogApplication?.appDir ?? (target === "platform" ? `Topolo${toPascalCase(id)}` : `topolo-${slug}`);
|
|
8918
9330
|
const appDir = target === "platform" ? normalizeFirstPartyAppDir(requestedAppDir) : requestedAppDir;
|
|
8919
9331
|
const name = options.name ?? catalogApplication?.name ?? `topolo-${slug}`;
|
|
@@ -8933,7 +9345,7 @@ function applicationScaffoldPayload(application, options = {}) {
|
|
|
8933
9345
|
const appId = options.appId ?? defaultAppId;
|
|
8934
9346
|
const productionUrl = browser ? options.productionUrl ?? catalogApplication?.productionUrl ?? `https://${slug}.topolo.app` : null;
|
|
8935
9347
|
const scopes = scaffoldScopes({ browser, api });
|
|
8936
|
-
const capabilities =
|
|
9348
|
+
const capabilities = capabilityComposition.selected;
|
|
8937
9349
|
const ciWorkflowPath = target === "platform" ? ".github/workflows/topolo-conformance.yml" : null;
|
|
8938
9350
|
const ciWorkflow = target === "platform" ? topoloConformanceWorkflow({ id, appDir, audience }) : null;
|
|
8939
9351
|
const applicationPayload = {
|
|
@@ -8959,12 +9371,13 @@ function applicationScaffoldPayload(application, options = {}) {
|
|
|
8959
9371
|
experience,
|
|
8960
9372
|
distributionModel,
|
|
8961
9373
|
capabilities,
|
|
9374
|
+
capabilityComposition,
|
|
8962
9375
|
application: applicationPayload,
|
|
8963
9376
|
scopes,
|
|
8964
9377
|
requirements: APPLICATION_REQUIREMENTS.filter(
|
|
8965
9378
|
(requirement) => requirement.appliesTo.some((scope) => scopes.includes(scope))
|
|
8966
9379
|
),
|
|
8967
|
-
packages:
|
|
9380
|
+
packages: capabilityComposition.packages,
|
|
8968
9381
|
files: scaffoldFiles({
|
|
8969
9382
|
payload: {
|
|
8970
9383
|
ownerType,
|
|
@@ -8977,7 +9390,8 @@ function applicationScaffoldPayload(application, options = {}) {
|
|
|
8977
9390
|
resources,
|
|
8978
9391
|
experience,
|
|
8979
9392
|
distributionModel,
|
|
8980
|
-
capabilities
|
|
9393
|
+
capabilities,
|
|
9394
|
+
capabilityComposition
|
|
8981
9395
|
},
|
|
8982
9396
|
application: applicationPayload,
|
|
8983
9397
|
target,
|
|
@@ -9038,28 +9452,6 @@ function scaffoldScopes(options) {
|
|
|
9038
9452
|
}
|
|
9039
9453
|
return scopes;
|
|
9040
9454
|
}
|
|
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
9455
|
function scaffoldFiles(options) {
|
|
9064
9456
|
const { application, target } = options;
|
|
9065
9457
|
const slug = toSlug(application.id);
|
|
@@ -9099,9 +9491,31 @@ function scaffoldFiles(options) {
|
|
|
9099
9491
|
content: packageJson(application, target, {
|
|
9100
9492
|
browser: options.browser,
|
|
9101
9493
|
api: options.api,
|
|
9102
|
-
resources: options.payload.resources
|
|
9494
|
+
resources: options.payload.resources,
|
|
9495
|
+
packages: options.payload.capabilityComposition.packages
|
|
9103
9496
|
})
|
|
9104
9497
|
},
|
|
9498
|
+
{
|
|
9499
|
+
path: `${base}/topolo.capability-contract.json`,
|
|
9500
|
+
purpose: "Resolved application-factory contract for packages, permissions, resources, artifacts, and staging proof.",
|
|
9501
|
+
requiredSignals: [
|
|
9502
|
+
"registry version matches topolo.app.json capability composition",
|
|
9503
|
+
"every selected capability names its canonical owner and verification contract"
|
|
9504
|
+
],
|
|
9505
|
+
content: `${json(capabilityContractProjection(options.payload.capabilityComposition))}
|
|
9506
|
+
`
|
|
9507
|
+
},
|
|
9508
|
+
...options.payload.capabilities.includes("billing") ? [{
|
|
9509
|
+
path: `${base}/pricing-manifest.json`,
|
|
9510
|
+
purpose: "Canonical TopoloPay/Nexus pricing and meter registration manifest.",
|
|
9511
|
+
requiredSignals: ["contains a valid default plan", "remains the source for app-specific plan and meter design"],
|
|
9512
|
+
content: `${json(defaultPricingManifest({
|
|
9513
|
+
appId: application.appId ?? "",
|
|
9514
|
+
displayName: toTitle(application.name),
|
|
9515
|
+
description: options.payload.experience.tagline
|
|
9516
|
+
}))}
|
|
9517
|
+
`
|
|
9518
|
+
}] : [],
|
|
9105
9519
|
{
|
|
9106
9520
|
path: `${base}/README.md`,
|
|
9107
9521
|
purpose: "Starter README with install, run, and verification commands.",
|
|
@@ -9433,7 +9847,8 @@ function scaffoldFiles(options) {
|
|
|
9433
9847
|
],
|
|
9434
9848
|
content: serviceManifestJson(application, {
|
|
9435
9849
|
audience: options.payload.audience,
|
|
9436
|
-
tenancy: options.payload.tenancy
|
|
9850
|
+
tenancy: options.payload.tenancy,
|
|
9851
|
+
servicePermissions: options.payload.capabilityComposition.servicePermissions
|
|
9437
9852
|
})
|
|
9438
9853
|
}
|
|
9439
9854
|
);
|
|
@@ -9454,7 +9869,8 @@ function scaffoldFiles(options) {
|
|
|
9454
9869
|
surfaceModel: options.payload.surfaceModel,
|
|
9455
9870
|
containerAppId: options.payload.containerAppId,
|
|
9456
9871
|
distributionModel: options.payload.distributionModel,
|
|
9457
|
-
capabilities: options.payload.capabilities
|
|
9872
|
+
capabilities: options.payload.capabilities,
|
|
9873
|
+
capabilityComposition: options.payload.capabilityComposition
|
|
9458
9874
|
})
|
|
9459
9875
|
},
|
|
9460
9876
|
{
|
|
@@ -9547,6 +9963,7 @@ export default defineConfig({
|
|
|
9547
9963
|
}
|
|
9548
9964
|
}
|
|
9549
9965
|
if (options.api) {
|
|
9966
|
+
const capabilityAdapter = target === "platform" ? managedCapabilityAdapter(options.payload.capabilities) : null;
|
|
9550
9967
|
files.push(
|
|
9551
9968
|
...target === "platform" ? [] : [{
|
|
9552
9969
|
path: `${apiBase}/wrangler.toml`,
|
|
@@ -9571,7 +9988,13 @@ export default defineConfig({
|
|
|
9571
9988
|
profile: options.payload.profile,
|
|
9572
9989
|
resources: options.payload.resources
|
|
9573
9990
|
})
|
|
9574
|
-
}
|
|
9991
|
+
},
|
|
9992
|
+
...capabilityAdapter ? [{
|
|
9993
|
+
path: `${apiBase}/src/topolo-capabilities.ts`,
|
|
9994
|
+
purpose: "Managed adapters for selected Topolo notification, Nexus, AI, integration, and webhook capabilities.",
|
|
9995
|
+
requiredSignals: ["uses verified organization context", "uses environment-scoped gateways and keyless service identity", "verifies webhook signatures over the raw body"],
|
|
9996
|
+
content: capabilityAdapter
|
|
9997
|
+
}] : []
|
|
9575
9998
|
);
|
|
9576
9999
|
if (target === "platform") {
|
|
9577
10000
|
files.push(
|
|
@@ -9775,7 +10198,14 @@ function buildProvisioningRequest(payload, supportEmail) {
|
|
|
9775
10198
|
"http://localhost:5173/auth/callback",
|
|
9776
10199
|
payload.application.productionUrl ? `${payload.application.productionUrl.replace(/\/$/, "")}/auth/callback` : null
|
|
9777
10200
|
].filter(Boolean),
|
|
9778
|
-
requestedScopes:
|
|
10201
|
+
requestedScopes: Array.from(/* @__PURE__ */ new Set([
|
|
10202
|
+
"openid",
|
|
10203
|
+
"profile",
|
|
10204
|
+
"email",
|
|
10205
|
+
"organization:read",
|
|
10206
|
+
...payload.capabilityComposition.authScopes,
|
|
10207
|
+
...payload.application.appId ? [`${payload.application.id}:read`] : []
|
|
10208
|
+
])),
|
|
9779
10209
|
marketplace: {
|
|
9780
10210
|
intent: payload.distributionModel === "marketplace" ? "submit_when_ready" : "not_requested",
|
|
9781
10211
|
status: "preview",
|
|
@@ -9784,7 +10214,7 @@ function buildProvisioningRequest(payload, supportEmail) {
|
|
|
9784
10214
|
description: payload.experience.tagline,
|
|
9785
10215
|
docsUrl: payload.application.productionUrl || "https://developers.topolo.app"
|
|
9786
10216
|
},
|
|
9787
|
-
notifications: { role: "consumer_only" },
|
|
10217
|
+
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
10218
|
applicationId: payload.application.id,
|
|
9789
10219
|
name: payload.application.name,
|
|
9790
10220
|
description: payload.experience.tagline,
|
|
@@ -9852,18 +10282,18 @@ async function applyProvisionedAppId(manifestPath, appId) {
|
|
|
9852
10282
|
} catch {
|
|
9853
10283
|
return { changed: false, previous: null };
|
|
9854
10284
|
}
|
|
9855
|
-
let
|
|
10285
|
+
let json2;
|
|
9856
10286
|
try {
|
|
9857
|
-
|
|
10287
|
+
json2 = JSON.parse(raw);
|
|
9858
10288
|
} catch {
|
|
9859
10289
|
return { changed: false, previous: null };
|
|
9860
10290
|
}
|
|
9861
|
-
const previous = typeof
|
|
10291
|
+
const previous = typeof json2.appId === "string" ? json2.appId : null;
|
|
9862
10292
|
if (previous === appId) {
|
|
9863
10293
|
return { changed: false, previous };
|
|
9864
10294
|
}
|
|
9865
|
-
|
|
9866
|
-
await writeFile3(manifestPath, `${JSON.stringify(
|
|
10295
|
+
json2.appId = appId;
|
|
10296
|
+
await writeFile3(manifestPath, `${JSON.stringify(json2, null, 2)}
|
|
9867
10297
|
`, "utf8");
|
|
9868
10298
|
return { changed: true, previous };
|
|
9869
10299
|
}
|
|
@@ -9872,13 +10302,283 @@ async function applyProvisionedAppId(manifestPath, appId) {
|
|
|
9872
10302
|
import { join as join3 } from "path";
|
|
9873
10303
|
|
|
9874
10304
|
// src/commands/app-reconcile.ts
|
|
9875
|
-
import { readFile as
|
|
9876
|
-
import
|
|
10305
|
+
import { readFile as readFile4, writeFile as writeFile5 } from "fs/promises";
|
|
10306
|
+
import path4 from "path";
|
|
9877
10307
|
import { createInterface as createInterface2 } from "readline/promises";
|
|
9878
10308
|
import { stdin as input2, stdout as output } from "process";
|
|
10309
|
+
|
|
10310
|
+
// src/commands/app-capability-composition.ts
|
|
10311
|
+
import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile4 } from "fs/promises";
|
|
10312
|
+
import path3 from "path";
|
|
10313
|
+
async function reconcileApplicationCapabilitySource(options) {
|
|
10314
|
+
const manifestPath = path3.resolve(options.manifestPath || "topolo.app.json");
|
|
10315
|
+
const root = path3.dirname(manifestPath);
|
|
10316
|
+
const packagePath = path3.join(root, "package.json");
|
|
10317
|
+
const contractPath = path3.join(root, "topolo.capability-contract.json");
|
|
10318
|
+
const manifest = parseRecord(await readFile3(manifestPath, "utf8"), manifestPath);
|
|
10319
|
+
const registration = record(manifest.registration);
|
|
10320
|
+
if (!registration) throw new Error("topolo.app.json must declare registration before capability composition.");
|
|
10321
|
+
if (registration.ownerType !== options.ownerType) {
|
|
10322
|
+
throw new Error(`${String(registration.ownerType)} capabilities cannot be composed through the ${options.ownerType} command surface.`);
|
|
10323
|
+
}
|
|
10324
|
+
const target = options.ownerType === "topolo_first_party" ? "platform" : "developer";
|
|
10325
|
+
const surfaces = resolveManifestSurfaces(manifest, registration);
|
|
10326
|
+
const existing = Array.isArray(registration.capabilities) ? registration.capabilities.filter((value) => typeof value === "string") : [];
|
|
10327
|
+
const capabilities = resolveApplicationCapabilities({
|
|
10328
|
+
target,
|
|
10329
|
+
browser: surfaces.browser,
|
|
10330
|
+
api: surfaces.api,
|
|
10331
|
+
mobile: surfaces.mobile,
|
|
10332
|
+
distributionModel: registration.distributionModel === "marketplace" ? "marketplace" : "organization_internal",
|
|
10333
|
+
requested: [...existing, ...options.additions ?? []]
|
|
10334
|
+
});
|
|
10335
|
+
const expectedComposition = capabilityCompositionProjection(capabilities);
|
|
10336
|
+
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" };
|
|
10337
|
+
const changes = [];
|
|
10338
|
+
compareChange(changes, "topolo.app.json#capabilityComposition", manifest.capabilityComposition, expectedComposition);
|
|
10339
|
+
compareChange(changes, "topolo.app.json#registration.capabilities", registration.capabilities, capabilities.selected);
|
|
10340
|
+
compareChange(changes, "topolo.app.json#notifications", manifest.notifications, expectedNotifications);
|
|
10341
|
+
const packageJson2 = parseRecord(await readFile3(packagePath, "utf8"), packagePath);
|
|
10342
|
+
const dependencies = record(packageJson2.dependencies) ?? {};
|
|
10343
|
+
const nextDependencies = { ...dependencies };
|
|
10344
|
+
for (const packageName of capabilities.packages) {
|
|
10345
|
+
if (typeof nextDependencies[packageName] !== "string") {
|
|
10346
|
+
nextDependencies[packageName] = packageVersion(packageName, target);
|
|
10347
|
+
}
|
|
10348
|
+
}
|
|
10349
|
+
const sortedDependencies = sortRecord(nextDependencies);
|
|
10350
|
+
compareChange(changes, "package.json#dependencies", dependencies, sortedDependencies);
|
|
10351
|
+
const expectedContract = capabilityContractProjection(capabilities);
|
|
10352
|
+
const existingContract = await readOptionalJson(contractPath);
|
|
10353
|
+
compareChange(changes, "topolo.capability-contract.json", existingContract, expectedContract);
|
|
10354
|
+
const adapter = target === "platform" ? managedCapabilityAdapter(capabilities.selected) : null;
|
|
10355
|
+
const apiDirectory = resolveApiDirectory(root, manifest, target);
|
|
10356
|
+
const adapterPath = path3.join(apiDirectory, "src", "topolo-capabilities.ts");
|
|
10357
|
+
if (adapter) compareChange(changes, relativePath(root, adapterPath), await readOptionalText(adapterPath), adapter);
|
|
10358
|
+
const pricingPath = path3.join(root, "pricing-manifest.json");
|
|
10359
|
+
const pricingManifest = capabilities.selected.includes("billing") ? defaultPricingManifest({
|
|
10360
|
+
appId: String(manifest.appId || manifest.app_id || "").trim(),
|
|
10361
|
+
displayName: String(registration.displayName || manifest.name || "Application"),
|
|
10362
|
+
description: String(registration.description || `${String(registration.displayName || manifest.name || "Application")} on Topolo.`)
|
|
10363
|
+
}) : null;
|
|
10364
|
+
if (pricingManifest) {
|
|
10365
|
+
compareChange(changes, "pricing-manifest.json", await readOptionalJson(pricingPath), pricingManifest);
|
|
10366
|
+
}
|
|
10367
|
+
const missingResources = capabilities.resources.filter((resource) => !manifestProvidesResource(manifest, resource));
|
|
10368
|
+
const verticalConfiguration = [];
|
|
10369
|
+
for (const [capability2, fileName, description] of VERTICAL_CAPABILITY_FILES) {
|
|
10370
|
+
if (capabilities.selected.includes(capability2) && !await fileExists(path3.join(root, fileName))) {
|
|
10371
|
+
verticalConfiguration.push(`${fileName}: ${description}`);
|
|
10372
|
+
}
|
|
10373
|
+
}
|
|
10374
|
+
if (options.mode === "apply" && changes.length > 0) {
|
|
10375
|
+
manifest.capabilityComposition = expectedComposition;
|
|
10376
|
+
registration.capabilities = capabilities.selected;
|
|
10377
|
+
manifest.registration = registration;
|
|
10378
|
+
manifest.notifications = expectedNotifications;
|
|
10379
|
+
packageJson2.dependencies = sortedDependencies;
|
|
10380
|
+
await writeJson(manifestPath, manifest);
|
|
10381
|
+
await writeJson(packagePath, packageJson2);
|
|
10382
|
+
await writeJson(contractPath, expectedContract);
|
|
10383
|
+
if (pricingManifest) await writeJson(pricingPath, pricingManifest);
|
|
10384
|
+
if (adapter) {
|
|
10385
|
+
await mkdir3(path3.dirname(adapterPath), { recursive: true });
|
|
10386
|
+
await writeFile4(adapterPath, adapter, "utf8");
|
|
10387
|
+
}
|
|
10388
|
+
}
|
|
10389
|
+
const blocked = missingResources.length > 0 || verticalConfiguration.length > 0;
|
|
10390
|
+
return {
|
|
10391
|
+
mode: options.mode,
|
|
10392
|
+
applied: options.mode === "apply" && changes.length > 0,
|
|
10393
|
+
status: blocked ? "blocked" : options.mode === "apply" || changes.length === 0 ? "in_sync" : "drift",
|
|
10394
|
+
manifestPath,
|
|
10395
|
+
packagePath,
|
|
10396
|
+
contractPath,
|
|
10397
|
+
capabilities,
|
|
10398
|
+
changes,
|
|
10399
|
+
missingResources,
|
|
10400
|
+
verticalConfiguration
|
|
10401
|
+
};
|
|
10402
|
+
}
|
|
10403
|
+
function registerApplicationCapabilityCommands(parent, ownerType) {
|
|
10404
|
+
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) => {
|
|
10405
|
+
const outputMode = resolveOutputMode(options.json);
|
|
10406
|
+
try {
|
|
10407
|
+
if (options.check && options.apply) throw new Error("Choose exactly one of --check or --apply.");
|
|
10408
|
+
const result = await reconcileApplicationCapabilitySource({
|
|
10409
|
+
manifestPath: options.manifest,
|
|
10410
|
+
ownerType,
|
|
10411
|
+
mode: options.apply ? "apply" : "check",
|
|
10412
|
+
additions: capabilities
|
|
10413
|
+
});
|
|
10414
|
+
emit(result, outputMode, () => formatCapabilitySource(result));
|
|
10415
|
+
if (result.status !== "in_sync" && !options.apply) process.exitCode = 2;
|
|
10416
|
+
if (result.status === "blocked") process.exitCode = 2;
|
|
10417
|
+
} catch (error) {
|
|
10418
|
+
fail(error, outputMode);
|
|
10419
|
+
}
|
|
10420
|
+
});
|
|
10421
|
+
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) => {
|
|
10422
|
+
const outputMode = resolveOutputMode(options.json);
|
|
10423
|
+
try {
|
|
10424
|
+
const result = await reconcileApplicationCapabilitySource({ manifestPath: options.manifest, ownerType, mode: "check" });
|
|
10425
|
+
emit(result, outputMode, () => formatCapabilitySource(result));
|
|
10426
|
+
if (result.status !== "in_sync") process.exitCode = 2;
|
|
10427
|
+
} catch (error) {
|
|
10428
|
+
fail(error, outputMode);
|
|
10429
|
+
}
|
|
10430
|
+
});
|
|
10431
|
+
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) => {
|
|
10432
|
+
const outputMode = resolveOutputMode(options.json);
|
|
10433
|
+
try {
|
|
10434
|
+
if (options.check && options.apply) throw new Error("Choose exactly one of --check or --apply.");
|
|
10435
|
+
const result = await reconcileApplicationCapabilitySource({
|
|
10436
|
+
manifestPath: options.manifest,
|
|
10437
|
+
ownerType,
|
|
10438
|
+
mode: options.apply ? "apply" : "check"
|
|
10439
|
+
});
|
|
10440
|
+
emit(result, outputMode, () => formatCapabilitySource(result));
|
|
10441
|
+
if (result.status !== "in_sync" && !options.apply) process.exitCode = 2;
|
|
10442
|
+
if (result.status === "blocked") process.exitCode = 2;
|
|
10443
|
+
} catch (error) {
|
|
10444
|
+
fail(error, outputMode);
|
|
10445
|
+
}
|
|
10446
|
+
});
|
|
10447
|
+
}
|
|
10448
|
+
function formatCapabilitySource(result) {
|
|
10449
|
+
const lines = [
|
|
10450
|
+
`Capability source: ${result.status}`,
|
|
10451
|
+
`Registry: ${result.capabilities.registryVersion}`,
|
|
10452
|
+
`Selected: ${result.capabilities.selected.join(", ")}`
|
|
10453
|
+
];
|
|
10454
|
+
for (const change of result.changes) lines.push(`- ${change.path}`);
|
|
10455
|
+
if (result.missingResources.length > 0) lines.push(`Missing provisioned resources: ${result.missingResources.join(", ")}`);
|
|
10456
|
+
for (const item of result.verticalConfiguration) lines.push(`Vertical configuration required: ${item}`);
|
|
10457
|
+
return lines.join("\n");
|
|
10458
|
+
}
|
|
10459
|
+
function resolveManifestSurfaces(manifest, registration) {
|
|
10460
|
+
const targets = record(manifest.targets) ?? {};
|
|
10461
|
+
const targetRecords = Object.values(targets).map(record).filter((value) => Boolean(value));
|
|
10462
|
+
const runtime = record(manifest.runtime);
|
|
10463
|
+
return {
|
|
10464
|
+
browser: targetRecords.some((target) => Boolean(target.assets) || target.dir === "apps/web" || target.kind === "web") || runtime?.browser === true,
|
|
10465
|
+
api: targetRecords.some((target) => target.dir === "apps/api" || target.kind === "worker" && !target.assets) || runtime?.api === true,
|
|
10466
|
+
mobile: registration.surfaceModel === "mobile_container" || registration.surfaceModel === "mobile_module"
|
|
10467
|
+
};
|
|
10468
|
+
}
|
|
10469
|
+
function manifestProvidesResource(manifest, resource) {
|
|
10470
|
+
const targets = record(manifest.targets) ?? {};
|
|
10471
|
+
for (const value of Object.values(targets)) {
|
|
10472
|
+
const target = record(value);
|
|
10473
|
+
const bindings = record(target?.bindings);
|
|
10474
|
+
if (!bindings) continue;
|
|
10475
|
+
if (resource === "d1" && Array.isArray(bindings.d1) && bindings.d1.length > 0) {
|
|
10476
|
+
return bindings.d1.every((value2) => {
|
|
10477
|
+
const binding = record(value2);
|
|
10478
|
+
const configurations = ["development", "staging", "production"].map((environment) => record(binding?.[environment])).filter((configuration) => Boolean(configuration));
|
|
10479
|
+
return configurations.length > 0 && configurations.every((configuration) => typeof configuration.databaseId === "string" && configuration.databaseId.length > 0 && configuration.databaseId !== "provision-after-scaffold");
|
|
10480
|
+
});
|
|
10481
|
+
}
|
|
10482
|
+
if (resource === "r2" && Array.isArray(bindings.r2) && bindings.r2.length > 0) return true;
|
|
10483
|
+
if (resource === "queue" && record(bindings.queues)) return true;
|
|
10484
|
+
if (resource === "workflow" && Array.isArray(bindings.workflows) && bindings.workflows.length > 0) return true;
|
|
10485
|
+
}
|
|
10486
|
+
return false;
|
|
10487
|
+
}
|
|
10488
|
+
function packageVersion(packageName, target) {
|
|
10489
|
+
if (target === "platform") {
|
|
10490
|
+
if (packageName === "@topolo-io/notifications") return "^0.6.8";
|
|
10491
|
+
return "latest";
|
|
10492
|
+
}
|
|
10493
|
+
if (packageName === "@topolo/login") return "^0.1.0";
|
|
10494
|
+
if (packageName === "@topolo/sdk") return "^0.8.1";
|
|
10495
|
+
throw new Error(`Capability registry exposed private package ${packageName} to a third-party application.`);
|
|
10496
|
+
}
|
|
10497
|
+
function compareChange(changes, changePath, before, after) {
|
|
10498
|
+
if (stableJson(before) !== stableJson(after)) changes.push({ path: changePath, before, after });
|
|
10499
|
+
}
|
|
9879
10500
|
function record(value) {
|
|
9880
10501
|
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
9881
10502
|
}
|
|
10503
|
+
function parseRecord(raw, filePath) {
|
|
10504
|
+
const value = JSON.parse(raw);
|
|
10505
|
+
const parsed = record(value);
|
|
10506
|
+
if (!parsed) throw new Error(`${filePath} must contain a JSON object.`);
|
|
10507
|
+
return parsed;
|
|
10508
|
+
}
|
|
10509
|
+
async function readOptionalJson(filePath) {
|
|
10510
|
+
try {
|
|
10511
|
+
return JSON.parse(await readFile3(filePath, "utf8"));
|
|
10512
|
+
} catch (error) {
|
|
10513
|
+
if (error.code === "ENOENT") return null;
|
|
10514
|
+
throw error;
|
|
10515
|
+
}
|
|
10516
|
+
}
|
|
10517
|
+
async function readOptionalText(filePath) {
|
|
10518
|
+
try {
|
|
10519
|
+
return await readFile3(filePath, "utf8");
|
|
10520
|
+
} catch (error) {
|
|
10521
|
+
if (error.code === "ENOENT") return null;
|
|
10522
|
+
throw error;
|
|
10523
|
+
}
|
|
10524
|
+
}
|
|
10525
|
+
async function fileExists(filePath) {
|
|
10526
|
+
try {
|
|
10527
|
+
await readFile3(filePath);
|
|
10528
|
+
return true;
|
|
10529
|
+
} catch (error) {
|
|
10530
|
+
if (error.code === "ENOENT") return false;
|
|
10531
|
+
throw error;
|
|
10532
|
+
}
|
|
10533
|
+
}
|
|
10534
|
+
async function writeJson(filePath, value) {
|
|
10535
|
+
await writeFile4(filePath, `${JSON.stringify(value, null, 2)}
|
|
10536
|
+
`, "utf8");
|
|
10537
|
+
}
|
|
10538
|
+
function sortRecord(value) {
|
|
10539
|
+
return Object.fromEntries(Object.entries(value).sort(([left], [right]) => left.localeCompare(right)));
|
|
10540
|
+
}
|
|
10541
|
+
function stableJson(value) {
|
|
10542
|
+
if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
|
|
10543
|
+
if (value && typeof value === "object") {
|
|
10544
|
+
return `{${Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => `${JSON.stringify(key)}:${stableJson(item)}`).join(",")}}`;
|
|
10545
|
+
}
|
|
10546
|
+
return JSON.stringify(value) ?? "undefined";
|
|
10547
|
+
}
|
|
10548
|
+
var VERTICAL_CAPABILITY_FILES = [
|
|
10549
|
+
["notification_producer", "topolo.notification-contracts.json", "define app-owned events and recipient policies"],
|
|
10550
|
+
["metering", "topolo.metering.json", "define app-owned meters and attribution semantics"],
|
|
10551
|
+
["nexus_ai", "topolo.ai.json", "define AI use cases, budget behavior, and model policy"],
|
|
10552
|
+
["integrations", "topolo.integrations.json", "define the external providers and connection purposes this app uses"],
|
|
10553
|
+
["webhooks", "topolo.webhooks.json", "define signed event routes, replay policy, and delivery ownership"],
|
|
10554
|
+
["automation", "topolo.automation.json", "define app-owned triggers and TopoloFlow actions"],
|
|
10555
|
+
["data", "topolo.data.json", "define the workspace data model and migration ownership"],
|
|
10556
|
+
["object_storage", "topolo.object-storage.json", "define object lifecycle, access, and deletion policy"],
|
|
10557
|
+
["background_jobs", "topolo.jobs.json", "define accepted job types, retry policy, and idempotency"],
|
|
10558
|
+
["durable_workflows", "topolo.workflows.json", "define workflow inputs, steps, and recovery semantics"],
|
|
10559
|
+
["privacy", "topolo.privacy.json", "define app-owned retention, export, and deletion adapters"],
|
|
10560
|
+
["chat", "topolo.chat.json", "define where the shared chat surface appears in this product"],
|
|
10561
|
+
["calendar", "topolo.calendar.json", "define the app-owned scheduling surface and resource mapping"],
|
|
10562
|
+
["mailbox", "topolo.mailbox.json", "define the app-owned mailbox surface and message context"]
|
|
10563
|
+
];
|
|
10564
|
+
function resolveApiDirectory(root, manifest, target) {
|
|
10565
|
+
if (target === "developer") return root;
|
|
10566
|
+
for (const value of Object.values(record(manifest.targets) ?? {})) {
|
|
10567
|
+
const targetRecord = record(value);
|
|
10568
|
+
if (targetRecord?.kind === "worker" && !targetRecord.assets && typeof targetRecord.dir === "string") {
|
|
10569
|
+
return path3.resolve(root, targetRecord.dir);
|
|
10570
|
+
}
|
|
10571
|
+
}
|
|
10572
|
+
return path3.join(root, "apps", "api");
|
|
10573
|
+
}
|
|
10574
|
+
function relativePath(root, filePath) {
|
|
10575
|
+
return path3.relative(root, filePath).split(path3.sep).join("/");
|
|
10576
|
+
}
|
|
10577
|
+
|
|
10578
|
+
// src/commands/app-reconcile.ts
|
|
10579
|
+
function record2(value) {
|
|
10580
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
10581
|
+
}
|
|
9882
10582
|
function environmentValue(value, environment) {
|
|
9883
10583
|
if (typeof value === "string") return value;
|
|
9884
10584
|
if (!value) return null;
|
|
@@ -9902,8 +10602,14 @@ function resolveManifestApplicationUrl(manifest, environment) {
|
|
|
9902
10602
|
}
|
|
9903
10603
|
return `https://${target.subdomain}.${domain}`;
|
|
9904
10604
|
}
|
|
10605
|
+
function resolveManifestHealthcheckPath(manifest) {
|
|
10606
|
+
const targets = manifest.targets || {};
|
|
10607
|
+
const targetName = manifest.primaryTarget || (targets.web ? "web" : Object.keys(targets)[0]);
|
|
10608
|
+
const healthcheckPath = targetName ? targets[targetName]?.healthcheckPath?.trim() : "";
|
|
10609
|
+
return healthcheckPath && healthcheckPath.startsWith("/") && !healthcheckPath.startsWith("//") ? healthcheckPath : "/health";
|
|
10610
|
+
}
|
|
9905
10611
|
function assertRegistration(value) {
|
|
9906
|
-
const registration =
|
|
10612
|
+
const registration = record2(value);
|
|
9907
10613
|
const required = [
|
|
9908
10614
|
"ownerType",
|
|
9909
10615
|
"applicationId",
|
|
@@ -9932,9 +10638,9 @@ function titleFromSlug(value) {
|
|
|
9932
10638
|
return value.replace(/^topolo[-_]/, "").split(/[-_]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
|
|
9933
10639
|
}
|
|
9934
10640
|
async function completeApplicationRegistrationInteractively(options) {
|
|
9935
|
-
const manifestPath =
|
|
9936
|
-
const manifest = JSON.parse(await
|
|
9937
|
-
const existing =
|
|
10641
|
+
const manifestPath = path4.resolve(options.manifestPath || "topolo.app.json");
|
|
10642
|
+
const manifest = JSON.parse(await readFile4(manifestPath, "utf8"));
|
|
10643
|
+
const existing = record2(manifest.registration) || {};
|
|
9938
10644
|
const appId = String(manifest.appId || manifest.app_id || "").trim();
|
|
9939
10645
|
const slug = String(existing.serviceSlug || manifest.name || "").trim();
|
|
9940
10646
|
const defaultName = titleFromSlug(slug);
|
|
@@ -9968,19 +10674,19 @@ async function completeApplicationRegistrationInteractively(options) {
|
|
|
9968
10674
|
containerAppId: existing.containerAppId,
|
|
9969
10675
|
distributionModel: existing.distributionModel || (options.expectedOwnerType === "topolo_first_party" ? "marketplace" : "organization_internal"),
|
|
9970
10676
|
version: String(existing.version || "1.0.0"),
|
|
9971
|
-
pricing:
|
|
10677
|
+
pricing: record2(existing.pricing) || { type: "free", monthlyPrice: null },
|
|
9972
10678
|
supportEmail,
|
|
9973
10679
|
productionUrl,
|
|
9974
10680
|
demoUrl: typeof existing.demoUrl === "string" ? existing.demoUrl : void 0,
|
|
9975
10681
|
repoUrl: typeof existing.repoUrl === "string" ? existing.repoUrl : void 0,
|
|
9976
10682
|
tags: Array.isArray(existing.tags) ? existing.tags : [],
|
|
9977
10683
|
screenshots: Array.isArray(existing.screenshots) ? existing.screenshots : [],
|
|
9978
|
-
auth:
|
|
10684
|
+
auth: record2(existing.auth) || {
|
|
9979
10685
|
callbackPath: "/auth/callback",
|
|
9980
10686
|
requestedScopes: ["openid", "profile", "email", "organization:read"]
|
|
9981
10687
|
},
|
|
9982
10688
|
capabilities: Array.isArray(existing.capabilities) ? existing.capabilities : ["login", "launcher"],
|
|
9983
|
-
marketplace:
|
|
10689
|
+
marketplace: record2(existing.marketplace) || {
|
|
9984
10690
|
intent: options.expectedOwnerType === "topolo_first_party" ? "submit_when_ready" : "not_requested",
|
|
9985
10691
|
status: "preview",
|
|
9986
10692
|
shortBlurb: description,
|
|
@@ -10004,7 +10710,7 @@ async function completeApplicationRegistrationInteractively(options) {
|
|
|
10004
10710
|
changed = true;
|
|
10005
10711
|
}
|
|
10006
10712
|
if (!appId) throw new Error("topolo.app.json must declare the immutable appId before reconciliation.");
|
|
10007
|
-
if (changed) await
|
|
10713
|
+
if (changed) await writeFile5(manifestPath, `${JSON.stringify(manifest, null, 2)}
|
|
10008
10714
|
`, "utf8");
|
|
10009
10715
|
return { changed, manifestPath };
|
|
10010
10716
|
} finally {
|
|
@@ -10012,8 +10718,8 @@ async function completeApplicationRegistrationInteractively(options) {
|
|
|
10012
10718
|
}
|
|
10013
10719
|
}
|
|
10014
10720
|
async function loadApplicationReconcileRequest(options) {
|
|
10015
|
-
const manifestPath =
|
|
10016
|
-
const raw = await
|
|
10721
|
+
const manifestPath = path4.resolve(options.manifestPath || "topolo.app.json");
|
|
10722
|
+
const raw = await readFile4(manifestPath, "utf8");
|
|
10017
10723
|
const manifest = JSON.parse(raw);
|
|
10018
10724
|
assertRegistration(manifest.registration);
|
|
10019
10725
|
const registration = manifest.registration;
|
|
@@ -10048,6 +10754,7 @@ async function loadApplicationReconcileRequest(options) {
|
|
|
10048
10754
|
pricingType: registration.pricing.type,
|
|
10049
10755
|
monthlyPrice: registration.pricing.monthlyPrice ?? null,
|
|
10050
10756
|
productionUrl,
|
|
10757
|
+
healthcheckPath: resolveManifestHealthcheckPath(manifest),
|
|
10051
10758
|
demoUrl: registration.ownerType === "topolo_first_party" ? productionUrl : registration.demoUrl ?? productionUrl,
|
|
10052
10759
|
repoUrl: registration.repoUrl ?? null,
|
|
10053
10760
|
supportEmail: registration.supportEmail,
|
|
@@ -10157,6 +10864,7 @@ function registerApplicationReconcileCommand(parent, ownerType, dependencies = {
|
|
|
10157
10864
|
const loadRequest = dependencies.loadRequest || loadApplicationReconcileRequest;
|
|
10158
10865
|
const reconcileRequest = dependencies.reconcileRequest || reconcileApplicationRequest;
|
|
10159
10866
|
const confirm = dependencies.confirm || confirmApplicationReconcile;
|
|
10867
|
+
const reconcileSource = dependencies.reconcileSource || reconcileApplicationCapabilitySource;
|
|
10160
10868
|
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
10869
|
const outputMode = resolveOutputMode(options.json);
|
|
10162
10870
|
try {
|
|
@@ -10167,6 +10875,20 @@ function registerApplicationReconcileCommand(parent, ownerType, dependencies = {
|
|
|
10167
10875
|
expectedOwnerType: ownerType
|
|
10168
10876
|
});
|
|
10169
10877
|
}
|
|
10878
|
+
const source = await reconcileSource({
|
|
10879
|
+
manifestPath: options.manifest,
|
|
10880
|
+
ownerType,
|
|
10881
|
+
mode: selection.mode
|
|
10882
|
+
});
|
|
10883
|
+
if (selection.mode === "apply" && source.status === "blocked") {
|
|
10884
|
+
emit(
|
|
10885
|
+
{ manifest: source.manifestPath, source, reconciliation: null },
|
|
10886
|
+
outputMode,
|
|
10887
|
+
() => formatCapabilitySource(source)
|
|
10888
|
+
);
|
|
10889
|
+
process.exitCode = 2;
|
|
10890
|
+
return;
|
|
10891
|
+
}
|
|
10170
10892
|
const loaded = await loadRequest({
|
|
10171
10893
|
manifestPath: options.manifest,
|
|
10172
10894
|
expectedOwnerType: ownerType,
|
|
@@ -10184,11 +10906,11 @@ function registerApplicationReconcileCommand(parent, ownerType, dependencies = {
|
|
|
10184
10906
|
}
|
|
10185
10907
|
}
|
|
10186
10908
|
emit(
|
|
10187
|
-
{ manifest: loaded.manifestPath, reconciliation: result },
|
|
10909
|
+
{ manifest: loaded.manifestPath, source, reconciliation: result },
|
|
10188
10910
|
outputMode,
|
|
10189
10911
|
() => formatApplicationReconcile(result, loaded.manifestPath)
|
|
10190
10912
|
);
|
|
10191
|
-
if (result.mode === "check" && result.plan.status !== "in_sync") process.exitCode = 2;
|
|
10913
|
+
if (source.status === "blocked" || result.mode === "check" && (result.plan.status !== "in_sync" || source.status !== "in_sync")) process.exitCode = 2;
|
|
10192
10914
|
} catch (error) {
|
|
10193
10915
|
fail(error, outputMode);
|
|
10194
10916
|
}
|
|
@@ -10226,6 +10948,7 @@ function registerAppsCommand(program2) {
|
|
|
10226
10948
|
}
|
|
10227
10949
|
});
|
|
10228
10950
|
registerApplicationReconcileCommand(apps, "third_party");
|
|
10951
|
+
registerApplicationCapabilityCommands(apps, "third_party");
|
|
10229
10952
|
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
10953
|
const mode = resolveOutputMode(opts.json);
|
|
10231
10954
|
try {
|
|
@@ -10295,7 +11018,7 @@ function registerAppsCommand(program2) {
|
|
|
10295
11018
|
).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
11019
|
"--surface-model <surfaceModel>",
|
|
10297
11020
|
"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(
|
|
11021
|
+
).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
11022
|
"--provision",
|
|
10300
11023
|
"Register the app in the current Topolo developer workspace using the authenticated session"
|
|
10301
11024
|
).action(async (application, opts) => {
|
|
@@ -10314,7 +11037,8 @@ function registerAppsCommand(program2) {
|
|
|
10314
11037
|
audience: resolveScaffoldAudience(opts.audience),
|
|
10315
11038
|
tenancy: resolveScaffoldTenancy(opts.tenancy),
|
|
10316
11039
|
surfaceModel: resolveScaffoldSurfaceModel(opts.surfaceModel),
|
|
10317
|
-
containerAppId: opts.containerAppId
|
|
11040
|
+
containerAppId: opts.containerAppId,
|
|
11041
|
+
capabilities: opts.capability
|
|
10318
11042
|
});
|
|
10319
11043
|
const writeResult = opts.write ? await writeApplicationScaffold(payload, { repoRoot: opts.repoRoot, force: opts.force }) : null;
|
|
10320
11044
|
const provisionResult = opts.provision ? await provisionApplicationScaffold(payload) : null;
|
|
@@ -10398,7 +11122,7 @@ function applicationAuditPayload(application, options = {}) {
|
|
|
10398
11122
|
}
|
|
10399
11123
|
function recordApplicationEvidence(application, options = {}) {
|
|
10400
11124
|
const appId = resolveApplication(application).id;
|
|
10401
|
-
const root =
|
|
11125
|
+
const root = path5.resolve(options.appRoot ?? process.cwd());
|
|
10402
11126
|
if (!existsSync4(root)) {
|
|
10403
11127
|
throw new Error(`Application root does not exist: ${root}`);
|
|
10404
11128
|
}
|
|
@@ -10410,8 +11134,8 @@ function recordApplicationEvidence(application, options = {}) {
|
|
|
10410
11134
|
commit = void 0;
|
|
10411
11135
|
}
|
|
10412
11136
|
}
|
|
10413
|
-
const evidenceDir =
|
|
10414
|
-
const evidencePath =
|
|
11137
|
+
const evidenceDir = path5.join(root, ".topolo");
|
|
11138
|
+
const evidencePath = path5.join(evidenceDir, "evidence.json");
|
|
10415
11139
|
let before = null;
|
|
10416
11140
|
if (existsSync4(evidencePath)) {
|
|
10417
11141
|
try {
|
|
@@ -10711,17 +11435,181 @@ function statusesAtOrWorse(threshold) {
|
|
|
10711
11435
|
|
|
10712
11436
|
// src/commands/platform.ts
|
|
10713
11437
|
import { readdirSync as readdirSync2, readFileSync as readFileSync6, statSync as statSync2 } from "fs";
|
|
10714
|
-
import
|
|
11438
|
+
import path7 from "path";
|
|
11439
|
+
|
|
11440
|
+
// src/commands/app-capability-provision.ts
|
|
11441
|
+
import { readFile as readFile5, writeFile as writeFile6 } from "fs/promises";
|
|
11442
|
+
import path6 from "path";
|
|
11443
|
+
async function provisionApplicationResources(options) {
|
|
11444
|
+
const manifestPath = path6.resolve(options.manifestPath || "topolo.app.json");
|
|
11445
|
+
const manifest = parseRecord2(await readFile5(manifestPath, "utf8"), manifestPath);
|
|
11446
|
+
const environments = requiredRecord(manifest.environments, "topolo.app.json#environments");
|
|
11447
|
+
const environment = requiredRecord(environments[options.environment], `topolo.app.json#environments.${options.environment}`);
|
|
11448
|
+
const accountId = requiredString(environment.accountId, `topolo.app.json#environments.${options.environment}.accountId`);
|
|
11449
|
+
const token = options.token ?? process.env.CLOUDFLARE_API_TOKEN;
|
|
11450
|
+
if (!token) throw new Error("CLOUDFLARE_API_TOKEN is required to inspect or provision Cloudflare application resources.");
|
|
11451
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
11452
|
+
const operations = [];
|
|
11453
|
+
let changed = false;
|
|
11454
|
+
for (const target of Object.values(record3(manifest.targets) ?? {})) {
|
|
11455
|
+
const bindings = record3(record3(target)?.bindings);
|
|
11456
|
+
if (!bindings) continue;
|
|
11457
|
+
for (const bindingValue of array(bindings.d1)) {
|
|
11458
|
+
const binding = record3(bindingValue);
|
|
11459
|
+
const config = record3(binding?.[options.environment]);
|
|
11460
|
+
if (!config) continue;
|
|
11461
|
+
const name = requiredString(config.databaseName, `D1 ${options.environment} databaseName`);
|
|
11462
|
+
const databases = await cloudflareList(fetchImpl, token, `/accounts/${accountId}/d1/database?name=${encodeURIComponent(name)}&per_page=10000`);
|
|
11463
|
+
const existing = databases.find((item) => item.name === name);
|
|
11464
|
+
if (existing) {
|
|
11465
|
+
const id = requiredString(existing.uuid ?? existing.id, `D1 database ${name} id`);
|
|
11466
|
+
const mismatched = config.databaseId !== id;
|
|
11467
|
+
operations.push({ kind: "d1", name, status: mismatched && options.mode === "check" ? "mismatch" : "exists", id });
|
|
11468
|
+
if (mismatched && options.mode === "apply") {
|
|
11469
|
+
config.databaseId = id;
|
|
11470
|
+
changed = true;
|
|
11471
|
+
}
|
|
11472
|
+
} else if (options.mode === "apply") {
|
|
11473
|
+
const created = await cloudflareCreate(fetchImpl, token, `/accounts/${accountId}/d1/database`, { name });
|
|
11474
|
+
const id = requiredString(created.uuid ?? created.id, `created D1 database ${name} id`);
|
|
11475
|
+
config.databaseId = id;
|
|
11476
|
+
changed = true;
|
|
11477
|
+
operations.push({ kind: "d1", name, status: "created", id });
|
|
11478
|
+
} else {
|
|
11479
|
+
operations.push({ kind: "d1", name, status: "missing" });
|
|
11480
|
+
}
|
|
11481
|
+
}
|
|
11482
|
+
for (const bindingValue of array(bindings.r2)) {
|
|
11483
|
+
const config = record3(record3(bindingValue)?.[options.environment]);
|
|
11484
|
+
if (!config) continue;
|
|
11485
|
+
const name = requiredString(config.bucketName, `R2 ${options.environment} bucketName`);
|
|
11486
|
+
const buckets = await cloudflareList(fetchImpl, token, `/accounts/${accountId}/r2/buckets?name_contains=${encodeURIComponent(name)}&per_page=1000`);
|
|
11487
|
+
const exists = buckets.some((item) => item.name === name);
|
|
11488
|
+
if (exists) operations.push({ kind: "r2", name, status: "exists" });
|
|
11489
|
+
else if (options.mode === "apply") {
|
|
11490
|
+
await cloudflareCreate(fetchImpl, token, `/accounts/${accountId}/r2/buckets`, { name });
|
|
11491
|
+
operations.push({ kind: "r2", name, status: "created" });
|
|
11492
|
+
} else operations.push({ kind: "r2", name, status: "missing" });
|
|
11493
|
+
}
|
|
11494
|
+
const queues = record3(bindings.queues);
|
|
11495
|
+
const queueNames = /* @__PURE__ */ new Set();
|
|
11496
|
+
for (const bindingValue of [...array(queues?.producers), ...array(queues?.consumers)]) {
|
|
11497
|
+
const config = record3(record3(bindingValue)?.[options.environment]);
|
|
11498
|
+
if (config && typeof config.queue === "string") queueNames.add(config.queue);
|
|
11499
|
+
}
|
|
11500
|
+
if (queueNames.size > 0) {
|
|
11501
|
+
const existingQueues = await cloudflareList(fetchImpl, token, `/accounts/${accountId}/queues?page=1&per_page=1000`);
|
|
11502
|
+
for (const name of queueNames) {
|
|
11503
|
+
const exists = existingQueues.some((item) => item.queue_name === name || item.name === name);
|
|
11504
|
+
if (exists) operations.push({ kind: "queue", name, status: "exists" });
|
|
11505
|
+
else if (options.mode === "apply") {
|
|
11506
|
+
await cloudflareCreate(fetchImpl, token, `/accounts/${accountId}/queues`, { queue_name: name });
|
|
11507
|
+
operations.push({ kind: "queue", name, status: "created" });
|
|
11508
|
+
} else operations.push({ kind: "queue", name, status: "missing" });
|
|
11509
|
+
}
|
|
11510
|
+
}
|
|
11511
|
+
}
|
|
11512
|
+
if (changed) await writeFile6(manifestPath, `${JSON.stringify(manifest, null, 2)}
|
|
11513
|
+
`, "utf8");
|
|
11514
|
+
const drift = operations.some((operation) => operation.status === "missing" || operation.status === "mismatch");
|
|
11515
|
+
return {
|
|
11516
|
+
mode: options.mode,
|
|
11517
|
+
environment: options.environment,
|
|
11518
|
+
manifestPath,
|
|
11519
|
+
accountId,
|
|
11520
|
+
changed,
|
|
11521
|
+
status: drift ? "drift" : "in_sync",
|
|
11522
|
+
operations
|
|
11523
|
+
};
|
|
11524
|
+
}
|
|
11525
|
+
function registerApplicationResourceProvisionCommand(parent) {
|
|
11526
|
+
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) => {
|
|
11527
|
+
const outputMode = resolveOutputMode(options.json);
|
|
11528
|
+
try {
|
|
11529
|
+
if (!isEnvironment(options.environment)) throw new Error("Environment must be development, staging, or production.");
|
|
11530
|
+
if (options.check && options.apply) throw new Error("Choose exactly one of --check or --apply.");
|
|
11531
|
+
if (options.environment === "production" && options.apply && !options.confirmProduction) {
|
|
11532
|
+
throw new Error("Production resource creation requires --confirm-production.");
|
|
11533
|
+
}
|
|
11534
|
+
const result = await provisionApplicationResources({
|
|
11535
|
+
manifestPath: options.manifest,
|
|
11536
|
+
environment: options.environment,
|
|
11537
|
+
mode: options.apply ? "apply" : "check"
|
|
11538
|
+
});
|
|
11539
|
+
emit(result, outputMode, () => formatResourceProvision(result));
|
|
11540
|
+
if (result.status !== "in_sync") process.exitCode = 2;
|
|
11541
|
+
} catch (error) {
|
|
11542
|
+
fail(error, outputMode);
|
|
11543
|
+
}
|
|
11544
|
+
});
|
|
11545
|
+
}
|
|
11546
|
+
function formatResourceProvision(result) {
|
|
11547
|
+
return [
|
|
11548
|
+
`Application resources: ${result.status}`,
|
|
11549
|
+
`Environment: ${result.environment}`,
|
|
11550
|
+
...result.operations.map((operation) => `- ${operation.kind} ${operation.name}: ${operation.status}`)
|
|
11551
|
+
].join("\n");
|
|
11552
|
+
}
|
|
11553
|
+
async function cloudflareList(fetchImpl, token, resourcePath) {
|
|
11554
|
+
const result = await cloudflareRequest(fetchImpl, token, resourcePath, "GET");
|
|
11555
|
+
if (Array.isArray(result)) return result.map(record3).filter((value) => Boolean(value));
|
|
11556
|
+
const resultRecord = record3(result);
|
|
11557
|
+
const values = array(resultRecord?.buckets ?? resultRecord?.queues ?? resultRecord?.databases);
|
|
11558
|
+
return values.map(record3).filter((value) => Boolean(value));
|
|
11559
|
+
}
|
|
11560
|
+
async function cloudflareCreate(fetchImpl, token, resourcePath, body) {
|
|
11561
|
+
const result = await cloudflareRequest(fetchImpl, token, resourcePath, "POST", body);
|
|
11562
|
+
return requiredRecord(result, `Cloudflare create response for ${resourcePath}`);
|
|
11563
|
+
}
|
|
11564
|
+
async function cloudflareRequest(fetchImpl, token, resourcePath, method, body) {
|
|
11565
|
+
const response = await fetchImpl(`https://api.cloudflare.com/client/v4${resourcePath}`, {
|
|
11566
|
+
method,
|
|
11567
|
+
headers: { Authorization: `Bearer ${token}`, ...body ? { "Content-Type": "application/json" } : {} },
|
|
11568
|
+
...body ? { body: JSON.stringify(body) } : {}
|
|
11569
|
+
});
|
|
11570
|
+
const payload = await response.json().catch(() => null);
|
|
11571
|
+
const envelope = record3(payload);
|
|
11572
|
+
if (!response.ok || envelope?.success === false) {
|
|
11573
|
+
throw new Error(`Cloudflare ${method} ${resourcePath} failed with HTTP ${response.status}.`);
|
|
11574
|
+
}
|
|
11575
|
+
return envelope && "result" in envelope ? envelope.result : payload;
|
|
11576
|
+
}
|
|
11577
|
+
function isEnvironment(value) {
|
|
11578
|
+
return value === "development" || value === "staging" || value === "production";
|
|
11579
|
+
}
|
|
11580
|
+
function parseRecord2(raw, filePath) {
|
|
11581
|
+
const value = JSON.parse(raw);
|
|
11582
|
+
return requiredRecord(value, filePath);
|
|
11583
|
+
}
|
|
11584
|
+
function requiredRecord(value, label) {
|
|
11585
|
+
const valueRecord = record3(value);
|
|
11586
|
+
if (!valueRecord) throw new Error(`${label} must be an object.`);
|
|
11587
|
+
return valueRecord;
|
|
11588
|
+
}
|
|
11589
|
+
function requiredString(value, label) {
|
|
11590
|
+
if (typeof value !== "string" || !value.trim()) throw new Error(`${label} must be a non-empty string.`);
|
|
11591
|
+
return value;
|
|
11592
|
+
}
|
|
11593
|
+
function record3(value) {
|
|
11594
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
11595
|
+
}
|
|
11596
|
+
function array(value) {
|
|
11597
|
+
return Array.isArray(value) ? value : [];
|
|
11598
|
+
}
|
|
11599
|
+
|
|
11600
|
+
// src/commands/platform.ts
|
|
10715
11601
|
function registerPlatformCommand(program2) {
|
|
10716
11602
|
const platform3 = program2.command("platform").description("Internal Topolo first-party platform and personal app commands");
|
|
10717
11603
|
const platformApps = platform3.command("apps").description("Scaffold and operate first-party Topolo platform and personal applications");
|
|
10718
11604
|
registerApplicationReconcileCommand(platformApps, "topolo_first_party");
|
|
11605
|
+
registerApplicationCapabilityCommands(platformApps, "topolo_first_party");
|
|
11606
|
+
registerApplicationResourceProvisionCommand(platformApps);
|
|
10719
11607
|
platformApps.command("scaffold <application>").description(
|
|
10720
11608
|
"Generate a first-party Topolo scaffold for the platform or personal portfolio."
|
|
10721
11609
|
).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
11610
|
"--surface-model <surfaceModel>",
|
|
10723
11611
|
"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("--
|
|
11612
|
+
).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
11613
|
"--provision",
|
|
10726
11614
|
"Provision the generated first-party app into Topolo Developers and Auth using the authenticated Topolo admin session"
|
|
10727
11615
|
).option(
|
|
@@ -10752,11 +11640,7 @@ function registerPlatformCommand(program2) {
|
|
|
10752
11640
|
surfaceModel: resolvePlatformSurfaceModel(opts.surfaceModel),
|
|
10753
11641
|
containerAppId: opts.containerAppId,
|
|
10754
11642
|
profile: opts.profile,
|
|
10755
|
-
|
|
10756
|
-
withR2: opts.withR2,
|
|
10757
|
-
withQueue: opts.withQueue,
|
|
10758
|
-
withWorkflow: opts.withWorkflow,
|
|
10759
|
-
withContainerProcessing: opts.withContainerProcessing,
|
|
11643
|
+
capabilities: opts.capability,
|
|
10760
11644
|
homePath: opts.homePath,
|
|
10761
11645
|
homeLabel: opts.homeLabel,
|
|
10762
11646
|
navigationMode: opts.navigationMode,
|
|
@@ -10829,12 +11713,12 @@ var SKIPPED_SCAN_DIRS = /* @__PURE__ */ new Set([
|
|
|
10829
11713
|
"output"
|
|
10830
11714
|
]);
|
|
10831
11715
|
function scanStagingProductionUrls(rootInput, allowPatterns) {
|
|
10832
|
-
const root =
|
|
11716
|
+
const root = path7.resolve(rootInput);
|
|
10833
11717
|
const allowlist = allowPatterns.map((pattern) => new RegExp(pattern));
|
|
10834
11718
|
const violations = [];
|
|
10835
11719
|
function shouldSkipFile(filePath) {
|
|
10836
|
-
const basename =
|
|
10837
|
-
const
|
|
11720
|
+
const basename = path7.basename(filePath);
|
|
11721
|
+
const relativePath2 = path7.relative(root, filePath);
|
|
10838
11722
|
if (basename === "package-lock.json" || basename === "pnpm-lock.yaml" || basename === "yarn.lock") return true;
|
|
10839
11723
|
if (basename === "topolo.cloudcontrol.json") return true;
|
|
10840
11724
|
if (basename === "wrangler.toml" || basename === "wrangler.jsonc") return true;
|
|
@@ -10843,29 +11727,29 @@ function scanStagingProductionUrls(rootInput, allowPatterns) {
|
|
|
10843
11727
|
if (basename.endsWith(".generated.ts")) return true;
|
|
10844
11728
|
if (basename.endsWith(".test.ts") || basename.endsWith(".test.tsx") || basename.endsWith(".spec.ts")) return true;
|
|
10845
11729
|
if (basename === ".env.production") return true;
|
|
10846
|
-
if (
|
|
10847
|
-
if (
|
|
10848
|
-
if (
|
|
10849
|
-
return !RUNTIME_EXTENSIONS.has(
|
|
11730
|
+
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;
|
|
11731
|
+
if (relativePath2.includes(`${path7.sep}test${path7.sep}`) || relativePath2.includes(`${path7.sep}tests${path7.sep}`) || relativePath2.includes(`${path7.sep}e2e${path7.sep}`)) return true;
|
|
11732
|
+
if (relativePath2.startsWith(`scripts${path7.sep}`) || relativePath2.includes(`${path7.sep}scripts${path7.sep}`)) return true;
|
|
11733
|
+
return !RUNTIME_EXTENSIONS.has(path7.extname(filePath));
|
|
10850
11734
|
}
|
|
10851
11735
|
function scanFile(filePath) {
|
|
10852
11736
|
if (shouldSkipFile(filePath)) return;
|
|
10853
|
-
const
|
|
11737
|
+
const relativePath2 = path7.relative(root, filePath);
|
|
10854
11738
|
const content = readFileSync6(filePath, "utf8");
|
|
10855
11739
|
const lines = content.split(/\r?\n/);
|
|
10856
11740
|
lines.forEach((line, index) => {
|
|
10857
11741
|
const matches = [...line.matchAll(PRODUCTION_TOPOLO_URL_PATTERN)].map((match) => match[0]);
|
|
10858
11742
|
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(`${
|
|
11743
|
+
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
11744
|
if (!allowed) {
|
|
10861
|
-
violations.push({ file:
|
|
11745
|
+
violations.push({ file: relativePath2, line: index + 1, url, text: line.trim() });
|
|
10862
11746
|
}
|
|
10863
11747
|
}
|
|
10864
11748
|
});
|
|
10865
11749
|
}
|
|
10866
11750
|
function walk(dir) {
|
|
10867
11751
|
for (const entry of readdirSync2(dir, { withFileTypes: true })) {
|
|
10868
|
-
const fullPath =
|
|
11752
|
+
const fullPath = path7.join(dir, entry.name);
|
|
10869
11753
|
if (entry.isDirectory()) {
|
|
10870
11754
|
if (!SKIPPED_SCAN_DIRS.has(entry.name)) walk(fullPath);
|
|
10871
11755
|
continue;
|
|
@@ -11033,7 +11917,7 @@ var DEFAULT_SEED_BASE_URL = "https://seed.stg.topolo.us";
|
|
|
11033
11917
|
function seedBaseUrl() {
|
|
11034
11918
|
return process.env.TOPOLO_SEED_URL || DEFAULT_SEED_BASE_URL;
|
|
11035
11919
|
}
|
|
11036
|
-
async function authedFetch(
|
|
11920
|
+
async function authedFetch(path8, init = {}) {
|
|
11037
11921
|
const credential = await refreshCredentialIfNeeded("staging");
|
|
11038
11922
|
if (!credential) {
|
|
11039
11923
|
throw new Error("No staging credential configured. Run `topolo auth login --env staging` or set TOPOLO_STAGING_API_KEY.");
|
|
@@ -11043,7 +11927,7 @@ async function authedFetch(path6, init = {}) {
|
|
|
11043
11927
|
else headers.set("Authorization", `Bearer ${credential.accessToken}`);
|
|
11044
11928
|
headers.set("X-Topolo-Client", `topolo-cli/${CLI_VERSION}`);
|
|
11045
11929
|
if (init.body && !headers.has("Content-Type")) headers.set("Content-Type", "application/json");
|
|
11046
|
-
const response = await fetch(`${seedBaseUrl()}${
|
|
11930
|
+
const response = await fetch(`${seedBaseUrl()}${path8}`, { ...init, headers });
|
|
11047
11931
|
const text = await response.text();
|
|
11048
11932
|
let body;
|
|
11049
11933
|
try {
|
|
@@ -11320,7 +12204,7 @@ function createOAuthClientPayload(options) {
|
|
|
11320
12204
|
logo_url: options.logoUrl
|
|
11321
12205
|
};
|
|
11322
12206
|
}
|
|
11323
|
-
async function authFetchJson(
|
|
12207
|
+
async function authFetchJson(path8, init = {}) {
|
|
11324
12208
|
const environment = selectedEnvironment();
|
|
11325
12209
|
const credential = await refreshCredentialIfNeeded(environment);
|
|
11326
12210
|
if (!credential) {
|
|
@@ -11342,7 +12226,7 @@ async function authFetchJson(path6, init = {}) {
|
|
|
11342
12226
|
throw err;
|
|
11343
12227
|
}
|
|
11344
12228
|
headers.set("Authorization", `Bearer ${credential.accessToken}`);
|
|
11345
|
-
const response = await fetch(`${authBaseUrl}${
|
|
12229
|
+
const response = await fetch(`${authBaseUrl}${path8}`, { ...init, headers });
|
|
11346
12230
|
const text = await response.text();
|
|
11347
12231
|
const payload = text ? JSON.parse(text) : {};
|
|
11348
12232
|
if (!response.ok || payload.success === false) {
|
|
@@ -11358,7 +12242,7 @@ function normalizeList(values, fallback = []) {
|
|
|
11358
12242
|
}
|
|
11359
12243
|
|
|
11360
12244
|
// src/commands/integrations.ts
|
|
11361
|
-
import { readdir, readFile as
|
|
12245
|
+
import { readdir, readFile as readFile6, stat } from "fs/promises";
|
|
11362
12246
|
import { join as join4, relative as relative3 } from "path";
|
|
11363
12247
|
function registerIntegrationsCommand(program2) {
|
|
11364
12248
|
const integrations = program2.command("integrations").description("Generate and validate Topolo integration handoffs.");
|
|
@@ -11502,7 +12386,7 @@ function detectFramework(packageJson2, haystack) {
|
|
|
11502
12386
|
}
|
|
11503
12387
|
async function readPackageJson2(repoRoot) {
|
|
11504
12388
|
try {
|
|
11505
|
-
return JSON.parse(await
|
|
12389
|
+
return JSON.parse(await readFile6(join4(repoRoot, "package.json"), "utf8"));
|
|
11506
12390
|
} catch {
|
|
11507
12391
|
return null;
|
|
11508
12392
|
}
|
|
@@ -11525,7 +12409,7 @@ async function collectFiles(repoRoot) {
|
|
|
11525
12409
|
if (info.size > 25e4) continue;
|
|
11526
12410
|
result.push({
|
|
11527
12411
|
path: relative3(root, fullPath),
|
|
11528
|
-
content: await
|
|
12412
|
+
content: await readFile6(fullPath, "utf8")
|
|
11529
12413
|
});
|
|
11530
12414
|
}
|
|
11531
12415
|
}
|
|
@@ -11543,7 +12427,7 @@ function formatCheckResult(result) {
|
|
|
11543
12427
|
// src/commands/skills.ts
|
|
11544
12428
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
11545
12429
|
import { existsSync as existsSync5 } from "fs";
|
|
11546
|
-
import { mkdir as
|
|
12430
|
+
import { mkdir as mkdir4, copyFile, readFile as readFile7, writeFile as writeFile7, rm } from "fs/promises";
|
|
11547
12431
|
import { homedir as homedir2 } from "os";
|
|
11548
12432
|
import { dirname as dirname4, join as join5, resolve as resolve3 } from "path";
|
|
11549
12433
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
@@ -11579,7 +12463,7 @@ function codexSkillDir() {
|
|
|
11579
12463
|
return join5(homedir2(), ".codex", "skills", "topolo");
|
|
11580
12464
|
}
|
|
11581
12465
|
async function installSkillFile(sourceDir, targetDir) {
|
|
11582
|
-
await
|
|
12466
|
+
await mkdir4(targetDir, { recursive: true });
|
|
11583
12467
|
const target = join5(targetDir, "SKILL.md");
|
|
11584
12468
|
await copyFile(join5(sourceDir, "topolo", "SKILL.md"), target);
|
|
11585
12469
|
return target;
|
|
@@ -11597,11 +12481,11 @@ function registerViaOfficialCli(cli, mcp, agentName) {
|
|
|
11597
12481
|
return result.status === 0;
|
|
11598
12482
|
}
|
|
11599
12483
|
async function registerClaudeViaConfig(mcp) {
|
|
11600
|
-
const
|
|
12484
|
+
const path8 = join5(homedir2(), ".claude.json");
|
|
11601
12485
|
let config = {};
|
|
11602
|
-
if (existsSync5(
|
|
12486
|
+
if (existsSync5(path8)) {
|
|
11603
12487
|
try {
|
|
11604
|
-
config = JSON.parse(await
|
|
12488
|
+
config = JSON.parse(await readFile7(path8, "utf8"));
|
|
11605
12489
|
} catch {
|
|
11606
12490
|
config = {};
|
|
11607
12491
|
}
|
|
@@ -11614,13 +12498,13 @@ async function registerClaudeViaConfig(mcp) {
|
|
|
11614
12498
|
env: { TOPOLO_AGENT_NAME: AGENT_NAME_FOR.claude }
|
|
11615
12499
|
};
|
|
11616
12500
|
config.mcpServers = servers;
|
|
11617
|
-
await
|
|
12501
|
+
await writeFile7(path8, `${JSON.stringify(config, null, 2)}
|
|
11618
12502
|
`, "utf8");
|
|
11619
12503
|
}
|
|
11620
12504
|
async function registerCodexViaConfig(mcp) {
|
|
11621
|
-
const
|
|
12505
|
+
const path8 = join5(homedir2(), ".codex", "config.toml");
|
|
11622
12506
|
let text = "";
|
|
11623
|
-
if (existsSync5(
|
|
12507
|
+
if (existsSync5(path8)) text = await readFile7(path8, "utf8");
|
|
11624
12508
|
text = text.replace(
|
|
11625
12509
|
/(^|\n)\[mcp_servers\.topolo(?:\.[a-z]+)?\][^\[]*/g,
|
|
11626
12510
|
(match) => match.startsWith("\n") ? "\n" : ""
|
|
@@ -11636,8 +12520,8 @@ command = "${mcp.command}"${argsToml}
|
|
|
11636
12520
|
[mcp_servers.topolo.env]
|
|
11637
12521
|
TOPOLO_AGENT_NAME = "${AGENT_NAME_FOR.codex}"
|
|
11638
12522
|
`;
|
|
11639
|
-
await
|
|
11640
|
-
await
|
|
12523
|
+
await mkdir4(dirname4(path8), { recursive: true });
|
|
12524
|
+
await writeFile7(path8, `${text}${block}`, "utf8");
|
|
11641
12525
|
}
|
|
11642
12526
|
async function installForAgent(agent, sourceDir, mcp, opts) {
|
|
11643
12527
|
const skillDir = agent === "claude" ? claudeSkillDir(opts.projectDir) : codexSkillDir();
|