@topolo/cli 0.1.29 → 0.1.31

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.
Files changed (2) hide show
  1. package/dist/index.js +1212 -1194
  2. package/package.json +13 -13
package/dist/index.js CHANGED
@@ -65,54 +65,53 @@ var TopoloHttpError = class extends TopoloSdkError {
65
65
  }
66
66
  };
67
67
  var SERVICES = {
68
- agent: "https://agent-api.topolo.app",
68
+ agent: "https://agent.topolo.app",
69
69
  auth: "https://auth.topolo.app",
70
70
  backup: "https://backup.topolo.app",
71
71
  books: "https://books.topolo.app",
72
72
  bugfix: "https://bugfix.topolo.app",
73
- bytes: "https://bytes-api.topolo.app",
73
+ bytes: "https://bytes.topolo.app",
74
74
  calendar: "https://calendar.topolo.app",
75
75
  capacity: "https://capacity.topolo.app",
76
76
  chat: "https://chat.topolo.app",
77
- commerce: "https://commerce-api.topolo.app",
77
+ commerce: "https://commerce.topolo.app",
78
78
  compose: "https://compose.topolo.app",
79
79
  consent: "https://consent.topolo.app",
80
- crm: "https://crm-api.topolo.app",
80
+ crm: "https://crm.topolo.app",
81
81
  developers: "https://developers.topolo.app",
82
82
  director: "https://director.topolo.app",
83
- feed: "https://feed-api.topolo.app",
83
+ feed: "https://feed.topolo.app",
84
84
  flow: "https://flow.topolo.app",
85
- forecast: "https://forecast-api.topolo.app",
85
+ forecast: "https://forecast.topolo.app",
86
86
  forms: "https://forms.topolo.app",
87
87
  home: "https://home.topolo.app",
88
88
  insights: "https://insights.topolo.app",
89
89
  inventory: "https://inventory.topolo.app",
90
- learn: "https://learn-api.topolo.app",
90
+ learn: "https://learn.topolo.app",
91
91
  localize: "https://localize.topolo.app",
92
- mail: "https://mail-api.topolo.app",
93
- mdm: "https://mdm-api.topolo.app",
92
+ mail: "https://mail.topolo.app",
93
+ mdm: "https://mdm.topolo.app",
94
94
  messages: "https://messages.topolo.app",
95
- nexus: "https://nexus-api.topolo.app",
95
+ nexus: "https://nexus.topolo.app",
96
96
  notify: "https://notify.topolo.app",
97
97
  observability_watch: "https://observability-watch.topolo.app",
98
- one: "https://one.topolo.app",
98
+ one: "https://www.topolo.app",
99
99
  p2p: "https://p2p.topolo.app",
100
100
  pay: "https://pay.topolo.app",
101
101
  people: "https://people.topolo.app",
102
- pulse: "https://pulse.topolo.app",
103
102
  "quro.api": "https://api.ol0.me",
104
103
  "quro.redirect": "https://ol0.me",
105
- roadmapper: "https://roadmapper-api.topolo.app",
104
+ roadmapper: "https://roadmapper.topolo.app",
106
105
  sign: "https://sign.topolo.app",
107
- social_studio: "https://studio-api.topolo.app",
108
- socialize: "https://socialize-api.topolo.app",
106
+ social_studio: "https://studio.topolo.app",
107
+ socialize: "https://socialize.topolo.app",
109
108
  spaces: "https://spaces.topolo.app",
110
109
  status: "https://status.topolo.app",
111
110
  success: "https://success.topolo.app",
112
111
  support: "https://support.topolo.app",
113
112
  survey: "https://survey.topolo.app",
114
113
  voice: "https://voice.topolo.app",
115
- web: "https://web-api.topolo.app"
114
+ web: "https://web.topolo.app"
116
115
  };
117
116
  var APPLICATIONS = {
118
117
  "admin": {
@@ -161,17 +160,15 @@ var APPLICATIONS = {
161
160
  "agent.topolo.app"
162
161
  ],
163
162
  "routes": [],
164
- "serviceId": null
163
+ "serviceId": "agent"
165
164
  },
166
165
  {
167
166
  "name": "topolo-agent-api",
168
167
  "kind": "worker",
169
- "apiUrl": "https://agent-api.topolo.app",
170
- "domains": [
171
- "agent-api.topolo.app"
172
- ],
168
+ "apiUrl": null,
169
+ "domains": [],
173
170
  "routes": [],
174
- "serviceId": "agent"
171
+ "serviceId": null
175
172
  }
176
173
  ]
177
174
  },
@@ -291,17 +288,15 @@ var APPLICATIONS = {
291
288
  "bytes.topolo.app"
292
289
  ],
293
290
  "routes": [],
294
- "serviceId": null
291
+ "serviceId": "bytes"
295
292
  },
296
293
  {
297
294
  "name": "topolo-bytes-api",
298
295
  "kind": "worker",
299
- "apiUrl": "https://bytes-api.topolo.app",
300
- "domains": [
301
- "bytes-api.topolo.app"
302
- ],
296
+ "apiUrl": null,
297
+ "domains": [],
303
298
  "routes": [],
304
- "serviceId": "bytes"
299
+ "serviceId": null
305
300
  }
306
301
  ]
307
302
  },
@@ -400,12 +395,10 @@ var APPLICATIONS = {
400
395
  {
401
396
  "name": "topolo-commerce-api",
402
397
  "kind": "worker",
403
- "apiUrl": "https://commerce-api.topolo.app",
404
- "domains": [
405
- "commerce-api.topolo.app"
406
- ],
398
+ "apiUrl": null,
399
+ "domains": [],
407
400
  "routes": [],
408
- "serviceId": "commerce"
401
+ "serviceId": null
409
402
  },
410
403
  {
411
404
  "name": "topolo-commerce-guest-web",
@@ -425,7 +418,7 @@ var APPLICATIONS = {
425
418
  "commerce.topolo.app"
426
419
  ],
427
420
  "routes": [],
428
- "serviceId": null
421
+ "serviceId": "commerce"
429
422
  },
430
423
  {
431
424
  "name": "topolo-commerce-station-web",
@@ -512,12 +505,10 @@ var APPLICATIONS = {
512
505
  {
513
506
  "name": "topolo-crm-api",
514
507
  "kind": "worker",
515
- "apiUrl": "https://crm-api.topolo.app",
516
- "domains": [
517
- "crm-api.topolo.app"
518
- ],
508
+ "apiUrl": null,
509
+ "domains": [],
519
510
  "routes": [],
520
- "serviceId": "crm"
511
+ "serviceId": null
521
512
  },
522
513
  {
523
514
  "name": "topolo-crm-app",
@@ -527,7 +518,7 @@ var APPLICATIONS = {
527
518
  "crm.topolo.app"
528
519
  ],
529
520
  "routes": [],
530
- "serviceId": null
521
+ "serviceId": "crm"
531
522
  }
532
523
  ]
533
524
  },
@@ -655,7 +646,7 @@ var APPLICATIONS = {
655
646
  "feed-api.topolo.app"
656
647
  ],
657
648
  "routes": [],
658
- "serviceId": "feed"
649
+ "serviceId": null
659
650
  },
660
651
  {
661
652
  "name": "topolo-feed-web",
@@ -665,7 +656,7 @@ var APPLICATIONS = {
665
656
  "feed.topolo.app"
666
657
  ],
667
658
  "routes": [],
668
- "serviceId": null
659
+ "serviceId": "feed"
669
660
  }
670
661
  ]
671
662
  },
@@ -717,17 +708,15 @@ var APPLICATIONS = {
717
708
  "forecast.topolo.app"
718
709
  ],
719
710
  "routes": [],
720
- "serviceId": null
711
+ "serviceId": "forecast"
721
712
  },
722
713
  {
723
714
  "name": "topolo-forecast-api",
724
715
  "kind": "worker",
725
- "apiUrl": "https://forecast-api.topolo.app",
726
- "domains": [
727
- "forecast-api.topolo.app"
728
- ],
716
+ "apiUrl": null,
717
+ "domains": [],
729
718
  "routes": [],
730
- "serviceId": "forecast"
719
+ "serviceId": null
731
720
  }
732
721
  ]
733
722
  },
@@ -869,17 +858,15 @@ var APPLICATIONS = {
869
858
  "learn.topolo.app"
870
859
  ],
871
860
  "routes": [],
872
- "serviceId": null
861
+ "serviceId": "learn"
873
862
  },
874
863
  {
875
864
  "name": "topolo-learn-api",
876
865
  "kind": "worker",
877
- "apiUrl": "https://learn-api.topolo.app",
878
- "domains": [
879
- "learn-api.topolo.app"
880
- ],
866
+ "apiUrl": null,
867
+ "domains": [],
881
868
  "routes": [],
882
- "serviceId": "learn"
869
+ "serviceId": null
883
870
  }
884
871
  ]
885
872
  },
@@ -925,17 +912,15 @@ var APPLICATIONS = {
925
912
  "mail.topolo.app"
926
913
  ],
927
914
  "routes": [],
928
- "serviceId": null
915
+ "serviceId": "mail"
929
916
  },
930
917
  {
931
918
  "name": "topolo-mail-api",
932
919
  "kind": "worker",
933
- "apiUrl": "https://mail-api.topolo.app",
934
- "domains": [
935
- "mail-api.topolo.app"
936
- ],
920
+ "apiUrl": null,
921
+ "domains": [],
937
922
  "routes": [],
938
- "serviceId": "mail"
923
+ "serviceId": null
939
924
  }
940
925
  ]
941
926
  },
@@ -952,12 +937,10 @@ var APPLICATIONS = {
952
937
  {
953
938
  "name": "topolo-mdm-api",
954
939
  "kind": "worker",
955
- "apiUrl": "https://mdm-api.topolo.app",
956
- "domains": [
957
- "mdm-api.topolo.app"
958
- ],
940
+ "apiUrl": null,
941
+ "domains": [],
959
942
  "routes": [],
960
- "serviceId": "mdm"
943
+ "serviceId": null
961
944
  },
962
945
  {
963
946
  "name": "topolo-mdm-console",
@@ -967,7 +950,7 @@ var APPLICATIONS = {
967
950
  "mdm.topolo.app"
968
951
  ],
969
952
  "routes": [],
970
- "serviceId": null
953
+ "serviceId": "mdm"
971
954
  }
972
955
  ]
973
956
  },
@@ -1032,9 +1015,9 @@ var APPLICATIONS = {
1032
1015
  {
1033
1016
  "name": "topolo-nexus-gateway",
1034
1017
  "kind": "worker",
1035
- "apiUrl": "https://nexus-api.topolo.app",
1018
+ "apiUrl": "https://nexus.topolo.app",
1036
1019
  "domains": [
1037
- "nexus-api.topolo.app"
1020
+ "nexus.topolo.app"
1038
1021
  ],
1039
1022
  "routes": [],
1040
1023
  "serviceId": "nexus"
@@ -1100,20 +1083,23 @@ var APPLICATIONS = {
1100
1083
  {
1101
1084
  "name": "topolo-one",
1102
1085
  "kind": "worker",
1103
- "apiUrl": null,
1104
- "domains": [],
1105
- "routes": [],
1086
+ "apiUrl": "https://one.topolo.app",
1087
+ "domains": [
1088
+ "one.topolo.app"
1089
+ ],
1090
+ "routes": [
1091
+ "one.topolo.app/*"
1092
+ ],
1106
1093
  "serviceId": null
1107
1094
  },
1108
1095
  {
1109
1096
  "name": "topolo-one-api",
1110
1097
  "kind": "worker",
1111
- "apiUrl": "https://one.topolo.app",
1098
+ "apiUrl": "https://www.topolo.app",
1112
1099
  "domains": [
1113
- "one.topolo.app"
1100
+ "www.topolo.app"
1114
1101
  ],
1115
1102
  "routes": [
1116
- "one.topolo.app/*",
1117
1103
  "www.topolo.app/*"
1118
1104
  ],
1119
1105
  "serviceId": "one"
@@ -1223,36 +1209,6 @@ var APPLICATIONS = {
1223
1209
  }
1224
1210
  ]
1225
1211
  },
1226
- "pulse": {
1227
- "id": "pulse",
1228
- "appDir": "TopoloPulse",
1229
- "name": "topolo-pulse",
1230
- "packageName": "topolo-pulse",
1231
- "productionUrl": "https://pulse.topolo.app",
1232
- "services": [
1233
- "pulse"
1234
- ],
1235
- "deployTargets": [
1236
- {
1237
- "name": "topolo-pulse",
1238
- "kind": "worker",
1239
- "apiUrl": "https://pulse.topolo.app",
1240
- "domains": [
1241
- "pulse.topolo.app"
1242
- ],
1243
- "routes": [],
1244
- "serviceId": "pulse"
1245
- },
1246
- {
1247
- "name": "topolo-pulse-api",
1248
- "kind": "worker",
1249
- "apiUrl": null,
1250
- "domains": [],
1251
- "routes": [],
1252
- "serviceId": null
1253
- }
1254
- ]
1255
- },
1256
1212
  "quro": {
1257
1213
  "id": "quro",
1258
1214
  "appDir": "TopoloQuro",
@@ -1318,17 +1274,15 @@ var APPLICATIONS = {
1318
1274
  "roadmapper.topolo.app"
1319
1275
  ],
1320
1276
  "routes": [],
1321
- "serviceId": null
1277
+ "serviceId": "roadmapper"
1322
1278
  },
1323
1279
  {
1324
1280
  "name": "topolo-roadmapper-api",
1325
1281
  "kind": "worker",
1326
- "apiUrl": "https://roadmapper-api.topolo.app",
1327
- "domains": [
1328
- "roadmapper-api.topolo.app"
1329
- ],
1282
+ "apiUrl": null,
1283
+ "domains": [],
1330
1284
  "routes": [],
1331
- "serviceId": "roadmapper"
1285
+ "serviceId": null
1332
1286
  }
1333
1287
  ]
1334
1288
  },
@@ -1380,17 +1334,15 @@ var APPLICATIONS = {
1380
1334
  "studio.topolo.app"
1381
1335
  ],
1382
1336
  "routes": [],
1383
- "serviceId": null
1337
+ "serviceId": "social_studio"
1384
1338
  },
1385
1339
  {
1386
1340
  "name": "topolo-social-studio-api",
1387
1341
  "kind": "worker",
1388
- "apiUrl": "https://studio-api.topolo.app",
1389
- "domains": [
1390
- "studio-api.topolo.app"
1391
- ],
1342
+ "apiUrl": null,
1343
+ "domains": [],
1392
1344
  "routes": [],
1393
- "serviceId": "social_studio"
1345
+ "serviceId": null
1394
1346
  },
1395
1347
  {
1396
1348
  "name": "topolo-social-studio-queue",
@@ -1420,7 +1372,7 @@ var APPLICATIONS = {
1420
1372
  "socialize.topolo.app"
1421
1373
  ],
1422
1374
  "routes": [],
1423
- "serviceId": null
1375
+ "serviceId": "socialize"
1424
1376
  },
1425
1377
  {
1426
1378
  "name": "topolo-socialize-api",
@@ -1430,7 +1382,7 @@ var APPLICATIONS = {
1430
1382
  "socialize-api.topolo.app"
1431
1383
  ],
1432
1384
  "routes": [],
1433
- "serviceId": "socialize"
1385
+ "serviceId": null
1434
1386
  },
1435
1387
  {
1436
1388
  "name": "topolo-socialize-scheduler",
@@ -1631,12 +1583,10 @@ var APPLICATIONS = {
1631
1583
  {
1632
1584
  "name": "topolo-web-api",
1633
1585
  "kind": "worker",
1634
- "apiUrl": "https://web-api.topolo.app",
1635
- "domains": [
1636
- "web-api.topolo.app"
1637
- ],
1586
+ "apiUrl": null,
1587
+ "domains": [],
1638
1588
  "routes": [],
1639
- "serviceId": "web"
1589
+ "serviceId": null
1640
1590
  },
1641
1591
  {
1642
1592
  "name": "topolo-web-runtime",
@@ -1659,12 +1609,12 @@ var APPLICATIONS = {
1659
1609
  "web.topolo.app"
1660
1610
  ],
1661
1611
  "routes": [],
1662
- "serviceId": null
1612
+ "serviceId": "web"
1663
1613
  }
1664
1614
  ]
1665
1615
  }
1666
1616
  };
1667
- var APPLICATION_REQUIREMENTS_VERSION = "2026-05-01.1";
1617
+ var APPLICATION_REQUIREMENTS_VERSION = "2026-06-07.1";
1668
1618
  var APPLICATION_REQUIREMENTS = [
1669
1619
  {
1670
1620
  id: "platform-metadata",
@@ -1720,16 +1670,20 @@ var APPLICATION_REQUIREMENTS = [
1720
1670
  summary: "Browser applications should feel like one platform and should not create isolated navigation or account surfaces.",
1721
1671
  appliesTo: ["browser"],
1722
1672
  evidence: [
1723
- "@topolo/ui-kit is used for the platform shell where the app has authenticated UI",
1724
- "The shared launcher/app switcher is reachable from authenticated app chrome",
1725
- "Account menu and sign-out behavior use TopoloAppShell or TopoloAccountMenu",
1673
+ "@topolo-io/app-shell is used for the platform shell where the app has authenticated UI",
1674
+ "@topolo-io/app-launcher keeps the shared launcher/app switcher reachable from authenticated app chrome",
1675
+ "Account menu and sign-out behavior use TopoloAppShell or the ui-kit TopoloAccountMenu primitive",
1676
+ "Repeated app body surfaces use shared TopoloAppLayout primitives instead of local stat cards, empty states, or status badges",
1677
+ "Local preview supports deterministic visual QA fixtures for overview, list, detail, settings, and empty/loading states",
1726
1678
  "Authenticated app body content stays inside the viewport on mobile and desktop widths"
1727
1679
  ],
1728
1680
  implementation: [
1729
- "Use TopoloAppShell for authenticated app layouts",
1730
- "Use TopoloAccountMenu when a partial-shell surface still needs account chrome",
1681
+ "Use @topolo-io/app-shell TopoloAppShell for authenticated app layouts",
1682
+ "Use the @topolo-io/ui-kit TopoloAccountMenu primitive when a partial-shell surface still needs account chrome",
1731
1683
  "Pass app-specific account actions as additive menu items instead of forking dropdowns",
1732
1684
  "Use shared design tokens and platform UI primitives before creating local equivalents",
1685
+ "Use TopoloAppMetricCard, TopoloAppEmptyState, TopoloAppStatusBadge, TopoloAppGrid, and TopoloAppSection for repeated app body patterns",
1686
+ "Keep visual QA fixtures available through visualQa=1 or __devpreview=1 so screenshot checks do not depend on live API state",
1733
1687
  "Keep mobile and desktop shell behavior aligned with the platform UI kit",
1734
1688
  "Constrain app-owned workspace wrappers with min-width: 0, width: auto, and viewport-aware max-width so shell padding cannot create page-level horizontal overflow"
1735
1689
  ]
@@ -1808,11 +1762,13 @@ var APPLICATION_REQUIREMENTS = [
1808
1762
  evidence: [
1809
1763
  "The app-owned API exposes GET /api/widget",
1810
1764
  "The endpoint returns TopoloWidgetApiResponse from @topolo/sdk",
1765
+ "Every returned widget includes snapshot.summary with app-owned operational prose",
1811
1766
  "The endpoint derives organization/user context from the app auth boundary"
1812
1767
  ],
1813
1768
  implementation: [
1814
1769
  "Import createTopoloWidgetResponse from @topolo/sdk in the app backend",
1815
1770
  "Return native app metrics or action widgets instead of generic launch-card fallbacks",
1771
+ "Populate snapshot.summary in the producing app for every widget instead of synthesizing summaries in TopoloOne",
1816
1772
  "Add a route-level test that validates the response with validateTopoloWidgetResponse"
1817
1773
  ]
1818
1774
  },
@@ -2105,6 +2061,8 @@ function firstString(...values) {
2105
2061
  var WRITE_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
2106
2062
  var TOPOLO_ACTION_ID_HEADER = "X-Topolo-Action-Id";
2107
2063
  var TOPOLO_ACTION_NAME_HEADER = "X-Topolo-Action-Name";
2064
+ var TOPOLO_RESOURCE_TYPE_HEADER = "X-Topolo-Resource-Type";
2065
+ var TOPOLO_RESOURCE_ID_HEADER = "X-Topolo-Resource-ID";
2108
2066
  var POSITIVE_CACHE_TTL_MS = 5 * 60 * 1e3;
2109
2067
  var NEGATIVE_CACHE_TTL_MS = 60 * 1e3;
2110
2068
  var CATALOG_CACHE_TTL_MS = 60 * 1e3;
@@ -2183,6 +2141,18 @@ var TopoloClient = class _TopoloClient {
2183
2141
  if (opts.headers) {
2184
2142
  for (const [k, v] of Object.entries(opts.headers)) headers.set(k, v);
2185
2143
  }
2144
+ if (opts.resource) {
2145
+ const resourceType = opts.resource.resourceType.trim();
2146
+ const resourceId = opts.resource.resourceId.trim();
2147
+ if (!resourceType || !resourceId) {
2148
+ throw new TopoloSdkError(
2149
+ "invalid_resource_context",
2150
+ "Both resource.resourceType and resource.resourceId are required when resource context is provided."
2151
+ );
2152
+ }
2153
+ headers.set(TOPOLO_RESOURCE_TYPE_HEADER, resourceType);
2154
+ headers.set(TOPOLO_RESOURCE_ID_HEADER, resourceId);
2155
+ }
2186
2156
  const controller = new AbortController();
2187
2157
  const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
2188
2158
  const signal = opts.signal ? mergeSignals(opts.signal, controller.signal) : controller.signal;
@@ -2254,10 +2224,14 @@ var TopoloClient = class _TopoloClient {
2254
2224
  if (!serviceKey) {
2255
2225
  throw new TopoloSdkError("unknown_service", "service is required");
2256
2226
  }
2257
- if (serviceKey === "auth") {
2227
+ if (serviceKey === "auth" || serviceKey === "developers") {
2258
2228
  const overrideUrl = resolveConfiguredServiceUrl(serviceKey, this.serviceUrls);
2259
2229
  if (overrideUrl) return { serviceKey, baseUrl: overrideUrl };
2260
- return { serviceKey, baseUrl: DEFAULT_SERVICE_URLS.auth };
2230
+ if (serviceKey === "developers") {
2231
+ const derived = deriveDevelopersFromAuth(resolveConfiguredServiceUrl("auth", this.serviceUrls));
2232
+ if (derived) return { serviceKey, baseUrl: derived };
2233
+ }
2234
+ return { serviceKey, baseUrl: DEFAULT_SERVICE_URLS[serviceKey] };
2261
2235
  }
2262
2236
  const live = await this.resolveLiveCallableService(serviceKey);
2263
2237
  if (live) {
@@ -2297,7 +2271,7 @@ var TopoloClient = class _TopoloClient {
2297
2271
  if (!normalized) return null;
2298
2272
  const configuredServiceId = resolveConfiguredServiceId(normalized, this.serviceIds);
2299
2273
  if (configuredServiceId) return configuredServiceId;
2300
- const authBase = resolveServiceUrl("auth", this.serviceUrls);
2274
+ const authBase = resolveServiceUrl("developers", this.serviceUrls);
2301
2275
  const cacheKey = `${authBase}|${normalized}`;
2302
2276
  const now = Date.now();
2303
2277
  const cached = _TopoloClient.serviceIdCache.get(cacheKey);
@@ -2351,7 +2325,7 @@ var TopoloClient = class _TopoloClient {
2351
2325
  if (this.serviceCatalogCache && this.serviceCatalogCache.expiresAt > now) {
2352
2326
  return this.serviceCatalogCache.entries;
2353
2327
  }
2354
- const res = await this.request({ service: "auth", method: "GET", path: "/api/services/catalog" });
2328
+ const res = await this.request({ service: "developers", method: "GET", path: "/api/services/catalog" });
2355
2329
  const raw = res.data?.services ?? res.services;
2356
2330
  if (!Array.isArray(raw)) {
2357
2331
  throw new TopoloSdkError(
@@ -2365,6 +2339,11 @@ var TopoloClient = class _TopoloClient {
2365
2339
  const serviceId = typeof s.service_id === "string" ? s.service_id : typeof s.id === "string" ? s.id : null;
2366
2340
  if (!serviceId) continue;
2367
2341
  const permissions = Array.isArray(s.granted_permissions) ? s.granted_permissions.filter((permission) => typeof permission === "string" && permission.trim().length > 0) : [];
2342
+ const catalogApiKeyResources = normalizeApiKeyResourceMetadataList(
2343
+ s.api_key_resources ?? s.apiKeyResources
2344
+ );
2345
+ const apiKeyResource = normalizeApiKeyResourceMetadata(s.api_key_resource ?? s.apiKeyResource) ?? catalogApiKeyResources[0] ?? null;
2346
+ const apiKeyResources = catalogApiKeyResources.length > 0 ? catalogApiKeyResources : apiKeyResource ? [apiKeyResource] : [];
2368
2347
  entries.push({
2369
2348
  serviceId,
2370
2349
  slug: typeof s.app_slug === "string" ? s.app_slug : null,
@@ -2375,7 +2354,9 @@ var TopoloClient = class _TopoloClient {
2375
2354
  status: typeof s.status === "string" ? s.status : null,
2376
2355
  surfaceKind: typeof s.surface_kind === "string" ? s.surface_kind : null,
2377
2356
  launchable: s.launchable === true,
2378
- permissions
2357
+ permissions,
2358
+ apiKeyResource,
2359
+ apiKeyResources
2379
2360
  });
2380
2361
  }
2381
2362
  this.serviceCatalogCache = {
@@ -2461,7 +2442,9 @@ var TopoloClient = class _TopoloClient {
2461
2442
  status: "bundled",
2462
2443
  surfaceKind: "application",
2463
2444
  launchable: Boolean(app.productionUrl),
2464
- permissions: []
2445
+ permissions: [],
2446
+ apiKeyResource: null,
2447
+ apiKeyResources: []
2465
2448
  }));
2466
2449
  }
2467
2450
  /**
@@ -2510,6 +2493,46 @@ function resolveConfiguredServiceId(slug, serviceIds) {
2510
2493
  }
2511
2494
  return null;
2512
2495
  }
2496
+ function normalizeApiKeyResourceMetadata(input2) {
2497
+ if (!isRecord(input2)) return null;
2498
+ const resourceType = readString(input2.resourceType ?? input2.resource_type);
2499
+ const label = readString(input2.label);
2500
+ const emptyLabel = readString(input2.emptyLabel ?? input2.empty_label);
2501
+ const resourceConcept = readString(input2.resourceConcept ?? input2.resource_concept);
2502
+ const canonicalResourceType = readString(input2.canonicalResourceType ?? input2.canonical_resource_type);
2503
+ const defaultEntitlement = readString(input2.defaultEntitlement ?? input2.default_entitlement);
2504
+ const exceptionPolicy = readString(input2.exceptionPolicy ?? input2.exception_policy);
2505
+ if (!resourceType || !label || !emptyLabel || resourceConcept !== "workspace" || canonicalResourceType !== "workspace" || defaultEntitlement !== "enterprise" || exceptionPolicy !== "app_or_organization") {
2506
+ return null;
2507
+ }
2508
+ return {
2509
+ resourceType,
2510
+ label,
2511
+ emptyLabel,
2512
+ resourceConcept,
2513
+ canonicalResourceType,
2514
+ defaultEntitlement,
2515
+ exceptionPolicy
2516
+ };
2517
+ }
2518
+ function normalizeApiKeyResourceMetadataList(input2) {
2519
+ if (!Array.isArray(input2)) return [];
2520
+ const resources = [];
2521
+ const seenTypes = /* @__PURE__ */ new Set();
2522
+ for (const item of input2) {
2523
+ const resource = normalizeApiKeyResourceMetadata(item);
2524
+ if (!resource || seenTypes.has(resource.resourceType)) continue;
2525
+ seenTypes.add(resource.resourceType);
2526
+ resources.push(resource);
2527
+ }
2528
+ return resources;
2529
+ }
2530
+ function readString(value) {
2531
+ return typeof value === "string" ? value.trim() : "";
2532
+ }
2533
+ function isRecord(value) {
2534
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2535
+ }
2513
2536
  function resolveConfiguredServiceUrl(slug, serviceUrls) {
2514
2537
  for (const serviceKey of serviceKeyCandidates2(slug)) {
2515
2538
  const configured = serviceUrls?.[serviceKey];
@@ -2533,6 +2556,17 @@ function serviceKeyCandidates2(slug) {
2533
2556
  function normalizeLookupKey(value) {
2534
2557
  return String(value || "").trim().toLowerCase().replace(/\s+/g, "-");
2535
2558
  }
2559
+ function deriveDevelopersFromAuth(authUrl) {
2560
+ if (!authUrl) return null;
2561
+ try {
2562
+ const url = new URL(authUrl);
2563
+ if (!/^auth\./i.test(url.hostname)) return null;
2564
+ url.hostname = url.hostname.replace(/^auth\./i, "developers.");
2565
+ return url.origin;
2566
+ } catch {
2567
+ return null;
2568
+ }
2569
+ }
2536
2570
  function catalogEntryMatches(entry, needle) {
2537
2571
  return catalogEntryLookupKeys(entry).includes(needle);
2538
2572
  }
@@ -3296,7 +3330,7 @@ async function pollUntilApproved(params) {
3296
3330
  throw new Error("Device code expired. Run `topolo auth login` again.");
3297
3331
  }
3298
3332
  function sleep(ms) {
3299
- return new Promise((resolve2) => setTimeout(resolve2, ms));
3333
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
3300
3334
  }
3301
3335
 
3302
3336
  // src/version.ts
@@ -3498,7 +3532,7 @@ function registerApiCommand(program2) {
3498
3532
  ).option("-d, --data <json>", "JSON body (for POST/PUT/PATCH)").option(
3499
3533
  "-q, --query <kv...>",
3500
3534
  "Query params as key=value (repeatable), e.g. --query source=cli page=2"
3501
- ).option("--confirm", "Acknowledge a mutating request").option("--json", "Output JSON", true).option("--no-json", "Output human-readable text").action(
3535
+ ).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(
3502
3536
  async (service, method, path4, opts) => {
3503
3537
  const mode = resolveOutputMode(opts.json);
3504
3538
  try {
@@ -3529,6 +3563,7 @@ function registerApiCommand(program2) {
3529
3563
  }
3530
3564
  }
3531
3565
  const query = parseQueryPairs(opts.query);
3566
+ const resource = parseResourceOption(opts.resourceType, opts.resourceId);
3532
3567
  const topolo = await buildClient();
3533
3568
  if (!topolo) {
3534
3569
  throw new Error(
@@ -3541,6 +3576,7 @@ function registerApiCommand(program2) {
3541
3576
  path: path4,
3542
3577
  body,
3543
3578
  ...query ? { query } : {},
3579
+ ...resource ? { resource } : {},
3544
3580
  confirm: opts.confirm === true
3545
3581
  });
3546
3582
  emit(result, mode, () => JSON.stringify(result, null, 2));
@@ -3550,6 +3586,15 @@ function registerApiCommand(program2) {
3550
3586
  }
3551
3587
  );
3552
3588
  }
3589
+ function parseResourceOption(resourceType, resourceId) {
3590
+ const normalizedType = resourceType?.trim() || "";
3591
+ const normalizedId = resourceId?.trim() || "";
3592
+ if (!normalizedType && !normalizedId) return void 0;
3593
+ if (!normalizedType || !normalizedId) {
3594
+ throw new Error("--resource-type and --resource-id must be provided together.");
3595
+ }
3596
+ return { resourceType: normalizedType, resourceId: normalizedId };
3597
+ }
3553
3598
  function resolveConfiguredServiceUrl2(service, serviceUrls) {
3554
3599
  const normalized = service.trim().toLowerCase();
3555
3600
  const withoutTopolo = normalized.startsWith("topolo-") ? normalized.slice("topolo-".length) : normalized;
@@ -3595,7 +3640,7 @@ import {
3595
3640
  import path from "path";
3596
3641
  var EVIDENCE_FRESHNESS_MS = 36 * 60 * 60 * 1e3;
3597
3642
  var REQUIRED_AUTH_CLIENT_VERSION = "1.1.9";
3598
- var REQUIRED_UI_KIT_VERSION = "0.4.67";
3643
+ var REQUIRED_UI_KIT_VERSION = "0.4.196";
3599
3644
  function readEvidenceFile(appRoot) {
3600
3645
  const candidate = path.join(appRoot, ".topolo", "evidence.json");
3601
3646
  if (!isFile(candidate)) return null;
@@ -3871,7 +3916,7 @@ function enrichFinding(application, finding2, evidence) {
3871
3916
  nextActions: []
3872
3917
  });
3873
3918
  }
3874
- if (shellEvidence.length === 0 && hasDocumentationSurface(application, evidence)) {
3919
+ if (hasDocumentationSurface(application, evidence)) {
3875
3920
  return mergeFinding(finding2, {
3876
3921
  status: "met",
3877
3922
  reason: "This is a documentation surface rather than an authenticated app workspace, so the shared application shell requirement does not apply.",
@@ -3889,7 +3934,7 @@ function enrichFinding(application, finding2, evidence) {
3889
3934
  if (shellEvidence.includes(`dependency-version=@topolo-io/ui-kit<${REQUIRED_UI_KIT_VERSION}`)) {
3890
3935
  return mergeFinding(finding2, {
3891
3936
  status: "needs_review",
3892
- reason: `The app depends on @topolo-io/ui-kit below ${REQUIRED_UI_KIT_VERSION}; launcher catalog cache/dedupe fixes require the current shared kit.`,
3937
+ reason: `The app depends on @topolo-io/ui-kit below ${REQUIRED_UI_KIT_VERSION}; shared primitive fixes require the current kit.`,
3893
3938
  evidence: shellEvidence,
3894
3939
  nextActions: [
3895
3940
  `Update @topolo-io/ui-kit to ${REQUIRED_UI_KIT_VERSION}`,
@@ -3922,13 +3967,61 @@ function enrichFinding(application, finding2, evidence) {
3922
3967
  ]
3923
3968
  });
3924
3969
  }
3970
+ if (shellEvidence.includes("local=raw-symbol-icons")) {
3971
+ return mergeFinding(finding2, {
3972
+ status: "needs_review",
3973
+ reason: "Raw symbolic UI icons are visible locally. Product apps must use TopoloPlatformIcon, TopoloAppIcon, or the shared @topolo-io/ui-kit/icons entrypoint so nav, status, and provider icons stay consistent.",
3974
+ evidence: shellEvidence,
3975
+ nextActions: [
3976
+ "Replace raw nav/status/provider glyphs with TopoloPlatformIcon or TopoloAppIcon",
3977
+ "Use @topolo-io/ui-kit/icons for lucide icons instead of text symbols",
3978
+ "Keep app page content on TopoloAppLayout primitives where available"
3979
+ ]
3980
+ });
3981
+ }
3982
+ if (shellEvidence.includes("local=direct-lucide-react")) {
3983
+ return mergeFinding(finding2, {
3984
+ status: "needs_review",
3985
+ reason: "The app imports lucide-react directly. First-party browser apps should import icons through @topolo-io/ui-kit/icons so the fleet shares one icon library contract.",
3986
+ evidence: shellEvidence,
3987
+ nextActions: [
3988
+ "Change direct lucide-react imports to @topolo-io/ui-kit/icons",
3989
+ "Use TopoloPlatformIcon for recurring platform, provider, capability, and status icons",
3990
+ "Remove the app-local lucide-react dependency when nothing else needs it"
3991
+ ]
3992
+ });
3993
+ }
3994
+ if (shellEvidence.includes("local=missing-shared-page-primitives")) {
3995
+ return mergeFinding(finding2, {
3996
+ status: "needs_review",
3997
+ reason: "The app uses shared shell chrome, but app-owned page content still appears to rely on local layout primitives. Use TopoloAppLayout primitives for metrics, empty states, status badges, grids, and page sections so app bodies stay visually aligned.",
3998
+ evidence: shellEvidence,
3999
+ nextActions: [
4000
+ "Replace repeated local stat cards, empty states, status pills, and page section wrappers with TopoloAppLayout primitives",
4001
+ "Keep app-owned workspace wrappers constrained with min-width: 0 and shared shell spacing tokens",
4002
+ "Rerun visual QA at desktop and mobile widths after moving the page body primitives"
4003
+ ]
4004
+ });
4005
+ }
4006
+ if (shellEvidence.includes("local=missing-visual-qa-fixtures")) {
4007
+ return mergeFinding(finding2, {
4008
+ status: "needs_review",
4009
+ reason: "The app has a browser workspace but no deterministic visual QA mode was found. Staging visual checks need fixture-backed pages so layout, icons, empty states, and loading states can be reviewed without live API/CORS noise.",
4010
+ evidence: shellEvidence,
4011
+ nextActions: [
4012
+ "Add a visualQa=1 or __devpreview=1 mode for local preview builds",
4013
+ "Provide fixture responses for overview, list/table, detail, settings, and empty/loading surfaces",
4014
+ "Use Playwright screenshots against the fixture mode before staging rollout"
4015
+ ]
4016
+ });
4017
+ }
3925
4018
  const usesCanonicalAppShell = shellEvidence.includes("source=TopoloAppShell");
3926
4019
  const usesSharedAccountPrimitive = shellEvidence.includes("source=TopoloAccountMenu");
3927
4020
  const usesSharedSuite = usesCanonicalAppShell || usesSharedAccountPrimitive;
3928
4021
  if (!usesSharedSuite) {
3929
4022
  return mergeFinding(finding2, {
3930
4023
  status: "needs_review",
3931
- reason: "The app depends on @topolo-io/ui-kit, but no canonical TopoloAppShell or shared account primitive is visible locally.",
4024
+ reason: "The app depends on shared Topolo browser packages, but no canonical TopoloAppShell or shared account primitive is visible locally.",
3932
4025
  evidence: shellEvidence,
3933
4026
  nextActions: [
3934
4027
  "Use TopoloAppShell for authenticated first-party app layouts",
@@ -4278,12 +4371,30 @@ function sharedShellEvidence(evidence) {
4278
4371
  const deps = dependencyNames(evidence);
4279
4372
  const text = evidence.source.text;
4280
4373
  const items = [];
4374
+ if (deps.has("@topolo-io/app-shell")) items.push("dependency=@topolo-io/app-shell");
4281
4375
  if (deps.has("@topolo-io/ui-kit")) items.push("dependency=@topolo-io/ui-kit");
4282
4376
  if (packageDependencyVersions(evidence, "@topolo-io/ui-kit").some((version) => isStaleDependencyVersion(version, REQUIRED_UI_KIT_VERSION))) {
4283
4377
  items.push(`dependency-version=@topolo-io/ui-kit<${REQUIRED_UI_KIT_VERSION}`);
4284
4378
  }
4285
4379
  if (text.includes("@topolo-io/ui-kit")) items.push("source=@topolo-io/ui-kit");
4380
+ if (text.includes("@topolo-io/ui-kit/icons")) items.push("source=@topolo-io/ui-kit/icons");
4381
+ if (text.includes("TopoloPlatformIcon")) items.push("source=TopoloPlatformIcon");
4382
+ const usesSharedPagePrimitives = /\bTopoloApp(?:MetricCard|Toolbar|EmptyState|StatusBadge|Page|PageHeader|Section|Grid)\b/.test(text);
4383
+ if (/\bTopoloApp(?:MetricCard|Toolbar|EmptyState|StatusBadge|Page|PageHeader|Section|Grid)\b/.test(text)) {
4384
+ items.push("source=TopoloAppLayout");
4385
+ }
4386
+ if (text.includes("@topolo-io/app-shell")) items.push("source=@topolo-io/app-shell");
4286
4387
  if (text.includes("TopoloAppShell")) items.push("source=TopoloAppShell");
4388
+ const hasVisualQaMode = /visualQa|__devpreview|visual qa/i.test(text);
4389
+ const hasVisualQaFixtures = hasVisualQaMode && /fixture|visualQaResponse|visualQaApi|mock[A-Z]\w*|devpreview/i.test(text) && /fetch|useQuery|api/i.test(text);
4390
+ if (hasVisualQaFixtures) items.push("source=visual-qa-fixtures");
4391
+ if (/from\s+['"]lucide-react['"]/.test(text)) items.push("local=direct-lucide-react");
4392
+ if (/(?:icon|Icon)\s*:\s*['"][◈◇⬡⬢▣◉✉▷✦○◆✕⚙🔑💡⚠✓♫]/.test(text)) {
4393
+ items.push("local=raw-symbol-icons");
4394
+ }
4395
+ if (/(?:<span[^>]*>|<div[^>]*>)[^<]{0,16}[◈◇⬡⬢▣◉✉▷✦○◆✕⚙🔑💡⚠✓♫]/.test(text)) {
4396
+ items.push("local=raw-symbol-icons");
4397
+ }
4287
4398
  if (text.includes(["Topolo", "Workspace", "Shell"].join(""))) items.push("forbidden=workspace-shell");
4288
4399
  if (/\bTopoloShell\b/.test(text)) items.push("legacy=TopoloShellFrame");
4289
4400
  if (text.includes("TopoloAccountMenu")) items.push("source=TopoloAccountMenu");
@@ -4291,6 +4402,12 @@ function sharedShellEvidence(evidence) {
4291
4402
  if (/\baccountMenuOpen\b|\bsetAccountMenuOpen\b|\buserMenuOpen\b|\bUserMenu\b|user-menu/.test(text)) {
4292
4403
  items.push("local=account-menu");
4293
4404
  }
4405
+ if (deps.has("@topolo-io/app-shell") && !usesSharedPagePrimitives) {
4406
+ items.push("local=missing-shared-page-primitives");
4407
+ }
4408
+ if (hasLocalBrowserSurface(evidence) && !hasVisualQaFixtures) {
4409
+ items.push("local=missing-visual-qa-fixtures");
4410
+ }
4294
4411
  return unique(items);
4295
4412
  }
4296
4413
  function hasDocumentationSurface(application, evidence) {
@@ -4299,7 +4416,7 @@ function hasDocumentationSurface(application, evidence) {
4299
4416
  }
4300
4417
  function hasLocalBrowserSurface(evidence) {
4301
4418
  const deps = dependencyNames(evidence);
4302
- if (deps.has("react") || deps.has("react-dom") || deps.has("astro") || deps.has("@vitejs/plugin-react") || deps.has("@topolo-io/ui-kit")) {
4419
+ if (deps.has("react") || deps.has("react-dom") || deps.has("astro") || deps.has("@vitejs/plugin-react") || deps.has("@topolo-io/app-shell") || deps.has("@topolo-io/ui-kit")) {
4303
4420
  return true;
4304
4421
  }
4305
4422
  return evidence.source.files.some(
@@ -4596,170 +4713,141 @@ function isFile(filePath) {
4596
4713
 
4597
4714
  // src/commands/app-scaffold.ts
4598
4715
  import { randomBytes } from "crypto";
4716
+ import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
4717
+ import { dirname as dirname3, resolve as resolve2 } from "path";
4718
+
4719
+ // src/commands/app-scaffold-normalizers.ts
4599
4720
  import { constants } from "fs";
4600
- import { access, mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
4601
- import { dirname as dirname3, relative, resolve } from "path";
4602
- var CLOUDFLARE_ACCOUNT_ID = "49ef1ba682ad8cfd720c86699ae17521";
4603
- var STAGING_CLOUDFLARE_ACCOUNT_ID = "4f4e1c69a3830946f9fea7b1eb7531ac";
4604
- var CLOUDFLARE_COMPATIBILITY_DATE = "2026-05-05";
4605
- var CURRENT_VERIFICATION_DATE = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
4606
- function generateOpaqueServiceId() {
4607
- return `srv_${randomBytes(9).toString("base64url")}`;
4721
+ import { access } from "fs/promises";
4722
+ import { relative, resolve } from "path";
4723
+ function normalizeTarget(value) {
4724
+ if (!value) return "platform";
4725
+ if (value === "platform" || value === "developer") return value;
4726
+ throw new Error("Invalid scaffold target. Use platform or developer.");
4608
4727
  }
4609
- function applicationScaffoldPayload(application, options = {}) {
4610
- const target = normalizeTarget(options.target);
4611
- const portfolio = target === "platform" ? normalizePortfolio(options.portfolio) : null;
4612
- const requestedId = normalizeApplicationId(application);
4613
- const id = target === "platform" ? normalizeFirstPartyApplicationId(requestedId) : requestedId;
4614
- const slug = toSlug(id);
4615
- const catalogApplication = APPLICATIONS[id];
4616
- const browser = options.browser !== false;
4617
- const api = options.api ?? target === "platform";
4618
- const audience = normalizeAudience(
4619
- options.audience,
4620
- target === "platform" ? portfolio : null
4728
+ function normalizePortfolio(value) {
4729
+ if (!value) return "platform";
4730
+ if (value === "platform" || value === "personal") return value;
4731
+ throw new Error("Invalid scaffold portfolio. Use platform or personal.");
4732
+ }
4733
+ function normalizeAudience(value, portfolio) {
4734
+ if (!value) {
4735
+ return portfolio === "personal" ? "personal" : "business";
4736
+ }
4737
+ if (value === "business" || value === "personal") {
4738
+ return value;
4739
+ }
4740
+ throw new Error("Invalid scaffold audience. Use business or personal.");
4741
+ }
4742
+ function normalizeTenancy(value, audience) {
4743
+ const resolved = value ?? (audience === "personal" ? "household" : "organization");
4744
+ if (resolved !== "organization" && resolved !== "household") {
4745
+ throw new Error("Invalid scaffold tenancy. Use organization or household.");
4746
+ }
4747
+ if (audience === "business" && resolved !== "organization") {
4748
+ throw new Error("Business scaffolds must use organization tenancy.");
4749
+ }
4750
+ if (audience === "personal" && resolved !== "household") {
4751
+ throw new Error("Personal scaffolds must use household tenancy.");
4752
+ }
4753
+ return resolved;
4754
+ }
4755
+ function normalizeSurfaceModel(value) {
4756
+ if (!value) return "web_app";
4757
+ if (value === "web_app" || value === "mobile_container" || value === "mobile_module" || value === "messaging_surface") {
4758
+ return value;
4759
+ }
4760
+ throw new Error(
4761
+ "Invalid scaffold surface model. Use web_app, mobile_container, mobile_module, or messaging_surface."
4621
4762
  );
4622
- const tenancy = normalizeTenancy(options.tenancy, audience);
4623
- const surfaceModel = normalizeSurfaceModel(options.surfaceModel);
4624
- const containerAppId = normalizeContainerAppId(options.containerAppId, surfaceModel);
4625
- const profile = normalizeScaffoldProfile(options.profile);
4626
- const resources = resolveScaffoldResources(profile, options);
4627
- const ownerType = target === "platform" ? "topolo_first_party" : "third_party";
4628
- const distributionModel = target === "platform" ? "marketplace" : "organization_internal";
4629
- const appDir = options.appDir ?? catalogApplication?.appDir ?? (target === "platform" ? `Topolo${toPascalCase(id)}` : `topolo-${slug}`);
4630
- const name = options.name ?? catalogApplication?.name ?? `topolo-${slug}`;
4631
- const packageName = options.packageName ?? catalogApplication?.packageName ?? `topolo-${slug}`;
4632
- const serviceSlug = api ? target === "platform" ? `topolo-${slug}` : id : null;
4633
- const defaultServiceId = target === "platform" ? generateOpaqueServiceId() : catalogApplication?.services[0] ?? id;
4634
- const serviceId = api ? options.service ?? defaultServiceId : null;
4635
- const productionUrl = browser ? options.productionUrl ?? catalogApplication?.productionUrl ?? `https://${slug}.topolo.app` : null;
4636
- const scopes = scaffoldScopes({ browser, api });
4637
- const capabilities = scaffoldCapabilities({ browser, api, distributionModel });
4638
- const ciWorkflowPath = target === "platform" ? ".github/workflows/topolo-conformance.yml" : null;
4639
- const ciWorkflow = target === "platform" ? topoloConformanceWorkflow({ id, appDir, audience }) : null;
4640
- const applicationPayload = {
4641
- id,
4642
- name,
4643
- appDir,
4644
- packageName,
4645
- productionUrl,
4646
- serviceSlug,
4647
- serviceId
4648
- };
4763
+ }
4764
+ function normalizeScaffoldProfile(value) {
4765
+ if (!value) return null;
4766
+ if (value === "spatial-assets") return value;
4767
+ throw new Error("Invalid scaffold profile. Use spatial-assets.");
4768
+ }
4769
+ function resolveScaffoldResources(profile, options) {
4770
+ 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 };
4649
4771
  return {
4650
- version: APPLICATION_REQUIREMENTS_VERSION,
4651
- target,
4652
- ownerType,
4653
- portfolio,
4654
- audience,
4655
- tenancy,
4656
- surfaceModel,
4657
- containerAppId,
4658
- profile,
4659
- resources,
4660
- distributionModel,
4661
- capabilities,
4662
- application: applicationPayload,
4663
- scopes,
4664
- requirements: APPLICATION_REQUIREMENTS.filter(
4665
- (requirement) => requirement.appliesTo.some((scope) => scopes.includes(scope))
4666
- ),
4667
- packages: scaffoldPackages({ browser, api, target }),
4668
- files: scaffoldFiles({
4669
- payload: {
4670
- ownerType,
4671
- portfolio,
4672
- audience,
4673
- tenancy,
4674
- surfaceModel,
4675
- containerAppId,
4676
- profile,
4677
- resources,
4678
- distributionModel,
4679
- capabilities
4680
- },
4681
- application: applicationPayload,
4682
- target,
4683
- browser,
4684
- api,
4685
- ciWorkflow
4686
- }),
4687
- commands: scaffoldCommands({
4688
- id,
4689
- target,
4690
- audience,
4691
- tenancy,
4692
- portfolio,
4693
- profile
4694
- }),
4695
- ciWorkflowPath,
4696
- ciWorkflow
4772
+ d1: Boolean(profileResources.d1 || options.withD1),
4773
+ r2: Boolean(profileResources.r2 || options.withR2),
4774
+ queue: Boolean(profileResources.queue || options.withQueue),
4775
+ workflow: Boolean(profileResources.workflow || options.withWorkflow),
4776
+ containerProcessing: Boolean(profileResources.containerProcessing || options.withContainerProcessing)
4697
4777
  };
4698
4778
  }
4699
- async function writeApplicationScaffold(payload, options = {}) {
4700
- const root = resolve(options.repoRoot ?? process.cwd());
4701
- const plannedFiles = payload.files.map((file) => ({
4702
- file,
4703
- absolutePath: resolveInside(root, file.path)
4704
- }));
4705
- const existingFiles = [];
4706
- for (const planned of plannedFiles) {
4707
- if (await pathExists(planned.absolutePath)) {
4708
- existingFiles.push(planned.file.path);
4779
+ function normalizeContainerAppId(value, surfaceModel) {
4780
+ const trimmed = String(value || "").trim();
4781
+ if (surfaceModel === "mobile_module") {
4782
+ if (!trimmed) {
4783
+ throw new Error("Mobile-module scaffolds require --container-app-id.");
4709
4784
  }
4785
+ return trimmed;
4710
4786
  }
4711
- if (existingFiles.length > 0 && !options.force) {
4712
- throw new Error(
4713
- [
4714
- "Refusing to overwrite existing scaffold files:",
4715
- ...existingFiles.map((filePath) => `- ${filePath}`),
4716
- "Pass --force to overwrite these files."
4717
- ].join("\n")
4718
- );
4787
+ return trimmed || null;
4788
+ }
4789
+ function normalizeApplicationId(application) {
4790
+ const raw = application.trim().toLowerCase();
4791
+ const normalized = raw.replace(/-/g, "_");
4792
+ if (Object.prototype.hasOwnProperty.call(APPLICATIONS, raw)) {
4793
+ return raw;
4719
4794
  }
4720
- const files = [];
4721
- for (const planned of plannedFiles) {
4722
- const existed = await pathExists(planned.absolutePath);
4723
- await mkdir2(dirname3(planned.absolutePath), { recursive: true });
4724
- await writeFile2(planned.absolutePath, planned.file.content, "utf8");
4725
- files.push({
4726
- path: planned.file.path,
4727
- status: existed ? "overwritten" : "created"
4728
- });
4795
+ if (Object.prototype.hasOwnProperty.call(APPLICATIONS, normalized)) {
4796
+ return normalized;
4729
4797
  }
4730
- return { root, files };
4731
- }
4732
- function scaffoldScopes(options) {
4733
- const scopes = ["all"];
4734
- if (options.browser) scopes.push("browser");
4735
- if (options.api) {
4736
- scopes.push("api", "agent_surface");
4798
+ if (/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(raw) || /^[a-z0-9](?:[a-z0-9_]*[a-z0-9])?$/.test(raw)) {
4799
+ return normalized;
4737
4800
  }
4738
- return scopes;
4801
+ throw new Error(
4802
+ "Application id must be kebab-case or snake_case using lowercase letters, numbers, hyphens, and underscores."
4803
+ );
4739
4804
  }
4740
- function scaffoldPackages(options) {
4741
- if (options.target === "developer") {
4742
- return options.browser ? ["@topolo/login"] : [];
4743
- }
4744
- const packages = /* @__PURE__ */ new Set();
4745
- if (options.browser) {
4746
- packages.add("@topolo-io/ui-kit");
4747
- packages.add("@topolo-io/auth-client");
4805
+ function normalizeFirstPartyApplicationId(applicationId) {
4806
+ if (applicationId.startsWith("topolo_") && applicationId.length > "topolo_".length) {
4807
+ return applicationId.slice("topolo_".length);
4748
4808
  }
4749
- if (options.api) {
4750
- packages.add("@topolo-io/worker-runtime");
4751
- packages.add("@topolo/sdk");
4809
+ return applicationId;
4810
+ }
4811
+ function toSlug(value) {
4812
+ return value.replace(/_/g, "-");
4813
+ }
4814
+ function toConstantCase(value) {
4815
+ return value.split(/[-_]/).filter(Boolean).join("_").toUpperCase();
4816
+ }
4817
+ function toPascalCase(value) {
4818
+ const acronyms = /* @__PURE__ */ new Set(["api", "cli", "crm", "mcp", "mdm", "p2p"]);
4819
+ return value.split(/[-_]/).filter(Boolean).map((part) => acronyms.has(part.toLowerCase()) ? part.toUpperCase() : part.slice(0, 1).toUpperCase() + part.slice(1)).join("");
4820
+ }
4821
+ function toTitle(value) {
4822
+ return value.replace(/^topolo[-_\s]*/i, "Topolo ").split(/[-_\s]+/).filter(Boolean).map((part) => part.slice(0, 1).toUpperCase() + part.slice(1)).join(" ");
4823
+ }
4824
+ function json(value) {
4825
+ return JSON.stringify(value, null, 2);
4826
+ }
4827
+ async function pathExists(path4) {
4828
+ try {
4829
+ await access(path4, constants.F_OK);
4830
+ return true;
4831
+ } catch {
4832
+ return false;
4752
4833
  }
4753
- return Array.from(packages);
4754
4834
  }
4755
- function scaffoldCapabilities(options) {
4756
- const capabilities = [];
4757
- if (options.browser) {
4758
- capabilities.push("login", "launcher");
4835
+ function resolveInside(root, filePath) {
4836
+ const resolved = resolve(root, filePath);
4837
+ const relativePath = relative(root, resolved);
4838
+ if (relativePath.startsWith("..") || relativePath === "" || relativePath.startsWith("/")) {
4839
+ throw new Error(`Scaffold file path escapes the target root: ${filePath}`);
4759
4840
  }
4760
- if (options.distributionModel === "marketplace") capabilities.push("marketplace");
4761
- if (options.api) capabilities.push("api", "widget");
4762
- return capabilities;
4841
+ return resolved;
4842
+ }
4843
+ function escapeHtml(value) {
4844
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
4845
+ }
4846
+ function escapeSingleQuoted(value) {
4847
+ return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
4848
+ }
4849
+ function escapeFrontmatter(value) {
4850
+ return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
4763
4851
  }
4764
4852
  function firstPartyAppPath(appDir, audience) {
4765
4853
  if (appDir === "TopoloCli" || appDir === "TopoloCLI") return "Apps/system/TopoloCLI";
@@ -4767,497 +4855,43 @@ function firstPartyAppPath(appDir, audience) {
4767
4855
  if (appDir === "TopoloAuth") return "Apps/system/TopoloAuth";
4768
4856
  return `${audience === "personal" ? "Apps/personal" : "Apps/business"}/${appDir}`;
4769
4857
  }
4770
- function scaffoldFiles(options) {
4771
- const { application, target } = options;
4772
- const slug = toSlug(application.id);
4773
- const base = target === "platform" ? firstPartyAppPath(application.appDir, options.payload.audience) : application.appDir;
4774
- const browserBase = target === "platform" ? `${base}/apps/web` : base;
4775
- const apiBase = target === "platform" ? `${base}/apps/api` : base;
4776
- const files = [
4777
- {
4778
- path: `${base}/package.json`,
4779
- purpose: "Package metadata and verification commands.",
4780
- requiredSignals: [
4781
- "name matches the packageName in the SDK application catalog",
4782
- "scripts include build plus typecheck where the stack supports them"
4783
- ],
4784
- content: packageJson(application, target, {
4785
- browser: options.browser,
4786
- api: options.api,
4787
- resources: options.payload.resources
4788
- })
4789
- },
4790
- {
4791
- path: `${base}/README.md`,
4792
- purpose: "Starter README with install, run, and verification commands.",
4793
- requiredSignals: [
4794
- "documents the generated app target",
4795
- "points maintainers to the shared Topolo packages used by the scaffold"
4796
- ],
4797
- content: readme(application, target, {
4798
- api: options.api,
4799
- audience: options.payload.audience,
4800
- tenancy: options.payload.tenancy,
4801
- portfolio: options.payload.portfolio,
4802
- profile: options.payload.profile,
4803
- resources: options.payload.resources
4804
- })
4805
- },
4806
- {
4807
- path: `${base}/.gitignore`,
4808
- purpose: "Generated-app ignore rules.",
4809
- requiredSignals: [
4810
- "ignores dependency installs and build output",
4811
- "keeps local environment files out of source control"
4812
- ],
4813
- content: gitignore()
4814
- },
4815
- {
4816
- path: `${base}/tsconfig.json`,
4817
- purpose: "TypeScript configuration for the generated app.",
4818
- requiredSignals: [
4819
- "uses strict mode",
4820
- "includes browser DOM types for React and Worker types for API-backed apps"
4821
- ],
4822
- content: tsconfigJson(options.api, target)
4823
- },
4824
- {
4825
- path: `${browserBase}/vite.config.ts`,
4826
- purpose: "Vite browser build configuration.",
4827
- requiredSignals: [
4828
- "builds the authenticated browser surface",
4829
- "keeps generated apps on the standard React/Vite baseline"
4830
- ],
4831
- content: viteConfig(target)
4832
- },
4833
- {
4834
- path: `${browserBase}/index.html`,
4835
- purpose: "Browser entry document.",
4836
- requiredSignals: [
4837
- "mounts the React app at #root",
4838
- "uses the generated application display name"
4839
- ],
4840
- content: indexHtml(application.name)
4841
- },
4842
- {
4843
- path: `${browserBase}/src/main.tsx`,
4844
- purpose: "React app entrypoint.",
4845
- requiredSignals: [
4846
- "renders the generated app component",
4847
- "imports the generated app stylesheet"
4848
- ],
4849
- content: mainTsx()
4850
- },
4851
- {
4852
- path: `${browserBase}/src/env.d.ts`,
4853
- purpose: "Vite environment variable typing.",
4854
- requiredSignals: [
4855
- "loads vite/client import.meta.env types",
4856
- "declares the Topolo environment variables consumed by the starter"
4857
- ],
4858
- content: envDts(options.api)
4859
- },
4860
- ...options.payload.resources.workflow ? [
4861
- {
4862
- path: `${base}/src/cloudflare-workers.d.ts`,
4863
- purpose: "Minimal Cloudflare Workflow entrypoint typing for scaffolded Workers.",
4864
- requiredSignals: [
4865
- "declares cloudflare:workers WorkflowEntrypoint for TypeScript",
4866
- "keeps Workflow imports deployable without adding a new dependency"
4867
- ],
4868
- content: cloudflareWorkersDts()
4869
- }
4870
- ] : [],
4871
- {
4872
- path: `${browserBase}/src/styles.css`,
4873
- purpose: "Minimal app-local styles outside the shared Topolo shell.",
4874
- requiredSignals: [
4875
- "does not fork the shared shell chrome",
4876
- "keeps app-specific layout scoped under generated classes"
4877
- ],
4878
- content: stylesCss()
4879
- }
4880
- ];
4881
- if (target === "platform") {
4882
- files.push(
4883
- {
4884
- path: `${base}/topolo.app.json`,
4885
- purpose: "Canonical app manifest that drives generated Wrangler and CloudControl metadata.",
4886
- requiredSignals: [
4887
- "declares web/api targets with canonical names and app-local target directories",
4888
- "keeps Cloudflare account, domain, service id, and bindings in one manifest source of truth"
4889
- ],
4890
- content: topoloAppManifestJson(application, {
4891
- api: options.api,
4892
- resources: options.payload.resources,
4893
- profile: options.payload.profile
4894
- })
4895
- },
4896
- {
4897
- path: `${base}/topolo.cloudcontrol.json`,
4898
- purpose: "CloudControl source of truth for deploy targets, routes, bindings, and CI metadata.",
4899
- requiredSignals: [
4900
- "production URLs and Cloudflare targets are explicit",
4901
- "resource bindings and required secrets are declared without secret values"
4902
- ],
4903
- content: cloudcontrolJson(application, {
4904
- api: options.api,
4905
- audience: options.payload.audience,
4906
- profile: options.payload.profile,
4907
- resources: options.payload.resources
4908
- })
4909
- },
4910
- {
4911
- path: `${base}/.github/workflows/topolo-conformance.yml`,
4912
- purpose: "PR/push gate that runs the local Topolo app audit with docs evidence.",
4913
- requiredSignals: [
4914
- "checks out this repo under its Apps lane directory",
4915
- "checks out TopoloDocs under Apps/system/TopoloDocs",
4916
- "checks out topolo-sdk and TopoloCli from main so the gate does not wait for an npm publish",
4917
- "runs topolo apps audit with --local and --fail-on needs_review"
4918
- ],
4919
- content: options.ciWorkflow ?? ""
4920
- },
4921
- {
4922
- path: `Apps/system/TopoloDocs/src/content/systems/topolo-${slug}.json`,
4923
- purpose: "Canonical system registry entry.",
4924
- requiredSignals: [
4925
- `repo_paths includes ${base}`,
4926
- "doc_paths includes internal handbook coverage",
4927
- "security_assurance records the auth, tenancy, deployment, and verification boundary"
4928
- ],
4929
- content: systemJson(application, {
4930
- api: options.api,
4931
- audience: options.payload.audience,
4932
- tenancy: options.payload.tenancy,
4933
- portfolio: options.payload.portfolio,
4934
- profile: options.payload.profile,
4935
- resources: options.payload.resources
4936
- })
4937
- },
4938
- {
4939
- path: `Apps/system/TopoloDocs/src/content/internal/apps/${slug}.mdx`,
4940
- purpose: "Internal operator handbook.",
4941
- requiredSignals: [
4942
- "documents ownership, routes, auth boundary, deployment, operations, and verification",
4943
- "last_verified is updated when behavior changes"
4944
- ],
4945
- content: internalDocs(application, {
4946
- api: options.api,
4947
- audience: options.payload.audience,
4948
- tenancy: options.payload.tenancy,
4949
- portfolio: options.payload.portfolio,
4950
- profile: options.payload.profile,
4951
- resources: options.payload.resources
4952
- })
4953
- },
4954
- {
4955
- path: `Apps/system/TopoloDocs/src/content/public/applications/${slug}.mdx`,
4956
- purpose: "Public application overview.",
4957
- requiredSignals: [
4958
- "documents the product surface without internal-only details",
4959
- "links back to the canonical system registry entry"
4960
- ],
4961
- content: publicDocs(application, {
4962
- api: options.api,
4963
- audience: options.payload.audience,
4964
- tenancy: options.payload.tenancy,
4965
- portfolio: options.payload.portfolio,
4966
- profile: options.payload.profile,
4967
- resources: options.payload.resources
4968
- })
4969
- }
4970
- );
4971
- if (options.api && application.serviceId) {
4972
- files.push(
4973
- {
4974
- path: `Apps/system/TopoloAuth/catalog/service-manifests/${application.serviceId}.json`,
4975
- purpose: "Auth service manifest for first-party permissions, role bundles, and API-key scopes.",
4976
- requiredSignals: [
4977
- "serviceId matches the generated Worker service id",
4978
- "permissions, role bundles, and API-key scopes exist before the app is catalog-visible",
4979
- "systems metadata maps the service back to the generated TopoloDocs entries"
4980
- ],
4981
- content: serviceManifestJson(application, {
4982
- audience: options.payload.audience,
4983
- tenancy: options.payload.tenancy
4984
- })
4985
- }
4986
- );
4987
- }
4988
- } else {
4989
- files.push(
4990
- {
4991
- path: `${base}/topolo.app.json`,
4992
- purpose: "Third-party Topolo application manifest.",
4993
- requiredSignals: [
4994
- "declares the external app id, callback URLs, and requested platform scopes",
4995
- "does not include internal Topolo CloudControl or canonical-docs ownership"
4996
- ],
4997
- content: developerManifest(application, {
4998
- api: options.api,
4999
- audience: options.payload.audience,
5000
- tenancy: options.payload.tenancy,
5001
- surfaceModel: options.payload.surfaceModel,
5002
- containerAppId: options.payload.containerAppId,
5003
- distributionModel: options.payload.distributionModel,
5004
- capabilities: options.payload.capabilities
5005
- })
5006
- },
5007
- {
5008
- path: `${base}/.env.example`,
5009
- purpose: "Developer environment template for local auth configuration.",
5010
- requiredSignals: [
5011
- "names the Topolo Auth URL",
5012
- "keeps OAuth client ids and service ids out of source control by default"
5013
- ],
5014
- content: envExample(application)
5015
- }
5016
- );
5017
- }
4858
+
4859
+ // src/commands/app-scaffold-package-template.ts
4860
+ function packageJson(application, target, options) {
4861
+ const dependencies = {};
4862
+ const devDependencies = {
4863
+ "@types/node": "^20.12.0",
4864
+ typescript: "^5.4.0",
4865
+ vite: "^5.4.0"
4866
+ };
4867
+ const scripts = {
4868
+ dev: target === "platform" ? "vite --config apps/web/vite.config.ts --host 0.0.0.0" : "vite --host 0.0.0.0",
4869
+ build: target === "platform" ? "vite build --config apps/web/vite.config.ts" : "vite build",
4870
+ preview: target === "platform" ? "vite preview --config apps/web/vite.config.ts --host 0.0.0.0" : "vite preview --host 0.0.0.0",
4871
+ typecheck: "tsc --noEmit"
4872
+ };
5018
4873
  if (options.browser) {
4874
+ dependencies.react = "^18.3.1";
4875
+ dependencies["react-dom"] = "^18.3.1";
4876
+ devDependencies["@types/react"] = "^18.3.3";
4877
+ devDependencies["@types/react-dom"] = "^18.3.0";
4878
+ devDependencies["@vitejs/plugin-react"] = "^4.7.0";
5019
4879
  if (target === "platform") {
5020
- files.push({
5021
- path: `${browserBase}/src/service-identity.ts`,
5022
- purpose: "Central service identity resolver shared by browser and Worker runtime surfaces.",
5023
- requiredSignals: [
5024
- "exports the generated app id, display name, and stable Auth service slug",
5025
- "requires the provisioned Auth service id from the Worker deployment config"
5026
- ],
5027
- content: serviceIdentityTs(application)
5028
- });
4880
+ dependencies["@topolo-io/auth-client"] = topoloPackageVersion("@topolo-io/auth-client", target);
4881
+ dependencies["@topolo-io/app-funnel"] = topoloPackageVersion("@topolo-io/app-funnel", target);
4882
+ dependencies["@topolo-io/app-launcher"] = topoloPackageVersion("@topolo-io/app-launcher", target);
4883
+ dependencies["@topolo-io/app-shell"] = topoloPackageVersion("@topolo-io/app-shell", target);
4884
+ dependencies["@topolo-io/design-tokens"] = topoloPackageVersion("@topolo-io/design-tokens", target);
4885
+ dependencies["@topolo-io/onboarding"] = topoloPackageVersion("@topolo-io/onboarding", target);
4886
+ dependencies["@topolo-io/tour"] = topoloPackageVersion("@topolo-io/tour", target);
4887
+ dependencies["@topolo-io/ui-kit"] = topoloPackageVersion("@topolo-io/ui-kit", target);
4888
+ dependencies["react-router-dom"] = "^7.6.2";
4889
+ } else {
4890
+ dependencies["@topolo/login"] = topoloPackageVersion("@topolo/login", target);
5029
4891
  }
5030
- files.push({
5031
- path: `${browserBase}/src/App.tsx`,
5032
- purpose: "Authenticated browser surface.",
5033
- requiredSignals: [
5034
- "imports shared TopoloAppShell from @topolo-io/ui-kit",
5035
- "uses @topolo-io/auth-client for app-origin auth flows",
5036
- "keeps launcher and account-menu behavior on the shared platform surface",
5037
- "passes app-specific account actions through accountMenuItems or TopoloAccountMenu"
5038
- ],
5039
- content: appTsx(application, target, {
5040
- audience: options.payload.audience,
5041
- portfolio: options.payload.portfolio
5042
- })
5043
- });
5044
4892
  }
5045
4893
  if (options.api) {
5046
- files.push(
5047
- {
5048
- path: `${apiBase}/wrangler.toml`,
5049
- purpose: "Cloudflare Worker runtime configuration.",
5050
- requiredSignals: [
5051
- "bindings match topolo.cloudcontrol.json or topolo.app.json",
5052
- "secrets are referenced by name only"
5053
- ],
5054
- content: target === "platform" ? platformApiWranglerToml(application, options.payload.resources, options.payload.profile) : wranglerToml(application, target, options.payload.resources, options.payload.profile)
5055
- },
5056
- {
5057
- path: `${apiBase}/src/worker.ts`,
5058
- purpose: "Protected service boundary.",
5059
- requiredSignals: [
5060
- "uses @topolo-io/worker-runtime for Worker auth, gatewaying, headers, and request helpers",
5061
- "derives organization from the credential rather than client-supplied orgId",
5062
- "emits or propagates X-Topolo-Request-Id and X-Topolo-Client"
5063
- ],
5064
- content: workerTs(application, target, {
5065
- audience: options.payload.audience,
5066
- tenancy: options.payload.tenancy,
5067
- profile: options.payload.profile,
5068
- resources: options.payload.resources
5069
- })
5070
- }
5071
- );
5072
- if (target === "platform") {
5073
- files.push(
5074
- {
5075
- path: `${apiBase}/src/service-identity.ts`,
5076
- purpose: "Runtime service identity resolver for the API Worker.",
5077
- requiredSignals: [
5078
- "resolves the concrete Auth service id from Worker vars",
5079
- "keeps the API Worker independent from browser-only import.meta usage"
5080
- ],
5081
- content: apiServiceIdentityTs(application)
5082
- },
5083
- {
5084
- path: `${browserBase}/src/worker.ts`,
5085
- purpose: "Browser Worker that serves assets and forwards API paths to the API Worker binding.",
5086
- requiredSignals: [
5087
- "keeps the frontend Worker thin",
5088
- "routes /api/* to the API service binding instead of owning backend routes locally"
5089
- ],
5090
- content: webWorkerTs(application)
5091
- },
5092
- {
5093
- path: `${browserBase}/wrangler.toml`,
5094
- purpose: "Cloudflare Worker static-assets configuration for the browser surface.",
5095
- requiredSignals: [
5096
- "serves Vite assets through a Worker with assets",
5097
- "binds the frontend Worker to the API Worker for /api/* requests"
5098
- ],
5099
- content: platformWebWranglerToml(application)
5100
- }
5101
- );
5102
- }
5103
- }
5104
- return files;
5105
- }
5106
- function scaffoldCommands(options) {
5107
- const scaffoldId = toSlug(options.id);
5108
- if (options.target === "developer") {
5109
- const audienceFlags = options.audience === "personal" ? " --audience personal --tenancy household" : "";
5110
- return [
5111
- `topolo apps scaffold ${scaffoldId}${audienceFlags} --write --repo-root .`,
5112
- `cd topolo-${scaffoldId}`,
5113
- "npm install",
5114
- "npm run typecheck",
5115
- "npm run build"
5116
- ];
5117
- }
5118
- const portfolioFlags = options.portfolio === "personal" ? " --portfolio personal" : "";
5119
- const profileFlags = options.profile ? ` --profile ${options.profile}` : "";
5120
- return [
5121
- `topolo platform apps scaffold ${scaffoldId}${portfolioFlags}${profileFlags} --write --provision --repo-root /Users/ashes/Projects/Topolo/Platform --json`,
5122
- "topolo apps requirements --json",
5123
- `topolo apps audit ${options.id} --local --repo-root /Users/ashes/Projects/Topolo/Platform --fail-on needs_review --json`,
5124
- `topolo apps audit ${options.id} --local --repo-root /Users/ashes/Projects/Topolo/Platform --fail-on partial --json`
5125
- ];
5126
- }
5127
- function topoloConformanceWorkflow(options) {
5128
- const appPath = firstPartyAppPath(options.appDir, options.audience);
5129
- const docsCheckoutStep = options.appDir === "TopoloDocs" ? [] : [
5130
- "",
5131
- " - name: Checkout TopoloDocs",
5132
- ` if: steps.app-token.outputs.token != '' || env.HAS_TOPOLO_CI_REPO_TOKEN == 'true'`,
5133
- " uses: actions/checkout@v4",
5134
- " with:",
5135
- " repository: Topolo-io/TopoloDocs",
5136
- " path: Apps/system/TopoloDocs",
5137
- " token: ${{ steps.app-token.outputs.token || secrets.TOPOLO_CI_REPO_TOKEN }}"
5138
- ];
5139
- const cliCheckoutStep = options.appDir === "TopoloCLI" || options.appDir === "TopoloCli" ? [] : [
5140
- "",
5141
- " - name: Checkout Topolo CLI",
5142
- ` if: steps.app-token.outputs.token != '' || env.HAS_TOPOLO_CI_REPO_TOKEN == 'true'`,
5143
- " uses: actions/checkout@v4",
5144
- " with:",
5145
- " repository: Topolo-io/TopoloCli",
5146
- " path: Apps/system/TopoloCLI",
5147
- " token: ${{ steps.app-token.outputs.token || secrets.TOPOLO_CI_REPO_TOKEN }}"
5148
- ];
5149
- return [
5150
- "name: Topolo App Conformance",
5151
- "",
5152
- "on:",
5153
- " pull_request:",
5154
- " push:",
5155
- " branches:",
5156
- " - main",
5157
- " workflow_dispatch:",
5158
- "",
5159
- "permissions:",
5160
- " contents: read",
5161
- "",
5162
- "jobs:",
5163
- " conformance:",
5164
- " runs-on: ubuntu-latest",
5165
- " env:",
5166
- ` HAS_TOPOLO_PACKAGES_APP: \${{ secrets.TOPOLO_PACKAGES_APP_ID != '' && secrets.TOPOLO_PACKAGES_APP_PRIVATE_KEY != '' }}`,
5167
- ` HAS_TOPOLO_CI_REPO_TOKEN: \${{ secrets.TOPOLO_CI_REPO_TOKEN != '' }}`,
5168
- " steps:",
5169
- " - name: Checkout application",
5170
- " uses: actions/checkout@v4",
5171
- " with:",
5172
- ` path: ${appPath}`,
5173
- "",
5174
- " - name: Mint Topolo cross-repo token",
5175
- " id: app-token",
5176
- ` if: env.HAS_TOPOLO_PACKAGES_APP == 'true'`,
5177
- " uses: actions/create-github-app-token@v1",
5178
- " with:",
5179
- ` app-id: \${{ secrets.TOPOLO_PACKAGES_APP_ID }}`,
5180
- ` private-key: \${{ secrets.TOPOLO_PACKAGES_APP_PRIVATE_KEY }}`,
5181
- " owner: Topolo-io",
5182
- ...docsCheckoutStep,
5183
- "",
5184
- " - name: Checkout Topolo SDK",
5185
- ` if: steps.app-token.outputs.token != '' || env.HAS_TOPOLO_CI_REPO_TOKEN == 'true'`,
5186
- " uses: actions/checkout@v4",
5187
- " with:",
5188
- " repository: Topolo-io/topolo-sdk",
5189
- " path: Apps/packages/topolo-sdk",
5190
- " token: ${{ steps.app-token.outputs.token || secrets.TOPOLO_CI_REPO_TOKEN }}",
5191
- ...cliCheckoutStep,
5192
- "",
5193
- " - name: Skip conformance without cross-repo token",
5194
- ` if: steps.app-token.outputs.token == '' && env.HAS_TOPOLO_CI_REPO_TOKEN != 'true'`,
5195
- " run: |",
5196
- ' echo "::warning::Skipping Topolo app conformance because no Topolo cross-repo token is available to this workflow context."',
5197
- "",
5198
- " - name: Setup Node.js",
5199
- ` if: steps.app-token.outputs.token != '' || env.HAS_TOPOLO_CI_REPO_TOKEN == 'true'`,
5200
- " uses: actions/setup-node@v4",
5201
- " with:",
5202
- " node-version: 20",
5203
- "",
5204
- " - name: Build Topolo SDK",
5205
- ` if: steps.app-token.outputs.token != '' || env.HAS_TOPOLO_CI_REPO_TOKEN == 'true'`,
5206
- " working-directory: Apps/packages/topolo-sdk",
5207
- " run: |",
5208
- " npm install --no-audit --no-fund",
5209
- " npm run build",
5210
- "",
5211
- " - name: Build Topolo CLI",
5212
- ` if: steps.app-token.outputs.token != '' || env.HAS_TOPOLO_CI_REPO_TOKEN == 'true'`,
5213
- " working-directory: Apps/system/TopoloCLI",
5214
- " run: |",
5215
- " npm install --no-audit --no-fund",
5216
- " npm run build",
5217
- "",
5218
- " - name: Run Topolo app conformance audit",
5219
- ` if: steps.app-token.outputs.token != '' || env.HAS_TOPOLO_CI_REPO_TOKEN == 'true'`,
5220
- " run: |",
5221
- ` node Apps/system/TopoloCLI/dist/index.js apps audit ${options.id} --local --repo-root "$GITHUB_WORKSPACE" --fail-on needs_review --json`,
5222
- "",
5223
- " - name: Record green CI evidence",
5224
- ` if: \${{ success() && (steps.app-token.outputs.token != '' || env.HAS_TOPOLO_CI_REPO_TOKEN == 'true') }}`,
5225
- ` working-directory: ${appPath}`,
5226
- " run: |",
5227
- ` node "$GITHUB_WORKSPACE/Apps/system/TopoloCLI/dist/index.js" apps record-evidence ${options.id} --build --typecheck --test --commit "$GITHUB_SHA"`
5228
- ].join("\n");
5229
- }
5230
- function packageJson(application, target, options) {
5231
- const dependencies = {};
5232
- const devDependencies = {
5233
- "@types/node": "^20.12.0",
5234
- typescript: "^5.4.0",
5235
- vite: "^5.4.0"
5236
- };
5237
- const scripts = {
5238
- dev: target === "platform" ? "vite --config apps/web/vite.config.ts --host 0.0.0.0" : "vite --host 0.0.0.0",
5239
- build: target === "platform" ? "vite build --config apps/web/vite.config.ts" : "vite build",
5240
- preview: target === "platform" ? "vite preview --config apps/web/vite.config.ts --host 0.0.0.0" : "vite preview --host 0.0.0.0",
5241
- typecheck: "tsc --noEmit"
5242
- };
5243
- if (options.browser) {
5244
- dependencies.react = "^18.3.1";
5245
- dependencies["react-dom"] = "^18.3.1";
5246
- devDependencies["@types/react"] = "^18.3.3";
5247
- devDependencies["@types/react-dom"] = "^18.3.0";
5248
- devDependencies["@vitejs/plugin-react"] = "^4.7.0";
5249
- if (target === "platform") {
5250
- dependencies["@topolo-io/auth-client"] = topoloPackageVersion("@topolo-io/auth-client", target);
5251
- dependencies["@topolo-io/design-tokens"] = topoloPackageVersion("@topolo-io/design-tokens", target);
5252
- dependencies["@topolo-io/onboarding"] = topoloPackageVersion("@topolo-io/onboarding", target);
5253
- dependencies["@topolo-io/tour"] = topoloPackageVersion("@topolo-io/tour", target);
5254
- dependencies["@topolo-io/ui-kit"] = topoloPackageVersion("@topolo-io/ui-kit", target);
5255
- } else {
5256
- dependencies["@topolo/login"] = topoloPackageVersion("@topolo/login", target);
5257
- }
5258
- }
5259
- if (options.api) {
5260
- devDependencies["@cloudflare/workers-types"] = "^4.20260413.1";
4894
+ devDependencies["@cloudflare/workers-types"] = "^4.20260413.1";
5261
4895
  if (target === "platform") {
5262
4896
  dependencies["@topolo-io/worker-runtime"] = topoloPackageVersion("@topolo-io/worker-runtime", target);
5263
4897
  dependencies["@topolo/sdk"] = topoloPackageVersion("@topolo/sdk", target);
@@ -5295,6 +4929,9 @@ function topoloPackageVersion(packageName, target) {
5295
4929
  const platformPackageFolders = {
5296
4930
  "@topolo/sdk": "topolo-sdk",
5297
4931
  "@topolo/login": "topolo-login",
4932
+ "@topolo-io/app-funnel": "topolo-app-funnel",
4933
+ "@topolo-io/app-launcher": "topolo-app-launcher",
4934
+ "@topolo-io/app-shell": "topolo-app-shell",
5298
4935
  "@topolo-io/auth-client": "topolo-auth-client",
5299
4936
  "@topolo-io/auth-middleware": "topolo-auth-middleware",
5300
4937
  "@topolo-io/worker-runtime": "topolo-worker-runtime",
@@ -5315,11 +4952,12 @@ function hasScaffoldResources(resources) {
5315
4952
  function resourceConfig(application, profile) {
5316
4953
  const slug = toSlug(application.id);
5317
4954
  const pascal = toPascalCase(application.id);
4955
+ const constant = toConstantCase(application.id);
5318
4956
  if (profile === "spatial-assets") {
5319
4957
  return {
5320
- d1Binding: "APP_DB",
5321
- d1ProductionName: `topolo-${slug}-production-db`,
5322
- d1StagingName: `topolo-${slug}-staging-db`,
4958
+ d1Binding: `${constant}_CORE_DB`,
4959
+ d1ProductionName: `topolo-${slug}-core-prod`,
4960
+ d1StagingName: `topolo-${slug}-core-staging`,
5323
4961
  r2Binding: "SPACES_ASSETS",
5324
4962
  r2ProductionName: `topolo-${slug}-assets`,
5325
4963
  r2StagingName: `topolo-${slug}-assets-staging`,
@@ -5335,9 +4973,9 @@ function resourceConfig(application, profile) {
5335
4973
  };
5336
4974
  }
5337
4975
  return {
5338
- d1Binding: "APP_DB",
5339
- d1ProductionName: `topolo-${slug}-prod`,
5340
- d1StagingName: `topolo-${slug}-staging`,
4976
+ d1Binding: `${constant}_CORE_DB`,
4977
+ d1ProductionName: `topolo-${slug}-core-prod`,
4978
+ d1StagingName: `topolo-${slug}-core-staging`,
5341
4979
  r2Binding: "APP_ASSETS",
5342
4980
  r2ProductionName: `topolo-${slug}-assets`,
5343
4981
  r2StagingName: `topolo-${slug}-assets-staging`,
@@ -5352,6 +4990,8 @@ function resourceConfig(application, profile) {
5352
4990
  containerStagingName: `topolo-${slug}-processor-staging`
5353
4991
  };
5354
4992
  }
4993
+
4994
+ // src/commands/app-scaffold-browser-template.ts
5355
4995
  function readme(application, target, options) {
5356
4996
  const displayName = toTitle(application.name);
5357
4997
  const slug = toSlug(application.id);
@@ -5380,7 +5020,10 @@ function readme(application, target, options) {
5380
5020
  lines.push(
5381
5021
  "## Shared Topolo Surface",
5382
5022
  "",
5383
- "- `@topolo-io/ui-kit` provides the shared shell, launcher, account menu, command palette, and bug reporter.",
5023
+ "- `@topolo-io/app-funnel` provides the generated public application funnel/landing surface.",
5024
+ "- `@topolo-io/app-shell` provides the generated route/auth bootstrap, product shell, login, callback, and loading surfaces.",
5025
+ "- `@topolo-io/app-launcher` provides shared app switching and handoff helpers.",
5026
+ "- `@topolo-io/ui-kit` provides primitive account, command palette, icon, transition, and bug reporter UI.",
5384
5027
  "- `@topolo-io/auth-client` owns browser auth, callback exchange, token refresh, and logout.",
5385
5028
  "- Generated first-party browser apps start on the shared landing page at `/`, use the shared login page at `/login`, and complete sign-in on `/auth/callback` before entering the workspace shell.",
5386
5029
  "- `vite.config.ts` includes the standard React plugin plus React runtime dedupe so linked Topolo packages resolve one browser React runtime."
@@ -5793,6 +5436,8 @@ function stylesCss() {
5793
5436
  ""
5794
5437
  ].join("\n");
5795
5438
  }
5439
+
5440
+ // src/commands/app-scaffold-app-template.ts
5796
5441
  function appTsx(application, target, options) {
5797
5442
  if (target === "developer") return developerAppTsx(application);
5798
5443
  return platformAppTsx(application, options);
@@ -5802,13 +5447,13 @@ function platformAppTsx(application, options) {
5802
5447
  const tagline = isPersonal ? "Topolo personal app" : "Topolo workspace";
5803
5448
  const workspaceTitle = isPersonal ? "Personal app" : "Workspace";
5804
5449
  const shellLabel = isPersonal ? "personal app shell" : "workspace shell";
5805
- const signedSurfaceLabel = isPersonal ? "personal app" : "workspace";
5806
5450
  const baselineHeading = isPersonal ? "Shared personal baseline" : "Shared workspace baseline";
5807
5451
  const baselineDescription = isPersonal ? "This scaffold starts on the shared landing and login surfaces, completes sign-in on the shared callback path, and renders the signed personal app inside the shared Topolo shell." : "This scaffold starts on the shared landing and login surfaces, completes sign-in on the shared callback path, and renders the signed workspace inside the shared Topolo shell.";
5808
5452
  return [
5809
5453
  "import { Component, useCallback, useEffect, useMemo, useState, type ErrorInfo, type ReactNode } from 'react';",
5810
- "import { createBrowserAuthSessionController, type AuthUser } from '@topolo-io/auth-client';",
5811
- "import { LandingPage, LoginPage, TopoloAppShell, TopoloAuthFlowScreen, TopoloLoadingScreen, setupBugReporterErrorCapture, type TopoloAppShellUser } from '@topolo-io/ui-kit';",
5454
+ "import { LandingPage } from '@topolo-io/app-funnel';",
5455
+ "import { LoginPage, TopoloAppLoadingScaffold, TopoloAppRoot, TopoloAuthFlowScreen, createTopoloAuth, useAuth, type AppRoute as TopoloAppRoute } from '@topolo-io/app-shell';",
5456
+ "import { setupBugReporterErrorCapture } from '@topolo-io/ui-kit';",
5812
5457
  "import { GettingStarted, type OnboardingGuideStep } from '@topolo-io/onboarding';",
5813
5458
  "import { IntroTour } from '@topolo-io/tour';",
5814
5459
  "import { SCAFFOLD_APP_ID, SCAFFOLD_APP_NAME, SCAFFOLD_SERVICE_SLUG, getBrowserAuthBaseUrl, resolveAppServiceId } from './service-identity';",
@@ -5817,11 +5462,6 @@ function platformAppTsx(application, options) {
5817
5462
  "// can attach the last few errors to any submitted bug report.",
5818
5463
  "setupBugReporterErrorCapture();",
5819
5464
  "",
5820
- "type NavItem = 'home' | 'getting-started';",
5821
- "type WorkspaceView = 'workspace' | 'getting-started';",
5822
- "type AppRoute = { kind: 'root' } | { kind: 'login' } | { kind: 'callback' };",
5823
- "type BrowserAuthModule = ReturnType<typeof createBrowserAuthSessionController>;",
5824
- "",
5825
5465
  "const APP_ID = SCAFFOLD_APP_ID;",
5826
5466
  "const APP_NAME = SCAFFOLD_APP_NAME;",
5827
5467
  "const AUTH_URL = getBrowserAuthBaseUrl();",
@@ -5843,69 +5483,17 @@ function platformAppTsx(application, options) {
5843
5483
  " authenticated_home_path: '/',",
5844
5484
  "};",
5845
5485
  "",
5846
- "let activeAuthServiceId = '';",
5847
- "let activeAuthModule: BrowserAuthModule | null = null;",
5848
- "",
5849
- "function getAuthModule(serviceId: string): BrowserAuthModule {",
5850
- " const normalizedServiceId = serviceId.trim();",
5851
- " if (!normalizedServiceId) throw new Error(`Missing Auth service id for ${SCAFFOLD_SERVICE_SLUG}`);",
5852
- " if (!activeAuthModule || activeAuthServiceId !== normalizedServiceId) {",
5853
- " activeAuthServiceId = normalizedServiceId;",
5854
- " activeAuthModule = createBrowserAuthSessionController({",
5855
- " authServiceUrl: AUTH_URL,",
5856
- " serviceId: normalizedServiceId,",
5857
- " devSessionKey: `${normalizedServiceId}_session`,",
5858
- " devSessionStorageEnabled: import.meta.env.DEV,",
5859
- " callbackPath: '/auth/callback',",
5860
- " debug: import.meta.env.DEV,",
5861
- " });",
5862
- " }",
5863
- " return activeAuthModule;",
5864
- "}",
5865
- "",
5866
- "function getAccessToken(): string | null {",
5867
- " return activeAuthModule?.accessTokenMemory.get() ?? activeAuthModule?.accessTokenSession.get() ?? null;",
5868
- "}",
5869
- "",
5870
- "function toShellUser(user: AuthUser | null): TopoloAppShellUser | null {",
5871
- " if (!user) return null;",
5872
- " return {",
5873
- " id: user.id,",
5874
- " name: user.name ?? user.email,",
5875
- " email: user.email,",
5876
- " role: user.role,",
5877
- " organizationName: user.organization?.name ?? user.orgSlug ?? null,",
5878
- " organizationId: user.organization?.id ?? user.orgId ?? null,",
5879
- " };",
5880
- "}",
5881
- "",
5882
5486
  "export default function App() {",
5883
5487
  " return (",
5884
5488
  " <AppErrorBoundary>",
5885
- " <AppRoot />",
5489
+ " <CanonicalAppRoot />",
5886
5490
  " </AppErrorBoundary>",
5887
5491
  " );",
5888
5492
  "}",
5889
5493
  "",
5890
- "function AppRoot() {",
5891
- " const route = getRoute();",
5892
- " const [user, setUser] = useState<AuthUser | null>(null);",
5494
+ "function CanonicalAppRoot() {",
5893
5495
  " const [serviceId, setServiceId] = useState('');",
5894
5496
  " const [serviceIdentityError, setServiceIdentityError] = useState('');",
5895
- " const [loading, setLoading] = useState(true);",
5896
- " const [authError, setAuthError] = useState('');",
5897
- " const [loginAttempted, setLoginAttempted] = useState(false);",
5898
- " const [view, setView] = useState<WorkspaceView>('workspace');",
5899
- " const auth = useMemo(() => serviceId ? getAuthModule(serviceId) : null, [serviceId]);",
5900
- " const openWorkspace = useCallback(() => setView('workspace'), []);",
5901
- " const openGettingStarted = useCallback(() => {",
5902
- " setView('getting-started');",
5903
- " if (typeof window !== 'undefined') window.scrollTo({ top: 0, behavior: 'smooth' });",
5904
- " }, []);",
5905
- " const handleNavSelect = useCallback((id: NavItem) => {",
5906
- " if (id === 'getting-started') openGettingStarted();",
5907
- " else openWorkspace();",
5908
- " }, [openGettingStarted, openWorkspace]);",
5909
5497
  "",
5910
5498
  " useEffect(() => {",
5911
5499
  " let active = true;",
@@ -5917,229 +5505,139 @@ function platformAppTsx(application, options) {
5917
5505
  " .catch((error) => {",
5918
5506
  " if (!active) return;",
5919
5507
  " setServiceIdentityError(errorMessage(error));",
5920
- " setLoading(false);",
5921
5508
  " });",
5922
5509
  " return () => {",
5923
5510
  " active = false;",
5924
5511
  " };",
5925
5512
  " }, []);",
5926
5513
  "",
5927
- " useEffect(() => {",
5928
- " if (!auth) return;",
5929
- " let active = true;",
5930
- " const syncAuth = async () => {",
5931
- " const publicRoute = route.kind === 'root' || route.kind === 'login';",
5932
- " if (publicRoute) setLoading(false);",
5933
- " else setLoading(true);",
5934
- " setAuthError('');",
5935
- "",
5936
- " if (route.kind === 'callback') {",
5937
- " const result = await auth.authClient.handleAuthCallbackAsync(window.location.href);",
5938
- " if (!active) return;",
5939
- " if (!result.authenticated) {",
5940
- " setAuthError(result.error || 'Authentication failed.');",
5941
- " setLoading(false);",
5942
- " return;",
5943
- " }",
5944
- " setUser(result.user ?? null);",
5945
- " window.location.replace('/');",
5946
- " return;",
5947
- " }",
5948
- "",
5949
- " const nextUser = await auth.authClient.initialize({",
5950
- " cookieRefresh: publicRoute ? 'background' : 'blocking',",
5951
- " });",
5952
- " if (!active) return;",
5953
- " setUser(nextUser);",
5954
- " setLoading(false);",
5955
- " };",
5956
- "",
5957
- " void syncAuth().catch((error) => {",
5958
- " if (!active) return;",
5959
- " setAuthError(errorMessage(error));",
5960
- " setLoading(false);",
5961
- " });",
5962
- " return () => {",
5963
- " active = false;",
5964
- " };",
5965
- " }, [auth, route.kind]);",
5966
- "",
5967
- " useEffect(() => {",
5968
- " if (!auth) return;",
5969
- " const handleSessionUpdated = () => {",
5970
- " const nextUser = auth.authClient.getHydratedCurrentUser() || auth.authClient.getTokenIdentity();",
5971
- " if (nextUser) setUser(nextUser);",
5972
- " };",
5973
- " window.addEventListener('auth:session-updated', handleSessionUpdated);",
5974
- " return () => window.removeEventListener('auth:session-updated', handleSessionUpdated);",
5975
- " }, [auth]);",
5976
- "",
5977
- " useEffect(() => {",
5978
- " if (!loading && user && route.kind === 'login') {",
5979
- " window.location.replace('/');",
5980
- " }",
5981
- " }, [loading, route.kind, user]);",
5982
- "",
5983
- " const shellUser = useMemo(() => toShellUser(user), [user]);",
5984
- " const activeServiceId = serviceId || SCAFFOLD_SERVICE_SLUG;",
5985
- "",
5986
- " if (!auth) {",
5987
- " if (serviceIdentityError) {",
5988
- " return (",
5989
- " <TopoloAuthFlowScreen",
5990
- " appName={APP_NAME}",
5991
- " appId={APP_ID}",
5992
- " serviceId={SCAFFOLD_SERVICE_SLUG}",
5993
- ' status="error"',
5994
- ' title="Service identity unavailable"',
5995
- " message={serviceIdentityError}",
5996
- ' actionLabel="Retry"',
5997
- " onAction={() => window.location.reload()}",
5998
- " />",
5999
- " );",
6000
- " }",
6001
- " return (",
6002
- " <TopoloLoadingScreen",
6003
- " appId={APP_ID}",
6004
- " serviceId={SCAFFOLD_SERVICE_SLUG}",
6005
- " appName={APP_NAME}",
6006
- " subtitle={`Loading ${APP_NAME}...`}",
6007
- " />",
6008
- " );",
6009
- " }",
5514
+ " const auth = useMemo(() => serviceId ? createTopoloAuth({",
5515
+ " authServiceUrl: AUTH_URL,",
5516
+ " serviceId,",
5517
+ " devSessionKey: `${serviceId}_session`,",
5518
+ " callbackPath: '/auth/callback',",
5519
+ " sessionStorageRestoreEnabled: import.meta.env.DEV,",
5520
+ " debug: import.meta.env.DEV,",
5521
+ " }) : null, [serviceId]);",
6010
5522
  "",
6011
- " if (route.kind === 'callback') {",
6012
- " if (loading) {",
6013
- " return (",
6014
- " <TopoloLoadingScreen",
6015
- " appId={APP_ID}",
6016
- " serviceId={activeServiceId}",
6017
- " appName={APP_NAME}",
6018
- ' subtitle="Completing secure sign-in..."',
6019
- " />",
6020
- " );",
6021
- " }",
5523
+ " const routes = useMemo<TopoloAppRoute[]>(() => serviceId ? [",
5524
+ " {",
5525
+ " path: '/workspace',",
5526
+ " nav: { id: 'home', label: '" + escapeSingleQuoted(workspaceTitle) + "' },",
5527
+ " loading: { shell: 'topbar', content: 'workspace', title: '" + escapeSingleQuoted(workspaceTitle) + "' },",
5528
+ " element: <ScaffoldWorkspace />,",
5529
+ " },",
5530
+ " {",
5531
+ " path: '/getting-started',",
5532
+ " nav: { id: 'getting-started', label: 'Getting started' },",
5533
+ " loading: { shell: 'topbar', content: 'card-grid', title: 'Getting started' },",
5534
+ " element: <ScaffoldGettingStartedRoute serviceId={serviceId} />,",
5535
+ " },",
5536
+ " ] : [], [serviceId]);",
6022
5537
  "",
5538
+ " if (serviceIdentityError) {",
6023
5539
  " return (",
6024
5540
  " <TopoloAuthFlowScreen",
6025
5541
  " appName={APP_NAME}",
6026
5542
  " appId={APP_ID}",
6027
- " serviceId={activeServiceId}",
5543
+ " serviceId={SCAFFOLD_SERVICE_SLUG}",
6028
5544
  ' status="error"',
6029
- ' title="Authentication failed"',
6030
- " message={authError || `We could not complete sign-in for ${APP_NAME}.`}",
6031
- ' actionLabel="Back to sign in"',
6032
- " onAction={() => window.location.replace('/login')}",
5545
+ ' title="Service identity unavailable"',
5546
+ " message={serviceIdentityError}",
5547
+ ' actionLabel="Retry"',
5548
+ " onAction={() => window.location.reload()}",
6033
5549
  " />",
6034
5550
  " );",
6035
5551
  " }",
6036
5552
  "",
6037
- " if (loading) {",
5553
+ " if (!auth) {",
6038
5554
  " return (",
6039
- " <TopoloLoadingScreen",
5555
+ " <TopoloAppLoadingScaffold",
6040
5556
  " appId={APP_ID}",
6041
- " serviceId={activeServiceId}",
5557
+ " serviceId={SCAFFOLD_SERVICE_SLUG}",
6042
5558
  " appName={APP_NAME}",
5559
+ " layout={{ shell: 'topbar', content: 'workspace' }}",
6043
5560
  " subtitle={`Loading ${APP_NAME}...`}",
6044
5561
  " />",
6045
5562
  " );",
6046
5563
  " }",
6047
5564
  "",
6048
- " if (!user) {",
6049
- " if (route.kind === 'login') {",
6050
- " return (",
5565
+ " return (",
5566
+ " <TopoloAppRoot",
5567
+ " appName={APP_NAME}",
5568
+ " serviceId={serviceId}",
5569
+ " auth={auth}",
5570
+ " routes={routes}",
5571
+ ' home="/workspace"',
5572
+ " publicLanding={",
5573
+ " <LandingPage",
5574
+ " serviceId={serviceId}",
5575
+ " authApiUrl={AUTH_URL}",
5576
+ " config={LANDING_CONFIG}",
5577
+ " onCTAClick={() => window.location.assign('/login')}",
5578
+ " />",
5579
+ " }",
5580
+ " loginElement={",
6051
5581
  " <LoginPage",
6052
- " serviceId={activeServiceId}",
5582
+ " serviceId={serviceId}",
6053
5583
  " authApiUrl={AUTH_URL}",
6054
5584
  " config={LOGIN_CONFIG}",
6055
- " isLoading={loginAttempted}",
6056
5585
  " onLogin={async (provider) => {",
6057
- " setLoginAttempted(true);",
6058
- " try {",
6059
- " await auth.authClient.login(provider || 'topolo');",
6060
- " } catch {",
6061
- " setLoginAttempted(false);",
6062
- " }",
5586
+ " await auth.login(provider || 'topolo');",
6063
5587
  " }}",
6064
5588
  " onPasswordLogin={async (request) => {",
6065
- " const result = await auth.authClient.loginWithPassword(request);",
6066
- " if (!result.authenticated) {",
6067
- " return result;",
6068
- " }",
6069
- " setUser(result.user ?? null);",
6070
- " window.location.replace('/');",
5589
+ " const result = await auth.loginWithPassword(request);",
5590
+ " if (!result.authenticated) return result;",
5591
+ " window.dispatchEvent(new CustomEvent('auth:session-updated', { detail: { tokens: result.tokens, user: result.user } }));",
6071
5592
  " return undefined;",
6072
5593
  " }}",
6073
5594
  " />",
6074
- " );",
6075
- " }",
6076
- "",
6077
- " return (",
6078
- " <LandingPage",
6079
- " serviceId={activeServiceId}",
6080
- " authApiUrl={AUTH_URL}",
6081
- " config={LANDING_CONFIG}",
6082
- " onCTAClick={() => window.location.assign('/login')}",
6083
- " />",
6084
- " );",
6085
- " }",
6086
- "",
6087
- " if (route.kind === 'login') {",
6088
- " return (",
6089
- " <TopoloLoadingScreen",
6090
- " appId={APP_ID}",
6091
- " serviceId={activeServiceId}",
6092
- " appName={APP_NAME}",
6093
- ` subtitle="Opening your ${escapeDoubleQuoted(signedSurfaceLabel)}..."`,
6094
- " />",
6095
- " );",
6096
- " }",
6097
- "",
6098
- " return (",
6099
- " <TopoloAppShell<NavItem>",
6100
- " appId={APP_ID}",
6101
- " serviceId={activeServiceId}",
6102
- " appName={APP_NAME}",
6103
- " user={shellUser}",
6104
- " orgId={user.activeContextType === 'organization' ? user.activeContextId ?? user.orgId ?? null : null}",
6105
- " activeContextType={user.activeContextType}",
6106
- " activeContextId={user.activeContextId}",
6107
- " activeNavItemId={view === 'getting-started' ? 'getting-started' : 'home'}",
6108
- ` navItems={[{ id: 'home', label: '${escapeSingleQuoted(workspaceTitle)}' }, { id: 'getting-started', label: 'Getting started' }]}`,
6109
- ' navigationMode="topbar"',
6110
- " onNavItemSelect={handleNavSelect}",
6111
- ` title={view === 'getting-started' ? 'Getting started' : '${escapeSingleQuoted(workspaceTitle)}'}`,
6112
- " authUrl={AUTH_URL}",
6113
- " getAccessToken={getAccessToken}",
6114
- " onSignOut={async () => {",
6115
- " await auth.authClient.logout();",
6116
- " window.location.assign('/login');",
6117
- " }}",
6118
- " commandPlaceholder={`Search ${APP_NAME}`}",
6119
- " bugReporterAppName={APP_NAME}",
6120
- " enableCommandPalette",
6121
- " enableBugReporter",
6122
- " >",
6123
- " {view === 'getting-started' ? (",
6124
- " <ScaffoldGettingStarted",
6125
- " serviceId={activeServiceId}",
6126
- " organizationId={user.organization?.id ?? user.orgId ?? ''}",
6127
- " role={user.role}",
6128
- " onOpenWorkspace={openWorkspace}",
5595
+ " }",
5596
+ " loadingFallback={({ loading }) => (",
5597
+ " <TopoloAppLoadingScaffold",
5598
+ " appId={APP_ID}",
5599
+ " serviceId={serviceId}",
5600
+ " appName={APP_NAME}",
5601
+ " layout={{ shell: loading?.shell || 'topbar', content: loading?.content || 'workspace' }}",
5602
+ " title={loading?.title}",
5603
+ " subtitle={`Loading ${APP_NAME}...`}",
6129
5604
  " />",
6130
- " ) : (",
6131
- ' <div className="topolo-starter-workspace">',
6132
- ' <section className="topolo-starter-card" data-tour="workspace-card">',
6133
- ` <h2>${baselineHeading}</h2>`,
6134
- " <p>",
6135
- ` ${baselineDescription}`,
6136
- " </p>",
6137
- " </section>",
6138
- " </div>",
6139
5605
  " )}",
5606
+ " shell={{",
5607
+ " appId: APP_ID,",
5608
+ " authUrl: AUTH_URL,",
5609
+ " navigationMode: 'topbar',",
5610
+ " title: APP_NAME,",
5611
+ " commandPlaceholder: `Search ${APP_NAME}`,",
5612
+ " bugReporterAppName: APP_NAME,",
5613
+ " enableCommandPalette: true,",
5614
+ " enableBugReporter: true,",
5615
+ " }}",
5616
+ " />",
5617
+ " );",
5618
+ "}",
5619
+ "",
5620
+ "function ScaffoldWorkspace() {",
5621
+ " const { user } = useAuth();",
5622
+ " const openWorkspace = useCallback(() => {",
5623
+ " window.history.pushState({}, '', '/workspace');",
5624
+ " window.dispatchEvent(new PopStateEvent('popstate'));",
5625
+ " }, []);",
5626
+ " const openGettingStarted = useCallback(() => {",
5627
+ " window.history.pushState({}, '', '/getting-started');",
5628
+ " window.dispatchEvent(new PopStateEvent('popstate'));",
5629
+ " }, []);",
5630
+ " return (",
5631
+ ' <div className="topolo-starter-workspace">',
5632
+ ' <section className="topolo-starter-card" data-tour="workspace-card">',
5633
+ ` <h2>${baselineHeading}</h2>`,
5634
+ " <p>",
5635
+ ` ${baselineDescription}`,
5636
+ " </p>",
5637
+ " </section>",
6140
5638
  " <IntroTour",
6141
5639
  " appName={APP_NAME}",
6142
- " role={user.role}",
5640
+ " role={user?.role}",
6143
5641
  " onOpenGettingStarted={openGettingStarted}",
6144
5642
  " extraSteps={[",
6145
5643
  " {",
@@ -6152,7 +5650,20 @@ function platformAppTsx(application, options) {
6152
5650
  " },",
6153
5651
  " ]}",
6154
5652
  " />",
6155
- " </TopoloAppShell>",
5653
+ " </div>",
5654
+ " );",
5655
+ "}",
5656
+ "",
5657
+ "function ScaffoldGettingStartedRoute({ serviceId }: { serviceId: string }) {",
5658
+ " const { user, tokens } = useAuth();",
5659
+ " return (",
5660
+ " <ScaffoldGettingStarted",
5661
+ " serviceId={serviceId}",
5662
+ " organizationId={String((user as any)?.organization?.id ?? user?.orgId ?? '')}",
5663
+ " role={user?.role}",
5664
+ " getAccessToken={() => tokens?.accessToken ?? null}",
5665
+ " onOpenWorkspace={() => window.location.assign('/workspace')}",
5666
+ " />",
6156
5667
  " );",
6157
5668
  "}",
6158
5669
  "",
@@ -6160,15 +5671,16 @@ function platformAppTsx(application, options) {
6160
5671
  " serviceId: string;",
6161
5672
  " organizationId: string;",
6162
5673
  " role?: string;",
5674
+ " getAccessToken: () => string | null;",
6163
5675
  " onOpenWorkspace: () => void;",
6164
5676
  "}) {",
6165
- " const { serviceId, organizationId, role, onOpenWorkspace } = props;",
5677
+ " const { serviceId, organizationId, role, getAccessToken, onOpenWorkspace } = props;",
6166
5678
  " const steps: OnboardingGuideStep[] = [",
6167
5679
  " {",
6168
5680
  " id: 'explore-workspace',",
6169
5681
  " title: 'Explore your workspace',",
6170
5682
  " description:",
6171
- " 'Your app renders inside the shared Topolo shell \u2014 launcher, command palette, and account menu are already wired in.',",
5683
+ " 'Your workspace lives in the Topolo shell \u2014 the launcher, command palette, and account menu are all one click away.',",
6172
5684
  " actionLabel: 'Open workspace',",
6173
5685
  " onAction: onOpenWorkspace,",
6174
5686
  " },",
@@ -6181,9 +5693,9 @@ function platformAppTsx(application, options) {
6181
5693
  " },",
6182
5694
  " {",
6183
5695
  " id: 'build-feature',",
6184
- " title: 'Build your first feature',",
5696
+ " title: 'Make it your own',",
6185
5697
  " description:",
6186
- " 'Replace the starter card with your product surface, and gate write actions with service scopes before you ship.',",
5698
+ " 'Open the account menu to adjust your settings and theme \u2014 your preferences stay in sync across every Topolo app.',",
6187
5699
  " },",
6188
5700
  " ];",
6189
5701
  " return (",
@@ -6192,7 +5704,7 @@ function platformAppTsx(application, options) {
6192
5704
  " authApiBaseUrl={`${AUTH_URL}/api`}",
6193
5705
  " organizationId={organizationId}",
6194
5706
  " serviceId={serviceId}",
6195
- " getAccessToken={() => getAccessToken()}",
5707
+ " getAccessToken={getAccessToken}",
6196
5708
  " role={role || undefined}",
6197
5709
  " steps={steps}",
6198
5710
  " eyebrow={`Welcome to ${APP_NAME}`}",
@@ -6208,13 +5720,6 @@ function platformAppTsx(application, options) {
6208
5720
  " );",
6209
5721
  "}",
6210
5722
  "",
6211
- "function getRoute(): AppRoute {",
6212
- " const pathname = window.location.pathname.replace(/\\/+$/, '') || '/';",
6213
- " if (pathname === '/login') return { kind: 'login' };",
6214
- " if (pathname === '/auth/callback') return { kind: 'callback' };",
6215
- " return { kind: 'root' };",
6216
- "}",
6217
- "",
6218
5723
  "function errorMessage(error: unknown): string {",
6219
5724
  " if (error instanceof Error && error.message) return error.message;",
6220
5725
  " return 'Unexpected authentication error.';",
@@ -6360,6 +5865,13 @@ function developerAppTsx(application) {
6360
5865
  ""
6361
5866
  ].join("\n");
6362
5867
  }
5868
+
5869
+ // src/commands/app-scaffold-cloudflare-constants.ts
5870
+ var CLOUDFLARE_ACCOUNT_ID = "49ef1ba682ad8cfd720c86699ae17521";
5871
+ var STAGING_CLOUDFLARE_ACCOUNT_ID = "4f4e1c69a3830946f9fea7b1eb7531ac";
5872
+ var CLOUDFLARE_COMPATIBILITY_DATE = "2026-05-05";
5873
+
5874
+ // src/commands/app-scaffold-worker-template.ts
6363
5875
  function webWorkerTs(application) {
6364
5876
  const serviceName = toTitle(application.name);
6365
5877
  return [
@@ -6477,7 +5989,7 @@ function platformApiWranglerToml(application, resources, profile) {
6477
5989
  "",
6478
5990
  "[vars]",
6479
5991
  'AUTH_BASE_URL = "https://auth.topolo.app"',
6480
- 'NEXUS_GATEWAY_URL = "https://nexus-api.topolo.app"',
5992
+ 'NEXUS_GATEWAY_URL = "https://nexus.topolo.app"',
6481
5993
  `NEXUS_APP_ID = "${slug}"`,
6482
5994
  `SERVICE_ID = "${application.serviceId ?? ""}"`,
6483
5995
  `VITE_TOPOLO_SERVICE_ID = "${application.serviceId ?? ""}"`,
@@ -6492,7 +6004,7 @@ function platformApiWranglerToml(application, resources, profile) {
6492
6004
  "",
6493
6005
  "[env.staging.vars]",
6494
6006
  'AUTH_BASE_URL = "https://auth.stg.topolo.us"',
6495
- 'NEXUS_GATEWAY_URL = "https://nexus-api.stg.topolo.us"',
6007
+ 'NEXUS_GATEWAY_URL = "https://nexus.stg.topolo.us"',
6496
6008
  `NEXUS_APP_ID = "${slug}"`,
6497
6009
  `SERVICE_ID = "${application.serviceId ?? ""}"`,
6498
6010
  `VITE_TOPOLO_SERVICE_ID = "${application.serviceId ?? ""}"`,
@@ -6507,7 +6019,7 @@ function platformApiWranglerToml(application, resources, profile) {
6507
6019
  "",
6508
6020
  "[env.production.vars]",
6509
6021
  'AUTH_BASE_URL = "https://auth.topolo.app"',
6510
- 'NEXUS_GATEWAY_URL = "https://nexus-api.topolo.app"',
6022
+ 'NEXUS_GATEWAY_URL = "https://nexus.topolo.app"',
6511
6023
  `NEXUS_APP_ID = "${slug}"`,
6512
6024
  `SERVICE_ID = "${application.serviceId ?? ""}"`,
6513
6025
  `VITE_TOPOLO_SERVICE_ID = "${application.serviceId ?? ""}"`,
@@ -6603,7 +6115,7 @@ function wranglerToml(application, target, resources, profile) {
6603
6115
  "",
6604
6116
  "[vars]",
6605
6117
  'AUTH_BASE_URL = "https://auth.topolo.app"',
6606
- 'NEXUS_GATEWAY_URL = "https://nexus-api.topolo.app"',
6118
+ 'NEXUS_GATEWAY_URL = "https://nexus.topolo.app"',
6607
6119
  runtimeIdentityVar,
6608
6120
  ""
6609
6121
  );
@@ -6658,7 +6170,7 @@ function wranglerToml(application, target, resources, profile) {
6658
6170
  "",
6659
6171
  "[env.staging.vars]",
6660
6172
  'AUTH_BASE_URL = "https://auth.stg.topolo.us"',
6661
- 'NEXUS_GATEWAY_URL = "https://nexus-api.stg.topolo.us"',
6173
+ 'NEXUS_GATEWAY_URL = "https://nexus.stg.topolo.us"',
6662
6174
  runtimeIdentityVar,
6663
6175
  ""
6664
6176
  );
@@ -6761,6 +6273,9 @@ function platformWorkerTs(application, options) {
6761
6273
  " return [",
6762
6274
  " {",
6763
6275
  " type: 'quick-actions',",
6276
+ " snapshot: {",
6277
+ " summary: `${SCAFFOLD_APP_NAME} is ready for workspace operations and exposes health checks for operators.`,",
6278
+ " },",
6764
6279
  " actions: [",
6765
6280
  ` { id: 'open-workspace', label: '${escapeSingleQuoted(openActionLabel)}', link: '/', primary: true },`,
6766
6281
  " { id: 'view-health', label: 'View health', link: '/api/health' },",
@@ -6872,6 +6387,9 @@ function developerWorkerTs(application) {
6872
6387
  " return [",
6873
6388
  " {",
6874
6389
  " type: 'quick-actions',",
6390
+ " snapshot: {",
6391
+ " summary: `${SERVICE_NAME} is ready for workspace operations and exposes health checks for operators.`,",
6392
+ " },",
6875
6393
  " actions: [",
6876
6394
  " { id: 'open-app', label: `Open ${SERVICE_NAME}`, link: '/', primary: true },",
6877
6395
  " { id: 'view-health', label: 'View health', link: '/api/health' },",
@@ -6952,6 +6470,632 @@ function developerWorkerTs(application) {
6952
6470
  ""
6953
6471
  ].join("\n");
6954
6472
  }
6473
+
6474
+ // src/commands/app-scaffold-workflows.ts
6475
+ function scaffoldCommands(options) {
6476
+ const scaffoldId = toSlug(options.id);
6477
+ if (options.target === "developer") {
6478
+ const audienceFlags = options.audience === "personal" ? " --audience personal --tenancy household" : "";
6479
+ return [
6480
+ `topolo apps scaffold ${scaffoldId}${audienceFlags} --write --repo-root .`,
6481
+ `cd topolo-${scaffoldId}`,
6482
+ "npm install",
6483
+ "npm run typecheck",
6484
+ "npm run build"
6485
+ ];
6486
+ }
6487
+ const portfolioFlags = options.portfolio === "personal" ? " --portfolio personal" : "";
6488
+ const profileFlags = options.profile ? ` --profile ${options.profile}` : "";
6489
+ return [
6490
+ `topolo platform apps scaffold ${scaffoldId}${portfolioFlags}${profileFlags} --write --provision --repo-root /Users/ashes/Projects/Topolo/Platform --json`,
6491
+ "topolo apps requirements --json",
6492
+ `topolo apps audit ${options.id} --local --repo-root /Users/ashes/Projects/Topolo/Platform --fail-on needs_review --json`,
6493
+ `topolo apps audit ${options.id} --local --repo-root /Users/ashes/Projects/Topolo/Platform --fail-on partial --json`
6494
+ ];
6495
+ }
6496
+ function topoloConformanceWorkflow(options) {
6497
+ const appPath = firstPartyAppPath(options.appDir, options.audience);
6498
+ const docsCheckoutStep = options.appDir === "TopoloDocs" ? [] : [
6499
+ "",
6500
+ " - name: Checkout TopoloDocs",
6501
+ ` if: steps.app-token.outputs.token != '' || env.HAS_TOPOLO_CI_REPO_TOKEN == 'true'`,
6502
+ " uses: actions/checkout@v4",
6503
+ " with:",
6504
+ " repository: Topolo-io/TopoloDocs",
6505
+ " path: Apps/system/TopoloDocs",
6506
+ " token: ${{ steps.app-token.outputs.token || secrets.TOPOLO_CI_REPO_TOKEN }}"
6507
+ ];
6508
+ const cliCheckoutStep = options.appDir === "TopoloCLI" || options.appDir === "TopoloCli" ? [] : [
6509
+ "",
6510
+ " - name: Checkout Topolo CLI",
6511
+ ` if: steps.app-token.outputs.token != '' || env.HAS_TOPOLO_CI_REPO_TOKEN == 'true'`,
6512
+ " uses: actions/checkout@v4",
6513
+ " with:",
6514
+ " repository: Topolo-io/TopoloCli",
6515
+ " path: Apps/system/TopoloCLI",
6516
+ " token: ${{ steps.app-token.outputs.token || secrets.TOPOLO_CI_REPO_TOKEN }}"
6517
+ ];
6518
+ return [
6519
+ "name: Topolo App Conformance",
6520
+ "",
6521
+ "on:",
6522
+ " pull_request:",
6523
+ " push:",
6524
+ " branches:",
6525
+ " - main",
6526
+ " workflow_dispatch:",
6527
+ "",
6528
+ "permissions:",
6529
+ " contents: read",
6530
+ "",
6531
+ "jobs:",
6532
+ " conformance:",
6533
+ " runs-on: ubuntu-latest",
6534
+ " env:",
6535
+ ` HAS_TOPOLO_PACKAGES_APP: \${{ secrets.TOPOLO_PACKAGES_APP_ID != '' && secrets.TOPOLO_PACKAGES_APP_PRIVATE_KEY != '' }}`,
6536
+ ` HAS_TOPOLO_CI_REPO_TOKEN: \${{ secrets.TOPOLO_CI_REPO_TOKEN != '' }}`,
6537
+ " steps:",
6538
+ " - name: Checkout application",
6539
+ " uses: actions/checkout@v4",
6540
+ " with:",
6541
+ ` path: ${appPath}`,
6542
+ "",
6543
+ " - name: Mint Topolo cross-repo token",
6544
+ " id: app-token",
6545
+ ` if: env.HAS_TOPOLO_PACKAGES_APP == 'true'`,
6546
+ " uses: actions/create-github-app-token@v1",
6547
+ " with:",
6548
+ ` app-id: \${{ secrets.TOPOLO_PACKAGES_APP_ID }}`,
6549
+ ` private-key: \${{ secrets.TOPOLO_PACKAGES_APP_PRIVATE_KEY }}`,
6550
+ " owner: Topolo-io",
6551
+ ...docsCheckoutStep,
6552
+ "",
6553
+ " - name: Checkout Topolo SDK",
6554
+ ` if: steps.app-token.outputs.token != '' || env.HAS_TOPOLO_CI_REPO_TOKEN == 'true'`,
6555
+ " uses: actions/checkout@v4",
6556
+ " with:",
6557
+ " repository: Topolo-io/topolo-sdk",
6558
+ " path: Apps/packages/topolo-sdk",
6559
+ " token: ${{ steps.app-token.outputs.token || secrets.TOPOLO_CI_REPO_TOKEN }}",
6560
+ ...cliCheckoutStep,
6561
+ "",
6562
+ " - name: Skip conformance without cross-repo token",
6563
+ ` if: steps.app-token.outputs.token == '' && env.HAS_TOPOLO_CI_REPO_TOKEN != 'true'`,
6564
+ " run: |",
6565
+ ' echo "::warning::Skipping Topolo app conformance because no Topolo cross-repo token is available to this workflow context."',
6566
+ "",
6567
+ " - name: Setup Node.js",
6568
+ ` if: steps.app-token.outputs.token != '' || env.HAS_TOPOLO_CI_REPO_TOKEN == 'true'`,
6569
+ " uses: actions/setup-node@v4",
6570
+ " with:",
6571
+ " node-version: 20",
6572
+ "",
6573
+ " - name: Build Topolo SDK",
6574
+ ` if: steps.app-token.outputs.token != '' || env.HAS_TOPOLO_CI_REPO_TOKEN == 'true'`,
6575
+ " working-directory: Apps/packages/topolo-sdk",
6576
+ " run: |",
6577
+ " npm install --no-audit --no-fund",
6578
+ " npm run build",
6579
+ "",
6580
+ " - name: Build Topolo CLI",
6581
+ ` if: steps.app-token.outputs.token != '' || env.HAS_TOPOLO_CI_REPO_TOKEN == 'true'`,
6582
+ " working-directory: Apps/system/TopoloCLI",
6583
+ " run: |",
6584
+ " npm install --no-audit --no-fund",
6585
+ " npm run build",
6586
+ "",
6587
+ " - name: Run Topolo app conformance audit",
6588
+ ` if: steps.app-token.outputs.token != '' || env.HAS_TOPOLO_CI_REPO_TOKEN == 'true'`,
6589
+ " run: |",
6590
+ ` node Apps/system/TopoloCLI/dist/index.js apps audit ${options.id} --local --repo-root "$GITHUB_WORKSPACE" --fail-on needs_review --json`,
6591
+ "",
6592
+ " - name: Record green CI evidence",
6593
+ ` if: \${{ success() && (steps.app-token.outputs.token != '' || env.HAS_TOPOLO_CI_REPO_TOKEN == 'true') }}`,
6594
+ ` working-directory: ${appPath}`,
6595
+ " run: |",
6596
+ ` node "$GITHUB_WORKSPACE/Apps/system/TopoloCLI/dist/index.js" apps record-evidence ${options.id} --build --typecheck --test --commit "$GITHUB_SHA"`
6597
+ ].join("\n");
6598
+ }
6599
+
6600
+ // src/commands/app-scaffold.ts
6601
+ var CURRENT_VERIFICATION_DATE = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
6602
+ function generateOpaqueServiceId() {
6603
+ return `srv_${randomBytes(9).toString("base64url")}`;
6604
+ }
6605
+ function applicationScaffoldPayload(application, options = {}) {
6606
+ const target = normalizeTarget(options.target);
6607
+ const portfolio = target === "platform" ? normalizePortfolio(options.portfolio) : null;
6608
+ const requestedId = normalizeApplicationId(application);
6609
+ const id = target === "platform" ? normalizeFirstPartyApplicationId(requestedId) : requestedId;
6610
+ const slug = toSlug(id);
6611
+ const catalogApplication = APPLICATIONS[id];
6612
+ const browser = options.browser !== false;
6613
+ const api = options.api ?? target === "platform";
6614
+ const audience = normalizeAudience(
6615
+ options.audience,
6616
+ target === "platform" ? portfolio : null
6617
+ );
6618
+ const tenancy = normalizeTenancy(options.tenancy, audience);
6619
+ const surfaceModel = normalizeSurfaceModel(options.surfaceModel);
6620
+ const containerAppId = normalizeContainerAppId(options.containerAppId, surfaceModel);
6621
+ const profile = normalizeScaffoldProfile(options.profile);
6622
+ const resources = resolveScaffoldResources(profile, options);
6623
+ const ownerType = target === "platform" ? "topolo_first_party" : "third_party";
6624
+ const distributionModel = target === "platform" ? "marketplace" : "organization_internal";
6625
+ const appDir = options.appDir ?? catalogApplication?.appDir ?? (target === "platform" ? `Topolo${toPascalCase(id)}` : `topolo-${slug}`);
6626
+ const name = options.name ?? catalogApplication?.name ?? `topolo-${slug}`;
6627
+ const packageName = options.packageName ?? catalogApplication?.packageName ?? `topolo-${slug}`;
6628
+ const serviceSlug = api ? target === "platform" ? `topolo-${slug}` : id : null;
6629
+ const defaultServiceId = target === "platform" ? generateOpaqueServiceId() : catalogApplication?.services[0] ?? id;
6630
+ const serviceId = api ? options.service ?? defaultServiceId : null;
6631
+ const productionUrl = browser ? options.productionUrl ?? catalogApplication?.productionUrl ?? `https://${slug}.topolo.app` : null;
6632
+ const scopes = scaffoldScopes({ browser, api });
6633
+ const capabilities = scaffoldCapabilities({ browser, api, distributionModel });
6634
+ const ciWorkflowPath = target === "platform" ? ".github/workflows/topolo-conformance.yml" : null;
6635
+ const ciWorkflow = target === "platform" ? topoloConformanceWorkflow({ id, appDir, audience }) : null;
6636
+ const applicationPayload = {
6637
+ id,
6638
+ name,
6639
+ appDir,
6640
+ packageName,
6641
+ productionUrl,
6642
+ serviceSlug,
6643
+ serviceId
6644
+ };
6645
+ return {
6646
+ version: APPLICATION_REQUIREMENTS_VERSION,
6647
+ target,
6648
+ ownerType,
6649
+ portfolio,
6650
+ audience,
6651
+ tenancy,
6652
+ surfaceModel,
6653
+ containerAppId,
6654
+ profile,
6655
+ resources,
6656
+ distributionModel,
6657
+ capabilities,
6658
+ application: applicationPayload,
6659
+ scopes,
6660
+ requirements: APPLICATION_REQUIREMENTS.filter(
6661
+ (requirement) => requirement.appliesTo.some((scope) => scopes.includes(scope))
6662
+ ),
6663
+ packages: scaffoldPackages({ browser, api, target }),
6664
+ files: scaffoldFiles({
6665
+ payload: {
6666
+ ownerType,
6667
+ portfolio,
6668
+ audience,
6669
+ tenancy,
6670
+ surfaceModel,
6671
+ containerAppId,
6672
+ profile,
6673
+ resources,
6674
+ distributionModel,
6675
+ capabilities
6676
+ },
6677
+ application: applicationPayload,
6678
+ target,
6679
+ browser,
6680
+ api,
6681
+ ciWorkflow
6682
+ }),
6683
+ commands: scaffoldCommands({
6684
+ id,
6685
+ target,
6686
+ audience,
6687
+ tenancy,
6688
+ portfolio,
6689
+ profile
6690
+ }),
6691
+ ciWorkflowPath,
6692
+ ciWorkflow
6693
+ };
6694
+ }
6695
+ async function writeApplicationScaffold(payload, options = {}) {
6696
+ const root = resolve2(options.repoRoot ?? process.cwd());
6697
+ const plannedFiles = payload.files.map((file) => ({
6698
+ file,
6699
+ absolutePath: resolveInside(root, file.path)
6700
+ }));
6701
+ const existingFiles = [];
6702
+ for (const planned of plannedFiles) {
6703
+ if (await pathExists(planned.absolutePath)) {
6704
+ existingFiles.push(planned.file.path);
6705
+ }
6706
+ }
6707
+ if (existingFiles.length > 0 && !options.force) {
6708
+ throw new Error(
6709
+ [
6710
+ "Refusing to overwrite existing scaffold files:",
6711
+ ...existingFiles.map((filePath) => `- ${filePath}`),
6712
+ "Pass --force to overwrite these files."
6713
+ ].join("\n")
6714
+ );
6715
+ }
6716
+ const files = [];
6717
+ for (const planned of plannedFiles) {
6718
+ const existed = await pathExists(planned.absolutePath);
6719
+ await mkdir2(dirname3(planned.absolutePath), { recursive: true });
6720
+ await writeFile2(planned.absolutePath, planned.file.content, "utf8");
6721
+ files.push({
6722
+ path: planned.file.path,
6723
+ status: existed ? "overwritten" : "created"
6724
+ });
6725
+ }
6726
+ return { root, files };
6727
+ }
6728
+ function scaffoldScopes(options) {
6729
+ const scopes = ["all"];
6730
+ if (options.browser) scopes.push("browser");
6731
+ if (options.api) {
6732
+ scopes.push("api", "agent_surface");
6733
+ }
6734
+ return scopes;
6735
+ }
6736
+ function scaffoldPackages(options) {
6737
+ if (options.target === "developer") {
6738
+ return options.browser ? ["@topolo/login"] : [];
6739
+ }
6740
+ const packages = /* @__PURE__ */ new Set();
6741
+ if (options.browser) {
6742
+ packages.add("@topolo-io/app-funnel");
6743
+ packages.add("@topolo-io/app-launcher");
6744
+ packages.add("@topolo-io/app-shell");
6745
+ packages.add("@topolo-io/auth-client");
6746
+ packages.add("@topolo-io/ui-kit");
6747
+ }
6748
+ if (options.api) {
6749
+ packages.add("@topolo-io/worker-runtime");
6750
+ packages.add("@topolo/sdk");
6751
+ }
6752
+ return Array.from(packages);
6753
+ }
6754
+ function scaffoldCapabilities(options) {
6755
+ const capabilities = [];
6756
+ if (options.browser) {
6757
+ capabilities.push("login", "launcher");
6758
+ }
6759
+ if (options.distributionModel === "marketplace") capabilities.push("marketplace");
6760
+ if (options.api) capabilities.push("api", "widget");
6761
+ return capabilities;
6762
+ }
6763
+ function scaffoldFiles(options) {
6764
+ const { application, target } = options;
6765
+ const slug = toSlug(application.id);
6766
+ const base = target === "platform" ? firstPartyAppPath(application.appDir, options.payload.audience) : application.appDir;
6767
+ const browserBase = target === "platform" ? `${base}/apps/web` : base;
6768
+ const apiBase = target === "platform" ? `${base}/apps/api` : base;
6769
+ const files = [
6770
+ {
6771
+ path: `${base}/package.json`,
6772
+ purpose: "Package metadata and verification commands.",
6773
+ requiredSignals: [
6774
+ "name matches the packageName in the SDK application catalog",
6775
+ "scripts include build plus typecheck where the stack supports them"
6776
+ ],
6777
+ content: packageJson(application, target, {
6778
+ browser: options.browser,
6779
+ api: options.api,
6780
+ resources: options.payload.resources
6781
+ })
6782
+ },
6783
+ {
6784
+ path: `${base}/README.md`,
6785
+ purpose: "Starter README with install, run, and verification commands.",
6786
+ requiredSignals: [
6787
+ "documents the generated app target",
6788
+ "points maintainers to the shared Topolo packages used by the scaffold"
6789
+ ],
6790
+ content: readme(application, target, {
6791
+ api: options.api,
6792
+ audience: options.payload.audience,
6793
+ tenancy: options.payload.tenancy,
6794
+ portfolio: options.payload.portfolio,
6795
+ profile: options.payload.profile,
6796
+ resources: options.payload.resources
6797
+ })
6798
+ },
6799
+ {
6800
+ path: `${base}/.gitignore`,
6801
+ purpose: "Generated-app ignore rules.",
6802
+ requiredSignals: [
6803
+ "ignores dependency installs and build output",
6804
+ "keeps local environment files out of source control"
6805
+ ],
6806
+ content: gitignore()
6807
+ },
6808
+ {
6809
+ path: `${base}/tsconfig.json`,
6810
+ purpose: "TypeScript configuration for the generated app.",
6811
+ requiredSignals: [
6812
+ "uses strict mode",
6813
+ "includes browser DOM types for React and Worker types for API-backed apps"
6814
+ ],
6815
+ content: tsconfigJson(options.api, target)
6816
+ },
6817
+ {
6818
+ path: `${browserBase}/vite.config.ts`,
6819
+ purpose: "Vite browser build configuration.",
6820
+ requiredSignals: [
6821
+ "builds the authenticated browser surface",
6822
+ "keeps generated apps on the standard React/Vite baseline"
6823
+ ],
6824
+ content: viteConfig(target)
6825
+ },
6826
+ {
6827
+ path: `${browserBase}/index.html`,
6828
+ purpose: "Browser entry document.",
6829
+ requiredSignals: [
6830
+ "mounts the React app at #root",
6831
+ "uses the generated application display name"
6832
+ ],
6833
+ content: indexHtml(application.name)
6834
+ },
6835
+ {
6836
+ path: `${browserBase}/src/main.tsx`,
6837
+ purpose: "React app entrypoint.",
6838
+ requiredSignals: [
6839
+ "renders the generated app component",
6840
+ "imports the generated app stylesheet"
6841
+ ],
6842
+ content: mainTsx()
6843
+ },
6844
+ {
6845
+ path: `${browserBase}/src/env.d.ts`,
6846
+ purpose: "Vite environment variable typing.",
6847
+ requiredSignals: [
6848
+ "loads vite/client import.meta.env types",
6849
+ "declares the Topolo environment variables consumed by the starter"
6850
+ ],
6851
+ content: envDts(options.api)
6852
+ },
6853
+ ...options.payload.resources.workflow ? [
6854
+ {
6855
+ path: `${base}/src/cloudflare-workers.d.ts`,
6856
+ purpose: "Minimal Cloudflare Workflow entrypoint typing for scaffolded Workers.",
6857
+ requiredSignals: [
6858
+ "declares cloudflare:workers WorkflowEntrypoint for TypeScript",
6859
+ "keeps Workflow imports deployable without adding a new dependency"
6860
+ ],
6861
+ content: cloudflareWorkersDts()
6862
+ }
6863
+ ] : [],
6864
+ {
6865
+ path: `${browserBase}/src/styles.css`,
6866
+ purpose: "Minimal app-local styles outside the shared Topolo shell.",
6867
+ requiredSignals: [
6868
+ "does not fork the shared shell chrome",
6869
+ "keeps app-specific layout scoped under generated classes"
6870
+ ],
6871
+ content: stylesCss()
6872
+ }
6873
+ ];
6874
+ if (target === "platform") {
6875
+ files.push(
6876
+ {
6877
+ path: `${base}/topolo.app.json`,
6878
+ purpose: "Canonical app manifest that drives generated Wrangler and CloudControl metadata.",
6879
+ requiredSignals: [
6880
+ "declares web/api targets with canonical names and app-local target directories",
6881
+ "keeps Cloudflare account, domain, service id, and bindings in one manifest source of truth"
6882
+ ],
6883
+ content: topoloAppManifestJson(application, {
6884
+ api: options.api,
6885
+ resources: options.payload.resources,
6886
+ profile: options.payload.profile
6887
+ })
6888
+ },
6889
+ {
6890
+ path: `${base}/topolo.cloudcontrol.json`,
6891
+ purpose: "CloudControl source of truth for deploy targets, routes, bindings, and CI metadata.",
6892
+ requiredSignals: [
6893
+ "production URLs and Cloudflare targets are explicit",
6894
+ "resource bindings and required secrets are declared without secret values"
6895
+ ],
6896
+ content: cloudcontrolJson(application, {
6897
+ api: options.api,
6898
+ audience: options.payload.audience,
6899
+ profile: options.payload.profile,
6900
+ resources: options.payload.resources
6901
+ })
6902
+ },
6903
+ {
6904
+ path: `${base}/.github/workflows/topolo-conformance.yml`,
6905
+ purpose: "PR/push gate that runs the local Topolo app audit with docs evidence.",
6906
+ requiredSignals: [
6907
+ "checks out this repo under its Apps lane directory",
6908
+ "checks out TopoloDocs under Apps/system/TopoloDocs",
6909
+ "checks out topolo-sdk and TopoloCli from main so the gate does not wait for an npm publish",
6910
+ "runs topolo apps audit with --local and --fail-on needs_review"
6911
+ ],
6912
+ content: options.ciWorkflow ?? ""
6913
+ },
6914
+ {
6915
+ path: `Apps/system/TopoloDocs/src/content/systems/topolo-${slug}.json`,
6916
+ purpose: "Canonical system registry entry.",
6917
+ requiredSignals: [
6918
+ `repo_paths includes ${base}`,
6919
+ "doc_paths includes internal handbook coverage",
6920
+ "security_assurance records the auth, tenancy, deployment, and verification boundary"
6921
+ ],
6922
+ content: systemJson(application, {
6923
+ api: options.api,
6924
+ audience: options.payload.audience,
6925
+ tenancy: options.payload.tenancy,
6926
+ portfolio: options.payload.portfolio,
6927
+ profile: options.payload.profile,
6928
+ resources: options.payload.resources
6929
+ })
6930
+ },
6931
+ {
6932
+ path: `Apps/system/TopoloDocs/src/content/internal/apps/${slug}.mdx`,
6933
+ purpose: "Internal operator handbook.",
6934
+ requiredSignals: [
6935
+ "documents ownership, routes, auth boundary, deployment, operations, and verification",
6936
+ "last_verified is updated when behavior changes"
6937
+ ],
6938
+ content: internalDocs(application, {
6939
+ api: options.api,
6940
+ audience: options.payload.audience,
6941
+ tenancy: options.payload.tenancy,
6942
+ portfolio: options.payload.portfolio,
6943
+ profile: options.payload.profile,
6944
+ resources: options.payload.resources
6945
+ })
6946
+ },
6947
+ {
6948
+ path: `Apps/system/TopoloDocs/src/content/public/applications/${slug}.mdx`,
6949
+ purpose: "Public application overview.",
6950
+ requiredSignals: [
6951
+ "documents the product surface without internal-only details",
6952
+ "links back to the canonical system registry entry"
6953
+ ],
6954
+ content: publicDocs(application, {
6955
+ api: options.api,
6956
+ audience: options.payload.audience,
6957
+ tenancy: options.payload.tenancy,
6958
+ portfolio: options.payload.portfolio,
6959
+ profile: options.payload.profile,
6960
+ resources: options.payload.resources
6961
+ })
6962
+ }
6963
+ );
6964
+ if (options.api && application.serviceId) {
6965
+ files.push(
6966
+ {
6967
+ path: `Apps/system/TopoloAuth/catalog/service-manifests/${application.serviceId}.json`,
6968
+ purpose: "Auth service manifest for first-party permissions, role bundles, and API-key scopes.",
6969
+ requiredSignals: [
6970
+ "serviceId matches the generated Worker service id",
6971
+ "permissions, role bundles, and API-key scopes exist before the app is catalog-visible",
6972
+ "systems metadata maps the service back to the generated TopoloDocs entries"
6973
+ ],
6974
+ content: serviceManifestJson(application, {
6975
+ audience: options.payload.audience,
6976
+ tenancy: options.payload.tenancy
6977
+ })
6978
+ }
6979
+ );
6980
+ }
6981
+ } else {
6982
+ files.push(
6983
+ {
6984
+ path: `${base}/topolo.app.json`,
6985
+ purpose: "Third-party Topolo application manifest.",
6986
+ requiredSignals: [
6987
+ "declares the external app id, callback URLs, and requested platform scopes",
6988
+ "does not include internal Topolo CloudControl or canonical-docs ownership"
6989
+ ],
6990
+ content: developerManifest(application, {
6991
+ api: options.api,
6992
+ audience: options.payload.audience,
6993
+ tenancy: options.payload.tenancy,
6994
+ surfaceModel: options.payload.surfaceModel,
6995
+ containerAppId: options.payload.containerAppId,
6996
+ distributionModel: options.payload.distributionModel,
6997
+ capabilities: options.payload.capabilities
6998
+ })
6999
+ },
7000
+ {
7001
+ path: `${base}/.env.example`,
7002
+ purpose: "Developer environment template for local auth configuration.",
7003
+ requiredSignals: [
7004
+ "names the Topolo Auth URL",
7005
+ "keeps OAuth client ids and service ids out of source control by default"
7006
+ ],
7007
+ content: envExample(application)
7008
+ }
7009
+ );
7010
+ }
7011
+ if (options.browser) {
7012
+ if (target === "platform") {
7013
+ files.push({
7014
+ path: `${browserBase}/src/service-identity.ts`,
7015
+ purpose: "Central service identity resolver shared by browser and Worker runtime surfaces.",
7016
+ requiredSignals: [
7017
+ "exports the generated app id, display name, and stable Auth service slug",
7018
+ "requires the provisioned Auth service id from the Worker deployment config"
7019
+ ],
7020
+ content: serviceIdentityTs(application)
7021
+ });
7022
+ }
7023
+ files.push({
7024
+ path: `${browserBase}/src/App.tsx`,
7025
+ purpose: "Authenticated browser surface.",
7026
+ requiredSignals: [
7027
+ "bootstraps routes, auth provider, and shared shell through @topolo-io/app-shell",
7028
+ "uses @topolo-io/auth-client through the app-shell wrapper for app-origin auth flows",
7029
+ "keeps launcher and account-menu behavior on the shared platform surface",
7030
+ "passes app-specific account actions through accountMenuItems or TopoloAccountMenu"
7031
+ ],
7032
+ content: appTsx(application, target, {
7033
+ audience: options.payload.audience,
7034
+ portfolio: options.payload.portfolio
7035
+ })
7036
+ });
7037
+ }
7038
+ if (options.api) {
7039
+ files.push(
7040
+ {
7041
+ path: `${apiBase}/wrangler.toml`,
7042
+ purpose: "Cloudflare Worker runtime configuration.",
7043
+ requiredSignals: [
7044
+ "bindings match topolo.cloudcontrol.json or topolo.app.json",
7045
+ "secrets are referenced by name only"
7046
+ ],
7047
+ content: target === "platform" ? platformApiWranglerToml(application, options.payload.resources, options.payload.profile) : wranglerToml(application, target, options.payload.resources, options.payload.profile)
7048
+ },
7049
+ {
7050
+ path: `${apiBase}/src/worker.ts`,
7051
+ purpose: "Protected service boundary.",
7052
+ requiredSignals: [
7053
+ "uses @topolo-io/worker-runtime for Worker auth, gatewaying, headers, and request helpers",
7054
+ "derives organization from the credential rather than client-supplied orgId",
7055
+ "emits or propagates X-Topolo-Request-Id and X-Topolo-Client"
7056
+ ],
7057
+ content: workerTs(application, target, {
7058
+ audience: options.payload.audience,
7059
+ tenancy: options.payload.tenancy,
7060
+ profile: options.payload.profile,
7061
+ resources: options.payload.resources
7062
+ })
7063
+ }
7064
+ );
7065
+ if (target === "platform") {
7066
+ files.push(
7067
+ {
7068
+ path: `${apiBase}/src/service-identity.ts`,
7069
+ purpose: "Runtime service identity resolver for the API Worker.",
7070
+ requiredSignals: [
7071
+ "resolves the concrete Auth service id from Worker vars",
7072
+ "keeps the API Worker independent from browser-only import.meta usage"
7073
+ ],
7074
+ content: apiServiceIdentityTs(application)
7075
+ },
7076
+ {
7077
+ path: `${browserBase}/src/worker.ts`,
7078
+ purpose: "Browser Worker that serves assets and forwards API paths to the API Worker binding.",
7079
+ requiredSignals: [
7080
+ "keeps the frontend Worker thin",
7081
+ "routes /api/* to the API service binding instead of owning backend routes locally"
7082
+ ],
7083
+ content: webWorkerTs(application)
7084
+ },
7085
+ {
7086
+ path: `${browserBase}/wrangler.toml`,
7087
+ purpose: "Cloudflare Worker static-assets configuration for the browser surface.",
7088
+ requiredSignals: [
7089
+ "serves Vite assets through a Worker with assets",
7090
+ "binds the frontend Worker to the API Worker for /api/* requests"
7091
+ ],
7092
+ content: platformWebWranglerToml(application)
7093
+ }
7094
+ );
7095
+ }
7096
+ }
7097
+ return files;
7098
+ }
6955
7099
  function topoloAppManifestJson(application, options) {
6956
7100
  const slug = toSlug(application.id);
6957
7101
  const resource = resourceConfig(application, options.profile);
@@ -7016,8 +7160,8 @@ function topoloAppManifestJson(application, options) {
7016
7160
  production: "https://auth.topolo.app"
7017
7161
  },
7018
7162
  NEXUS_GATEWAY_URL: {
7019
- staging: "https://nexus-api.stg.topolo.us",
7020
- production: "https://nexus-api.topolo.app"
7163
+ staging: "https://nexus.stg.topolo.us",
7164
+ production: "https://nexus.topolo.app"
7021
7165
  },
7022
7166
  NEXUS_APP_ID: slug
7023
7167
  },
@@ -7438,7 +7582,7 @@ function internalDocs(application, options) {
7438
7582
  "",
7439
7583
  "## Architecture",
7440
7584
  "",
7441
- "The generated browser surface in `apps/web` uses `@topolo-io/ui-kit` for the shared landing page, shared login page, auth callback state, and shared signed shell. `apps/web/vite.config.ts` includes the standard React plugin plus React runtime dedupe so linked Topolo packages resolve a single browser React runtime.",
7585
+ "The generated browser surface in `apps/web` uses `@topolo-io/app-funnel` for the shared public landing page, `@topolo-io/app-shell` for shared login, auth callback state, and signed shell, `@topolo-io/app-launcher` for app switching, and `@topolo-io/ui-kit` for shared primitives. `apps/web/vite.config.ts` includes the standard React plugin plus React runtime dedupe so linked Topolo packages resolve a single browser React runtime.",
7442
7586
  options.api ? `The generated app keeps browser service identity in \`${appPath}/apps/web/src/service-identity.ts\` and API runtime service identity in \`${appPath}/apps/api/src/service-identity.ts\`. Runtime code reads the provisioned Auth service id from the API Worker \`SERVICE_ID\` binding and exposes it to the browser through the same-origin service-identity route forwarded by the web Worker.` : `The generated app centralizes browser service identity in \`${appPath}/apps/web/src/service-identity.ts\` and reads Auth identity from the app runtime instead of calling Auth for slug lookups.`,
7443
7587
  "",
7444
7588
  "## Runtime Surfaces",
@@ -7720,135 +7864,6 @@ function envExample(application) {
7720
7864
  ""
7721
7865
  ].join("\n");
7722
7866
  }
7723
- function normalizeTarget(value) {
7724
- if (!value) return "platform";
7725
- if (value === "platform" || value === "developer") return value;
7726
- throw new Error("Invalid scaffold target. Use platform or developer.");
7727
- }
7728
- function normalizePortfolio(value) {
7729
- if (!value) return "platform";
7730
- if (value === "platform" || value === "personal") return value;
7731
- throw new Error("Invalid scaffold portfolio. Use platform or personal.");
7732
- }
7733
- function normalizeAudience(value, portfolio) {
7734
- if (!value) {
7735
- return portfolio === "personal" ? "personal" : "business";
7736
- }
7737
- if (value === "business" || value === "personal") {
7738
- return value;
7739
- }
7740
- throw new Error("Invalid scaffold audience. Use business or personal.");
7741
- }
7742
- function normalizeTenancy(value, audience) {
7743
- const resolved = value ?? (audience === "personal" ? "household" : "organization");
7744
- if (resolved !== "organization" && resolved !== "household") {
7745
- throw new Error("Invalid scaffold tenancy. Use organization or household.");
7746
- }
7747
- if (audience === "business" && resolved !== "organization") {
7748
- throw new Error("Business scaffolds must use organization tenancy.");
7749
- }
7750
- if (audience === "personal" && resolved !== "household") {
7751
- throw new Error("Personal scaffolds must use household tenancy.");
7752
- }
7753
- return resolved;
7754
- }
7755
- function normalizeSurfaceModel(value) {
7756
- if (!value) return "web_app";
7757
- if (value === "web_app" || value === "mobile_container" || value === "mobile_module" || value === "messaging_surface") {
7758
- return value;
7759
- }
7760
- throw new Error(
7761
- "Invalid scaffold surface model. Use web_app, mobile_container, mobile_module, or messaging_surface."
7762
- );
7763
- }
7764
- function normalizeScaffoldProfile(value) {
7765
- if (!value) return null;
7766
- if (value === "spatial-assets") return value;
7767
- throw new Error("Invalid scaffold profile. Use spatial-assets.");
7768
- }
7769
- function resolveScaffoldResources(profile, options) {
7770
- 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 };
7771
- return {
7772
- d1: Boolean(profileResources.d1 || options.withD1),
7773
- r2: Boolean(profileResources.r2 || options.withR2),
7774
- queue: Boolean(profileResources.queue || options.withQueue),
7775
- workflow: Boolean(profileResources.workflow || options.withWorkflow),
7776
- containerProcessing: Boolean(profileResources.containerProcessing || options.withContainerProcessing)
7777
- };
7778
- }
7779
- function normalizeContainerAppId(value, surfaceModel) {
7780
- const trimmed = String(value || "").trim();
7781
- if (surfaceModel === "mobile_module") {
7782
- if (!trimmed) {
7783
- throw new Error("Mobile-module scaffolds require --container-app-id.");
7784
- }
7785
- return trimmed;
7786
- }
7787
- return trimmed || null;
7788
- }
7789
- function normalizeApplicationId(application) {
7790
- const raw = application.trim().toLowerCase();
7791
- const normalized = raw.replace(/-/g, "_");
7792
- if (Object.prototype.hasOwnProperty.call(APPLICATIONS, raw)) {
7793
- return raw;
7794
- }
7795
- if (Object.prototype.hasOwnProperty.call(APPLICATIONS, normalized)) {
7796
- return normalized;
7797
- }
7798
- if (/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(raw) || /^[a-z0-9](?:[a-z0-9_]*[a-z0-9])?$/.test(raw)) {
7799
- return normalized;
7800
- }
7801
- throw new Error(
7802
- "Application id must be kebab-case or snake_case using lowercase letters, numbers, hyphens, and underscores."
7803
- );
7804
- }
7805
- function normalizeFirstPartyApplicationId(applicationId) {
7806
- if (applicationId.startsWith("topolo_") && applicationId.length > "topolo_".length) {
7807
- return applicationId.slice("topolo_".length);
7808
- }
7809
- return applicationId;
7810
- }
7811
- function toSlug(value) {
7812
- return value.replace(/_/g, "-");
7813
- }
7814
- function toPascalCase(value) {
7815
- const acronyms = /* @__PURE__ */ new Set(["api", "cli", "crm", "mcp", "mdm", "p2p"]);
7816
- return value.split(/[-_]/).filter(Boolean).map((part) => acronyms.has(part.toLowerCase()) ? part.toUpperCase() : part.slice(0, 1).toUpperCase() + part.slice(1)).join("");
7817
- }
7818
- function toTitle(value) {
7819
- return value.replace(/^topolo[-_\s]*/i, "Topolo ").split(/[-_\s]+/).filter(Boolean).map((part) => part.slice(0, 1).toUpperCase() + part.slice(1)).join(" ");
7820
- }
7821
- function json(value) {
7822
- return JSON.stringify(value, null, 2);
7823
- }
7824
- async function pathExists(path4) {
7825
- try {
7826
- await access(path4, constants.F_OK);
7827
- return true;
7828
- } catch {
7829
- return false;
7830
- }
7831
- }
7832
- function resolveInside(root, filePath) {
7833
- const resolved = resolve(root, filePath);
7834
- const relativePath = relative(root, resolved);
7835
- if (relativePath.startsWith("..") || relativePath === "" || relativePath.startsWith("/")) {
7836
- throw new Error(`Scaffold file path escapes the target root: ${filePath}`);
7837
- }
7838
- return resolved;
7839
- }
7840
- function escapeHtml(value) {
7841
- return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
7842
- }
7843
- function escapeSingleQuoted(value) {
7844
- return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
7845
- }
7846
- function escapeDoubleQuoted(value) {
7847
- return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
7848
- }
7849
- function escapeFrontmatter(value) {
7850
- return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
7851
- }
7852
7867
 
7853
7868
  // src/commands/app-scaffold-provision.ts
7854
7869
  import { readFile as readFile2, writeFile as writeFile3 } from "fs/promises";
@@ -7959,17 +7974,17 @@ async function provisionApplicationScaffold(payload) {
7959
7974
  developersOrigin,
7960
7975
  developersApp: {
7961
7976
  id: appId,
7962
- slug: readString(app.slug),
7963
- listingStatus: readString(app.listingStatus),
7964
- organizationId: readString(app.organizationId)
7977
+ slug: readString2(app.slug),
7978
+ listingStatus: readString2(app.listingStatus),
7979
+ organizationId: readString2(app.organizationId)
7965
7980
  },
7966
7981
  authService: service ? {
7967
7982
  id: readRequiredString(
7968
7983
  service.id,
7969
7984
  "Auth registration did not return a service id."
7970
7985
  ),
7971
- status: readString(service.status),
7972
- baseUrl: readString(service.base_url) ?? readString(service.baseUrl)
7986
+ status: readString2(service.status),
7987
+ baseUrl: readString2(service.base_url) ?? readString2(service.baseUrl)
7973
7988
  } : null,
7974
7989
  authConfig: authConfig ? {
7975
7990
  login: authConfig.login === true,
@@ -8035,7 +8050,7 @@ async function resolveServiceIdBySlug(authOrigin, slug) {
8035
8050
  function pickRecord(value) {
8036
8051
  return value && typeof value === "object" ? value : null;
8037
8052
  }
8038
- function readString(value) {
8053
+ function readString2(value) {
8039
8054
  if (typeof value !== "string") {
8040
8055
  return null;
8041
8056
  }
@@ -8043,7 +8058,7 @@ function readString(value) {
8043
8058
  return trimmed ? trimmed : null;
8044
8059
  }
8045
8060
  function readRequiredString(value, message) {
8046
- const resolved = readString(value);
8061
+ const resolved = readString2(value);
8047
8062
  if (!resolved) {
8048
8063
  throw new Error(message);
8049
8064
  }
@@ -8818,7 +8833,9 @@ function buildRows(catalog, overrides, environment = selectedEnvironment()) {
8818
8833
  effectiveUrl,
8819
8834
  overridden: Boolean(configuredUrl && configuredUrl !== defaultUrl),
8820
8835
  environmentSafe: environment !== "staging" || !effectiveUrl || isStagingUrl(effectiveUrl),
8821
- permissions: [...entry.permissions]
8836
+ permissions: [...entry.permissions],
8837
+ apiKeyResource: entry.apiKeyResource,
8838
+ apiKeyResources: [...entry.apiKeyResources]
8822
8839
  };
8823
8840
  }).sort((left, right) => left.id.localeCompare(right.id));
8824
8841
  }
@@ -8828,12 +8845,13 @@ function formatRows2(rows, environment = selectedEnvironment()) {
8828
8845
  const lines = [];
8829
8846
  lines.push(`Environment: ${environment}`);
8830
8847
  lines.push("");
8831
- lines.push(`${"ID".padEnd(idWidth)} ${"URL".padEnd(urlWidth)} OVERRIDE READY`);
8832
- lines.push(`${"-".repeat(idWidth)} ${"-".repeat(urlWidth)} -------- -----`);
8848
+ lines.push(`${"ID".padEnd(idWidth)} ${"URL".padEnd(urlWidth)} OVERRIDE READY API KEY RESOURCE`);
8849
+ lines.push(`${"-".repeat(idWidth)} ${"-".repeat(urlWidth)} -------- ----- ----------------`);
8833
8850
  for (const r of rows) {
8834
8851
  const effectiveUrl = r.effectiveUrl ?? "-";
8852
+ const apiKeyResource = r.apiKeyResources.length > 0 ? r.apiKeyResources.map((resource) => `${resource.resourceType}:${resource.canonicalResourceType}`).join(",") : "-";
8835
8853
  lines.push(
8836
- `${r.id.padEnd(idWidth)} ${effectiveUrl.padEnd(urlWidth)} ${r.overridden ? "yes" : "".padEnd(8)} ${r.environmentSafe ? "yes" : "no"}`
8854
+ `${r.id.padEnd(idWidth)} ${effectiveUrl.padEnd(urlWidth)} ${r.overridden ? "yes" : "".padEnd(8)} ${r.environmentSafe ? "yes" : "no"} ${apiKeyResource}`
8837
8855
  );
8838
8856
  }
8839
8857
  return lines.join("\n");
@@ -9205,7 +9223,7 @@ function normalizeList(values, fallback = []) {
9205
9223
 
9206
9224
  // src/commands/integrations.ts
9207
9225
  import { readdir, readFile as readFile3, stat } from "fs/promises";
9208
- import { join as join4, relative as relative2 } from "path";
9226
+ import { join as join4, relative as relative3 } from "path";
9209
9227
  function registerIntegrationsCommand(program2) {
9210
9228
  const integrations = program2.command("integrations").description("Generate and validate Topolo integration handoffs.");
9211
9229
  integrations.command("prompt").description("Print an agent prompt for adding a Topolo integration.").argument("<integration>", "Integration prompt id, currently login-with-topolo").option("--framework <framework>", "auto, authjs, express, or generic", "auto").option("--json", "Emit JSON output").action((integration, options) => {
@@ -9370,7 +9388,7 @@ async function collectFiles(repoRoot) {
9370
9388
  const info = await stat(fullPath);
9371
9389
  if (info.size > 25e4) continue;
9372
9390
  result.push({
9373
- path: relative2(root, fullPath),
9391
+ path: relative3(root, fullPath),
9374
9392
  content: await readFile3(fullPath, "utf8")
9375
9393
  });
9376
9394
  }