@topolo/cli 0.1.21 → 0.1.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -2
- package/dist/index.js +1551 -349
- package/package.json +5 -3
package/dist/index.js
CHANGED
|
@@ -9,7 +9,7 @@ import { platform as platform2 } from "os";
|
|
|
9
9
|
import { createInterface } from "readline/promises";
|
|
10
10
|
import { stdin as input } from "process";
|
|
11
11
|
|
|
12
|
-
//
|
|
12
|
+
// ../topolo-sdk/dist/index.js
|
|
13
13
|
function applyAuthHeaders(headers, credential) {
|
|
14
14
|
if (credential.kind === "api_key") {
|
|
15
15
|
headers.set("X-Api-Key", credential.apiKey);
|
|
@@ -95,56 +95,34 @@ var SERVICES = {
|
|
|
95
95
|
nexus: "https://nexus-api.topolo.app",
|
|
96
96
|
notify: "https://notify.topolo.app",
|
|
97
97
|
one: "https://one.topolo.app",
|
|
98
|
-
"one.ai": "https://ai.one.topolo.app",
|
|
99
98
|
p2p: "https://p2p.topolo.app",
|
|
100
99
|
pay: "https://pay.topolo.app",
|
|
101
100
|
people: "https://people.topolo.app",
|
|
101
|
+
pulse: "https://pulse.topolo.app",
|
|
102
102
|
"quro.api": "https://api.ol0.me",
|
|
103
103
|
"quro.redirect": "https://ol0.me",
|
|
104
104
|
roadmapper: "https://roadmapper-api.topolo.app",
|
|
105
105
|
sign: "https://sign.topolo.app",
|
|
106
106
|
social_studio: "https://studio-api.topolo.app",
|
|
107
107
|
socialize: "https://socialize-api.topolo.app",
|
|
108
|
+
spaces: "https://spaces.topolo.app",
|
|
108
109
|
status: "https://status.topolo.app",
|
|
109
110
|
support: "https://support.topolo.app",
|
|
110
111
|
survey: "https://survey.topolo.app",
|
|
111
112
|
voice: "https://voice.topolo.app",
|
|
112
113
|
web: "https://web-api.topolo.app"
|
|
113
114
|
};
|
|
114
|
-
var PLATFORM_SERVICE_IDS = {
|
|
115
|
-
books: "srv_topolo_books",
|
|
116
|
-
bugfix: "srv_topolo_bugfix",
|
|
117
|
-
compose: "srv_topolo_compose",
|
|
118
|
-
consent: "srv_topolo_consent",
|
|
119
|
-
crm: "srv_iCwM4jGXcwlj",
|
|
120
|
-
customer_success: "srv_topolo_success",
|
|
121
|
-
developers: "svc_topolo_developers",
|
|
122
|
-
director: "srv_topolo_director",
|
|
123
|
-
flow: "srv_topolo_flow",
|
|
124
|
-
forms: "srv_topolo_forms",
|
|
125
|
-
home: "srv_topolo_home",
|
|
126
|
-
insights: "srv_topolo_insights",
|
|
127
|
-
inventory: "srv_topolo_inventory",
|
|
128
|
-
localize: "srv_topolo_localize",
|
|
129
|
-
notify: "svc_topolo_notify",
|
|
130
|
-
p2p: "srv_topolo_p2p",
|
|
131
|
-
people: "srv_topolo_people",
|
|
132
|
-
sign: "srv_topolo_sign",
|
|
133
|
-
status: "srv_topolo_status",
|
|
134
|
-
survey: "srv_topolo_survey",
|
|
135
|
-
voice: "srv_topolo_voice"
|
|
136
|
-
};
|
|
137
115
|
var APPLICATIONS = {
|
|
138
116
|
"admin": {
|
|
139
117
|
"id": "admin",
|
|
140
118
|
"appDir": "TopoloAdmin",
|
|
141
|
-
"name": "
|
|
119
|
+
"name": "topolo-admin",
|
|
142
120
|
"packageName": "topolo-admin",
|
|
143
121
|
"productionUrl": "https://admin.topolo.app",
|
|
144
122
|
"services": [],
|
|
145
123
|
"deployTargets": [
|
|
146
124
|
{
|
|
147
|
-
"name": "
|
|
125
|
+
"name": "topolo-admin",
|
|
148
126
|
"kind": "pages",
|
|
149
127
|
"apiUrl": null,
|
|
150
128
|
"domains": [
|
|
@@ -167,7 +145,7 @@ var APPLICATIONS = {
|
|
|
167
145
|
"deployTargets": [
|
|
168
146
|
{
|
|
169
147
|
"name": "topolo-agent",
|
|
170
|
-
"kind": "
|
|
148
|
+
"kind": "worker",
|
|
171
149
|
"apiUrl": null,
|
|
172
150
|
"domains": [
|
|
173
151
|
"agent.topolo.app"
|
|
@@ -211,6 +189,24 @@ var APPLICATIONS = {
|
|
|
211
189
|
}
|
|
212
190
|
]
|
|
213
191
|
},
|
|
192
|
+
"backup": {
|
|
193
|
+
"id": "backup",
|
|
194
|
+
"appDir": "TopoloBackup",
|
|
195
|
+
"name": "topolo-backup",
|
|
196
|
+
"packageName": "topolo-backup",
|
|
197
|
+
"productionUrl": null,
|
|
198
|
+
"services": [],
|
|
199
|
+
"deployTargets": [
|
|
200
|
+
{
|
|
201
|
+
"name": "topolo-backup",
|
|
202
|
+
"kind": "worker",
|
|
203
|
+
"apiUrl": null,
|
|
204
|
+
"domains": [],
|
|
205
|
+
"routes": [],
|
|
206
|
+
"serviceId": null
|
|
207
|
+
}
|
|
208
|
+
]
|
|
209
|
+
},
|
|
214
210
|
"books": {
|
|
215
211
|
"id": "books",
|
|
216
212
|
"appDir": "TopoloBooks",
|
|
@@ -361,15 +357,6 @@ var APPLICATIONS = {
|
|
|
361
357
|
}
|
|
362
358
|
]
|
|
363
359
|
},
|
|
364
|
-
"cli": {
|
|
365
|
-
"id": "cli",
|
|
366
|
-
"appDir": "TopoloCli",
|
|
367
|
-
"name": "@topolo/cli",
|
|
368
|
-
"packageName": "@topolo/cli",
|
|
369
|
-
"productionUrl": null,
|
|
370
|
-
"services": [],
|
|
371
|
-
"deployTargets": []
|
|
372
|
-
},
|
|
373
360
|
"cloudcontrol": {
|
|
374
361
|
"id": "cloudcontrol",
|
|
375
362
|
"appDir": "TopoloCloudControl",
|
|
@@ -501,7 +488,7 @@ var APPLICATIONS = {
|
|
|
501
488
|
},
|
|
502
489
|
{
|
|
503
490
|
"name": "topolo-crm-app",
|
|
504
|
-
"kind": "
|
|
491
|
+
"kind": "worker",
|
|
505
492
|
"apiUrl": null,
|
|
506
493
|
"domains": [
|
|
507
494
|
"crm.topolo.app"
|
|
@@ -535,24 +522,6 @@ var APPLICATIONS = {
|
|
|
535
522
|
}
|
|
536
523
|
]
|
|
537
524
|
},
|
|
538
|
-
"d1_backup": {
|
|
539
|
-
"id": "d1_backup",
|
|
540
|
-
"appDir": "TopoloD1Backup",
|
|
541
|
-
"name": "topolo-d1-backup",
|
|
542
|
-
"packageName": "topolo-d1-backup",
|
|
543
|
-
"productionUrl": null,
|
|
544
|
-
"services": [],
|
|
545
|
-
"deployTargets": [
|
|
546
|
-
{
|
|
547
|
-
"name": "topolo-d1-backup",
|
|
548
|
-
"kind": "worker",
|
|
549
|
-
"apiUrl": null,
|
|
550
|
-
"domains": [],
|
|
551
|
-
"routes": [],
|
|
552
|
-
"serviceId": null
|
|
553
|
-
}
|
|
554
|
-
]
|
|
555
|
-
},
|
|
556
525
|
"developers": {
|
|
557
526
|
"id": "developers",
|
|
558
527
|
"appDir": "TopoloDevelopers",
|
|
@@ -699,7 +668,7 @@ var APPLICATIONS = {
|
|
|
699
668
|
"deployTargets": [
|
|
700
669
|
{
|
|
701
670
|
"name": "topolo-forecast",
|
|
702
|
-
"kind": "
|
|
671
|
+
"kind": "worker",
|
|
703
672
|
"apiUrl": null,
|
|
704
673
|
"domains": [
|
|
705
674
|
"forecast.topolo.app"
|
|
@@ -827,7 +796,7 @@ var APPLICATIONS = {
|
|
|
827
796
|
"deployTargets": [
|
|
828
797
|
{
|
|
829
798
|
"name": "topolo-learn",
|
|
830
|
-
"kind": "
|
|
799
|
+
"kind": "worker",
|
|
831
800
|
"apiUrl": null,
|
|
832
801
|
"domains": [
|
|
833
802
|
"learn.topolo.app"
|
|
@@ -883,7 +852,7 @@ var APPLICATIONS = {
|
|
|
883
852
|
"deployTargets": [
|
|
884
853
|
{
|
|
885
854
|
"name": "topolo-mail",
|
|
886
|
-
"kind": "
|
|
855
|
+
"kind": "worker",
|
|
887
856
|
"apiUrl": "https://mail.topolo.app",
|
|
888
857
|
"domains": [
|
|
889
858
|
"mail.topolo.app"
|
|
@@ -895,15 +864,6 @@ var APPLICATIONS = {
|
|
|
895
864
|
}
|
|
896
865
|
]
|
|
897
866
|
},
|
|
898
|
-
"mcp": {
|
|
899
|
-
"id": "mcp",
|
|
900
|
-
"appDir": "TopoloMCP",
|
|
901
|
-
"name": "@topolo/mcp",
|
|
902
|
-
"packageName": "@topolo/mcp",
|
|
903
|
-
"productionUrl": null,
|
|
904
|
-
"services": [],
|
|
905
|
-
"deployTargets": []
|
|
906
|
-
},
|
|
907
867
|
"mdm": {
|
|
908
868
|
"id": "mdm",
|
|
909
869
|
"appDir": "TopoloMDM",
|
|
@@ -928,7 +888,7 @@ var APPLICATIONS = {
|
|
|
928
888
|
},
|
|
929
889
|
{
|
|
930
890
|
"name": "topolo-mdm-console",
|
|
931
|
-
"kind": "
|
|
891
|
+
"kind": "worker",
|
|
932
892
|
"apiUrl": null,
|
|
933
893
|
"domains": [
|
|
934
894
|
"mdm.topolo.app"
|
|
@@ -952,7 +912,7 @@ var APPLICATIONS = {
|
|
|
952
912
|
"deployTargets": [
|
|
953
913
|
{
|
|
954
914
|
"name": "topolo-messaging",
|
|
955
|
-
"kind": "
|
|
915
|
+
"kind": "worker",
|
|
956
916
|
"apiUrl": "https://messages.topolo.app",
|
|
957
917
|
"domains": [
|
|
958
918
|
"messages.topolo.app"
|
|
@@ -962,7 +922,7 @@ var APPLICATIONS = {
|
|
|
962
922
|
},
|
|
963
923
|
{
|
|
964
924
|
"name": "topolo-messaging-dispatch",
|
|
965
|
-
"kind": "
|
|
925
|
+
"kind": "worker",
|
|
966
926
|
"apiUrl": null,
|
|
967
927
|
"domains": [],
|
|
968
928
|
"routes": [],
|
|
@@ -982,7 +942,7 @@ var APPLICATIONS = {
|
|
|
982
942
|
"deployTargets": [
|
|
983
943
|
{
|
|
984
944
|
"name": "topolo-nexus-dashboard",
|
|
985
|
-
"kind": "
|
|
945
|
+
"kind": "worker",
|
|
986
946
|
"apiUrl": null,
|
|
987
947
|
"domains": [
|
|
988
948
|
"nexus.topolo.app"
|
|
@@ -1038,24 +998,13 @@ var APPLICATIONS = {
|
|
|
1038
998
|
"one": {
|
|
1039
999
|
"id": "one",
|
|
1040
1000
|
"appDir": "TopoloOne",
|
|
1041
|
-
"name": "
|
|
1001
|
+
"name": "TopoloOne",
|
|
1042
1002
|
"packageName": "topolo-oneclick-sso",
|
|
1043
1003
|
"productionUrl": "https://one.topolo.app",
|
|
1044
1004
|
"services": [
|
|
1045
|
-
"one"
|
|
1046
|
-
"one.ai"
|
|
1005
|
+
"one"
|
|
1047
1006
|
],
|
|
1048
1007
|
"deployTargets": [
|
|
1049
|
-
{
|
|
1050
|
-
"name": "ai-chat-rag",
|
|
1051
|
-
"kind": "worker",
|
|
1052
|
-
"apiUrl": "https://ai.one.topolo.app",
|
|
1053
|
-
"domains": [
|
|
1054
|
-
"ai.one.topolo.app"
|
|
1055
|
-
],
|
|
1056
|
-
"routes": [],
|
|
1057
|
-
"serviceId": "one.ai"
|
|
1058
|
-
},
|
|
1059
1008
|
{
|
|
1060
1009
|
"name": "topolo-one",
|
|
1061
1010
|
"kind": "pages",
|
|
@@ -1170,9 +1119,24 @@ var APPLICATIONS = {
|
|
|
1170
1119
|
"appDir": "TopoloPulse",
|
|
1171
1120
|
"name": "topolo-pulse",
|
|
1172
1121
|
"packageName": "topolo-pulse",
|
|
1173
|
-
"productionUrl":
|
|
1174
|
-
"services": [
|
|
1175
|
-
|
|
1122
|
+
"productionUrl": "https://pulse.topolo.app",
|
|
1123
|
+
"services": [
|
|
1124
|
+
"pulse"
|
|
1125
|
+
],
|
|
1126
|
+
"deployTargets": [
|
|
1127
|
+
{
|
|
1128
|
+
"name": "topolo-pulse",
|
|
1129
|
+
"kind": "worker",
|
|
1130
|
+
"apiUrl": "https://pulse.topolo.app",
|
|
1131
|
+
"domains": [
|
|
1132
|
+
"pulse.topolo.app"
|
|
1133
|
+
],
|
|
1134
|
+
"routes": [
|
|
1135
|
+
"pulse.topolo.app"
|
|
1136
|
+
],
|
|
1137
|
+
"serviceId": "pulse"
|
|
1138
|
+
}
|
|
1139
|
+
]
|
|
1176
1140
|
},
|
|
1177
1141
|
"quro": {
|
|
1178
1142
|
"id": "quro",
|
|
@@ -1209,7 +1173,7 @@ var APPLICATIONS = {
|
|
|
1209
1173
|
},
|
|
1210
1174
|
{
|
|
1211
1175
|
"name": "quro-ui",
|
|
1212
|
-
"kind": "
|
|
1176
|
+
"kind": "worker",
|
|
1213
1177
|
"apiUrl": null,
|
|
1214
1178
|
"domains": [
|
|
1215
1179
|
"quro.topolo.app"
|
|
@@ -1231,7 +1195,7 @@ var APPLICATIONS = {
|
|
|
1231
1195
|
"deployTargets": [
|
|
1232
1196
|
{
|
|
1233
1197
|
"name": "topolo-roadmapper",
|
|
1234
|
-
"kind": "
|
|
1198
|
+
"kind": "worker",
|
|
1235
1199
|
"apiUrl": null,
|
|
1236
1200
|
"domains": [
|
|
1237
1201
|
"roadmapper.topolo.app"
|
|
@@ -1287,7 +1251,7 @@ var APPLICATIONS = {
|
|
|
1287
1251
|
"deployTargets": [
|
|
1288
1252
|
{
|
|
1289
1253
|
"name": "topolo-social-studio",
|
|
1290
|
-
"kind": "
|
|
1254
|
+
"kind": "worker",
|
|
1291
1255
|
"apiUrl": null,
|
|
1292
1256
|
"domains": [
|
|
1293
1257
|
"studio.topolo.app"
|
|
@@ -1355,6 +1319,30 @@ var APPLICATIONS = {
|
|
|
1355
1319
|
}
|
|
1356
1320
|
]
|
|
1357
1321
|
},
|
|
1322
|
+
"spaces": {
|
|
1323
|
+
"id": "spaces",
|
|
1324
|
+
"appDir": "TopoloSpaces",
|
|
1325
|
+
"name": "topolo-spaces",
|
|
1326
|
+
"packageName": "@topolo/spaces",
|
|
1327
|
+
"productionUrl": "https://spaces.topolo.app",
|
|
1328
|
+
"services": [
|
|
1329
|
+
"spaces"
|
|
1330
|
+
],
|
|
1331
|
+
"deployTargets": [
|
|
1332
|
+
{
|
|
1333
|
+
"name": "topolo-spaces",
|
|
1334
|
+
"kind": "worker",
|
|
1335
|
+
"apiUrl": "https://spaces.topolo.app",
|
|
1336
|
+
"domains": [
|
|
1337
|
+
"spaces.topolo.app"
|
|
1338
|
+
],
|
|
1339
|
+
"routes": [
|
|
1340
|
+
"spaces.topolo.app"
|
|
1341
|
+
],
|
|
1342
|
+
"serviceId": "spaces"
|
|
1343
|
+
}
|
|
1344
|
+
]
|
|
1345
|
+
},
|
|
1358
1346
|
"status": {
|
|
1359
1347
|
"id": "status",
|
|
1360
1348
|
"appDir": "TopoloStatus",
|
|
@@ -1391,7 +1379,7 @@ var APPLICATIONS = {
|
|
|
1391
1379
|
"deployTargets": [
|
|
1392
1380
|
{
|
|
1393
1381
|
"name": "topolo-support",
|
|
1394
|
-
"kind": "
|
|
1382
|
+
"kind": "worker",
|
|
1395
1383
|
"apiUrl": "https://support.topolo.app",
|
|
1396
1384
|
"domains": [
|
|
1397
1385
|
"support.topolo.app"
|
|
@@ -1483,7 +1471,7 @@ var APPLICATIONS = {
|
|
|
1483
1471
|
},
|
|
1484
1472
|
{
|
|
1485
1473
|
"name": "topolo-web-studio",
|
|
1486
|
-
"kind": "
|
|
1474
|
+
"kind": "worker",
|
|
1487
1475
|
"apiUrl": null,
|
|
1488
1476
|
"domains": [
|
|
1489
1477
|
"web.topolo.app"
|
|
@@ -1679,14 +1667,15 @@ var APPLICATION_REQUIREMENTS = [
|
|
|
1679
1667
|
];
|
|
1680
1668
|
function applicationRequirementScopes(application) {
|
|
1681
1669
|
const scopes = /* @__PURE__ */ new Set(["all"]);
|
|
1670
|
+
const appId = application.id;
|
|
1682
1671
|
const hasBrowserSurface = Boolean(application.productionUrl) || application.deployTargets.some((target) => target.kind === "pages");
|
|
1683
1672
|
const hasApiSurface = application.services.length > 0 || application.deployTargets.some((target) => Boolean(target.apiUrl));
|
|
1684
1673
|
if (hasBrowserSurface) scopes.add("browser");
|
|
1685
1674
|
if (hasApiSurface) scopes.add("api");
|
|
1686
|
-
if (
|
|
1675
|
+
if (appId === "cli" || appId === "mcp" || appId === "cloudcontrol") {
|
|
1687
1676
|
scopes.add("tooling");
|
|
1688
1677
|
}
|
|
1689
|
-
if (
|
|
1678
|
+
if (appId === "cli" || appId === "mcp" || hasApiSurface) {
|
|
1690
1679
|
scopes.add("agent_surface");
|
|
1691
1680
|
}
|
|
1692
1681
|
return Array.from(scopes);
|
|
@@ -1805,12 +1794,13 @@ function evaluateRequirement(application, requirement) {
|
|
|
1805
1794
|
return finding(requirement, "met", "CloudControl deploy target metadata is present.", evidence);
|
|
1806
1795
|
}
|
|
1807
1796
|
case "agent-and-operator-surfaces": {
|
|
1797
|
+
const appId = application.id;
|
|
1808
1798
|
if (application.services.length > 0) {
|
|
1809
1799
|
evidence.push(`services=${application.services.join(",")}`);
|
|
1810
1800
|
return finding(requirement, "partial", "SDK service discovery exists; typed SDK/CLI/MCP workflow coverage should be checked as APIs stabilize.", evidence);
|
|
1811
1801
|
}
|
|
1812
|
-
if (
|
|
1813
|
-
evidence.push(`tooling=${
|
|
1802
|
+
if (appId === "cli" || appId === "mcp" || appId === "cloudcontrol") {
|
|
1803
|
+
evidence.push(`tooling=${appId}`);
|
|
1814
1804
|
return finding(requirement, "met", "This application is itself an agent/operator surface.", evidence);
|
|
1815
1805
|
}
|
|
1816
1806
|
return finding(requirement, "missing", "No SDK service or tooling surface is registered.", evidence);
|
|
@@ -1881,20 +1871,38 @@ function resolveServiceUrl(service, overrides) {
|
|
|
1881
1871
|
return DEFAULT_SERVICE_URLS[service];
|
|
1882
1872
|
}
|
|
1883
1873
|
var WRITE_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
1884
|
-
var
|
|
1874
|
+
var POSITIVE_CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
1875
|
+
var NEGATIVE_CACHE_TTL_MS = 60 * 1e3;
|
|
1876
|
+
var TopoloClient = class _TopoloClient {
|
|
1885
1877
|
credential;
|
|
1886
1878
|
agent;
|
|
1887
1879
|
serviceUrls;
|
|
1880
|
+
serviceIds;
|
|
1888
1881
|
requireConfirmForWrites;
|
|
1889
1882
|
timeoutMs;
|
|
1890
1883
|
fetchImpl;
|
|
1891
1884
|
debug;
|
|
1885
|
+
/**
|
|
1886
|
+
* Process-wide cache for slug → service-id lookups. Keyed by
|
|
1887
|
+
* `${authBaseUrl}|${slug}` so multiple `TopoloClient` instances pointing at
|
|
1888
|
+
* the same Auth share, but staging vs prod don't poison each other.
|
|
1889
|
+
* Cleared via `TopoloClient.clearServiceIdCache()` if a test needs it.
|
|
1890
|
+
*/
|
|
1891
|
+
static serviceIdCache = /* @__PURE__ */ new Map();
|
|
1892
|
+
/**
|
|
1893
|
+
* Drop the slug→service-id cache. Test-only — production callers should
|
|
1894
|
+
* let the 5-minute TTL expire naturally.
|
|
1895
|
+
*/
|
|
1896
|
+
static clearServiceIdCache() {
|
|
1897
|
+
_TopoloClient.serviceIdCache.clear();
|
|
1898
|
+
}
|
|
1892
1899
|
constructor(options) {
|
|
1893
1900
|
if (!options.credential) throw new TopoloAuthError("credential is required");
|
|
1894
1901
|
if (!options.agent?.clientName) throw new TopoloAuthError("agent.clientName is required");
|
|
1895
1902
|
this.credential = options.credential;
|
|
1896
1903
|
this.agent = options.agent;
|
|
1897
1904
|
this.serviceUrls = options.serviceUrls;
|
|
1905
|
+
this.serviceIds = options.serviceIds;
|
|
1898
1906
|
this.requireConfirmForWrites = options.requireConfirmForWrites !== false;
|
|
1899
1907
|
this.timeoutMs = options.timeoutMs ?? 3e4;
|
|
1900
1908
|
this.fetchImpl = options.fetch ?? fetch;
|
|
@@ -1929,7 +1937,7 @@ var TopoloClient = class {
|
|
|
1929
1937
|
const headers = new Headers();
|
|
1930
1938
|
headers.set("Accept", "application/json");
|
|
1931
1939
|
if (opts.body !== void 0) headers.set("Content-Type", "application/json");
|
|
1932
|
-
const platformServiceId =
|
|
1940
|
+
const platformServiceId = resolveConfiguredServiceId(opts.service, this.serviceIds);
|
|
1933
1941
|
if (platformServiceId) headers.set("X-Service-ID", platformServiceId);
|
|
1934
1942
|
applyAuthHeaders(headers, this.credential);
|
|
1935
1943
|
const requestId = generateRequestId();
|
|
@@ -2003,6 +2011,63 @@ var TopoloClient = class {
|
|
|
2003
2011
|
});
|
|
2004
2012
|
return parsed;
|
|
2005
2013
|
}
|
|
2014
|
+
/**
|
|
2015
|
+
* Resolve a service-slug → opaque service-id via caller config or TopoloAuth.
|
|
2016
|
+
*
|
|
2017
|
+
* Cache: 5 minutes per process per `(slug, auth_base_url)`. Negative
|
|
2018
|
+
* results (404) are cached for 60s so a typo doesn't hot-loop the
|
|
2019
|
+
* resolver. Returns null when the slug isn't registered — callers decide
|
|
2020
|
+
* whether to throw.
|
|
2021
|
+
*
|
|
2022
|
+
* No credential is sent: the endpoint is public (slugs aren't secret).
|
|
2023
|
+
* This keeps the resolver usable during early bootstrap before a caller
|
|
2024
|
+
* has acquired an access token.
|
|
2025
|
+
*/
|
|
2026
|
+
async getServiceIdBySlug(slug) {
|
|
2027
|
+
if (typeof slug !== "string") return null;
|
|
2028
|
+
const normalized = slug.trim().toLowerCase();
|
|
2029
|
+
if (!normalized) return null;
|
|
2030
|
+
const configuredServiceId = resolveConfiguredServiceId(normalized, this.serviceIds);
|
|
2031
|
+
if (configuredServiceId) return configuredServiceId;
|
|
2032
|
+
const authBase = resolveServiceUrl("auth", this.serviceUrls);
|
|
2033
|
+
const cacheKey = `${authBase}|${normalized}`;
|
|
2034
|
+
const now = Date.now();
|
|
2035
|
+
const cached = _TopoloClient.serviceIdCache.get(cacheKey);
|
|
2036
|
+
if (cached && cached.expiresAt > now) {
|
|
2037
|
+
return cached.value;
|
|
2038
|
+
}
|
|
2039
|
+
const url = new URL(
|
|
2040
|
+
`/api/services/by-slug/${encodeURIComponent(normalized)}`,
|
|
2041
|
+
authBase.endsWith("/") ? authBase : `${authBase}/`
|
|
2042
|
+
).toString();
|
|
2043
|
+
let res;
|
|
2044
|
+
try {
|
|
2045
|
+
res = await this.fetchImpl(url, {
|
|
2046
|
+
method: "GET",
|
|
2047
|
+
headers: { Accept: "application/json" }
|
|
2048
|
+
});
|
|
2049
|
+
} catch {
|
|
2050
|
+
return null;
|
|
2051
|
+
}
|
|
2052
|
+
if (res.status === 404) {
|
|
2053
|
+
_TopoloClient.serviceIdCache.set(cacheKey, {
|
|
2054
|
+
value: null,
|
|
2055
|
+
expiresAt: now + NEGATIVE_CACHE_TTL_MS
|
|
2056
|
+
});
|
|
2057
|
+
return null;
|
|
2058
|
+
}
|
|
2059
|
+
if (!res.ok) {
|
|
2060
|
+
return null;
|
|
2061
|
+
}
|
|
2062
|
+
const body = await res.json().catch(() => null);
|
|
2063
|
+
const serviceId = body?.data?.service_id;
|
|
2064
|
+
const value = typeof serviceId === "string" && serviceId ? serviceId : null;
|
|
2065
|
+
_TopoloClient.serviceIdCache.set(cacheKey, {
|
|
2066
|
+
value,
|
|
2067
|
+
expiresAt: now + POSITIVE_CACHE_TTL_MS
|
|
2068
|
+
});
|
|
2069
|
+
return value;
|
|
2070
|
+
}
|
|
2006
2071
|
/**
|
|
2007
2072
|
* Introspect the current credential. Returns the caller's resolved identity,
|
|
2008
2073
|
* organization, and permission set. Used by CLI `whoami` and by MCP to gate
|
|
@@ -2039,6 +2104,26 @@ var TopoloClient = class {
|
|
|
2039
2104
|
};
|
|
2040
2105
|
}
|
|
2041
2106
|
};
|
|
2107
|
+
function resolveConfiguredServiceId(slug, serviceIds) {
|
|
2108
|
+
for (const serviceKey of serviceKeyCandidates(slug)) {
|
|
2109
|
+
const configured = serviceIds?.[serviceKey];
|
|
2110
|
+
if (typeof configured === "string" && configured.trim()) return configured.trim();
|
|
2111
|
+
const envKey = `TOPOLO_SERVICE_ID_${serviceKey.replace(/\./g, "_").toUpperCase()}`;
|
|
2112
|
+
const envValue = typeof process !== "undefined" ? process.env?.[envKey] : void 0;
|
|
2113
|
+
if (typeof envValue === "string" && envValue.trim()) return envValue.trim();
|
|
2114
|
+
}
|
|
2115
|
+
return null;
|
|
2116
|
+
}
|
|
2117
|
+
function serviceKeyCandidates(slug) {
|
|
2118
|
+
const normalized = slug.trim().toLowerCase();
|
|
2119
|
+
const withoutTopolo = normalized.startsWith("topolo-") ? normalized.slice("topolo-".length) : normalized;
|
|
2120
|
+
return [.../* @__PURE__ */ new Set([
|
|
2121
|
+
normalized,
|
|
2122
|
+
normalized.replace(/-/g, "_"),
|
|
2123
|
+
withoutTopolo,
|
|
2124
|
+
withoutTopolo.replace(/-/g, "_")
|
|
2125
|
+
])];
|
|
2126
|
+
}
|
|
2042
2127
|
function describeError(body, fallback) {
|
|
2043
2128
|
if (body && typeof body === "object") {
|
|
2044
2129
|
const maybe = body;
|
|
@@ -2971,6 +3056,8 @@ import {
|
|
|
2971
3056
|
} from "fs";
|
|
2972
3057
|
import path from "path";
|
|
2973
3058
|
var EVIDENCE_FRESHNESS_MS = 36 * 60 * 60 * 1e3;
|
|
3059
|
+
var REQUIRED_AUTH_CLIENT_VERSION = "1.1.8";
|
|
3060
|
+
var REQUIRED_UI_KIT_VERSION = "0.4.63";
|
|
2974
3061
|
function readEvidenceFile(appRoot) {
|
|
2975
3062
|
const candidate = path.join(appRoot, ".topolo", "evidence.json");
|
|
2976
3063
|
if (!isFile(candidate)) return null;
|
|
@@ -3132,27 +3219,52 @@ function enrichFinding(application, finding2, evidence) {
|
|
|
3132
3219
|
evidence: ["service=auth"]
|
|
3133
3220
|
});
|
|
3134
3221
|
}
|
|
3222
|
+
const usesWorkerRuntime = authEvidence.includes("source=@topolo-io/worker-runtime") || authEvidence.includes("source=topolo-worker-runtime") || authEvidence.includes("source=createTopoloWorkerRuntime");
|
|
3135
3223
|
const usesAuthMiddleware = authEvidence.includes("source=@topolo-io/auth-middleware") || authEvidence.includes("source=topolo-auth-middleware") || authEvidence.includes("source=TopoloAuthMiddleware");
|
|
3136
3224
|
const usesAuthClient = authEvidence.includes("source=@topolo-io/auth-client") || authEvidence.includes("source=topolo-auth-client");
|
|
3137
3225
|
const usesTopoloClient = authEvidence.includes("source=TopoloClient");
|
|
3138
3226
|
const usesAuthValidateFetch = authEvidence.includes("source=auth-validate-fetch");
|
|
3139
|
-
if (
|
|
3227
|
+
if (authEvidence.includes(`dependency-version=@topolo-io/auth-client<${REQUIRED_AUTH_CLIENT_VERSION}`)) {
|
|
3228
|
+
return mergeFinding(finding2, {
|
|
3229
|
+
status: "needs_review",
|
|
3230
|
+
reason: `The app depends on @topolo-io/auth-client below ${REQUIRED_AUTH_CLIENT_VERSION}; public-route auth boot and session-channel fixes require the current shared client.`,
|
|
3231
|
+
evidence: authEvidence,
|
|
3232
|
+
nextActions: [
|
|
3233
|
+
`Update @topolo-io/auth-client to ${REQUIRED_AUTH_CLIENT_VERSION} or the workspace package that resolves to it`,
|
|
3234
|
+
"Regenerate the lockfile without reverting unrelated app changes",
|
|
3235
|
+
"Rerun this audit after the package update"
|
|
3236
|
+
]
|
|
3237
|
+
});
|
|
3238
|
+
}
|
|
3239
|
+
if (authEvidence.includes("auth-boot=blocking-cookie-refresh") && !authEvidence.includes("auth-boot=background-cookie-refresh")) {
|
|
3240
|
+
return mergeFinding(finding2, {
|
|
3241
|
+
status: "needs_review",
|
|
3242
|
+
reason: "The app appears to block all browser routes on auth cookie refresh. Public landing and login routes must render immediately and restore the cookie session in the background.",
|
|
3243
|
+
evidence: authEvidence,
|
|
3244
|
+
nextActions: [
|
|
3245
|
+
"Use authClient.initialize({ cookieRefresh: 'background' }) or initializeSession({ cookieRefresh: 'background' }) on public landing/login routes",
|
|
3246
|
+
"Keep cookieRefresh: 'blocking' for protected workspace routes",
|
|
3247
|
+
"Subscribe to auth:session-updated so background restore updates the visible user state"
|
|
3248
|
+
]
|
|
3249
|
+
});
|
|
3250
|
+
}
|
|
3251
|
+
if (usesWorkerRuntime || usesAuthMiddleware || usesAuthClient || usesTopoloClient || usesAuthValidateFetch) {
|
|
3140
3252
|
return mergeFinding(finding2, {
|
|
3141
3253
|
status: "met",
|
|
3142
|
-
reason: "Canonical Topolo auth wiring is present (auth-middleware verifyRequest, auth-client browser
|
|
3254
|
+
reason: "Canonical Topolo auth wiring is present (worker-runtime, auth-middleware verifyRequest, auth-client browser session controller, or TopoloClient).",
|
|
3143
3255
|
evidence: authEvidence
|
|
3144
3256
|
});
|
|
3145
3257
|
}
|
|
3146
3258
|
return mergeFinding(finding2, {
|
|
3147
3259
|
status: "partial",
|
|
3148
|
-
reason: "Shared Topolo auth dependency is declared but the canonical wiring (verifyRequest /
|
|
3260
|
+
reason: "Shared Topolo auth dependency is declared but the canonical wiring (verifyRequest / createBrowserAuthSessionController / useTopoloAuth / TopoloClient) is not visible.",
|
|
3149
3261
|
evidence: authEvidence
|
|
3150
3262
|
});
|
|
3151
3263
|
}
|
|
3152
3264
|
case "service-registration-and-scopes": {
|
|
3153
3265
|
const declaredServices = application.services ?? [];
|
|
3154
3266
|
if (declaredServices.length === 0) {
|
|
3155
|
-
if (application.id === "
|
|
3267
|
+
if (application.id === "cloudcontrol") {
|
|
3156
3268
|
return mergeFinding(finding2, {
|
|
3157
3269
|
status: "met",
|
|
3158
3270
|
reason: "This application is itself a tooling/agent surface and does not register a callable platform service.",
|
|
@@ -3235,6 +3347,18 @@ function enrichFinding(application, finding2, evidence) {
|
|
|
3235
3347
|
});
|
|
3236
3348
|
}
|
|
3237
3349
|
if (shellEvidence.length === 0) return finding2;
|
|
3350
|
+
if (shellEvidence.includes(`dependency-version=@topolo-io/ui-kit<${REQUIRED_UI_KIT_VERSION}`)) {
|
|
3351
|
+
return mergeFinding(finding2, {
|
|
3352
|
+
status: "needs_review",
|
|
3353
|
+
reason: `The app depends on @topolo-io/ui-kit below ${REQUIRED_UI_KIT_VERSION}; launcher catalog cache/dedupe fixes require the current shared kit.`,
|
|
3354
|
+
evidence: shellEvidence,
|
|
3355
|
+
nextActions: [
|
|
3356
|
+
`Update @topolo-io/ui-kit to ${REQUIRED_UI_KIT_VERSION}`,
|
|
3357
|
+
"Regenerate the lockfile without reverting unrelated app changes",
|
|
3358
|
+
"Rerun this audit after the package update"
|
|
3359
|
+
]
|
|
3360
|
+
});
|
|
3361
|
+
}
|
|
3238
3362
|
if (shellEvidence.includes("forbidden=workspace-shell")) {
|
|
3239
3363
|
return mergeFinding(finding2, {
|
|
3240
3364
|
status: "needs_review",
|
|
@@ -3301,7 +3425,7 @@ function enrichFinding(application, finding2, evidence) {
|
|
|
3301
3425
|
}
|
|
3302
3426
|
const text = evidence.source.text;
|
|
3303
3427
|
const hasOrganizationIdColumn = orgEvidence.includes("source=organization_id") || orgEvidence.includes("source=organizationId") || orgEvidence.includes("source=tenantId") || orgEvidence.includes("source=tenant_id");
|
|
3304
|
-
const hasCredentialBoundary = orgEvidence.includes("source=organization-auth-boundary") || text.includes("verifyRequest") || text.includes("
|
|
3428
|
+
const hasCredentialBoundary = orgEvidence.includes("source=organization-auth-boundary") || text.includes("verifyRequest") || text.includes("createBrowserAuthSessionController") || text.includes("useTopoloAuth");
|
|
3305
3429
|
if (hasOrganizationIdColumn && hasCredentialBoundary) {
|
|
3306
3430
|
return mergeFinding(finding2, {
|
|
3307
3431
|
status: "met",
|
|
@@ -3325,10 +3449,10 @@ function enrichFinding(application, finding2, evidence) {
|
|
|
3325
3449
|
items.push("source=confirm-write-gate");
|
|
3326
3450
|
}
|
|
3327
3451
|
if (text.includes("@topolo/sdk")) items.push("source=@topolo/sdk");
|
|
3328
|
-
if (application.id === "
|
|
3452
|
+
if (application.id === "cloudcontrol") {
|
|
3329
3453
|
return mergeFinding(finding2, {
|
|
3330
3454
|
status: "met",
|
|
3331
|
-
reason: "This application is itself an agent/operator surface (
|
|
3455
|
+
reason: "This application is itself an agent/operator surface (CloudControl).",
|
|
3332
3456
|
evidence: [`tooling=${application.id}`, ...items],
|
|
3333
3457
|
nextActions: []
|
|
3334
3458
|
});
|
|
@@ -3576,15 +3700,24 @@ function sharedAuthEvidence(evidence) {
|
|
|
3576
3700
|
const text = evidence.source.text;
|
|
3577
3701
|
const items = [];
|
|
3578
3702
|
if (deps.has("@topolo-io/auth-client")) items.push("dependency=@topolo-io/auth-client");
|
|
3703
|
+
if (packageDependencyVersions(evidence, "@topolo-io/auth-client").some((version) => isStaleDependencyVersion(version, REQUIRED_AUTH_CLIENT_VERSION))) {
|
|
3704
|
+
items.push(`dependency-version=@topolo-io/auth-client<${REQUIRED_AUTH_CLIENT_VERSION}`);
|
|
3705
|
+
}
|
|
3579
3706
|
if (deps.has("@topolo-io/auth-middleware")) items.push("dependency=@topolo-io/auth-middleware");
|
|
3707
|
+
if (deps.has("@topolo-io/worker-runtime")) items.push("dependency=@topolo-io/worker-runtime");
|
|
3580
3708
|
if (deps.has("@topolo/sdk")) items.push("dependency=@topolo/sdk");
|
|
3581
3709
|
if (text.includes("@topolo-io/auth-client")) items.push("source=@topolo-io/auth-client");
|
|
3582
3710
|
if (text.includes("@topolo-io/auth-middleware")) items.push("source=@topolo-io/auth-middleware");
|
|
3711
|
+
if (text.includes("@topolo-io/worker-runtime")) items.push("source=@topolo-io/worker-runtime");
|
|
3712
|
+
if (text.includes("topolo-worker-runtime")) items.push("source=topolo-worker-runtime");
|
|
3713
|
+
if (text.includes("createTopoloWorkerRuntime")) items.push("source=createTopoloWorkerRuntime");
|
|
3583
3714
|
if (text.includes("topolo-auth-client")) items.push("source=topolo-auth-client");
|
|
3584
3715
|
if (text.includes("topolo-auth-middleware")) items.push("source=topolo-auth-middleware");
|
|
3585
3716
|
if (text.includes("TopoloAuthMiddleware")) items.push("source=TopoloAuthMiddleware");
|
|
3586
3717
|
if (text.includes("TopoloClient")) items.push("source=TopoloClient");
|
|
3587
3718
|
if (text.includes("TopoloOAuth")) items.push("source=TopoloOAuth");
|
|
3719
|
+
if (/\.initialize(?:Session)?\(\s*\)/.test(text)) items.push("auth-boot=blocking-cookie-refresh");
|
|
3720
|
+
if (/cookieRefresh:\s*['"]background['"]/.test(text)) items.push("auth-boot=background-cookie-refresh");
|
|
3588
3721
|
if (/Authorization.*Bearer/i.test(text) && (text.includes("/validate") || text.includes("TOPOLO_AUTH_URL") || text.includes("AUTH_BASE_URL") || text.includes("auth.topolo.app"))) {
|
|
3589
3722
|
items.push("source=auth-validate-fetch");
|
|
3590
3723
|
}
|
|
@@ -3606,6 +3739,9 @@ function sharedShellEvidence(evidence) {
|
|
|
3606
3739
|
const text = evidence.source.text;
|
|
3607
3740
|
const items = [];
|
|
3608
3741
|
if (deps.has("@topolo-io/ui-kit")) items.push("dependency=@topolo-io/ui-kit");
|
|
3742
|
+
if (packageDependencyVersions(evidence, "@topolo-io/ui-kit").some((version) => isStaleDependencyVersion(version, REQUIRED_UI_KIT_VERSION))) {
|
|
3743
|
+
items.push(`dependency-version=@topolo-io/ui-kit<${REQUIRED_UI_KIT_VERSION}`);
|
|
3744
|
+
}
|
|
3609
3745
|
if (text.includes("@topolo-io/ui-kit")) items.push("source=@topolo-io/ui-kit");
|
|
3610
3746
|
if (text.includes("TopoloAppShell")) items.push("source=TopoloAppShell");
|
|
3611
3747
|
if (text.includes(["Topolo", "Workspace", "Shell"].join(""))) items.push("forbidden=workspace-shell");
|
|
@@ -3712,6 +3848,36 @@ function dependencyNames(evidence) {
|
|
|
3712
3848
|
])
|
|
3713
3849
|
);
|
|
3714
3850
|
}
|
|
3851
|
+
function packageDependencyVersions(evidence, packageName) {
|
|
3852
|
+
return unique(
|
|
3853
|
+
evidence.packageJsons.flatMap((record) => {
|
|
3854
|
+
const specs = [
|
|
3855
|
+
record.packageJson.dependencies?.[packageName],
|
|
3856
|
+
record.packageJson.devDependencies?.[packageName],
|
|
3857
|
+
record.packageJson.peerDependencies?.[packageName]
|
|
3858
|
+
];
|
|
3859
|
+
return specs.filter((spec) => typeof spec === "string");
|
|
3860
|
+
})
|
|
3861
|
+
);
|
|
3862
|
+
}
|
|
3863
|
+
function isStaleDependencyVersion(spec, minimumVersion) {
|
|
3864
|
+
if (spec.startsWith("workspace:")) return false;
|
|
3865
|
+
const current = parseDependencyVersion(spec);
|
|
3866
|
+
const minimum = parseDependencyVersion(minimumVersion);
|
|
3867
|
+
if (!current || !minimum) return false;
|
|
3868
|
+
const currentParts = current;
|
|
3869
|
+
const minimumParts = minimum;
|
|
3870
|
+
for (const index of [0, 1, 2]) {
|
|
3871
|
+
if (currentParts[index] < minimumParts[index]) return true;
|
|
3872
|
+
if (currentParts[index] > minimumParts[index]) return false;
|
|
3873
|
+
}
|
|
3874
|
+
return false;
|
|
3875
|
+
}
|
|
3876
|
+
function parseDependencyVersion(spec) {
|
|
3877
|
+
const match = spec.match(/(\d+)\.(\d+)\.(\d+)/);
|
|
3878
|
+
if (!match) return null;
|
|
3879
|
+
return [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
3880
|
+
}
|
|
3715
3881
|
function matchingSystems(repoRoot, application) {
|
|
3716
3882
|
const docsRoot = topoloDocsRoot(repoRoot);
|
|
3717
3883
|
const systemsRoot = path.join(docsRoot, "src", "content", "systems");
|
|
@@ -3889,13 +4055,17 @@ function isFile(filePath) {
|
|
|
3889
4055
|
}
|
|
3890
4056
|
|
|
3891
4057
|
// src/commands/app-scaffold.ts
|
|
4058
|
+
import { randomBytes } from "crypto";
|
|
3892
4059
|
import { constants } from "fs";
|
|
3893
4060
|
import { access, mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
|
|
3894
4061
|
import { dirname as dirname3, relative, resolve } from "path";
|
|
3895
4062
|
var CLOUDFLARE_ACCOUNT_ID = "49ef1ba682ad8cfd720c86699ae17521";
|
|
3896
4063
|
var STAGING_CLOUDFLARE_ACCOUNT_ID = "4f4e1c69a3830946f9fea7b1eb7531ac";
|
|
3897
4064
|
var CLOUDFLARE_COMPATIBILITY_DATE = "2026-05-05";
|
|
3898
|
-
var CURRENT_VERIFICATION_DATE =
|
|
4065
|
+
var CURRENT_VERIFICATION_DATE = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
4066
|
+
function generateOpaqueServiceId() {
|
|
4067
|
+
return `srv_${randomBytes(9).toString("base64url")}`;
|
|
4068
|
+
}
|
|
3899
4069
|
function applicationScaffoldPayload(application, options = {}) {
|
|
3900
4070
|
const target = normalizeTarget(options.target);
|
|
3901
4071
|
const portfolio = target === "platform" ? normalizePortfolio(options.portfolio) : null;
|
|
@@ -3912,23 +4082,28 @@ function applicationScaffoldPayload(application, options = {}) {
|
|
|
3912
4082
|
const tenancy = normalizeTenancy(options.tenancy, audience);
|
|
3913
4083
|
const surfaceModel = normalizeSurfaceModel(options.surfaceModel);
|
|
3914
4084
|
const containerAppId = normalizeContainerAppId(options.containerAppId, surfaceModel);
|
|
4085
|
+
const profile = normalizeScaffoldProfile(options.profile);
|
|
4086
|
+
const resources = resolveScaffoldResources(profile, options);
|
|
3915
4087
|
const ownerType = target === "platform" ? "topolo_first_party" : "third_party";
|
|
3916
4088
|
const distributionModel = target === "platform" ? "marketplace" : "organization_internal";
|
|
3917
4089
|
const appDir = options.appDir ?? catalogApplication?.appDir ?? (target === "platform" ? `Topolo${toPascalCase(id)}` : `topolo-${slug}`);
|
|
3918
4090
|
const name = options.name ?? catalogApplication?.name ?? `topolo-${slug}`;
|
|
3919
4091
|
const packageName = options.packageName ?? catalogApplication?.packageName ?? `topolo-${slug}`;
|
|
3920
|
-
const
|
|
3921
|
-
const
|
|
4092
|
+
const serviceSlug = api ? target === "platform" ? `topolo-${slug}` : id : null;
|
|
4093
|
+
const defaultServiceId = target === "platform" ? generateOpaqueServiceId() : catalogApplication?.services[0] ?? id;
|
|
4094
|
+
const serviceId = api ? options.service ?? defaultServiceId : null;
|
|
3922
4095
|
const productionUrl = browser ? options.productionUrl ?? catalogApplication?.productionUrl ?? `https://${slug}.topolo.app` : null;
|
|
3923
4096
|
const scopes = scaffoldScopes({ browser, api });
|
|
4097
|
+
const capabilities = scaffoldCapabilities({ browser, api, distributionModel });
|
|
3924
4098
|
const ciWorkflowPath = target === "platform" ? ".github/workflows/topolo-conformance.yml" : null;
|
|
3925
|
-
const ciWorkflow = target === "platform" ? topoloConformanceWorkflow({ id, appDir }) : null;
|
|
4099
|
+
const ciWorkflow = target === "platform" ? topoloConformanceWorkflow({ id, appDir, audience }) : null;
|
|
3926
4100
|
const applicationPayload = {
|
|
3927
4101
|
id,
|
|
3928
4102
|
name,
|
|
3929
4103
|
appDir,
|
|
3930
4104
|
packageName,
|
|
3931
4105
|
productionUrl,
|
|
4106
|
+
serviceSlug,
|
|
3932
4107
|
serviceId
|
|
3933
4108
|
};
|
|
3934
4109
|
return {
|
|
@@ -3940,7 +4115,10 @@ function applicationScaffoldPayload(application, options = {}) {
|
|
|
3940
4115
|
tenancy,
|
|
3941
4116
|
surfaceModel,
|
|
3942
4117
|
containerAppId,
|
|
4118
|
+
profile,
|
|
4119
|
+
resources,
|
|
3943
4120
|
distributionModel,
|
|
4121
|
+
capabilities,
|
|
3944
4122
|
application: applicationPayload,
|
|
3945
4123
|
scopes,
|
|
3946
4124
|
requirements: APPLICATION_REQUIREMENTS.filter(
|
|
@@ -3955,7 +4133,10 @@ function applicationScaffoldPayload(application, options = {}) {
|
|
|
3955
4133
|
tenancy,
|
|
3956
4134
|
surfaceModel,
|
|
3957
4135
|
containerAppId,
|
|
3958
|
-
|
|
4136
|
+
profile,
|
|
4137
|
+
resources,
|
|
4138
|
+
distributionModel,
|
|
4139
|
+
capabilities
|
|
3959
4140
|
},
|
|
3960
4141
|
application: applicationPayload,
|
|
3961
4142
|
target,
|
|
@@ -3968,7 +4149,8 @@ function applicationScaffoldPayload(application, options = {}) {
|
|
|
3968
4149
|
target,
|
|
3969
4150
|
audience,
|
|
3970
4151
|
tenancy,
|
|
3971
|
-
portfolio
|
|
4152
|
+
portfolio,
|
|
4153
|
+
profile
|
|
3972
4154
|
}),
|
|
3973
4155
|
ciWorkflowPath,
|
|
3974
4156
|
ciWorkflow
|
|
@@ -4017,7 +4199,7 @@ function scaffoldScopes(options) {
|
|
|
4017
4199
|
}
|
|
4018
4200
|
function scaffoldPackages(options) {
|
|
4019
4201
|
if (options.target === "developer") {
|
|
4020
|
-
return [];
|
|
4202
|
+
return options.browser ? ["@topolo/login"] : [];
|
|
4021
4203
|
}
|
|
4022
4204
|
const packages = /* @__PURE__ */ new Set();
|
|
4023
4205
|
if (options.browser) {
|
|
@@ -4025,15 +4207,30 @@ function scaffoldPackages(options) {
|
|
|
4025
4207
|
packages.add("@topolo-io/auth-client");
|
|
4026
4208
|
}
|
|
4027
4209
|
if (options.api) {
|
|
4028
|
-
packages.add("@topolo-io/
|
|
4210
|
+
packages.add("@topolo-io/worker-runtime");
|
|
4029
4211
|
packages.add("@topolo/sdk");
|
|
4030
4212
|
}
|
|
4031
4213
|
return Array.from(packages);
|
|
4032
4214
|
}
|
|
4215
|
+
function scaffoldCapabilities(options) {
|
|
4216
|
+
const capabilities = [];
|
|
4217
|
+
if (options.browser) {
|
|
4218
|
+
capabilities.push("login", "launcher");
|
|
4219
|
+
}
|
|
4220
|
+
if (options.distributionModel === "marketplace") capabilities.push("marketplace");
|
|
4221
|
+
if (options.api) capabilities.push("api", "widget");
|
|
4222
|
+
return capabilities;
|
|
4223
|
+
}
|
|
4224
|
+
function firstPartyAppPath(appDir, audience) {
|
|
4225
|
+
if (appDir === "TopoloCli" || appDir === "TopoloCLI") return "Apps/system/TopoloCLI";
|
|
4226
|
+
if (appDir === "TopoloDocs") return "Apps/system/TopoloDocs";
|
|
4227
|
+
if (appDir === "TopoloAuth") return "Apps/system/TopoloAuth";
|
|
4228
|
+
return `${audience === "personal" ? "Apps/personal" : "Apps/business"}/${appDir}`;
|
|
4229
|
+
}
|
|
4033
4230
|
function scaffoldFiles(options) {
|
|
4034
4231
|
const { application, target } = options;
|
|
4035
4232
|
const slug = toSlug(application.id);
|
|
4036
|
-
const base = target === "platform" ?
|
|
4233
|
+
const base = target === "platform" ? firstPartyAppPath(application.appDir, options.payload.audience) : application.appDir;
|
|
4037
4234
|
const files = [
|
|
4038
4235
|
{
|
|
4039
4236
|
path: `${base}/package.json`,
|
|
@@ -4042,7 +4239,11 @@ function scaffoldFiles(options) {
|
|
|
4042
4239
|
"name matches the packageName in the SDK application catalog",
|
|
4043
4240
|
"scripts include build plus typecheck where the stack supports them"
|
|
4044
4241
|
],
|
|
4045
|
-
content: packageJson(application, target, {
|
|
4242
|
+
content: packageJson(application, target, {
|
|
4243
|
+
browser: options.browser,
|
|
4244
|
+
api: options.api,
|
|
4245
|
+
resources: options.payload.resources
|
|
4246
|
+
})
|
|
4046
4247
|
},
|
|
4047
4248
|
{
|
|
4048
4249
|
path: `${base}/README.md`,
|
|
@@ -4055,7 +4256,9 @@ function scaffoldFiles(options) {
|
|
|
4055
4256
|
api: options.api,
|
|
4056
4257
|
audience: options.payload.audience,
|
|
4057
4258
|
tenancy: options.payload.tenancy,
|
|
4058
|
-
portfolio: options.payload.portfolio
|
|
4259
|
+
portfolio: options.payload.portfolio,
|
|
4260
|
+
profile: options.payload.profile,
|
|
4261
|
+
resources: options.payload.resources
|
|
4059
4262
|
})
|
|
4060
4263
|
},
|
|
4061
4264
|
{
|
|
@@ -4112,6 +4315,17 @@ function scaffoldFiles(options) {
|
|
|
4112
4315
|
],
|
|
4113
4316
|
content: envDts(options.api)
|
|
4114
4317
|
},
|
|
4318
|
+
...options.payload.resources.workflow ? [
|
|
4319
|
+
{
|
|
4320
|
+
path: `${base}/src/cloudflare-workers.d.ts`,
|
|
4321
|
+
purpose: "Minimal Cloudflare Workflow entrypoint typing for scaffolded Workers.",
|
|
4322
|
+
requiredSignals: [
|
|
4323
|
+
"declares cloudflare:workers WorkflowEntrypoint for TypeScript",
|
|
4324
|
+
"keeps Workflow imports deployable without adding a new dependency"
|
|
4325
|
+
],
|
|
4326
|
+
content: cloudflareWorkersDts()
|
|
4327
|
+
}
|
|
4328
|
+
] : [],
|
|
4115
4329
|
{
|
|
4116
4330
|
path: `${base}/src/styles.css`,
|
|
4117
4331
|
purpose: "Minimal app-local styles outside the shared Topolo shell.",
|
|
@@ -4131,21 +4345,26 @@ function scaffoldFiles(options) {
|
|
|
4131
4345
|
"production URLs and Cloudflare targets are explicit",
|
|
4132
4346
|
"resource bindings and required secrets are declared without secret values"
|
|
4133
4347
|
],
|
|
4134
|
-
content: cloudcontrolJson(application, {
|
|
4348
|
+
content: cloudcontrolJson(application, {
|
|
4349
|
+
api: options.api,
|
|
4350
|
+
audience: options.payload.audience,
|
|
4351
|
+
profile: options.payload.profile,
|
|
4352
|
+
resources: options.payload.resources
|
|
4353
|
+
})
|
|
4135
4354
|
},
|
|
4136
4355
|
{
|
|
4137
4356
|
path: `${base}/.github/workflows/topolo-conformance.yml`,
|
|
4138
4357
|
purpose: "PR/push gate that runs the local Topolo app audit with docs evidence.",
|
|
4139
4358
|
requiredSignals: [
|
|
4140
|
-
"checks out this repo under its
|
|
4141
|
-
"checks out TopoloDocs under
|
|
4359
|
+
"checks out this repo under its Apps lane directory",
|
|
4360
|
+
"checks out TopoloDocs under Apps/system/TopoloDocs",
|
|
4142
4361
|
"checks out topolo-sdk and TopoloCli from main so the gate does not wait for an npm publish",
|
|
4143
4362
|
"runs topolo apps audit with --local and --fail-on needs_review"
|
|
4144
4363
|
],
|
|
4145
4364
|
content: options.ciWorkflow ?? ""
|
|
4146
4365
|
},
|
|
4147
4366
|
{
|
|
4148
|
-
path: `
|
|
4367
|
+
path: `Apps/system/TopoloDocs/src/content/systems/topolo-${slug}.json`,
|
|
4149
4368
|
purpose: "Canonical system registry entry.",
|
|
4150
4369
|
requiredSignals: [
|
|
4151
4370
|
`repo_paths includes ${base}`,
|
|
@@ -4156,11 +4375,13 @@ function scaffoldFiles(options) {
|
|
|
4156
4375
|
api: options.api,
|
|
4157
4376
|
audience: options.payload.audience,
|
|
4158
4377
|
tenancy: options.payload.tenancy,
|
|
4159
|
-
portfolio: options.payload.portfolio
|
|
4378
|
+
portfolio: options.payload.portfolio,
|
|
4379
|
+
profile: options.payload.profile,
|
|
4380
|
+
resources: options.payload.resources
|
|
4160
4381
|
})
|
|
4161
4382
|
},
|
|
4162
4383
|
{
|
|
4163
|
-
path: `
|
|
4384
|
+
path: `Apps/system/TopoloDocs/src/content/internal/apps/${slug}.mdx`,
|
|
4164
4385
|
purpose: "Internal operator handbook.",
|
|
4165
4386
|
requiredSignals: [
|
|
4166
4387
|
"documents ownership, routes, auth boundary, deployment, operations, and verification",
|
|
@@ -4170,11 +4391,13 @@ function scaffoldFiles(options) {
|
|
|
4170
4391
|
api: options.api,
|
|
4171
4392
|
audience: options.payload.audience,
|
|
4172
4393
|
tenancy: options.payload.tenancy,
|
|
4173
|
-
portfolio: options.payload.portfolio
|
|
4394
|
+
portfolio: options.payload.portfolio,
|
|
4395
|
+
profile: options.payload.profile,
|
|
4396
|
+
resources: options.payload.resources
|
|
4174
4397
|
})
|
|
4175
4398
|
},
|
|
4176
4399
|
{
|
|
4177
|
-
path: `
|
|
4400
|
+
path: `Apps/system/TopoloDocs/src/content/public/applications/${slug}.mdx`,
|
|
4178
4401
|
purpose: "Public application overview.",
|
|
4179
4402
|
requiredSignals: [
|
|
4180
4403
|
"documents the product surface without internal-only details",
|
|
@@ -4184,14 +4407,16 @@ function scaffoldFiles(options) {
|
|
|
4184
4407
|
api: options.api,
|
|
4185
4408
|
audience: options.payload.audience,
|
|
4186
4409
|
tenancy: options.payload.tenancy,
|
|
4187
|
-
portfolio: options.payload.portfolio
|
|
4410
|
+
portfolio: options.payload.portfolio,
|
|
4411
|
+
profile: options.payload.profile,
|
|
4412
|
+
resources: options.payload.resources
|
|
4188
4413
|
})
|
|
4189
4414
|
}
|
|
4190
4415
|
);
|
|
4191
4416
|
if (options.api && application.serviceId) {
|
|
4192
4417
|
files.push(
|
|
4193
4418
|
{
|
|
4194
|
-
path: `
|
|
4419
|
+
path: `Apps/system/TopoloAuth/catalog/service-manifests/${application.serviceId}.json`,
|
|
4195
4420
|
purpose: "Auth service manifest for first-party permissions, role bundles, and API-key scopes.",
|
|
4196
4421
|
requiredSignals: [
|
|
4197
4422
|
"serviceId matches the generated Worker service id",
|
|
@@ -4199,6 +4424,7 @@ function scaffoldFiles(options) {
|
|
|
4199
4424
|
"systems metadata maps the service back to the generated TopoloDocs entries"
|
|
4200
4425
|
],
|
|
4201
4426
|
content: serviceManifestJson(application, {
|
|
4427
|
+
audience: options.payload.audience,
|
|
4202
4428
|
tenancy: options.payload.tenancy
|
|
4203
4429
|
})
|
|
4204
4430
|
}
|
|
@@ -4219,7 +4445,8 @@ function scaffoldFiles(options) {
|
|
|
4219
4445
|
tenancy: options.payload.tenancy,
|
|
4220
4446
|
surfaceModel: options.payload.surfaceModel,
|
|
4221
4447
|
containerAppId: options.payload.containerAppId,
|
|
4222
|
-
distributionModel: options.payload.distributionModel
|
|
4448
|
+
distributionModel: options.payload.distributionModel,
|
|
4449
|
+
capabilities: options.payload.capabilities
|
|
4223
4450
|
})
|
|
4224
4451
|
},
|
|
4225
4452
|
{
|
|
@@ -4234,6 +4461,17 @@ function scaffoldFiles(options) {
|
|
|
4234
4461
|
);
|
|
4235
4462
|
}
|
|
4236
4463
|
if (options.browser) {
|
|
4464
|
+
if (target === "platform") {
|
|
4465
|
+
files.push({
|
|
4466
|
+
path: `${base}/src/service-identity.ts`,
|
|
4467
|
+
purpose: "Central service identity resolver shared by browser and Worker runtime surfaces.",
|
|
4468
|
+
requiredSignals: [
|
|
4469
|
+
"exports the generated app id, display name, and stable Auth service slug",
|
|
4470
|
+
"requires the provisioned Auth service id from the Worker deployment config"
|
|
4471
|
+
],
|
|
4472
|
+
content: serviceIdentityTs(application)
|
|
4473
|
+
});
|
|
4474
|
+
}
|
|
4237
4475
|
files.push({
|
|
4238
4476
|
path: `${base}/src/App.tsx`,
|
|
4239
4477
|
purpose: "Authenticated browser surface.",
|
|
@@ -4258,19 +4496,21 @@ function scaffoldFiles(options) {
|
|
|
4258
4496
|
"bindings match topolo.cloudcontrol.json or topolo.app.json",
|
|
4259
4497
|
"secrets are referenced by name only"
|
|
4260
4498
|
],
|
|
4261
|
-
content: wranglerToml(application, target)
|
|
4499
|
+
content: wranglerToml(application, target, options.payload.resources, options.payload.profile)
|
|
4262
4500
|
},
|
|
4263
4501
|
{
|
|
4264
4502
|
path: `${base}/src/worker.ts`,
|
|
4265
4503
|
purpose: "Protected service boundary.",
|
|
4266
4504
|
requiredSignals: [
|
|
4267
|
-
"uses @topolo-io/
|
|
4505
|
+
"uses @topolo-io/worker-runtime for Worker auth, gatewaying, headers, and request helpers",
|
|
4268
4506
|
"derives organization from the credential rather than client-supplied orgId",
|
|
4269
4507
|
"emits or propagates X-Topolo-Request-Id and X-Topolo-Client"
|
|
4270
4508
|
],
|
|
4271
4509
|
content: workerTs(application, target, {
|
|
4272
4510
|
audience: options.payload.audience,
|
|
4273
|
-
tenancy: options.payload.tenancy
|
|
4511
|
+
tenancy: options.payload.tenancy,
|
|
4512
|
+
profile: options.payload.profile,
|
|
4513
|
+
resources: options.payload.resources
|
|
4274
4514
|
})
|
|
4275
4515
|
}
|
|
4276
4516
|
);
|
|
@@ -4290,30 +4530,32 @@ function scaffoldCommands(options) {
|
|
|
4290
4530
|
];
|
|
4291
4531
|
}
|
|
4292
4532
|
const portfolioFlags = options.portfolio === "personal" ? " --portfolio personal" : "";
|
|
4533
|
+
const profileFlags = options.profile ? ` --profile ${options.profile}` : "";
|
|
4293
4534
|
return [
|
|
4294
|
-
`topolo platform apps scaffold ${scaffoldId}${portfolioFlags} --write --provision --repo-root /Users/ashes/Projects/
|
|
4535
|
+
`topolo platform apps scaffold ${scaffoldId}${portfolioFlags}${profileFlags} --write --provision --repo-root /Users/ashes/Projects/Topolo/Platform --json`,
|
|
4295
4536
|
"topolo apps requirements --json",
|
|
4296
|
-
`topolo apps audit ${options.id} --local --repo-root /Users/ashes/Projects/
|
|
4297
|
-
`topolo apps audit ${options.id} --local --repo-root /Users/ashes/Projects/
|
|
4537
|
+
`topolo apps audit ${options.id} --local --repo-root /Users/ashes/Projects/Topolo/Platform --fail-on needs_review --json`,
|
|
4538
|
+
`topolo apps audit ${options.id} --local --repo-root /Users/ashes/Projects/Topolo/Platform --fail-on partial --json`
|
|
4298
4539
|
];
|
|
4299
4540
|
}
|
|
4300
4541
|
function topoloConformanceWorkflow(options) {
|
|
4542
|
+
const appPath = firstPartyAppPath(options.appDir, options.audience);
|
|
4301
4543
|
const docsCheckoutStep = options.appDir === "TopoloDocs" ? [] : [
|
|
4302
4544
|
"",
|
|
4303
4545
|
" - name: Checkout TopoloDocs",
|
|
4304
4546
|
" uses: actions/checkout@v4",
|
|
4305
4547
|
" with:",
|
|
4306
4548
|
" repository: Topolo-io/docs",
|
|
4307
|
-
" path:
|
|
4549
|
+
" path: Apps/system/TopoloDocs",
|
|
4308
4550
|
" token: ${{ secrets.TOPOLO_CI_REPO_TOKEN || github.token }}"
|
|
4309
4551
|
];
|
|
4310
|
-
const cliCheckoutStep = options.appDir === "TopoloCli" ? [] : [
|
|
4552
|
+
const cliCheckoutStep = options.appDir === "TopoloCLI" || options.appDir === "TopoloCli" ? [] : [
|
|
4311
4553
|
"",
|
|
4312
4554
|
" - name: Checkout Topolo CLI",
|
|
4313
4555
|
" uses: actions/checkout@v4",
|
|
4314
4556
|
" with:",
|
|
4315
4557
|
" repository: Topolo-io/TopoloCli",
|
|
4316
|
-
" path:
|
|
4558
|
+
" path: Apps/system/TopoloCLI",
|
|
4317
4559
|
" token: ${{ secrets.TOPOLO_CI_REPO_TOKEN || github.token }}"
|
|
4318
4560
|
];
|
|
4319
4561
|
return [
|
|
@@ -4336,14 +4578,14 @@ function topoloConformanceWorkflow(options) {
|
|
|
4336
4578
|
" - name: Checkout application",
|
|
4337
4579
|
" uses: actions/checkout@v4",
|
|
4338
4580
|
" with:",
|
|
4339
|
-
` path:
|
|
4581
|
+
` path: ${appPath}`,
|
|
4340
4582
|
...docsCheckoutStep,
|
|
4341
4583
|
"",
|
|
4342
4584
|
" - name: Checkout Topolo SDK",
|
|
4343
4585
|
" uses: actions/checkout@v4",
|
|
4344
4586
|
" with:",
|
|
4345
4587
|
" repository: Topolo-io/topolo-sdk",
|
|
4346
|
-
" path:
|
|
4588
|
+
" path: Apps/packages/topolo-sdk",
|
|
4347
4589
|
" token: ${{ secrets.TOPOLO_CI_REPO_TOKEN || github.token }}",
|
|
4348
4590
|
...cliCheckoutStep,
|
|
4349
4591
|
"",
|
|
@@ -4353,26 +4595,26 @@ function topoloConformanceWorkflow(options) {
|
|
|
4353
4595
|
" node-version: 20",
|
|
4354
4596
|
"",
|
|
4355
4597
|
" - name: Build Topolo SDK",
|
|
4356
|
-
" working-directory:
|
|
4598
|
+
" working-directory: Apps/packages/topolo-sdk",
|
|
4357
4599
|
" run: |",
|
|
4358
4600
|
" npm install --no-audit --no-fund",
|
|
4359
4601
|
" npm run build",
|
|
4360
4602
|
"",
|
|
4361
4603
|
" - name: Build Topolo CLI",
|
|
4362
|
-
" working-directory:
|
|
4604
|
+
" working-directory: Apps/system/TopoloCLI",
|
|
4363
4605
|
" run: |",
|
|
4364
4606
|
" npm install --no-audit --no-fund",
|
|
4365
4607
|
" npm run build",
|
|
4366
4608
|
"",
|
|
4367
4609
|
" - name: Run Topolo app conformance audit",
|
|
4368
4610
|
" run: |",
|
|
4369
|
-
` node
|
|
4611
|
+
` node Apps/system/TopoloCLI/dist/index.js apps audit ${options.id} --local --repo-root "$GITHUB_WORKSPACE" --fail-on needs_review --json`,
|
|
4370
4612
|
"",
|
|
4371
4613
|
" - name: Record green CI evidence",
|
|
4372
4614
|
` if: \${{ success() }}`,
|
|
4373
|
-
` working-directory:
|
|
4615
|
+
` working-directory: ${appPath}`,
|
|
4374
4616
|
" run: |",
|
|
4375
|
-
` node "$GITHUB_WORKSPACE/
|
|
4617
|
+
` node "$GITHUB_WORKSPACE/Apps/system/TopoloCLI/dist/index.js" apps record-evidence ${options.id} --build --typecheck --test --commit "$GITHUB_SHA"`
|
|
4376
4618
|
].join("\n");
|
|
4377
4619
|
}
|
|
4378
4620
|
function packageJson(application, target, options) {
|
|
@@ -4398,20 +4640,27 @@ function packageJson(application, target, options) {
|
|
|
4398
4640
|
dependencies["@topolo-io/auth-client"] = topoloPackageVersion("@topolo-io/auth-client", target);
|
|
4399
4641
|
dependencies["@topolo-io/design-tokens"] = topoloPackageVersion("@topolo-io/design-tokens", target);
|
|
4400
4642
|
dependencies["@topolo-io/ui-kit"] = topoloPackageVersion("@topolo-io/ui-kit", target);
|
|
4643
|
+
} else {
|
|
4644
|
+
dependencies["@topolo/login"] = topoloPackageVersion("@topolo/login", target);
|
|
4401
4645
|
}
|
|
4402
4646
|
}
|
|
4403
4647
|
if (options.api) {
|
|
4404
4648
|
if (target === "platform") {
|
|
4405
|
-
dependencies["@topolo-io/
|
|
4649
|
+
dependencies["@topolo-io/worker-runtime"] = topoloPackageVersion("@topolo-io/worker-runtime", target);
|
|
4650
|
+
dependencies["@topolo-io/widget-server"] = topoloPackageVersion("@topolo-io/widget-server", target);
|
|
4406
4651
|
dependencies["@topolo/sdk"] = topoloPackageVersion("@topolo/sdk", target);
|
|
4407
4652
|
scripts.deploy = `node -e 'console.error("\\nX Bare deploy is disabled. Use deploy:staging or deploy:production.\\n"); process.exit(1)'`;
|
|
4408
4653
|
scripts["deploy:staging"] = "DEPLOY_ENVIRONMENT=staging VITE_TOPOLO_AUTH_URL=https://auth.stg.topolo.us npm run build && wrangler deploy --env staging";
|
|
4409
|
-
scripts["deploy:production"] = "DEPLOY_ENVIRONMENT=production npm run build && wrangler deploy";
|
|
4654
|
+
scripts["deploy:production"] = "DEPLOY_ENVIRONMENT=production VITE_TOPOLO_AUTH_URL=https://auth.topolo.app npm run build && wrangler deploy";
|
|
4410
4655
|
} else {
|
|
4656
|
+
dependencies["@topolo-io/widget-server"] = topoloPackageVersion("@topolo-io/widget-server", target);
|
|
4411
4657
|
scripts.deploy = "npm run build && wrangler deploy";
|
|
4412
4658
|
}
|
|
4413
4659
|
devDependencies.wrangler = "^4.40.2";
|
|
4414
4660
|
}
|
|
4661
|
+
if (target === "platform" && hasScaffoldResources(options.resources)) {
|
|
4662
|
+
scripts["check:deploy-config"] = "wrangler deploy --dry-run --outdir .wrangler/build";
|
|
4663
|
+
}
|
|
4415
4664
|
return `${json({
|
|
4416
4665
|
name: application.packageName,
|
|
4417
4666
|
version: "0.1.0",
|
|
@@ -4425,15 +4674,20 @@ function packageJson(application, target, options) {
|
|
|
4425
4674
|
}
|
|
4426
4675
|
function topoloPackageVersion(packageName, target) {
|
|
4427
4676
|
if (target === "developer") {
|
|
4677
|
+
if (packageName === "@topolo/login") return "^0.1.0";
|
|
4428
4678
|
if (packageName === "@topolo-io/auth-client") return "^1.0.0";
|
|
4679
|
+
if (packageName === "@topolo-io/widget-server") return "^0.1.1";
|
|
4429
4680
|
return "^0.1.0";
|
|
4430
4681
|
}
|
|
4431
4682
|
const platformPackageFolders = {
|
|
4432
4683
|
"@topolo/sdk": "topolo-sdk",
|
|
4684
|
+
"@topolo/login": "topolo-login",
|
|
4433
4685
|
"@topolo-io/auth-client": "topolo-auth-client",
|
|
4434
4686
|
"@topolo-io/auth-middleware": "topolo-auth-middleware",
|
|
4687
|
+
"@topolo-io/worker-runtime": "topolo-worker-runtime",
|
|
4435
4688
|
"@topolo-io/design-tokens": "topolo-design-tokens",
|
|
4436
|
-
"@topolo-io/ui-kit": "topolo-ui-kit"
|
|
4689
|
+
"@topolo-io/ui-kit": "topolo-ui-kit",
|
|
4690
|
+
"@topolo-io/widget-server": "topolo-widget-server"
|
|
4437
4691
|
};
|
|
4438
4692
|
const folder = platformPackageFolders[packageName];
|
|
4439
4693
|
if (!folder) {
|
|
@@ -4441,6 +4695,49 @@ function topoloPackageVersion(packageName, target) {
|
|
|
4441
4695
|
}
|
|
4442
4696
|
return `file:../packages/${folder}`;
|
|
4443
4697
|
}
|
|
4698
|
+
function hasScaffoldResources(resources) {
|
|
4699
|
+
return resources.d1 || resources.r2 || resources.queue || resources.workflow || resources.containerProcessing;
|
|
4700
|
+
}
|
|
4701
|
+
function resourceConfig(application, profile) {
|
|
4702
|
+
const slug = toSlug(application.id);
|
|
4703
|
+
const pascal = toPascalCase(application.id);
|
|
4704
|
+
if (profile === "spatial-assets") {
|
|
4705
|
+
return {
|
|
4706
|
+
d1Binding: "APP_DB",
|
|
4707
|
+
d1ProductionName: `topolo-${slug}-production-db`,
|
|
4708
|
+
d1StagingName: `topolo-${slug}-staging-db`,
|
|
4709
|
+
r2Binding: "SPACES_ASSETS",
|
|
4710
|
+
r2ProductionName: `topolo-${slug}-assets`,
|
|
4711
|
+
r2StagingName: `topolo-${slug}-assets-staging`,
|
|
4712
|
+
queueBinding: "SPACES_PROCESSING_QUEUE",
|
|
4713
|
+
queueProductionName: `topolo-${slug}-processing`,
|
|
4714
|
+
queueStagingName: `topolo-${slug}-processing-staging`,
|
|
4715
|
+
workflowBinding: "SPACES_IMPORT_WORKFLOW",
|
|
4716
|
+
workflowClassName: `${pascal}ImportWorkflow`,
|
|
4717
|
+
workflowProductionName: `topolo-${slug}-import`,
|
|
4718
|
+
workflowStagingName: `topolo-${slug}-import-staging`,
|
|
4719
|
+
containerProductionName: `topolo-${slug}-processor`,
|
|
4720
|
+
containerStagingName: `topolo-${slug}-processor-staging`
|
|
4721
|
+
};
|
|
4722
|
+
}
|
|
4723
|
+
return {
|
|
4724
|
+
d1Binding: "APP_DB",
|
|
4725
|
+
d1ProductionName: `topolo-${slug}-prod`,
|
|
4726
|
+
d1StagingName: `topolo-${slug}-staging`,
|
|
4727
|
+
r2Binding: "APP_ASSETS",
|
|
4728
|
+
r2ProductionName: `topolo-${slug}-assets`,
|
|
4729
|
+
r2StagingName: `topolo-${slug}-assets-staging`,
|
|
4730
|
+
queueBinding: "APP_QUEUE",
|
|
4731
|
+
queueProductionName: `topolo-${slug}-queue`,
|
|
4732
|
+
queueStagingName: `topolo-${slug}-queue-staging`,
|
|
4733
|
+
workflowBinding: "APP_WORKFLOW",
|
|
4734
|
+
workflowClassName: `${pascal}Workflow`,
|
|
4735
|
+
workflowProductionName: `topolo-${slug}-workflow`,
|
|
4736
|
+
workflowStagingName: `topolo-${slug}-workflow-staging`,
|
|
4737
|
+
containerProductionName: `topolo-${slug}-processor`,
|
|
4738
|
+
containerStagingName: `topolo-${slug}-processor-staging`
|
|
4739
|
+
};
|
|
4740
|
+
}
|
|
4444
4741
|
function readme(application, target, options) {
|
|
4445
4742
|
const displayName = toTitle(application.name);
|
|
4446
4743
|
const slug = toSlug(application.id);
|
|
@@ -4453,6 +4750,7 @@ function readme(application, target, options) {
|
|
|
4453
4750
|
`Audience: ${options.audience}`,
|
|
4454
4751
|
`Tenancy: ${options.tenancy}`,
|
|
4455
4752
|
...target === "platform" && options.portfolio ? [`Portfolio: ${options.portfolio}`] : [],
|
|
4753
|
+
...options.profile ? [`Profile: ${options.profile}`] : [],
|
|
4456
4754
|
"",
|
|
4457
4755
|
"## Development",
|
|
4458
4756
|
"",
|
|
@@ -4474,7 +4772,12 @@ function readme(application, target, options) {
|
|
|
4474
4772
|
"- `vite.config.ts` includes the standard React plugin plus React runtime dedupe so linked Topolo packages resolve one browser React runtime."
|
|
4475
4773
|
);
|
|
4476
4774
|
if (options.api) {
|
|
4477
|
-
lines.push("- `@topolo-io/
|
|
4775
|
+
lines.push("- `@topolo-io/worker-runtime` owns Worker auth validation, `/api/auth/*` gatewaying, security headers, request ids, and BugFix reporting.");
|
|
4776
|
+
}
|
|
4777
|
+
if (hasScaffoldResources(options.resources)) {
|
|
4778
|
+
lines.push(
|
|
4779
|
+
"- Generated resource bindings are declared in both `wrangler.toml` and `topolo.cloudcontrol.json`."
|
|
4780
|
+
);
|
|
4478
4781
|
}
|
|
4479
4782
|
} else {
|
|
4480
4783
|
lines.push(
|
|
@@ -4501,6 +4804,15 @@ function readme(application, target, options) {
|
|
|
4501
4804
|
"`topolo.app.json` is the app manifest used to register callback URLs, requested scopes, and local development metadata with Topolo."
|
|
4502
4805
|
);
|
|
4503
4806
|
}
|
|
4807
|
+
if (hasScaffoldResources(options.resources)) {
|
|
4808
|
+
lines.push(
|
|
4809
|
+
"",
|
|
4810
|
+
"## Resource Verification",
|
|
4811
|
+
"",
|
|
4812
|
+
"- Keep CloudControl resource bindings aligned with Wrangler bindings before deploy.",
|
|
4813
|
+
"- Run `npm run check:deploy-config` after changing Worker bindings."
|
|
4814
|
+
);
|
|
4815
|
+
}
|
|
4504
4816
|
return `${lines.join("\n")}
|
|
4505
4817
|
`;
|
|
4506
4818
|
}
|
|
@@ -4551,8 +4863,7 @@ function envDts(api) {
|
|
|
4551
4863
|
" readonly VITE_TOPOLO_AUTH_URL?: string;",
|
|
4552
4864
|
" readonly VITE_TOPOLO_CLIENT_ID?: string;",
|
|
4553
4865
|
" readonly VITE_TOPOLO_REDIRECT_URI?: string;",
|
|
4554
|
-
" readonly VITE_TOPOLO_SCOPES?: string;"
|
|
4555
|
-
" readonly VITE_TOPOLO_SERVICE_ID?: string;"
|
|
4866
|
+
" readonly VITE_TOPOLO_SCOPES?: string;"
|
|
4556
4867
|
];
|
|
4557
4868
|
if (api) {
|
|
4558
4869
|
lines.push(" readonly VITE_TOPOLO_API_URL?: string;");
|
|
@@ -4567,6 +4878,105 @@ function envDts(api) {
|
|
|
4567
4878
|
);
|
|
4568
4879
|
return lines.join("\n");
|
|
4569
4880
|
}
|
|
4881
|
+
function serviceIdentityTs(application) {
|
|
4882
|
+
const appName = toTitle(application.name);
|
|
4883
|
+
const serviceSlug = application.serviceSlug ?? `topolo-${toSlug(application.id)}`;
|
|
4884
|
+
return [
|
|
4885
|
+
`export const SCAFFOLD_APP_ID = '${application.id}';`,
|
|
4886
|
+
`export const SCAFFOLD_APP_NAME = '${escapeSingleQuoted(appName)}';`,
|
|
4887
|
+
`export const SCAFFOLD_SERVICE_SLUG = '${escapeSingleQuoted(serviceSlug)}';`,
|
|
4888
|
+
"",
|
|
4889
|
+
"interface ServiceIdentityEnv {",
|
|
4890
|
+
" SERVICE_ID?: string;",
|
|
4891
|
+
"}",
|
|
4892
|
+
"",
|
|
4893
|
+
"let cachedBrowserServiceId = '';",
|
|
4894
|
+
"",
|
|
4895
|
+
"declare global {",
|
|
4896
|
+
" interface Window {",
|
|
4897
|
+
" __TOPOLO_SCAFFOLD_SERVICE_ID__?: string;",
|
|
4898
|
+
" }",
|
|
4899
|
+
"}",
|
|
4900
|
+
"",
|
|
4901
|
+
"export function getBrowserAuthBaseUrl(): string {",
|
|
4902
|
+
" const defaultAuthUrl =",
|
|
4903
|
+
" typeof window !== 'undefined' && window.location.hostname.endsWith('.stg.topolo.us')",
|
|
4904
|
+
" ? 'https://auth.stg.topolo.us'",
|
|
4905
|
+
" : 'https://auth.topolo.app';",
|
|
4906
|
+
" return (import.meta.env.VITE_TOPOLO_AUTH_URL || defaultAuthUrl).replace(/\\/+$/, '');",
|
|
4907
|
+
"}",
|
|
4908
|
+
"",
|
|
4909
|
+
"export function getAppServiceId(): string {",
|
|
4910
|
+
" const runtimeServiceId =",
|
|
4911
|
+
" typeof window !== 'undefined'",
|
|
4912
|
+
" ? window.__TOPOLO_SCAFFOLD_SERVICE_ID__",
|
|
4913
|
+
" : undefined;",
|
|
4914
|
+
" return (cachedBrowserServiceId || runtimeServiceId || '').trim();",
|
|
4915
|
+
"}",
|
|
4916
|
+
"",
|
|
4917
|
+
"export async function resolveAppServiceId(): Promise<string> {",
|
|
4918
|
+
" const current = getAppServiceId();",
|
|
4919
|
+
" if (current) {",
|
|
4920
|
+
" cachedBrowserServiceId = current;",
|
|
4921
|
+
" return current;",
|
|
4922
|
+
" }",
|
|
4923
|
+
"",
|
|
4924
|
+
" const response = await fetchServiceIdentity();",
|
|
4925
|
+
" const payload = await response.json().catch(() => null);",
|
|
4926
|
+
" const serviceId = payload?.serviceId || payload?.data?.serviceId || payload?.data?.service_id;",
|
|
4927
|
+
" if (typeof serviceId !== 'string' || !serviceId.trim()) {",
|
|
4928
|
+
" throw new Error(`Missing Auth service id for ${SCAFFOLD_SERVICE_SLUG}`);",
|
|
4929
|
+
" }",
|
|
4930
|
+
" cachedBrowserServiceId = serviceId.trim();",
|
|
4931
|
+
" if (typeof window !== 'undefined') {",
|
|
4932
|
+
" window.__TOPOLO_SCAFFOLD_SERVICE_ID__ = cachedBrowserServiceId;",
|
|
4933
|
+
" }",
|
|
4934
|
+
" return cachedBrowserServiceId;",
|
|
4935
|
+
"}",
|
|
4936
|
+
"",
|
|
4937
|
+
"async function fetchServiceIdentity(): Promise<Response> {",
|
|
4938
|
+
" const response = await fetch('/api/service-identity', {",
|
|
4939
|
+
" headers: { Accept: 'application/json' },",
|
|
4940
|
+
" );",
|
|
4941
|
+
" if (!response.ok) {",
|
|
4942
|
+
" throw new Error(`Unable to resolve ${SCAFFOLD_SERVICE_SLUG} service identity`);",
|
|
4943
|
+
" }",
|
|
4944
|
+
" return response;",
|
|
4945
|
+
"}",
|
|
4946
|
+
"",
|
|
4947
|
+
"function requireServiceId(value: unknown): string {",
|
|
4948
|
+
" const serviceId = typeof value === 'string' ? value.trim() : '';",
|
|
4949
|
+
" if (!serviceId) {",
|
|
4950
|
+
" throw new Error(`SERVICE_ID is required for ${SCAFFOLD_SERVICE_SLUG}`);",
|
|
4951
|
+
" }",
|
|
4952
|
+
" return serviceId;",
|
|
4953
|
+
"}",
|
|
4954
|
+
"",
|
|
4955
|
+
"export function resolveRuntimeServiceId(env: ServiceIdentityEnv): Promise<string> {",
|
|
4956
|
+
" return Promise.resolve(requireServiceId(env.SERVICE_ID));",
|
|
4957
|
+
"}",
|
|
4958
|
+
""
|
|
4959
|
+
].join("\n");
|
|
4960
|
+
}
|
|
4961
|
+
function cloudflareWorkersDts() {
|
|
4962
|
+
return [
|
|
4963
|
+
"declare module 'cloudflare:workers' {",
|
|
4964
|
+
" export interface WorkflowEvent<T = unknown> {",
|
|
4965
|
+
" payload: T;",
|
|
4966
|
+
" }",
|
|
4967
|
+
"",
|
|
4968
|
+
" export interface WorkflowStep {",
|
|
4969
|
+
" do<T>(name: string, callback: () => Promise<T> | T): Promise<T>;",
|
|
4970
|
+
" }",
|
|
4971
|
+
"",
|
|
4972
|
+
" export abstract class WorkflowEntrypoint<Env = unknown, Params = unknown> {",
|
|
4973
|
+
" env: Env;",
|
|
4974
|
+
" abstract run(event: Readonly<WorkflowEvent<Params>>, step: WorkflowStep): Promise<unknown>;",
|
|
4975
|
+
" }",
|
|
4976
|
+
"}",
|
|
4977
|
+
""
|
|
4978
|
+
].join("\n");
|
|
4979
|
+
}
|
|
4570
4980
|
function viteConfig(target) {
|
|
4571
4981
|
if (target === "developer") {
|
|
4572
4982
|
return [
|
|
@@ -4744,8 +5154,6 @@ function appTsx(application, target, options) {
|
|
|
4744
5154
|
return platformAppTsx(application, options);
|
|
4745
5155
|
}
|
|
4746
5156
|
function platformAppTsx(application, options) {
|
|
4747
|
-
const appName = toTitle(application.name);
|
|
4748
|
-
const serviceId = application.serviceId ?? application.id;
|
|
4749
5157
|
const isPersonal = options.audience === "personal";
|
|
4750
5158
|
const tagline = isPersonal ? "Topolo personal app" : "Topolo workspace";
|
|
4751
5159
|
const workspaceTitle = isPersonal ? "Personal app" : "Workspace";
|
|
@@ -4755,8 +5163,9 @@ function platformAppTsx(application, options) {
|
|
|
4755
5163
|
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.";
|
|
4756
5164
|
return [
|
|
4757
5165
|
"import { Component, useEffect, useMemo, useState, type ErrorInfo, type ReactNode } from 'react';",
|
|
4758
|
-
"import {
|
|
5166
|
+
"import { createBrowserAuthSessionController, type AuthUser } from '@topolo-io/auth-client';",
|
|
4759
5167
|
"import { LandingPage, LoginPage, TopoloAppShell, TopoloAuthFlowScreen, TopoloLoadingScreen, setupBugReporterErrorCapture, type TopoloAppShellUser } from '@topolo-io/ui-kit';",
|
|
5168
|
+
"import { SCAFFOLD_APP_ID, SCAFFOLD_APP_NAME, SCAFFOLD_SERVICE_SLUG, getBrowserAuthBaseUrl, resolveAppServiceId } from './service-identity';",
|
|
4760
5169
|
"",
|
|
4761
5170
|
"// Wire global error/console capture so BugReporter (enabled in TopoloAppShell below)",
|
|
4762
5171
|
"// can attach the last few errors to any submitted bug report.",
|
|
@@ -4764,16 +5173,11 @@ function platformAppTsx(application, options) {
|
|
|
4764
5173
|
"",
|
|
4765
5174
|
"type NavItem = 'home';",
|
|
4766
5175
|
"type AppRoute = { kind: 'root' } | { kind: 'login' } | { kind: 'callback' };",
|
|
5176
|
+
"type BrowserAuthModule = ReturnType<typeof createBrowserAuthSessionController>;",
|
|
4767
5177
|
"",
|
|
4768
|
-
|
|
4769
|
-
|
|
4770
|
-
|
|
4771
|
-
"const DEFAULT_AUTH_URL =",
|
|
4772
|
-
" typeof window !== 'undefined' && window.location.hostname.endsWith('.stg.topolo.us')",
|
|
4773
|
-
" ? 'https://auth.stg.topolo.us'",
|
|
4774
|
-
" : 'https://auth.topolo.app';",
|
|
4775
|
-
"const AUTH_URL = import.meta.env.VITE_TOPOLO_AUTH_URL ?? DEFAULT_AUTH_URL;",
|
|
4776
|
-
"const SERVICE_ID = import.meta.env.VITE_TOPOLO_SERVICE_ID ?? DEFAULT_SERVICE_ID;",
|
|
5178
|
+
"const APP_ID = SCAFFOLD_APP_ID;",
|
|
5179
|
+
"const APP_NAME = SCAFFOLD_APP_NAME;",
|
|
5180
|
+
"const AUTH_URL = getBrowserAuthBaseUrl();",
|
|
4777
5181
|
"const LANDING_CONFIG = {",
|
|
4778
5182
|
" app_name: APP_NAME,",
|
|
4779
5183
|
` app_tagline: '${escapeSingleQuoted(tagline)}',`,
|
|
@@ -4792,17 +5196,28 @@ function platformAppTsx(application, options) {
|
|
|
4792
5196
|
" authenticated_home_path: '/',",
|
|
4793
5197
|
"};",
|
|
4794
5198
|
"",
|
|
4795
|
-
"
|
|
4796
|
-
"
|
|
4797
|
-
"
|
|
4798
|
-
"
|
|
4799
|
-
"
|
|
4800
|
-
"
|
|
4801
|
-
"
|
|
4802
|
-
"
|
|
5199
|
+
"let activeAuthServiceId = '';",
|
|
5200
|
+
"let activeAuthModule: BrowserAuthModule | null = null;",
|
|
5201
|
+
"",
|
|
5202
|
+
"function getAuthModule(serviceId: string): BrowserAuthModule {",
|
|
5203
|
+
" const normalizedServiceId = serviceId.trim();",
|
|
5204
|
+
" if (!normalizedServiceId) throw new Error(`Missing Auth service id for ${SCAFFOLD_SERVICE_SLUG}`);",
|
|
5205
|
+
" if (!activeAuthModule || activeAuthServiceId !== normalizedServiceId) {",
|
|
5206
|
+
" activeAuthServiceId = normalizedServiceId;",
|
|
5207
|
+
" activeAuthModule = createBrowserAuthSessionController({",
|
|
5208
|
+
" authServiceUrl: AUTH_URL,",
|
|
5209
|
+
" serviceId: normalizedServiceId,",
|
|
5210
|
+
" devSessionKey: `${normalizedServiceId}_session`,",
|
|
5211
|
+
" devSessionStorageEnabled: import.meta.env.DEV,",
|
|
5212
|
+
" callbackPath: '/auth/callback',",
|
|
5213
|
+
" debug: import.meta.env.DEV,",
|
|
5214
|
+
" });",
|
|
5215
|
+
" }",
|
|
5216
|
+
" return activeAuthModule;",
|
|
5217
|
+
"}",
|
|
4803
5218
|
"",
|
|
4804
5219
|
"function getAccessToken(): string | null {",
|
|
4805
|
-
" return
|
|
5220
|
+
" return activeAuthModule?.accessTokenMemory.get() ?? activeAuthModule?.accessTokenSession.get() ?? null;",
|
|
4806
5221
|
"}",
|
|
4807
5222
|
"",
|
|
4808
5223
|
"function toShellUser(user: AuthUser | null): TopoloAppShellUser | null {",
|
|
@@ -4828,14 +5243,37 @@ function platformAppTsx(application, options) {
|
|
|
4828
5243
|
"function AppRoot() {",
|
|
4829
5244
|
" const route = getRoute();",
|
|
4830
5245
|
" const [user, setUser] = useState<AuthUser | null>(null);",
|
|
5246
|
+
" const [serviceId, setServiceId] = useState('');",
|
|
5247
|
+
" const [serviceIdentityError, setServiceIdentityError] = useState('');",
|
|
4831
5248
|
" const [loading, setLoading] = useState(true);",
|
|
4832
5249
|
" const [authError, setAuthError] = useState('');",
|
|
4833
5250
|
" const [loginAttempted, setLoginAttempted] = useState(false);",
|
|
5251
|
+
" const auth = useMemo(() => serviceId ? getAuthModule(serviceId) : null, [serviceId]);",
|
|
5252
|
+
"",
|
|
5253
|
+
" useEffect(() => {",
|
|
5254
|
+
" let active = true;",
|
|
5255
|
+
" setServiceIdentityError('');",
|
|
5256
|
+
" void resolveAppServiceId()",
|
|
5257
|
+
" .then((nextServiceId) => {",
|
|
5258
|
+
" if (active) setServiceId(nextServiceId);",
|
|
5259
|
+
" })",
|
|
5260
|
+
" .catch((error) => {",
|
|
5261
|
+
" if (!active) return;",
|
|
5262
|
+
" setServiceIdentityError(errorMessage(error));",
|
|
5263
|
+
" setLoading(false);",
|
|
5264
|
+
" });",
|
|
5265
|
+
" return () => {",
|
|
5266
|
+
" active = false;",
|
|
5267
|
+
" };",
|
|
5268
|
+
" }, []);",
|
|
4834
5269
|
"",
|
|
4835
5270
|
" useEffect(() => {",
|
|
5271
|
+
" if (!auth) return;",
|
|
4836
5272
|
" let active = true;",
|
|
4837
5273
|
" const syncAuth = async () => {",
|
|
4838
|
-
"
|
|
5274
|
+
" const publicRoute = route.kind === 'root' || route.kind === 'login';",
|
|
5275
|
+
" if (publicRoute) setLoading(false);",
|
|
5276
|
+
" else setLoading(true);",
|
|
4839
5277
|
" setAuthError('');",
|
|
4840
5278
|
"",
|
|
4841
5279
|
" if (route.kind === 'callback') {",
|
|
@@ -4851,7 +5289,9 @@ function platformAppTsx(application, options) {
|
|
|
4851
5289
|
" return;",
|
|
4852
5290
|
" }",
|
|
4853
5291
|
"",
|
|
4854
|
-
" const nextUser = await auth.authClient.initialize(
|
|
5292
|
+
" const nextUser = await auth.authClient.initialize({",
|
|
5293
|
+
" cookieRefresh: publicRoute ? 'background' : 'blocking',",
|
|
5294
|
+
" });",
|
|
4855
5295
|
" if (!active) return;",
|
|
4856
5296
|
" setUser(nextUser);",
|
|
4857
5297
|
" setLoading(false);",
|
|
@@ -4865,7 +5305,17 @@ function platformAppTsx(application, options) {
|
|
|
4865
5305
|
" return () => {",
|
|
4866
5306
|
" active = false;",
|
|
4867
5307
|
" };",
|
|
4868
|
-
" }, [route.kind]);",
|
|
5308
|
+
" }, [auth, route.kind]);",
|
|
5309
|
+
"",
|
|
5310
|
+
" useEffect(() => {",
|
|
5311
|
+
" if (!auth) return;",
|
|
5312
|
+
" const handleSessionUpdated = () => {",
|
|
5313
|
+
" const nextUser = auth.authClient.getHydratedCurrentUser() || auth.authClient.getTokenIdentity();",
|
|
5314
|
+
" if (nextUser) setUser(nextUser);",
|
|
5315
|
+
" };",
|
|
5316
|
+
" window.addEventListener('auth:session-updated', handleSessionUpdated);",
|
|
5317
|
+
" return () => window.removeEventListener('auth:session-updated', handleSessionUpdated);",
|
|
5318
|
+
" }, [auth]);",
|
|
4869
5319
|
"",
|
|
4870
5320
|
" useEffect(() => {",
|
|
4871
5321
|
" if (!loading && user && route.kind === 'login') {",
|
|
@@ -4874,24 +5324,50 @@ function platformAppTsx(application, options) {
|
|
|
4874
5324
|
" }, [loading, route.kind, user]);",
|
|
4875
5325
|
"",
|
|
4876
5326
|
" const shellUser = useMemo(() => toShellUser(user), [user]);",
|
|
5327
|
+
" const activeServiceId = serviceId || SCAFFOLD_SERVICE_SLUG;",
|
|
4877
5328
|
"",
|
|
4878
|
-
" if (
|
|
4879
|
-
" if (
|
|
5329
|
+
" if (!auth) {",
|
|
5330
|
+
" if (serviceIdentityError) {",
|
|
4880
5331
|
" return (",
|
|
4881
|
-
" <
|
|
4882
|
-
" appId={APP_ID}",
|
|
4883
|
-
" serviceId={SERVICE_ID}",
|
|
5332
|
+
" <TopoloAuthFlowScreen",
|
|
4884
5333
|
" appName={APP_NAME}",
|
|
4885
|
-
|
|
5334
|
+
" appId={APP_ID}",
|
|
5335
|
+
" serviceId={SCAFFOLD_SERVICE_SLUG}",
|
|
5336
|
+
' status="error"',
|
|
5337
|
+
' title="Service identity unavailable"',
|
|
5338
|
+
" message={serviceIdentityError}",
|
|
5339
|
+
' actionLabel="Retry"',
|
|
5340
|
+
" onAction={() => window.location.reload()}",
|
|
4886
5341
|
" />",
|
|
4887
5342
|
" );",
|
|
4888
5343
|
" }",
|
|
4889
|
-
"",
|
|
4890
5344
|
" return (",
|
|
4891
|
-
" <
|
|
4892
|
-
"
|
|
5345
|
+
" <TopoloLoadingScreen",
|
|
5346
|
+
" appId={APP_ID}",
|
|
5347
|
+
" serviceId={SCAFFOLD_SERVICE_SLUG}",
|
|
5348
|
+
" appName={APP_NAME}",
|
|
5349
|
+
" subtitle={`Loading ${APP_NAME}...`}",
|
|
5350
|
+
" />",
|
|
5351
|
+
" );",
|
|
5352
|
+
" }",
|
|
5353
|
+
"",
|
|
5354
|
+
" if (route.kind === 'callback') {",
|
|
5355
|
+
" if (loading) {",
|
|
5356
|
+
" return (",
|
|
5357
|
+
" <TopoloLoadingScreen",
|
|
5358
|
+
" appId={APP_ID}",
|
|
5359
|
+
" serviceId={activeServiceId}",
|
|
5360
|
+
" appName={APP_NAME}",
|
|
5361
|
+
' subtitle="Completing secure sign-in..."',
|
|
5362
|
+
" />",
|
|
5363
|
+
" );",
|
|
5364
|
+
" }",
|
|
5365
|
+
"",
|
|
5366
|
+
" return (",
|
|
5367
|
+
" <TopoloAuthFlowScreen",
|
|
5368
|
+
" appName={APP_NAME}",
|
|
4893
5369
|
" appId={APP_ID}",
|
|
4894
|
-
" serviceId={
|
|
5370
|
+
" serviceId={activeServiceId}",
|
|
4895
5371
|
' status="error"',
|
|
4896
5372
|
' title="Authentication failed"',
|
|
4897
5373
|
" message={authError || `We could not complete sign-in for ${APP_NAME}.`}",
|
|
@@ -4905,7 +5381,7 @@ function platformAppTsx(application, options) {
|
|
|
4905
5381
|
" return (",
|
|
4906
5382
|
" <TopoloLoadingScreen",
|
|
4907
5383
|
" appId={APP_ID}",
|
|
4908
|
-
" serviceId={
|
|
5384
|
+
" serviceId={activeServiceId}",
|
|
4909
5385
|
" appName={APP_NAME}",
|
|
4910
5386
|
" subtitle={`Loading ${APP_NAME}...`}",
|
|
4911
5387
|
" />",
|
|
@@ -4916,7 +5392,7 @@ function platformAppTsx(application, options) {
|
|
|
4916
5392
|
" if (route.kind === 'login') {",
|
|
4917
5393
|
" return (",
|
|
4918
5394
|
" <LoginPage",
|
|
4919
|
-
" serviceId={
|
|
5395
|
+
" serviceId={activeServiceId}",
|
|
4920
5396
|
" authApiUrl={AUTH_URL}",
|
|
4921
5397
|
" config={LOGIN_CONFIG}",
|
|
4922
5398
|
" isLoading={loginAttempted}",
|
|
@@ -4943,7 +5419,7 @@ function platformAppTsx(application, options) {
|
|
|
4943
5419
|
"",
|
|
4944
5420
|
" return (",
|
|
4945
5421
|
" <LandingPage",
|
|
4946
|
-
" serviceId={
|
|
5422
|
+
" serviceId={activeServiceId}",
|
|
4947
5423
|
" authApiUrl={AUTH_URL}",
|
|
4948
5424
|
" config={LANDING_CONFIG}",
|
|
4949
5425
|
" onCTAClick={() => window.location.assign('/login')}",
|
|
@@ -4955,7 +5431,7 @@ function platformAppTsx(application, options) {
|
|
|
4955
5431
|
" return (",
|
|
4956
5432
|
" <TopoloLoadingScreen",
|
|
4957
5433
|
" appId={APP_ID}",
|
|
4958
|
-
" serviceId={
|
|
5434
|
+
" serviceId={activeServiceId}",
|
|
4959
5435
|
" appName={APP_NAME}",
|
|
4960
5436
|
` subtitle="Opening your ${escapeDoubleQuoted(signedSurfaceLabel)}..."`,
|
|
4961
5437
|
" />",
|
|
@@ -4965,7 +5441,7 @@ function platformAppTsx(application, options) {
|
|
|
4965
5441
|
" return (",
|
|
4966
5442
|
" <TopoloAppShell<NavItem>",
|
|
4967
5443
|
" appId={APP_ID}",
|
|
4968
|
-
" serviceId={
|
|
5444
|
+
" serviceId={activeServiceId}",
|
|
4969
5445
|
" appName={APP_NAME}",
|
|
4970
5446
|
" user={shellUser}",
|
|
4971
5447
|
" orgId={user.activeContextType === 'organization' ? user.activeContextId ?? user.orgId ?? null : null}",
|
|
@@ -5030,7 +5506,7 @@ function platformAppTsx(application, options) {
|
|
|
5030
5506
|
" <TopoloAuthFlowScreen",
|
|
5031
5507
|
" appName={APP_NAME}",
|
|
5032
5508
|
" appId={APP_ID}",
|
|
5033
|
-
" serviceId={
|
|
5509
|
+
" serviceId={SCAFFOLD_SERVICE_SLUG}",
|
|
5034
5510
|
' status="error"',
|
|
5035
5511
|
" title={APP_NAME + ' hit an unexpected error'}",
|
|
5036
5512
|
" message={this.state.error.message}",
|
|
@@ -5048,19 +5524,21 @@ function developerAppTsx(application) {
|
|
|
5048
5524
|
const defaultClientId = `${toSlug(application.id)}_local_client`;
|
|
5049
5525
|
return [
|
|
5050
5526
|
"import { useEffect, useMemo, useState } from 'react';",
|
|
5527
|
+
"import { createAccountLinkPayload, createTopoloAuthorizationUrl, exchangeTopoloCode, fetchTopoloUserInfo, type TopoloUserInfo } from '@topolo/login';",
|
|
5051
5528
|
"",
|
|
5052
5529
|
"type AuthState = 'ready' | 'missing_client' | 'authorizing' | 'authenticated' | 'error';",
|
|
5053
5530
|
"",
|
|
5054
5531
|
`const APP_NAME = '${escapeSingleQuoted(appName)}';`,
|
|
5055
|
-
"const AUTH_URL = import.meta.env.VITE_TOPOLO_AUTH_URL ?? 'https://auth.topolo.app';",
|
|
5532
|
+
"const AUTH_URL = import.meta.env.VITE_TOPOLO_AUTH_URL ?? import.meta.env.VITE_TOPOLO_AUTH_BASE_URL ?? 'https://auth.topolo.app';",
|
|
5056
5533
|
`const CLIENT_ID = import.meta.env.VITE_TOPOLO_CLIENT_ID || '${defaultClientId}';`,
|
|
5057
5534
|
"const REDIRECT_URI = import.meta.env.VITE_TOPOLO_REDIRECT_URI || `${window.location.origin}/auth/callback`;",
|
|
5058
|
-
"const REQUESTED_SCOPES = (import.meta.env.VITE_TOPOLO_SCOPES || '
|
|
5535
|
+
"const REQUESTED_SCOPES = (import.meta.env.VITE_TOPOLO_SCOPES || 'openid profile email organization:read entitlements:read').split(/[\\s,]+/).filter(Boolean);",
|
|
5059
5536
|
"",
|
|
5060
5537
|
"export default function App() {",
|
|
5061
5538
|
" const [authState, setAuthState] = useState<AuthState>(() => CLIENT_ID ? 'ready' : 'missing_client');",
|
|
5062
5539
|
" const [message, setMessage] = useState<string | null>(null);",
|
|
5063
5540
|
" const [tokenPreview, setTokenPreview] = useState<string | null>(null);",
|
|
5541
|
+
" const [userInfo, setUserInfo] = useState<TopoloUserInfo | null>(null);",
|
|
5064
5542
|
"",
|
|
5065
5543
|
" const callbackCode = useMemo(() => new URLSearchParams(window.location.search).get('code'), []);",
|
|
5066
5544
|
" const callbackError = useMemo(() => new URLSearchParams(window.location.search).get('error'), []);",
|
|
@@ -5075,11 +5553,18 @@ function developerAppTsx(application) {
|
|
|
5075
5553
|
"",
|
|
5076
5554
|
" setAuthState('authorizing');",
|
|
5077
5555
|
" void exchangeAuthorizationCode(callbackCode)",
|
|
5078
|
-
" .then((tokens) => {",
|
|
5556
|
+
" .then(async (tokens) => {",
|
|
5079
5557
|
" sessionStorage.setItem(`${CLIENT_ID}:topolo_access_token`, tokens.access_token);",
|
|
5080
5558
|
" if (tokens.refresh_token) {",
|
|
5081
5559
|
" sessionStorage.setItem(`${CLIENT_ID}:topolo_refresh_token`, tokens.refresh_token);",
|
|
5082
5560
|
" }",
|
|
5561
|
+
" const profile = await fetchTopoloUserInfo({ authBaseUrl: AUTH_URL, accessToken: tokens.access_token });",
|
|
5562
|
+
" const link = createAccountLinkPayload({",
|
|
5563
|
+
" localUserId: profile.email ? `local:${profile.email}` : `local:${profile.sub}`,",
|
|
5564
|
+
" userInfo: profile,",
|
|
5565
|
+
" });",
|
|
5566
|
+
" sessionStorage.setItem(`${CLIENT_ID}:topolo_account_link`, JSON.stringify(link));",
|
|
5567
|
+
" setUserInfo(profile);",
|
|
5083
5568
|
" setTokenPreview(`${tokens.access_token.slice(0, 18)}...`);",
|
|
5084
5569
|
" setAuthState('authenticated');",
|
|
5085
5570
|
" window.history.replaceState({}, document.title, '/');",
|
|
@@ -5099,7 +5584,7 @@ function developerAppTsx(application) {
|
|
|
5099
5584
|
" <p>Set VITE_TOPOLO_CLIENT_ID after registering this app in Topolo Developers.</p>",
|
|
5100
5585
|
" )}",
|
|
5101
5586
|
" {authState === 'authenticated' && (",
|
|
5102
|
-
" <p>Topolo sign-in complete. Access token: {tokenPreview}</p>",
|
|
5587
|
+
" <p>Topolo sign-in complete for {userInfo?.email ?? userInfo?.sub}. Access token: {tokenPreview}</p>",
|
|
5103
5588
|
" )}",
|
|
5104
5589
|
" {authState === 'error' && <p>{message}</p>}",
|
|
5105
5590
|
` <button type="button" disabled={!CLIENT_ID || authState === 'authorizing'} onClick={() => void startTopoloLogin()}>`,
|
|
@@ -5111,21 +5596,15 @@ function developerAppTsx(application) {
|
|
|
5111
5596
|
"}",
|
|
5112
5597
|
"",
|
|
5113
5598
|
"async function startTopoloLogin(): Promise<void> {",
|
|
5114
|
-
" const
|
|
5115
|
-
"
|
|
5116
|
-
"
|
|
5117
|
-
"
|
|
5118
|
-
"
|
|
5119
|
-
"",
|
|
5120
|
-
"
|
|
5121
|
-
"
|
|
5122
|
-
"
|
|
5123
|
-
" authorizeUrl.searchParams.set('redirect_uri', REDIRECT_URI);",
|
|
5124
|
-
" authorizeUrl.searchParams.set('scope', REQUESTED_SCOPES);",
|
|
5125
|
-
" authorizeUrl.searchParams.set('state', state);",
|
|
5126
|
-
" authorizeUrl.searchParams.set('code_challenge', challenge);",
|
|
5127
|
-
" authorizeUrl.searchParams.set('code_challenge_method', 'S256');",
|
|
5128
|
-
" window.location.assign(authorizeUrl.toString());",
|
|
5599
|
+
" const authorization = await createTopoloAuthorizationUrl({",
|
|
5600
|
+
" authBaseUrl: AUTH_URL,",
|
|
5601
|
+
" clientId: CLIENT_ID,",
|
|
5602
|
+
" redirectUri: REDIRECT_URI,",
|
|
5603
|
+
" scopes: REQUESTED_SCOPES,",
|
|
5604
|
+
" });",
|
|
5605
|
+
" sessionStorage.setItem(`${CLIENT_ID}:topolo_pkce_verifier`, authorization.codeVerifier);",
|
|
5606
|
+
" sessionStorage.setItem(`${CLIENT_ID}:topolo_oauth_state`, authorization.state);",
|
|
5607
|
+
" window.location.assign(authorization.url);",
|
|
5129
5608
|
"}",
|
|
5130
5609
|
"",
|
|
5131
5610
|
"async function exchangeAuthorizationCode(code: string): Promise<{ access_token: string; refresh_token?: string }> {",
|
|
@@ -5137,49 +5616,23 @@ function developerAppTsx(application) {
|
|
|
5137
5616
|
" const verifier = sessionStorage.getItem(`${CLIENT_ID}:topolo_pkce_verifier`);",
|
|
5138
5617
|
" if (!verifier) throw new Error('Missing PKCE verifier. Restart sign-in.');",
|
|
5139
5618
|
"",
|
|
5140
|
-
"
|
|
5141
|
-
"
|
|
5142
|
-
"
|
|
5143
|
-
" headers: { 'Content-Type': 'application/json' },",
|
|
5144
|
-
" body: JSON.stringify({",
|
|
5145
|
-
" grant_type: 'authorization_code',",
|
|
5146
|
-
" client_id: CLIENT_ID,",
|
|
5619
|
+
" return exchangeTopoloCode({",
|
|
5620
|
+
" authBaseUrl: AUTH_URL,",
|
|
5621
|
+
" clientId: CLIENT_ID,",
|
|
5147
5622
|
" code,",
|
|
5148
|
-
"
|
|
5149
|
-
"
|
|
5150
|
-
" }),",
|
|
5623
|
+
" redirectUri: REDIRECT_URI,",
|
|
5624
|
+
" codeVerifier: verifier,",
|
|
5151
5625
|
" });",
|
|
5152
|
-
" const payload = await response.json().catch(() => null);",
|
|
5153
|
-
" if (!response.ok || !payload?.access_token) {",
|
|
5154
|
-
" throw new Error(payload?.error_description || payload?.error || 'Token exchange failed.');",
|
|
5155
|
-
" }",
|
|
5156
|
-
" return payload;",
|
|
5157
|
-
"}",
|
|
5158
|
-
"",
|
|
5159
|
-
"function randomString(length: number): string {",
|
|
5160
|
-
" const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';",
|
|
5161
|
-
" const bytes = crypto.getRandomValues(new Uint8Array(length));",
|
|
5162
|
-
" return Array.from(bytes, (byte) => alphabet[byte % alphabet.length]).join('');",
|
|
5163
|
-
"}",
|
|
5164
|
-
"",
|
|
5165
|
-
"async function pkceChallenge(verifier: string): Promise<string> {",
|
|
5166
|
-
" const data = new TextEncoder().encode(verifier);",
|
|
5167
|
-
" const digest = await crypto.subtle.digest('SHA-256', data);",
|
|
5168
|
-
" return base64Url(new Uint8Array(digest));",
|
|
5169
|
-
"}",
|
|
5170
|
-
"",
|
|
5171
|
-
"function base64Url(bytes: Uint8Array): string {",
|
|
5172
|
-
" let binary = '';",
|
|
5173
|
-
" for (const byte of bytes) binary += String.fromCharCode(byte);",
|
|
5174
|
-
" return btoa(binary).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/g, '');",
|
|
5175
5626
|
"}",
|
|
5176
5627
|
""
|
|
5177
5628
|
].join("\n");
|
|
5178
5629
|
}
|
|
5179
|
-
function wranglerToml(application, target) {
|
|
5630
|
+
function wranglerToml(application, target, resources, profile) {
|
|
5180
5631
|
const slug = toSlug(application.id);
|
|
5181
5632
|
const workerName = target === "platform" ? `topolo-${slug}` : application.packageName;
|
|
5182
5633
|
const routeHost = application.productionUrl ? new URL(application.productionUrl).host : null;
|
|
5634
|
+
const resource = resourceConfig(application, profile);
|
|
5635
|
+
const runtimeIdentityVar = target === "platform" ? `SERVICE_ID = "${application.serviceId ?? ""}"` : `SERVICE_ID = "${application.serviceId ?? application.id}"`;
|
|
5183
5636
|
const lines = [
|
|
5184
5637
|
`name = "${workerName}"`,
|
|
5185
5638
|
'main = "src/worker.ts"',
|
|
@@ -5215,9 +5668,47 @@ function wranglerToml(application, target) {
|
|
|
5215
5668
|
"",
|
|
5216
5669
|
"[vars]",
|
|
5217
5670
|
'AUTH_BASE_URL = "https://auth.topolo.app"',
|
|
5218
|
-
|
|
5671
|
+
runtimeIdentityVar,
|
|
5219
5672
|
""
|
|
5220
5673
|
);
|
|
5674
|
+
if (resources.d1) {
|
|
5675
|
+
lines.push(
|
|
5676
|
+
"[[d1_databases]]",
|
|
5677
|
+
`binding = "${resource.d1Binding}"`,
|
|
5678
|
+
`database_name = "${resource.d1ProductionName}"`,
|
|
5679
|
+
'database_id = "provision-after-scaffold"',
|
|
5680
|
+
'migrations_dir = "migrations"',
|
|
5681
|
+
""
|
|
5682
|
+
);
|
|
5683
|
+
}
|
|
5684
|
+
if (resources.r2) {
|
|
5685
|
+
lines.push(
|
|
5686
|
+
"[[r2_buckets]]",
|
|
5687
|
+
`binding = "${resource.r2Binding}"`,
|
|
5688
|
+
`bucket_name = "${resource.r2ProductionName}"`,
|
|
5689
|
+
""
|
|
5690
|
+
);
|
|
5691
|
+
}
|
|
5692
|
+
if (resources.queue) {
|
|
5693
|
+
lines.push(
|
|
5694
|
+
"[[queues.producers]]",
|
|
5695
|
+
`binding = "${resource.queueBinding}"`,
|
|
5696
|
+
`queue = "${resource.queueProductionName}"`,
|
|
5697
|
+
"",
|
|
5698
|
+
"[[queues.consumers]]",
|
|
5699
|
+
`queue = "${resource.queueProductionName}"`,
|
|
5700
|
+
""
|
|
5701
|
+
);
|
|
5702
|
+
}
|
|
5703
|
+
if (resources.workflow) {
|
|
5704
|
+
lines.push(
|
|
5705
|
+
"[[workflows]]",
|
|
5706
|
+
`binding = "${resource.workflowBinding}"`,
|
|
5707
|
+
`name = "${resource.workflowProductionName}"`,
|
|
5708
|
+
`class_name = "${resource.workflowClassName}"`,
|
|
5709
|
+
""
|
|
5710
|
+
);
|
|
5711
|
+
}
|
|
5221
5712
|
if (target === "platform") {
|
|
5222
5713
|
lines.push(
|
|
5223
5714
|
"# Staging deploys use the separate Topolo Staging Cloudflare account.",
|
|
@@ -5231,9 +5722,47 @@ function wranglerToml(application, target) {
|
|
|
5231
5722
|
"",
|
|
5232
5723
|
"[env.staging.vars]",
|
|
5233
5724
|
'AUTH_BASE_URL = "https://auth.stg.topolo.us"',
|
|
5234
|
-
|
|
5725
|
+
runtimeIdentityVar,
|
|
5235
5726
|
""
|
|
5236
5727
|
);
|
|
5728
|
+
if (resources.d1) {
|
|
5729
|
+
lines.push(
|
|
5730
|
+
"[[env.staging.d1_databases]]",
|
|
5731
|
+
`binding = "${resource.d1Binding}"`,
|
|
5732
|
+
`database_name = "${resource.d1StagingName}"`,
|
|
5733
|
+
'database_id = "provision-after-scaffold"',
|
|
5734
|
+
'migrations_dir = "migrations"',
|
|
5735
|
+
""
|
|
5736
|
+
);
|
|
5737
|
+
}
|
|
5738
|
+
if (resources.r2) {
|
|
5739
|
+
lines.push(
|
|
5740
|
+
"[[env.staging.r2_buckets]]",
|
|
5741
|
+
`binding = "${resource.r2Binding}"`,
|
|
5742
|
+
`bucket_name = "${resource.r2StagingName}"`,
|
|
5743
|
+
""
|
|
5744
|
+
);
|
|
5745
|
+
}
|
|
5746
|
+
if (resources.queue) {
|
|
5747
|
+
lines.push(
|
|
5748
|
+
"[[env.staging.queues.producers]]",
|
|
5749
|
+
`binding = "${resource.queueBinding}"`,
|
|
5750
|
+
`queue = "${resource.queueStagingName}"`,
|
|
5751
|
+
"",
|
|
5752
|
+
"[[env.staging.queues.consumers]]",
|
|
5753
|
+
`queue = "${resource.queueStagingName}"`,
|
|
5754
|
+
""
|
|
5755
|
+
);
|
|
5756
|
+
}
|
|
5757
|
+
if (resources.workflow) {
|
|
5758
|
+
lines.push(
|
|
5759
|
+
"[[env.staging.workflows]]",
|
|
5760
|
+
`binding = "${resource.workflowBinding}"`,
|
|
5761
|
+
`name = "${resource.workflowStagingName}"`,
|
|
5762
|
+
`class_name = "${resource.workflowClassName}"`,
|
|
5763
|
+
""
|
|
5764
|
+
);
|
|
5765
|
+
}
|
|
5237
5766
|
}
|
|
5238
5767
|
return lines.join("\n");
|
|
5239
5768
|
}
|
|
@@ -5242,12 +5771,29 @@ function workerTs(application, target, options) {
|
|
|
5242
5771
|
return platformWorkerTs(application, options);
|
|
5243
5772
|
}
|
|
5244
5773
|
function platformWorkerTs(application, options) {
|
|
5245
|
-
const serviceId = application.serviceId ?? application.id;
|
|
5246
5774
|
const serviceName = toTitle(application.name);
|
|
5247
5775
|
const openActionLabel = options.audience === "personal" ? "Open app" : "Open workspace";
|
|
5776
|
+
const resource = resourceConfig(application, options.profile);
|
|
5777
|
+
const envBindings = [
|
|
5778
|
+
" AUTH_BASE_URL?: string;",
|
|
5779
|
+
" SERVICE_ID?: string;",
|
|
5780
|
+
" TOPOLO_SERVICE_API_KEY?: string;",
|
|
5781
|
+
...options.resources.d1 ? [` ${resource.d1Binding}?: unknown;`] : [],
|
|
5782
|
+
...options.resources.r2 ? [` ${resource.r2Binding}?: unknown;`] : [],
|
|
5783
|
+
...options.resources.queue ? [` ${resource.queueBinding}?: { send(message: unknown): Promise<void> };`] : [],
|
|
5784
|
+
...options.resources.workflow ? [` ${resource.workflowBinding}?: unknown;`] : []
|
|
5785
|
+
];
|
|
5786
|
+
const workflowExport = options.resources.workflow ? [
|
|
5787
|
+
"",
|
|
5788
|
+
`export class ${resource.workflowClassName} extends WorkflowEntrypoint<Env, unknown> {`,
|
|
5789
|
+
" async run(_event: Readonly<WorkflowEvent<unknown>>, step: WorkflowStep): Promise<unknown> {",
|
|
5790
|
+
" return step.do('scaffold no-op', async () => ({ status: 'pending_implementation' }));",
|
|
5791
|
+
" }",
|
|
5792
|
+
"}"
|
|
5793
|
+
] : [];
|
|
5248
5794
|
const bootstrapBody = options.tenancy === "household" ? [
|
|
5249
5795
|
" const userContext = auth.user && typeof auth.user === 'object' ? (auth.user as unknown as Record<string, unknown>) : {};",
|
|
5250
|
-
" return json({",
|
|
5796
|
+
" return rt.json({",
|
|
5251
5797
|
" user: auth.user,",
|
|
5252
5798
|
" activeContextType: userContext.activeContextType ?? null,",
|
|
5253
5799
|
" activeContextId: userContext.activeContextId ?? null,",
|
|
@@ -5256,82 +5802,111 @@ function platformWorkerTs(application, options) {
|
|
|
5256
5802
|
" selectedHousehold: userContext.selectedHousehold ?? null,",
|
|
5257
5803
|
" household: userContext.selectedHousehold ?? null,",
|
|
5258
5804
|
" householdMemberships: userContext.householdMemberships ?? [],",
|
|
5259
|
-
" }, { status: 200 }
|
|
5805
|
+
" }, { status: 200 });"
|
|
5260
5806
|
] : [
|
|
5261
|
-
" return json({",
|
|
5807
|
+
" return rt.json({",
|
|
5262
5808
|
" user: auth.user,",
|
|
5263
5809
|
" organization: auth.organization,",
|
|
5264
|
-
" }, { status: 200 }
|
|
5810
|
+
" }, { status: 200 });"
|
|
5265
5811
|
];
|
|
5266
5812
|
return [
|
|
5267
|
-
"import {
|
|
5268
|
-
"import {
|
|
5813
|
+
"import { createWidgetHandler } from '@topolo-io/widget-server';",
|
|
5814
|
+
"import { createTopoloWorkerRuntime } from '@topolo-io/worker-runtime';",
|
|
5815
|
+
"import { SCAFFOLD_APP_ID, SCAFFOLD_APP_NAME, SCAFFOLD_SERVICE_SLUG, resolveRuntimeServiceId } from './service-identity';",
|
|
5816
|
+
...options.resources.workflow ? ["import { WorkflowEntrypoint, type WorkflowEvent, type WorkflowStep } from 'cloudflare:workers';"] : [],
|
|
5269
5817
|
"",
|
|
5270
5818
|
"export interface Env {",
|
|
5271
|
-
|
|
5272
|
-
" SERVICE_ID?: string;",
|
|
5273
|
-
" TOPOLO_SERVICE_API_KEY?: string;",
|
|
5819
|
+
...envBindings,
|
|
5274
5820
|
"}",
|
|
5275
5821
|
"",
|
|
5276
|
-
"
|
|
5822
|
+
"// Required by the platform `native-dashboard-widget` requirement \u2014",
|
|
5823
|
+
"// TopoloOne renders this in the user's live workspace whenever the app",
|
|
5824
|
+
"// is installed. Replace the quick-actions placeholder with org-scoped",
|
|
5825
|
+
"// stats once your data model is ready: ctx.user / ctx.orgId / ctx.orgSlug",
|
|
5826
|
+
"// / ctx.isPlatformAdmin / ctx.contextType / ctx.householdId are all",
|
|
5827
|
+
"// pre-resolved. Builder exceptions degrade to empty widgets, not 500s.",
|
|
5828
|
+
"const widgetHandler = createWidgetHandler<Env>({",
|
|
5829
|
+
" serviceId: SCAFFOLD_SERVICE_SLUG,",
|
|
5830
|
+
` serviceName: '${escapeSingleQuoted(serviceName)}',`,
|
|
5831
|
+
" authBaseUrl: (env) => env.AUTH_BASE_URL,",
|
|
5832
|
+
" serviceApiKey: (env) => env.TOPOLO_SERVICE_API_KEY,",
|
|
5833
|
+
" build: () => [",
|
|
5834
|
+
" {",
|
|
5835
|
+
" type: 'quick-actions',",
|
|
5836
|
+
" actions: [",
|
|
5837
|
+
` { id: 'open-workspace', label: '${escapeSingleQuoted(openActionLabel)}', link: '/', primary: true },`,
|
|
5838
|
+
" { id: 'view-health', label: 'View health', link: '/api/health' },",
|
|
5839
|
+
" ],",
|
|
5840
|
+
" },",
|
|
5841
|
+
" ],",
|
|
5842
|
+
"});",
|
|
5277
5843
|
"",
|
|
5278
|
-
"
|
|
5279
|
-
"
|
|
5280
|
-
"
|
|
5844
|
+
"const runtime = createTopoloWorkerRuntime<Env>({",
|
|
5845
|
+
" appName: SCAFFOLD_APP_NAME,",
|
|
5846
|
+
" serviceSlug: SCAFFOLD_SERVICE_SLUG,",
|
|
5847
|
+
" resolveServiceId: (env) => resolveRuntimeServiceId(env),",
|
|
5848
|
+
" authBaseUrl: (env) => env.AUTH_BASE_URL,",
|
|
5849
|
+
" serviceApiKey: (env) => env.TOPOLO_SERVICE_API_KEY,",
|
|
5850
|
+
"});",
|
|
5281
5851
|
"",
|
|
5282
|
-
"
|
|
5283
|
-
"
|
|
5852
|
+
"const handleFetch = runtime.worker(async (request, env, _ctx, rt): Promise<Response> => {",
|
|
5853
|
+
" const url = rt.url;",
|
|
5854
|
+
"",
|
|
5855
|
+
" let serviceId: string;",
|
|
5856
|
+
" try {",
|
|
5857
|
+
" serviceId = await resolveRuntimeServiceId(env);",
|
|
5858
|
+
" } catch (error) {",
|
|
5859
|
+
" return rt.json({ error: 'service_identity_unavailable', message: errorMessage(error) }, { status: 503 });",
|
|
5284
5860
|
" }",
|
|
5285
5861
|
"",
|
|
5286
|
-
"
|
|
5287
|
-
"
|
|
5288
|
-
"
|
|
5289
|
-
" serviceApiKey: env.TOPOLO_SERVICE_API_KEY,",
|
|
5290
|
-
" });",
|
|
5862
|
+
" if (url.pathname === '/api/health') {",
|
|
5863
|
+
" return rt.json({ ok: true, service: serviceId, serviceSlug: SCAFFOLD_SERVICE_SLUG }, { status: 200 });",
|
|
5864
|
+
" }",
|
|
5291
5865
|
"",
|
|
5292
|
-
" if (
|
|
5293
|
-
" return json({
|
|
5866
|
+
" if (url.pathname === '/api/service-identity') {",
|
|
5867
|
+
" return rt.json({",
|
|
5868
|
+
" appId: SCAFFOLD_APP_ID,",
|
|
5869
|
+
" appName: SCAFFOLD_APP_NAME,",
|
|
5870
|
+
" serviceSlug: SCAFFOLD_SERVICE_SLUG,",
|
|
5871
|
+
" serviceId,",
|
|
5872
|
+
" }, { status: 200 });",
|
|
5294
5873
|
" }",
|
|
5295
5874
|
"",
|
|
5296
|
-
"
|
|
5297
|
-
"
|
|
5298
|
-
"
|
|
5299
|
-
|
|
5300
|
-
"
|
|
5301
|
-
"
|
|
5302
|
-
"
|
|
5303
|
-
"
|
|
5304
|
-
` { id: 'open-workspace', label: '${escapeSingleQuoted(openActionLabel)}', link: '/', primary: true },`,
|
|
5305
|
-
" { id: 'view-health', label: 'View health', link: '/api/health' },",
|
|
5306
|
-
" ],",
|
|
5307
|
-
" },",
|
|
5308
|
-
" ],",
|
|
5309
|
-
" }), { status: 200 }, request);",
|
|
5875
|
+
" // /api/widget delegates fully to @topolo-io/widget-server \u2014 it owns",
|
|
5876
|
+
" // auth, context parsing, response wrapping, and error degradation.",
|
|
5877
|
+
" if (url.pathname === '/api/widget') return widgetHandler(request, env);",
|
|
5878
|
+
"",
|
|
5879
|
+
" const auth = await rt.verifyAuth({ serviceId });",
|
|
5880
|
+
"",
|
|
5881
|
+
" if (!auth.ok) {",
|
|
5882
|
+
" return rt.authError(auth);",
|
|
5310
5883
|
" }",
|
|
5311
5884
|
"",
|
|
5312
5885
|
" if (url.pathname === '/api/bootstrap') {",
|
|
5313
5886
|
...bootstrapBody,
|
|
5314
5887
|
" }",
|
|
5315
5888
|
"",
|
|
5316
|
-
" return
|
|
5317
|
-
"
|
|
5318
|
-
"};",
|
|
5889
|
+
" return rt.notFound();",
|
|
5890
|
+
"});",
|
|
5319
5891
|
"",
|
|
5320
|
-
|
|
5892
|
+
"export default {",
|
|
5893
|
+
" fetch: handleFetch,",
|
|
5894
|
+
"};",
|
|
5321
5895
|
"",
|
|
5322
|
-
"function
|
|
5323
|
-
"
|
|
5324
|
-
"
|
|
5325
|
-
" headers.set('X-Topolo-Client', CLIENT_HEADER);",
|
|
5326
|
-
" headers.set('X-Topolo-Request-Id', request.headers.get('X-Topolo-Request-Id') ?? crypto.randomUUID());",
|
|
5327
|
-
" return new Response(JSON.stringify(body), { ...init, headers });",
|
|
5896
|
+
"function errorMessage(error: unknown): string {",
|
|
5897
|
+
" if (error instanceof Error && error.message) return error.message;",
|
|
5898
|
+
" return 'Unexpected service identity error.';",
|
|
5328
5899
|
"}",
|
|
5900
|
+
...workflowExport,
|
|
5329
5901
|
""
|
|
5330
5902
|
].join("\n");
|
|
5331
5903
|
}
|
|
5332
5904
|
function developerWorkerTs(application) {
|
|
5333
5905
|
const serviceId = application.serviceId ?? application.id;
|
|
5906
|
+
const serviceName = application.name || "Topolo App";
|
|
5334
5907
|
return [
|
|
5908
|
+
"import { createWidgetHandler } from '@topolo-io/widget-server';",
|
|
5909
|
+
"",
|
|
5335
5910
|
"export interface Env {",
|
|
5336
5911
|
" AUTH_BASE_URL?: string;",
|
|
5337
5912
|
" SERVICE_ID?: string;",
|
|
@@ -5339,6 +5914,28 @@ function developerWorkerTs(application) {
|
|
|
5339
5914
|
"",
|
|
5340
5915
|
"const CLIENT_HEADER = 'topolo-developer-starter';",
|
|
5341
5916
|
`const DEFAULT_SERVICE_ID = '${serviceId}';`,
|
|
5917
|
+
`const SERVICE_NAME = '${escapeSingleQuoted(serviceName)}';`,
|
|
5918
|
+
"",
|
|
5919
|
+
"// Required by the platform `native-dashboard-widget` requirement \u2014",
|
|
5920
|
+
"// TopoloOne renders this in the user's live workspace whenever the app",
|
|
5921
|
+
"// is installed. Replace the quick-actions placeholder with org-scoped",
|
|
5922
|
+
"// stats once your data model is ready: ctx.user / ctx.orgId / ctx.orgSlug",
|
|
5923
|
+
"// / ctx.isPlatformAdmin / ctx.contextType / ctx.householdId are all",
|
|
5924
|
+
"// pre-resolved. Builder exceptions degrade to empty widgets, not 500s.",
|
|
5925
|
+
"const widgetHandler = createWidgetHandler<Env>({",
|
|
5926
|
+
" serviceId: DEFAULT_SERVICE_ID,",
|
|
5927
|
+
" serviceName: SERVICE_NAME,",
|
|
5928
|
+
" authBaseUrl: (env) => env.AUTH_BASE_URL,",
|
|
5929
|
+
" build: () => [",
|
|
5930
|
+
" {",
|
|
5931
|
+
" type: 'quick-actions',",
|
|
5932
|
+
" actions: [",
|
|
5933
|
+
" { id: 'open-app', label: `Open ${SERVICE_NAME}`, link: '/', primary: true },",
|
|
5934
|
+
" { id: 'view-health', label: 'View health', link: '/api/health' },",
|
|
5935
|
+
" ],",
|
|
5936
|
+
" },",
|
|
5937
|
+
" ],",
|
|
5938
|
+
"});",
|
|
5342
5939
|
"",
|
|
5343
5940
|
"export default {",
|
|
5344
5941
|
" async fetch(request: Request, env: Env): Promise<Response> {",
|
|
@@ -5348,6 +5945,9 @@ function developerWorkerTs(application) {
|
|
|
5348
5945
|
" return json({ ok: true, service: env.SERVICE_ID ?? DEFAULT_SERVICE_ID }, { status: 200 }, request);",
|
|
5349
5946
|
" }",
|
|
5350
5947
|
"",
|
|
5948
|
+
" // /api/widget delegates fully to @topolo-io/widget-server.",
|
|
5949
|
+
" if (url.pathname === '/api/widget') return widgetHandler(request, env);",
|
|
5950
|
+
"",
|
|
5351
5951
|
" const auth = await verifyTopoloRequest(request, env);",
|
|
5352
5952
|
" if (!auth.ok) {",
|
|
5353
5953
|
" return json({ error: auth.error }, { status: auth.status }, request);",
|
|
@@ -5404,8 +6004,108 @@ function developerWorkerTs(application) {
|
|
|
5404
6004
|
}
|
|
5405
6005
|
function cloudcontrolJson(application, options) {
|
|
5406
6006
|
const slug = toSlug(application.id);
|
|
6007
|
+
const appPath = firstPartyAppPath(application.appDir, options.audience);
|
|
5407
6008
|
const host = application.productionUrl ? new URL(application.productionUrl).host : `${slug}.topolo.app`;
|
|
5408
6009
|
const stagingHost = `${slug}.stg.topolo.us`;
|
|
6010
|
+
const serviceSlug = application.serviceSlug ?? `topolo-${slug}`;
|
|
6011
|
+
const resources = resourceConfig(application, options.profile);
|
|
6012
|
+
const resourceBindings = [];
|
|
6013
|
+
if (options.resources.d1) {
|
|
6014
|
+
resourceBindings.push(
|
|
6015
|
+
{
|
|
6016
|
+
resource_type: "d1",
|
|
6017
|
+
binding: resources.d1Binding,
|
|
6018
|
+
environment: "production",
|
|
6019
|
+
account_id: CLOUDFLARE_ACCOUNT_ID,
|
|
6020
|
+
resource_name: resources.d1ProductionName,
|
|
6021
|
+
resource_id: "provision-after-scaffold",
|
|
6022
|
+
migrations_dir: "migrations"
|
|
6023
|
+
},
|
|
6024
|
+
{
|
|
6025
|
+
resource_type: "d1",
|
|
6026
|
+
binding: resources.d1Binding,
|
|
6027
|
+
environment: "staging",
|
|
6028
|
+
account_id: STAGING_CLOUDFLARE_ACCOUNT_ID,
|
|
6029
|
+
resource_name: resources.d1StagingName,
|
|
6030
|
+
resource_id: "provision-after-scaffold",
|
|
6031
|
+
migrations_dir: "migrations"
|
|
6032
|
+
}
|
|
6033
|
+
);
|
|
6034
|
+
}
|
|
6035
|
+
if (options.resources.r2) {
|
|
6036
|
+
resourceBindings.push(
|
|
6037
|
+
{
|
|
6038
|
+
resource_type: "r2",
|
|
6039
|
+
binding: resources.r2Binding,
|
|
6040
|
+
environment: "production",
|
|
6041
|
+
account_id: CLOUDFLARE_ACCOUNT_ID,
|
|
6042
|
+
resource_name: resources.r2ProductionName
|
|
6043
|
+
},
|
|
6044
|
+
{
|
|
6045
|
+
resource_type: "r2",
|
|
6046
|
+
binding: resources.r2Binding,
|
|
6047
|
+
environment: "staging",
|
|
6048
|
+
account_id: STAGING_CLOUDFLARE_ACCOUNT_ID,
|
|
6049
|
+
resource_name: resources.r2StagingName
|
|
6050
|
+
}
|
|
6051
|
+
);
|
|
6052
|
+
}
|
|
6053
|
+
if (options.resources.queue) {
|
|
6054
|
+
resourceBindings.push(
|
|
6055
|
+
{
|
|
6056
|
+
resource_type: "queue",
|
|
6057
|
+
binding: resources.queueBinding,
|
|
6058
|
+
environment: "production",
|
|
6059
|
+
account_id: CLOUDFLARE_ACCOUNT_ID,
|
|
6060
|
+
resource_name: resources.queueProductionName
|
|
6061
|
+
},
|
|
6062
|
+
{
|
|
6063
|
+
resource_type: "queue",
|
|
6064
|
+
binding: resources.queueBinding,
|
|
6065
|
+
environment: "staging",
|
|
6066
|
+
account_id: STAGING_CLOUDFLARE_ACCOUNT_ID,
|
|
6067
|
+
resource_name: resources.queueStagingName
|
|
6068
|
+
}
|
|
6069
|
+
);
|
|
6070
|
+
}
|
|
6071
|
+
if (options.resources.workflow) {
|
|
6072
|
+
resourceBindings.push(
|
|
6073
|
+
{
|
|
6074
|
+
resource_type: "workflow",
|
|
6075
|
+
binding: resources.workflowBinding,
|
|
6076
|
+
environment: "production",
|
|
6077
|
+
account_id: CLOUDFLARE_ACCOUNT_ID,
|
|
6078
|
+
resource_name: resources.workflowProductionName,
|
|
6079
|
+
class_name: resources.workflowClassName
|
|
6080
|
+
},
|
|
6081
|
+
{
|
|
6082
|
+
resource_type: "workflow",
|
|
6083
|
+
binding: resources.workflowBinding,
|
|
6084
|
+
environment: "staging",
|
|
6085
|
+
account_id: STAGING_CLOUDFLARE_ACCOUNT_ID,
|
|
6086
|
+
resource_name: resources.workflowStagingName,
|
|
6087
|
+
class_name: resources.workflowClassName
|
|
6088
|
+
}
|
|
6089
|
+
);
|
|
6090
|
+
}
|
|
6091
|
+
if (options.resources.containerProcessing) {
|
|
6092
|
+
resourceBindings.push(
|
|
6093
|
+
{
|
|
6094
|
+
resource_type: "container",
|
|
6095
|
+
environment: "production",
|
|
6096
|
+
account_id: CLOUDFLARE_ACCOUNT_ID,
|
|
6097
|
+
resource_name: resources.containerProductionName,
|
|
6098
|
+
purpose: "processing"
|
|
6099
|
+
},
|
|
6100
|
+
{
|
|
6101
|
+
resource_type: "container",
|
|
6102
|
+
environment: "staging",
|
|
6103
|
+
account_id: STAGING_CLOUDFLARE_ACCOUNT_ID,
|
|
6104
|
+
resource_name: resources.containerStagingName,
|
|
6105
|
+
purpose: "processing"
|
|
6106
|
+
}
|
|
6107
|
+
);
|
|
6108
|
+
}
|
|
5409
6109
|
const deployTargets = options.api ? [
|
|
5410
6110
|
{
|
|
5411
6111
|
kind: "worker",
|
|
@@ -5420,7 +6120,7 @@ function cloudcontrolJson(application, options) {
|
|
|
5420
6120
|
domains: [host],
|
|
5421
6121
|
routes: [host],
|
|
5422
6122
|
api_url: application.productionUrl,
|
|
5423
|
-
|
|
6123
|
+
service_slug: serviceSlug
|
|
5424
6124
|
},
|
|
5425
6125
|
{
|
|
5426
6126
|
kind: "worker",
|
|
@@ -5430,12 +6130,12 @@ function cloudcontrolJson(application, options) {
|
|
|
5430
6130
|
config_path: "wrangler.toml",
|
|
5431
6131
|
build_command: "VITE_TOPOLO_AUTH_URL=https://auth.stg.topolo.us npm run build",
|
|
5432
6132
|
output_dir: "dist",
|
|
5433
|
-
production_branch: "
|
|
6133
|
+
production_branch: "main",
|
|
5434
6134
|
healthcheck_path: "/api/health",
|
|
5435
6135
|
domains: [stagingHost],
|
|
5436
6136
|
routes: [stagingHost],
|
|
5437
6137
|
api_url: `https://${stagingHost}`,
|
|
5438
|
-
|
|
6138
|
+
service_slug: serviceSlug
|
|
5439
6139
|
}
|
|
5440
6140
|
] : [
|
|
5441
6141
|
{
|
|
@@ -5455,14 +6155,14 @@ function cloudcontrolJson(application, options) {
|
|
|
5455
6155
|
];
|
|
5456
6156
|
return `${json({
|
|
5457
6157
|
name: `topolo-${slug}`,
|
|
5458
|
-
description: `Canonical documentation for ${toTitle(application.name)} lives in
|
|
6158
|
+
description: `Canonical documentation for ${toTitle(application.name)} lives in Apps/system/TopoloDocs.`,
|
|
5459
6159
|
deploy_method: options.api ? "wrangler" : "cloudflare_pages",
|
|
5460
6160
|
ci_provider: "github_actions",
|
|
5461
6161
|
repo_identity: {
|
|
5462
6162
|
provider: "github",
|
|
5463
6163
|
owner: "Topolo-io",
|
|
5464
|
-
repo:
|
|
5465
|
-
remote_url: `git@github.com:Topolo-io
|
|
6164
|
+
repo: application.appDir,
|
|
6165
|
+
remote_url: `git@github.com:Topolo-io/${application.appDir}.git`,
|
|
5466
6166
|
default_branch: "main"
|
|
5467
6167
|
},
|
|
5468
6168
|
environments: application.productionUrl ? [
|
|
@@ -5475,12 +6175,12 @@ function cloudcontrolJson(application, options) {
|
|
|
5475
6175
|
{
|
|
5476
6176
|
name: "staging",
|
|
5477
6177
|
url: `https://${stagingHost}`,
|
|
5478
|
-
branch: "
|
|
6178
|
+
branch: "main",
|
|
5479
6179
|
status: "unknown"
|
|
5480
6180
|
}
|
|
5481
6181
|
] : [],
|
|
5482
6182
|
deploy_targets: deployTargets,
|
|
5483
|
-
resource_bindings:
|
|
6183
|
+
resource_bindings: resourceBindings,
|
|
5484
6184
|
ci_workflows: [
|
|
5485
6185
|
{
|
|
5486
6186
|
provider: "github_actions",
|
|
@@ -5490,8 +6190,25 @@ function cloudcontrolJson(application, options) {
|
|
|
5490
6190
|
required: true
|
|
5491
6191
|
}
|
|
5492
6192
|
],
|
|
5493
|
-
ci_targets: [
|
|
5494
|
-
|
|
6193
|
+
ci_targets: [
|
|
6194
|
+
{
|
|
6195
|
+
name: "typecheck",
|
|
6196
|
+
command: "npm run typecheck",
|
|
6197
|
+
required: true
|
|
6198
|
+
},
|
|
6199
|
+
{
|
|
6200
|
+
name: "test",
|
|
6201
|
+
command: "npm run test",
|
|
6202
|
+
required: true
|
|
6203
|
+
},
|
|
6204
|
+
{
|
|
6205
|
+
name: "build",
|
|
6206
|
+
command: "npm run build",
|
|
6207
|
+
required: true
|
|
6208
|
+
}
|
|
6209
|
+
],
|
|
6210
|
+
tags: ["application", "cloudflare", "typescript", ...hasScaffoldResources(options.resources) ? ["resource-backed"] : []],
|
|
6211
|
+
source_paths: [appPath],
|
|
5495
6212
|
cloudflare_account_id: CLOUDFLARE_ACCOUNT_ID
|
|
5496
6213
|
})}
|
|
5497
6214
|
`;
|
|
@@ -5500,7 +6217,9 @@ function systemJson(application, options) {
|
|
|
5500
6217
|
const slug = toSlug(application.id);
|
|
5501
6218
|
const suiteLabel = options.audience === "personal" ? "personal" : "platform";
|
|
5502
6219
|
const summaryLabel = options.audience === "personal" ? "first-party Topolo personal application" : "first-party Topolo platform application";
|
|
5503
|
-
const
|
|
6220
|
+
const appPath = firstPartyAppPath(application.appDir, options.audience);
|
|
6221
|
+
const resourceNotes = hasScaffoldResources(options.resources) ? " Generated resource bindings are declared in Wrangler and CloudControl for durable platform-owned state and processing." : "";
|
|
6222
|
+
const apiNotes = options.api ? options.tenancy === "household" ? `Protected Worker routes validate Topolo Auth credentials and derive active personal context plus selected household context from the credential. The browser baseline starts on shared landing and login routes, completes auth on /auth/callback, and dedupes React when linked Topolo packages are installed locally.${resourceNotes}` : `Protected Worker routes validate Topolo Auth credentials and derive organization context from the credential. The browser baseline starts on shared landing and login routes, completes auth on /auth/callback, and dedupes React when linked Topolo packages are installed locally.${resourceNotes}` : "Browser-only application surface using shared Topolo Auth landing/login/callback routes, shared shell packages, and React runtime dedupe.";
|
|
5504
6223
|
const sensitiveData = options.tenancy === "household" ? ["identity", "personal_data", "household_data"] : ["identity", "org_data"];
|
|
5505
6224
|
const privacySensitiveData = options.tenancy === "household" ? ["identity", "personal", "household"] : ["identity", "organization"];
|
|
5506
6225
|
return `${json({
|
|
@@ -5512,19 +6231,19 @@ function systemJson(application, options) {
|
|
|
5512
6231
|
lifecycle: "inventory",
|
|
5513
6232
|
last_verified: CURRENT_VERIFICATION_DATE,
|
|
5514
6233
|
owners: ["platform-engineering"],
|
|
5515
|
-
repo_paths: [
|
|
6234
|
+
repo_paths: [appPath],
|
|
5516
6235
|
service_ids: options.api && application.serviceId ? [application.serviceId] : [],
|
|
5517
6236
|
visibility: "internal",
|
|
5518
6237
|
api_contract: {
|
|
5519
6238
|
type: options.api ? "curated" : "none",
|
|
5520
|
-
source: options.api ?
|
|
6239
|
+
source: options.api ? `${appPath}/src/worker.ts` : `${appPath}/src/App.tsx`,
|
|
5521
6240
|
notes: apiNotes
|
|
5522
6241
|
},
|
|
5523
6242
|
primary_hosts: application.productionUrl ? [application.productionUrl] : [],
|
|
5524
6243
|
doc_paths: [`internal/apps/${slug}`, `public/applications/${slug}`],
|
|
5525
6244
|
security_assurance: {
|
|
5526
6245
|
risk_tier: "internal",
|
|
5527
|
-
auth_boundary: "Topolo Auth through @topolo-io/auth-client for browser sessions and @topolo-io/
|
|
6246
|
+
auth_boundary: "Topolo Auth through @topolo-io/auth-client for browser sessions and @topolo-io/worker-runtime for Worker requests.",
|
|
5528
6247
|
tenant_isolation: options.tenancy === "household" ? "household_scoped" : "organization_scoped",
|
|
5529
6248
|
external_inputs: ["browser", ...options.api ? ["api"] : []],
|
|
5530
6249
|
sensitive_data: sensitiveData,
|
|
@@ -5536,7 +6255,13 @@ function systemJson(application, options) {
|
|
|
5536
6255
|
data_privacy: {
|
|
5537
6256
|
classification: "confidential",
|
|
5538
6257
|
sensitive_data_classes: privacySensitiveData,
|
|
5539
|
-
storage_locations: [
|
|
6258
|
+
storage_locations: [
|
|
6259
|
+
"source_control",
|
|
6260
|
+
...options.resources.d1 ? ["d1"] : [],
|
|
6261
|
+
...options.resources.r2 ? ["r2"] : [],
|
|
6262
|
+
...options.resources.queue ? ["queue"] : [],
|
|
6263
|
+
...options.resources.workflow ? ["workflow"] : []
|
|
6264
|
+
],
|
|
5540
6265
|
encryption_at_rest: "platform_managed",
|
|
5541
6266
|
encryption_in_transit: "https_only",
|
|
5542
6267
|
key_management: "cloudflare_managed",
|
|
@@ -5554,11 +6279,24 @@ function systemJson(application, options) {
|
|
|
5554
6279
|
}
|
|
5555
6280
|
function internalDocs(application, options) {
|
|
5556
6281
|
const slug = toSlug(application.id);
|
|
6282
|
+
const serviceSlug = application.serviceSlug ?? `topolo-${slug}`;
|
|
5557
6283
|
const title = toTitle(application.name);
|
|
6284
|
+
const appPath = firstPartyAppPath(application.appDir, options.audience);
|
|
5558
6285
|
const suiteLabel = options.audience === "personal" ? "personal" : "platform";
|
|
5559
6286
|
const tenantLabel = options.tenancy === "household" ? "household" : "organization";
|
|
5560
6287
|
const apiContextLabel = options.tenancy === "household" ? "active personal context and selected household context" : "organization context";
|
|
5561
6288
|
const boundaryFailureText = options.tenancy === "household" ? "Domain data is added without a household-scoped backend boundary." : "Domain data is added without an organization-scoped backend boundary.";
|
|
6289
|
+
const resource = resourceConfig(application, options.profile);
|
|
6290
|
+
const resourceLines = hasScaffoldResources(options.resources) ? [
|
|
6291
|
+
"",
|
|
6292
|
+
"## Resource Bindings",
|
|
6293
|
+
"",
|
|
6294
|
+
...options.resources.d1 ? [`- D1 binding \`${resource.d1Binding}\` for durable metadata.`] : [],
|
|
6295
|
+
...options.resources.r2 ? [`- R2 binding \`${resource.r2Binding}\` for object assets.`] : [],
|
|
6296
|
+
...options.resources.queue ? [`- Queue binding \`${resource.queueBinding}\` for asynchronous work intake.`] : [],
|
|
6297
|
+
...options.resources.workflow ? [`- Workflow binding \`${resource.workflowBinding}\` for import or processing orchestration.`] : [],
|
|
6298
|
+
...options.resources.containerProcessing ? ["- Container processing metadata is declared in CloudControl for CPU/file-heavy jobs."] : []
|
|
6299
|
+
] : [];
|
|
5562
6300
|
return [
|
|
5563
6301
|
"---",
|
|
5564
6302
|
`title: "${escapeFrontmatter(title)} Handbook"`,
|
|
@@ -5567,8 +6305,8 @@ function internalDocs(application, options) {
|
|
|
5567
6305
|
'status: "draft"',
|
|
5568
6306
|
`last_verified: "${CURRENT_VERIFICATION_DATE}"`,
|
|
5569
6307
|
"source_repos:",
|
|
5570
|
-
` - "
|
|
5571
|
-
' - "
|
|
6308
|
+
` - "${appPath}"`,
|
|
6309
|
+
' - "Apps/system/TopoloDocs"',
|
|
5572
6310
|
'audience: "internal"',
|
|
5573
6311
|
"tags:",
|
|
5574
6312
|
` - "${slug}"`,
|
|
@@ -5588,16 +6326,18 @@ function internalDocs(application, options) {
|
|
|
5588
6326
|
"## Architecture",
|
|
5589
6327
|
"",
|
|
5590
6328
|
"The generated browser surface uses `@topolo-io/ui-kit` for the shared landing page, shared login page, auth callback state, and shared signed shell. `vite.config.ts` includes the standard React plugin plus React runtime dedupe so linked Topolo packages resolve a single browser React runtime.",
|
|
6329
|
+
options.api ? `The generated app centralizes service identity in \`${appPath}/src/service-identity.ts\`. Runtime code reads the provisioned Auth service id from the Worker \`SERVICE_ID\` binding and exposes it to the browser through the same-origin service-identity route.` : `The generated app centralizes browser service identity in \`${appPath}/src/service-identity.ts\` and reads Auth identity from the app runtime instead of calling Auth for slug lookups.`,
|
|
5591
6330
|
"",
|
|
5592
6331
|
"## Runtime Surfaces",
|
|
5593
6332
|
"",
|
|
5594
|
-
`-
|
|
6333
|
+
`- \`${appPath}/src/App.tsx\``,
|
|
6334
|
+
`- \`${appPath}/src/service-identity.ts\``,
|
|
5595
6335
|
"- public landing route: `/`",
|
|
5596
6336
|
"- public sign-in route: `/login`",
|
|
5597
6337
|
"- auth callback route: `/auth/callback`",
|
|
5598
|
-
...options.api ? [`-
|
|
5599
|
-
`-
|
|
5600
|
-
...options.api && application.serviceId ? [`- \`
|
|
6338
|
+
...options.api ? [`- \`${appPath}/src/worker.ts\``] : [],
|
|
6339
|
+
`- \`${appPath}/topolo.cloudcontrol.json\``,
|
|
6340
|
+
...options.api && application.serviceId ? [`- \`Apps/system/TopoloAuth/catalog/service-manifests/${application.serviceId}.json\``] : [],
|
|
5601
6341
|
"",
|
|
5602
6342
|
"## API Reference",
|
|
5603
6343
|
"",
|
|
@@ -5605,11 +6345,12 @@ function internalDocs(application, options) {
|
|
|
5605
6345
|
"",
|
|
5606
6346
|
"## Auth and Permissions",
|
|
5607
6347
|
"",
|
|
5608
|
-
options.api ? `Browser sessions and Worker requests must be validated through Topolo Auth
|
|
6348
|
+
options.api ? `Browser sessions and Worker requests must be validated through Topolo Auth with the provisioned \`SERVICE_ID\` for \`${serviceSlug}\`. Worker routes derive ${apiContextLabel} from the verified credential.` : "Browser sessions must be established through Topolo Auth and rendered inside the shared Topolo shell.",
|
|
5609
6349
|
"",
|
|
5610
6350
|
"## Data Ownership",
|
|
5611
6351
|
"",
|
|
5612
6352
|
`The generated scaffold stores no durable application data yet. Future domain tables, Durable Objects, queues, KV namespaces, R2 buckets, and external provider records must be scoped to the authenticated ${tenantLabel} context at the backend boundary.`,
|
|
6353
|
+
...resourceLines,
|
|
5613
6354
|
"",
|
|
5614
6355
|
"## Deployments",
|
|
5615
6356
|
"",
|
|
@@ -5617,7 +6358,7 @@ function internalDocs(application, options) {
|
|
|
5617
6358
|
"",
|
|
5618
6359
|
"## Failure Modes",
|
|
5619
6360
|
"",
|
|
5620
|
-
|
|
6361
|
+
`- the Worker \`SERVICE_ID\` binding drifts from the Auth service manifest for \`${serviceSlug}\`.`,
|
|
5621
6362
|
"- CloudControl metadata does not match the eventual Cloudflare project or route.",
|
|
5622
6363
|
"- linked Topolo packages resolve a second React runtime and the browser shell renders blank instead of mounting the shared surfaces.",
|
|
5623
6364
|
`- ${boundaryFailureText}`,
|
|
@@ -5625,9 +6366,9 @@ function internalDocs(application, options) {
|
|
|
5625
6366
|
"## Debugging",
|
|
5626
6367
|
"",
|
|
5627
6368
|
"- run `npm run typecheck` and `npm run build` in the application directory",
|
|
5628
|
-
"- run `topolo apps audit <app> --local --repo-root /Users/ashes/Projects/
|
|
6369
|
+
"- run `topolo apps audit <app> --local --repo-root /Users/ashes/Projects/Topolo/Platform --fail-on needs_review --json` after SDK catalog registration",
|
|
5629
6370
|
"- inspect `topolo.cloudcontrol.json` before deployment changes",
|
|
5630
|
-
...options.api
|
|
6371
|
+
...options.api ? ["- inspect the Worker `SERVICE_ID` binding and matching Auth service manifest when permission grants fail"] : [],
|
|
5631
6372
|
...options.api && application.serviceId ? [`- re-run \`topolo platform apps scaffold ${slug} --write --provision\` when embedded app login, launcher visibility, or callback origins drift`] : [],
|
|
5632
6373
|
"",
|
|
5633
6374
|
"## Change Log / Verification",
|
|
@@ -5641,7 +6382,9 @@ function internalDocs(application, options) {
|
|
|
5641
6382
|
}
|
|
5642
6383
|
function publicDocs(application, options) {
|
|
5643
6384
|
const slug = toSlug(application.id);
|
|
6385
|
+
const serviceSlug = application.serviceSlug ?? `topolo-${slug}`;
|
|
5644
6386
|
const title = toTitle(application.name);
|
|
6387
|
+
const appPath = firstPartyAppPath(application.appDir, options.audience);
|
|
5645
6388
|
const suiteLabel = options.audience === "personal" ? "personal" : "business";
|
|
5646
6389
|
const tenantLabel = options.tenancy === "household" ? "household" : "organization";
|
|
5647
6390
|
const apiRouteLabel = options.tenancy === "household" ? "authenticated personal and household routes" : "authenticated workspace routes";
|
|
@@ -5654,7 +6397,7 @@ function publicDocs(application, options) {
|
|
|
5654
6397
|
'status: "draft"',
|
|
5655
6398
|
`last_verified: "${CURRENT_VERIFICATION_DATE}"`,
|
|
5656
6399
|
"source_repos:",
|
|
5657
|
-
` - "
|
|
6400
|
+
` - "${appPath}"`,
|
|
5658
6401
|
'audience: "public"',
|
|
5659
6402
|
"tags:",
|
|
5660
6403
|
` - "${slug}"`,
|
|
@@ -5685,11 +6428,11 @@ function publicDocs(application, options) {
|
|
|
5685
6428
|
"",
|
|
5686
6429
|
"## Auth and Permissions",
|
|
5687
6430
|
"",
|
|
5688
|
-
options.api
|
|
6431
|
+
options.api ? `Signed browser and API requests use Topolo Auth through the \`${serviceSlug}\` service slug, resolving the concrete environment-specific service id at runtime.` : "Signed browser requests use Topolo Auth and the shared Topolo shell.",
|
|
5689
6432
|
"",
|
|
5690
6433
|
"## Data Ownership",
|
|
5691
6434
|
"",
|
|
5692
|
-
`The generated baseline has no durable domain data yet. Future data must be ${tenantLabel}-scoped at the backend boundary.`,
|
|
6435
|
+
hasScaffoldResources(options.resources) ? `Generated resource bindings are ready for ${tenantLabel}-scoped durable data and assets; product code must still enforce that boundary.` : `The generated baseline has no durable domain data yet. Future data must be ${tenantLabel}-scoped at the backend boundary.`,
|
|
5693
6436
|
"",
|
|
5694
6437
|
"## Deployments",
|
|
5695
6438
|
"",
|
|
@@ -5714,16 +6457,20 @@ function publicDocs(application, options) {
|
|
|
5714
6457
|
function serviceManifestJson(application, options) {
|
|
5715
6458
|
const slug = toSlug(application.id);
|
|
5716
6459
|
const title = toTitle(application.name);
|
|
6460
|
+
const appPath = firstPartyAppPath(application.appDir, options.audience);
|
|
5717
6461
|
const permissionPrefix = application.id.replace(/[^a-z0-9_]/g, "_");
|
|
5718
6462
|
const resourceLabel = options.tenancy === "household" ? "household" : "workspace";
|
|
5719
|
-
const launcherSettings =
|
|
6463
|
+
const launcherSettings = {
|
|
5720
6464
|
settings: {
|
|
5721
6465
|
launcher: {
|
|
5722
|
-
|
|
5723
|
-
|
|
6466
|
+
category: "first_party",
|
|
6467
|
+
source: "first_party",
|
|
6468
|
+
app_id: slug,
|
|
6469
|
+
supported_contexts: options.tenancy === "household" ? ["personal"] : ["organization"],
|
|
6470
|
+
...options.tenancy === "household" ? { household_capable: true } : {}
|
|
5724
6471
|
}
|
|
5725
6472
|
}
|
|
5726
|
-
}
|
|
6473
|
+
};
|
|
5727
6474
|
return `${json({
|
|
5728
6475
|
serviceId: application.serviceId,
|
|
5729
6476
|
...launcherSettings,
|
|
@@ -5731,7 +6478,7 @@ function serviceManifestJson(application, options) {
|
|
|
5731
6478
|
{
|
|
5732
6479
|
systemId: `topolo-${slug}`,
|
|
5733
6480
|
name: title,
|
|
5734
|
-
repoPaths: [
|
|
6481
|
+
repoPaths: [appPath],
|
|
5735
6482
|
docPaths: [`applications/${slug}`, `internal/apps/${slug}`]
|
|
5736
6483
|
}
|
|
5737
6484
|
],
|
|
@@ -5773,6 +6520,10 @@ function serviceManifestJson(application, options) {
|
|
|
5773
6520
|
`;
|
|
5774
6521
|
}
|
|
5775
6522
|
function developerManifest(application, options) {
|
|
6523
|
+
const callbackPath = "/auth/callback";
|
|
6524
|
+
const localCallbackUrl = "http://localhost:5173/auth/callback";
|
|
6525
|
+
const productionCallbackUrl = application.productionUrl ? `${application.productionUrl.replace(/\/$/, "")}${callbackPath}` : null;
|
|
6526
|
+
const requestedScopes = options.api ? ["openid", "profile", "email", "organization:read", "entitlements:read", `${application.id}:read`] : ["openid", "profile", "email", "organization:read"];
|
|
5776
6527
|
return `${json({
|
|
5777
6528
|
schema: "https://topolo.app/schemas/topolo-app-manifest.v1.json",
|
|
5778
6529
|
id: application.id,
|
|
@@ -5785,17 +6536,49 @@ function developerManifest(application, options) {
|
|
|
5785
6536
|
container_app_id: options.containerAppId,
|
|
5786
6537
|
package_name: application.packageName,
|
|
5787
6538
|
service_id: options.api ? application.serviceId : null,
|
|
6539
|
+
capabilities: {
|
|
6540
|
+
selected: options.capabilities,
|
|
6541
|
+
supported_v1: [
|
|
6542
|
+
"login",
|
|
6543
|
+
"launcher",
|
|
6544
|
+
"marketplace",
|
|
6545
|
+
"api",
|
|
6546
|
+
"widget",
|
|
6547
|
+
"billing",
|
|
6548
|
+
"nexus_ai",
|
|
6549
|
+
"agent_access",
|
|
6550
|
+
"hosting"
|
|
6551
|
+
]
|
|
6552
|
+
},
|
|
5788
6553
|
auth: {
|
|
5789
6554
|
provider: "topolo",
|
|
5790
6555
|
flow: "authorization_code_pkce",
|
|
5791
|
-
callback_path:
|
|
5792
|
-
|
|
5793
|
-
|
|
5794
|
-
|
|
6556
|
+
callback_path: callbackPath,
|
|
6557
|
+
callback_urls: [localCallbackUrl, productionCallbackUrl].filter(Boolean),
|
|
6558
|
+
local_callback_url: localCallbackUrl,
|
|
6559
|
+
production_callback_url: productionCallbackUrl,
|
|
6560
|
+
requested_scopes: requestedScopes,
|
|
6561
|
+
account_linking: {
|
|
6562
|
+
required_columns: [
|
|
6563
|
+
"local_user_id",
|
|
6564
|
+
"topolo_user_id",
|
|
6565
|
+
"topolo_org_id",
|
|
6566
|
+
"topolo_context_id",
|
|
6567
|
+
"topolo_service_id",
|
|
6568
|
+
"connected_at",
|
|
6569
|
+
"last_seen_at"
|
|
6570
|
+
],
|
|
6571
|
+
email_match_policy: "confirm_or_current_session_only"
|
|
6572
|
+
}
|
|
5795
6573
|
},
|
|
5796
6574
|
runtime: {
|
|
6575
|
+
strategy: "external",
|
|
5797
6576
|
browser: true,
|
|
5798
|
-
api: options.api
|
|
6577
|
+
api: options.api,
|
|
6578
|
+
health_endpoint: options.api ? "/api/health" : null
|
|
6579
|
+
},
|
|
6580
|
+
marketplace: {
|
|
6581
|
+
intent: options.distributionModel === "marketplace" ? "submit_when_ready" : "not_requested"
|
|
5799
6582
|
}
|
|
5800
6583
|
})}
|
|
5801
6584
|
`;
|
|
@@ -5803,10 +6586,10 @@ function developerManifest(application, options) {
|
|
|
5803
6586
|
function envExample(application) {
|
|
5804
6587
|
return [
|
|
5805
6588
|
"VITE_TOPOLO_AUTH_URL=https://auth.topolo.app",
|
|
6589
|
+
"VITE_TOPOLO_AUTH_BASE_URL=https://auth.topolo.app",
|
|
5806
6590
|
"VITE_TOPOLO_CLIENT_ID=",
|
|
5807
6591
|
"VITE_TOPOLO_REDIRECT_URI=http://localhost:5173/auth/callback",
|
|
5808
|
-
"VITE_TOPOLO_SCOPES=
|
|
5809
|
-
`VITE_TOPOLO_SERVICE_ID=${application.serviceId ?? application.id}`,
|
|
6592
|
+
"VITE_TOPOLO_SCOPES=openid profile email organization:read entitlements:read",
|
|
5810
6593
|
""
|
|
5811
6594
|
].join("\n");
|
|
5812
6595
|
}
|
|
@@ -5851,6 +6634,21 @@ function normalizeSurfaceModel(value) {
|
|
|
5851
6634
|
"Invalid scaffold surface model. Use web_app, mobile_container, mobile_module, or messaging_surface."
|
|
5852
6635
|
);
|
|
5853
6636
|
}
|
|
6637
|
+
function normalizeScaffoldProfile(value) {
|
|
6638
|
+
if (!value) return null;
|
|
6639
|
+
if (value === "spatial-assets") return value;
|
|
6640
|
+
throw new Error("Invalid scaffold profile. Use spatial-assets.");
|
|
6641
|
+
}
|
|
6642
|
+
function resolveScaffoldResources(profile, options) {
|
|
6643
|
+
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 };
|
|
6644
|
+
return {
|
|
6645
|
+
d1: Boolean(profileResources.d1 || options.withD1),
|
|
6646
|
+
r2: Boolean(profileResources.r2 || options.withR2),
|
|
6647
|
+
queue: Boolean(profileResources.queue || options.withQueue),
|
|
6648
|
+
workflow: Boolean(profileResources.workflow || options.withWorkflow),
|
|
6649
|
+
containerProcessing: Boolean(profileResources.containerProcessing || options.withContainerProcessing)
|
|
6650
|
+
};
|
|
6651
|
+
}
|
|
5854
6652
|
function normalizeContainerAppId(value, surfaceModel) {
|
|
5855
6653
|
const trimmed = String(value || "").trim();
|
|
5856
6654
|
if (surfaceModel === "mobile_module") {
|
|
@@ -5887,7 +6685,8 @@ function toSlug(value) {
|
|
|
5887
6685
|
return value.replace(/_/g, "-");
|
|
5888
6686
|
}
|
|
5889
6687
|
function toPascalCase(value) {
|
|
5890
|
-
|
|
6688
|
+
const acronyms = /* @__PURE__ */ new Set(["api", "cli", "crm", "mcp", "mdm", "p2p"]);
|
|
6689
|
+
return value.split(/[-_]/).filter(Boolean).map((part) => acronyms.has(part.toLowerCase()) ? part.toUpperCase() : part.slice(0, 1).toUpperCase() + part.slice(1)).join("");
|
|
5891
6690
|
}
|
|
5892
6691
|
function toTitle(value) {
|
|
5893
6692
|
return value.replace(/^topolo[-_\s]*/i, "Topolo ").split(/[-_\s]+/).filter(Boolean).map((part) => part.slice(0, 1).toUpperCase() + part.slice(1)).join(" ");
|
|
@@ -5925,7 +6724,30 @@ function escapeFrontmatter(value) {
|
|
|
5925
6724
|
}
|
|
5926
6725
|
|
|
5927
6726
|
// src/commands/app-scaffold-provision.ts
|
|
5928
|
-
|
|
6727
|
+
import { readFile as readFile2, writeFile as writeFile3 } from "fs/promises";
|
|
6728
|
+
var DEVELOPERS_SERVICE_SLUG = "topolo-developers";
|
|
6729
|
+
var SERVICE_ID_PLACEHOLDER_RE = /TODO|FILL[_-]?IN|PLACEHOLDER|CHANGE[_-]?ME|XXX/i;
|
|
6730
|
+
function assertProvisioningPayloadComplete(payload) {
|
|
6731
|
+
const app = payload.application;
|
|
6732
|
+
const missing = [];
|
|
6733
|
+
if (!app.name?.trim()) missing.push("name");
|
|
6734
|
+
if (!app.serviceSlug?.trim()) missing.push("serviceSlug");
|
|
6735
|
+
if (!payload.audience) missing.push("audience");
|
|
6736
|
+
if (!payload.tenancy) missing.push("tenancy");
|
|
6737
|
+
if (!payload.surfaceModel) missing.push("surfaceModel");
|
|
6738
|
+
if (!payload.ownerType) missing.push("ownerType");
|
|
6739
|
+
if (missing.length > 0) {
|
|
6740
|
+
throw new Error(
|
|
6741
|
+
`Scaffold provisioning is missing required fields: ${missing.join(", ")}. An app must meet the minimum standard before it can be projected into the platform.`
|
|
6742
|
+
);
|
|
6743
|
+
}
|
|
6744
|
+
const serviceId = app.serviceId?.trim();
|
|
6745
|
+
if (serviceId && SERVICE_ID_PLACEHOLDER_RE.test(serviceId)) {
|
|
6746
|
+
throw new Error(
|
|
6747
|
+
`Refusing to provision with a placeholder service id (${serviceId}). Remove the placeholder and let provisioning mint/assign the canonical srv_ id.`
|
|
6748
|
+
);
|
|
6749
|
+
}
|
|
6750
|
+
}
|
|
5929
6751
|
async function provisionApplicationScaffold(payload) {
|
|
5930
6752
|
if (!payload.application.productionUrl) {
|
|
5931
6753
|
throw new Error(
|
|
@@ -5937,6 +6759,7 @@ async function provisionApplicationScaffold(payload) {
|
|
|
5937
6759
|
"Platform scaffold provisioning requires a service id. Re-run without --no-api or pass --service."
|
|
5938
6760
|
);
|
|
5939
6761
|
}
|
|
6762
|
+
assertProvisioningPayloadComplete(payload);
|
|
5940
6763
|
const environment = selectedEnvironment();
|
|
5941
6764
|
const credential = await refreshCredentialIfNeeded(environment);
|
|
5942
6765
|
if (!credential) {
|
|
@@ -5953,12 +6776,20 @@ async function provisionApplicationScaffold(payload) {
|
|
|
5953
6776
|
const identity = await client.client.introspect();
|
|
5954
6777
|
const config = await readConfig();
|
|
5955
6778
|
const profile = resolvedProfileConfig(config, environment);
|
|
6779
|
+
const authOrigin = sanitizeOrigin(profile.serviceUrls?.auth ?? defaultAuthOrigin(environment));
|
|
6780
|
+
assertServiceUrlForEnvironment("auth", authOrigin, environment);
|
|
5956
6781
|
const developersOrigin = sanitizeOrigin(profile.serviceUrls?.developers ?? "https://developers.topolo.app");
|
|
5957
6782
|
assertServiceUrlForEnvironment("developers", developersOrigin, environment);
|
|
6783
|
+
const developersServiceId = await resolveServiceIdBySlug(authOrigin, DEVELOPERS_SERVICE_SLUG);
|
|
6784
|
+
if (!developersServiceId) {
|
|
6785
|
+
throw new Error(
|
|
6786
|
+
`Could not resolve Developers service id from Auth slug ${DEVELOPERS_SERVICE_SLUG}.`
|
|
6787
|
+
);
|
|
6788
|
+
}
|
|
5958
6789
|
const endpoint = payload.target === "platform" ? "/api/developer-console/admin/first-party-app-scaffolds" : "/api/developer-console/apps/scaffold";
|
|
5959
6790
|
const headers = new Headers({
|
|
5960
6791
|
"Content-Type": "application/json",
|
|
5961
|
-
"X-Service-ID":
|
|
6792
|
+
"X-Service-ID": developersServiceId,
|
|
5962
6793
|
"X-Topolo-Client": `topolo-cli/${CLI_VERSION}`
|
|
5963
6794
|
});
|
|
5964
6795
|
const agentName = resolveAgentName(profile.agentName);
|
|
@@ -6030,11 +6861,21 @@ function buildProvisioningRequest(payload) {
|
|
|
6030
6861
|
surfaceModel: payload.surfaceModel,
|
|
6031
6862
|
containerAppId: payload.containerAppId,
|
|
6032
6863
|
distributionModel: payload.distributionModel,
|
|
6864
|
+
capabilities: payload.capabilities,
|
|
6865
|
+
callbackUrls: [
|
|
6866
|
+
"http://localhost:5173/auth/callback",
|
|
6867
|
+
payload.application.productionUrl ? `${payload.application.productionUrl.replace(/\/$/, "")}/auth/callback` : null
|
|
6868
|
+
].filter(Boolean),
|
|
6869
|
+
healthEndpoint: payload.application.productionUrl ? `${payload.application.productionUrl.replace(/\/$/, "")}/api/health` : null,
|
|
6870
|
+
runtimeStrategy: "external",
|
|
6871
|
+
requestedScopes: payload.application.serviceId ? ["openid", "profile", "email", "organization:read", "entitlements:read", `${payload.application.id}:read`] : ["openid", "profile", "email", "organization:read"],
|
|
6872
|
+
marketplaceIntent: payload.distributionModel === "marketplace" ? "submit_when_ready" : "not_requested",
|
|
6033
6873
|
applicationId: payload.application.id,
|
|
6034
6874
|
appDir: payload.application.appDir,
|
|
6035
6875
|
packageName: payload.application.packageName,
|
|
6036
6876
|
name: payload.application.name,
|
|
6037
6877
|
productionUrl: payload.application.productionUrl,
|
|
6878
|
+
serviceSlug: payload.application.serviceSlug,
|
|
6038
6879
|
serviceId: payload.application.serviceId
|
|
6039
6880
|
};
|
|
6040
6881
|
}
|
|
@@ -6051,6 +6892,19 @@ function resolveProvisioningError(envelope, status, target) {
|
|
|
6051
6892
|
function sanitizeOrigin(value) {
|
|
6052
6893
|
return value.replace(/\/+$/, "");
|
|
6053
6894
|
}
|
|
6895
|
+
function defaultAuthOrigin(environment) {
|
|
6896
|
+
return environment === "staging" ? "https://auth.stg.topolo.us" : "https://auth.topolo.app";
|
|
6897
|
+
}
|
|
6898
|
+
async function resolveServiceIdBySlug(authOrigin, slug) {
|
|
6899
|
+
const response = await fetch(
|
|
6900
|
+
`${authOrigin}/api/services/by-slug/${encodeURIComponent(slug)}`,
|
|
6901
|
+
{ headers: { Accept: "application/json" } }
|
|
6902
|
+
).catch(() => null);
|
|
6903
|
+
if (!response?.ok) return null;
|
|
6904
|
+
const payload = await response.json().catch(() => null);
|
|
6905
|
+
const serviceId = payload?.data?.service_id;
|
|
6906
|
+
return typeof serviceId === "string" && serviceId.trim() ? serviceId.trim() : null;
|
|
6907
|
+
}
|
|
6054
6908
|
function pickRecord(value) {
|
|
6055
6909
|
return value && typeof value === "object" ? value : null;
|
|
6056
6910
|
}
|
|
@@ -6068,8 +6922,31 @@ function readRequiredString(value, message) {
|
|
|
6068
6922
|
}
|
|
6069
6923
|
return resolved;
|
|
6070
6924
|
}
|
|
6925
|
+
async function applyProvisionedServiceId(manifestPath, serviceId) {
|
|
6926
|
+
let raw;
|
|
6927
|
+
try {
|
|
6928
|
+
raw = await readFile2(manifestPath, "utf8");
|
|
6929
|
+
} catch {
|
|
6930
|
+
return { changed: false, previous: null };
|
|
6931
|
+
}
|
|
6932
|
+
let json2;
|
|
6933
|
+
try {
|
|
6934
|
+
json2 = JSON.parse(raw);
|
|
6935
|
+
} catch {
|
|
6936
|
+
return { changed: false, previous: null };
|
|
6937
|
+
}
|
|
6938
|
+
const previous = typeof json2.serviceId === "string" ? json2.serviceId : null;
|
|
6939
|
+
if (previous === serviceId) {
|
|
6940
|
+
return { changed: false, previous };
|
|
6941
|
+
}
|
|
6942
|
+
json2.serviceId = serviceId;
|
|
6943
|
+
await writeFile3(manifestPath, `${JSON.stringify(json2, null, 2)}
|
|
6944
|
+
`, "utf8");
|
|
6945
|
+
return { changed: true, previous };
|
|
6946
|
+
}
|
|
6071
6947
|
|
|
6072
6948
|
// src/commands/apps.ts
|
|
6949
|
+
import { join as join3 } from "path";
|
|
6073
6950
|
var APPLICATION_IDS = Object.keys(APPLICATIONS).sort();
|
|
6074
6951
|
function registerAppsCommand(program2) {
|
|
6075
6952
|
const apps = program2.command("apps").description("List every Topolo platform application known to the SDK catalog").option("--json", "Output JSON", true).option("--no-json", "Output human-readable table").action((opts) => {
|
|
@@ -6147,7 +7024,7 @@ function registerAppsCommand(program2) {
|
|
|
6147
7024
|
"Developer app surface model: web_app, mobile_container, mobile_module, or messaging_surface"
|
|
6148
7025
|
).option("--container-app-id <appId>", "Container app id when --surface-model mobile_module is used").option("--write", "Write the generated scaffold files to disk").option("--repo-root <path>", "Directory where scaffold files should be written").option("--force", "Overwrite existing scaffold files when --write is set").option("--with-api", "Include API/service files for developer scaffolds").option("--no-api", "Do not include API/service requirements").option("--no-browser", "Do not include browser-shell requirements").option(
|
|
6149
7026
|
"--provision",
|
|
6150
|
-
"
|
|
7027
|
+
"Register the app in the current Topolo developer workspace using the authenticated session"
|
|
6151
7028
|
).action(async (application, opts) => {
|
|
6152
7029
|
const mode = opts.json === false || process.argv.includes("--no-json") ? "pretty" : resolveOutputMode(opts.json);
|
|
6153
7030
|
try {
|
|
@@ -6166,20 +7043,27 @@ function registerAppsCommand(program2) {
|
|
|
6166
7043
|
surfaceModel: resolveScaffoldSurfaceModel(opts.surfaceModel),
|
|
6167
7044
|
containerAppId: opts.containerAppId
|
|
6168
7045
|
});
|
|
6169
|
-
if (opts.provision && !opts.write) {
|
|
6170
|
-
throw new Error("--provision requires --write so remote developer records match written scaffold files.");
|
|
6171
|
-
}
|
|
6172
7046
|
const writeResult = opts.write ? await writeApplicationScaffold(payload, { repoRoot: opts.repoRoot, force: opts.force }) : null;
|
|
6173
7047
|
const provisionResult = opts.provision ? await provisionApplicationScaffold(payload) : null;
|
|
7048
|
+
let serviceIdWriteBack = null;
|
|
7049
|
+
if (writeResult && provisionResult?.authService?.id) {
|
|
7050
|
+
const manifestFile = writeResult.files.find((f) => f.path.endsWith("topolo.app.json"));
|
|
7051
|
+
if (manifestFile) {
|
|
7052
|
+
const manifestPath = join3(writeResult.root, manifestFile.path);
|
|
7053
|
+
const result = await applyProvisionedServiceId(manifestPath, provisionResult.authService.id);
|
|
7054
|
+
serviceIdWriteBack = { ...result, path: manifestFile.path };
|
|
7055
|
+
}
|
|
7056
|
+
}
|
|
6174
7057
|
const output = {
|
|
6175
7058
|
...payload,
|
|
6176
7059
|
...writeResult ? { write: writeResult } : {},
|
|
6177
|
-
...provisionResult ? { provision: provisionResult } : {}
|
|
7060
|
+
...provisionResult ? { provision: provisionResult } : {},
|
|
7061
|
+
...serviceIdWriteBack ? { serviceIdWriteBack } : {}
|
|
6178
7062
|
};
|
|
6179
7063
|
emit(
|
|
6180
7064
|
output,
|
|
6181
7065
|
mode,
|
|
6182
|
-
() => formatScaffold(payload, writeResult, provisionResult)
|
|
7066
|
+
() => formatScaffold(payload, writeResult, provisionResult, serviceIdWriteBack)
|
|
6183
7067
|
);
|
|
6184
7068
|
} catch (err) {
|
|
6185
7069
|
fail(err, mode);
|
|
@@ -6413,7 +7297,7 @@ function formatAudit(payload) {
|
|
|
6413
7297
|
}
|
|
6414
7298
|
return lines.join("\n");
|
|
6415
7299
|
}
|
|
6416
|
-
function formatScaffold(payload, writeResult = null, provisionResult = null) {
|
|
7300
|
+
function formatScaffold(payload, writeResult = null, provisionResult = null, serviceIdWriteBack = null) {
|
|
6417
7301
|
const lines = [
|
|
6418
7302
|
`Version: ${payload.version}`,
|
|
6419
7303
|
`Target: ${payload.target}`,
|
|
@@ -6473,6 +7357,11 @@ function formatScaffold(payload, writeResult = null, provisionResult = null) {
|
|
|
6473
7357
|
);
|
|
6474
7358
|
}
|
|
6475
7359
|
}
|
|
7360
|
+
if (serviceIdWriteBack) {
|
|
7361
|
+
lines.push(
|
|
7362
|
+
serviceIdWriteBack.changed ? ` - service id written back to ${serviceIdWriteBack.path} (was ${serviceIdWriteBack.previous ?? "unset"})` : ` - service id already current in ${serviceIdWriteBack.path}`
|
|
7363
|
+
);
|
|
7364
|
+
}
|
|
6476
7365
|
return lines.join("\n");
|
|
6477
7366
|
}
|
|
6478
7367
|
function resolveScaffoldAudience(value) {
|
|
@@ -6534,7 +7423,7 @@ function registerPlatformCommand(program2) {
|
|
|
6534
7423
|
).option("--json", "Output JSON").option("--no-json", "Output human-readable checklist").option("--app-dir <path>", "Application directory under PlatformApplications").option("--name <name>", "Application display/package source name").option("--package-name <name>", "package.json name").option("--production-url <url>", "Production browser URL").option("--service <id>", "Stable SDK/Auth service id for API-backed apps").option("--portfolio <portfolio>", "First-party portfolio: platform or personal").option(
|
|
6535
7424
|
"--surface-model <surfaceModel>",
|
|
6536
7425
|
"First-party surface model: web_app, mobile_container, mobile_module, or messaging_surface"
|
|
6537
|
-
).option("--container-app-id <appId>", "Container app id when --surface-model mobile_module is used").option("--write", "Write the generated scaffold files to disk").option("--repo-root <path>", "Directory where scaffold files should be written").option("--force", "Overwrite existing scaffold files when --write is set").option("--with-api", "Include the API/service baseline").option("--no-api", "Do not include API/service requirements").option("--no-browser", "Do not include browser-shell requirements").option(
|
|
7426
|
+
).option("--container-app-id <appId>", "Container app id when --surface-model mobile_module is used").option("--profile <profile>", "First-party scaffold profile such as spatial-assets").option("--with-d1", "Declare a D1 binding in the first-party scaffold").option("--with-r2", "Declare an R2 binding in the first-party scaffold").option("--with-queue", "Declare a Queue binding in the first-party scaffold").option("--with-workflow", "Declare a Workflow binding in the first-party scaffold").option("--with-container-processing", "Declare container-processing metadata in CloudControl").option("--write", "Write the generated scaffold files to disk").option("--repo-root <path>", "Directory where scaffold files should be written").option("--force", "Overwrite existing scaffold files when --write is set").option("--with-api", "Include the API/service baseline").option("--no-api", "Do not include API/service requirements").option("--no-browser", "Do not include browser-shell requirements").option(
|
|
6538
7427
|
"--provision",
|
|
6539
7428
|
"Provision the generated first-party app into Topolo Developers and Auth using the authenticated Topolo admin session"
|
|
6540
7429
|
).option(
|
|
@@ -6543,10 +7432,14 @@ function registerPlatformCommand(program2) {
|
|
|
6543
7432
|
).option("--workflow-only", "Output only the generated GitHub Actions workflow in human mode").action(async (application, opts) => {
|
|
6544
7433
|
const mode = opts.json === false || process.argv.includes("--no-json") ? "pretty" : resolveOutputMode(opts.json);
|
|
6545
7434
|
try {
|
|
6546
|
-
await requirePlatformAdminSession();
|
|
6547
7435
|
if (opts.workflowOnly && opts.write) {
|
|
6548
7436
|
throw new Error("--workflow-only cannot be used with --write.");
|
|
6549
7437
|
}
|
|
7438
|
+
const provisionRequested = process.argv.includes("--provision");
|
|
7439
|
+
const shouldProvision = Boolean(opts.write) && opts.provision !== false;
|
|
7440
|
+
if (shouldProvision) {
|
|
7441
|
+
await requirePlatformAdminSession();
|
|
7442
|
+
}
|
|
6550
7443
|
const api = resolveScaffoldApiOption(opts.api, opts.withApi);
|
|
6551
7444
|
const payload = applicationScaffoldPayload(application, {
|
|
6552
7445
|
appDir: opts.appDir,
|
|
@@ -6559,13 +7452,17 @@ function registerPlatformCommand(program2) {
|
|
|
6559
7452
|
target: "platform",
|
|
6560
7453
|
portfolio: resolvePlatformPortfolio(opts.portfolio),
|
|
6561
7454
|
surfaceModel: resolvePlatformSurfaceModel(opts.surfaceModel),
|
|
6562
|
-
containerAppId: opts.containerAppId
|
|
7455
|
+
containerAppId: opts.containerAppId,
|
|
7456
|
+
profile: opts.profile,
|
|
7457
|
+
withD1: opts.withD1,
|
|
7458
|
+
withR2: opts.withR2,
|
|
7459
|
+
withQueue: opts.withQueue,
|
|
7460
|
+
withWorkflow: opts.withWorkflow,
|
|
7461
|
+
withContainerProcessing: opts.withContainerProcessing
|
|
6563
7462
|
});
|
|
6564
7463
|
if (opts.workflowOnly && !payload.ciWorkflow) {
|
|
6565
7464
|
throw new Error("--workflow-only is only available for first-party scaffolds.");
|
|
6566
7465
|
}
|
|
6567
|
-
const provisionRequested = process.argv.includes("--provision");
|
|
6568
|
-
const shouldProvision = Boolean(opts.write) && opts.provision !== false;
|
|
6569
7466
|
if (provisionRequested && !opts.write) {
|
|
6570
7467
|
throw new Error("--provision requires --write so remote first-party records match written scaffold files.");
|
|
6571
7468
|
}
|
|
@@ -6698,6 +7595,14 @@ function isPlatformAdminIdentity(identity) {
|
|
|
6698
7595
|
const orgSlug = String(identity?.organization?.slug || "").trim().toLowerCase();
|
|
6699
7596
|
return orgSlug === "admin" && (role === "platform_super_admin" || role === "platform_admin");
|
|
6700
7597
|
}
|
|
7598
|
+
function isTopoloWorkspaceAdminIdentity(identity) {
|
|
7599
|
+
const role = String(identity?.user?.role || "").trim().toLowerCase();
|
|
7600
|
+
const orgSlug = String(identity?.organization?.slug || "").trim().toLowerCase();
|
|
7601
|
+
return orgSlug === "topolo" && (role === "owner" || role === "admin" || role === "super_admin");
|
|
7602
|
+
}
|
|
7603
|
+
function canProvisionFirstPartyScaffoldIdentity(identity) {
|
|
7604
|
+
return isPlatformAdminIdentity(identity) || isTopoloWorkspaceAdminIdentity(identity);
|
|
7605
|
+
}
|
|
6701
7606
|
async function requirePlatformAdminSession() {
|
|
6702
7607
|
const client = await buildClient();
|
|
6703
7608
|
if (!client) {
|
|
@@ -6706,9 +7611,9 @@ async function requirePlatformAdminSession() {
|
|
|
6706
7611
|
);
|
|
6707
7612
|
}
|
|
6708
7613
|
const identity = await client.client.introspect();
|
|
6709
|
-
if (!
|
|
7614
|
+
if (!canProvisionFirstPartyScaffoldIdentity(identity)) {
|
|
6710
7615
|
throw new Error(
|
|
6711
|
-
"`topolo platform ...` is restricted to Topolo platform admins in the Auth `admin` organization."
|
|
7616
|
+
"`topolo platform ...` is restricted to Topolo platform admins in the Auth `admin` organization or owner/admin users in the Topolo developer workspace."
|
|
6712
7617
|
);
|
|
6713
7618
|
}
|
|
6714
7619
|
}
|
|
@@ -6975,6 +7880,301 @@ function formatManifestsTable(manifests) {
|
|
|
6975
7880
|
return lines.join("\n");
|
|
6976
7881
|
}
|
|
6977
7882
|
|
|
7883
|
+
// src/commands/oauth.ts
|
|
7884
|
+
function registerOAuthCommands(program2) {
|
|
7885
|
+
const oauth = program2.command("oauth").description("Manage Topolo OAuth resources.");
|
|
7886
|
+
const clients = oauth.command("clients").description("Manage Login with Topolo OAuth clients.");
|
|
7887
|
+
clients.command("list").description("List OAuth clients owned by the authenticated developer organization.").option("--json", "Emit JSON output").action(async (options) => {
|
|
7888
|
+
const mode = resolveOutputMode(options.json);
|
|
7889
|
+
try {
|
|
7890
|
+
const payload = await authFetchJson("/api/developer-oauth/clients");
|
|
7891
|
+
emit(payload, mode, () => {
|
|
7892
|
+
const clients2 = Array.isArray(payload.data?.clients) ? payload.data.clients : [];
|
|
7893
|
+
if (clients2.length === 0) return "No OAuth clients found.";
|
|
7894
|
+
return clients2.map((client) => `${client.client_id} ${client.name} ${client.status}`).join("\n");
|
|
7895
|
+
});
|
|
7896
|
+
} catch (err) {
|
|
7897
|
+
fail(err, mode);
|
|
7898
|
+
}
|
|
7899
|
+
});
|
|
7900
|
+
clients.command("create").description("Create a Login with Topolo OAuth client.").requiredOption("--name <name>", "Client display name").requiredOption("--developer-id <id>", "Topolo developer id that owns this client").option("--developer-app-id <id>", "Developer app id to associate with this client").option("--description <description>", "Client description").option("--redirect-uri <uri...>", "Allowed callback URI. Repeat or pass a space-separated list.").option("--scope <scope...>", "Allowed scope. Defaults to openid profile email.").option("--public", "Create a public PKCE client instead of a confidential client").option("--homepage-url <url>", "Client homepage URL").option("--logo-url <url>", "Client logo URL").option("--json", "Emit JSON output").action(async (options) => {
|
|
7901
|
+
const mode = resolveOutputMode(options.json);
|
|
7902
|
+
try {
|
|
7903
|
+
const payload = createOAuthClientPayload(options);
|
|
7904
|
+
const response = await authFetchJson("/api/developer-oauth/clients", {
|
|
7905
|
+
method: "POST",
|
|
7906
|
+
body: JSON.stringify(payload)
|
|
7907
|
+
});
|
|
7908
|
+
emit(response, mode, () => {
|
|
7909
|
+
const client = response.data?.client;
|
|
7910
|
+
const secret = response.data?.client_secret;
|
|
7911
|
+
return [
|
|
7912
|
+
`Created OAuth client ${client?.client_id ?? ""}`,
|
|
7913
|
+
secret ? `Client secret: ${secret}` : "Public client: no secret issued"
|
|
7914
|
+
].join("\n");
|
|
7915
|
+
});
|
|
7916
|
+
} catch (err) {
|
|
7917
|
+
fail(err, mode);
|
|
7918
|
+
}
|
|
7919
|
+
});
|
|
7920
|
+
clients.command("revoke").description("Revoke an OAuth client for all users.").argument("<id>", "OAuth client record id, for example oac_...").option("--json", "Emit JSON output").action(async (id, options) => {
|
|
7921
|
+
const mode = resolveOutputMode(options.json);
|
|
7922
|
+
try {
|
|
7923
|
+
const payload = await authFetchJson(`/api/developer-oauth/clients/${encodeURIComponent(id)}`, {
|
|
7924
|
+
method: "DELETE"
|
|
7925
|
+
});
|
|
7926
|
+
emit(payload, mode, () => `Revoked OAuth client ${id}`);
|
|
7927
|
+
} catch (err) {
|
|
7928
|
+
fail(err, mode);
|
|
7929
|
+
}
|
|
7930
|
+
});
|
|
7931
|
+
clients.command("rotate-secret").description("Rotate a confidential OAuth client secret.").argument("<id>", "OAuth client record id, for example oac_...").option("--json", "Emit JSON output").action(async (id, options) => {
|
|
7932
|
+
const mode = resolveOutputMode(options.json);
|
|
7933
|
+
try {
|
|
7934
|
+
const payload = await authFetchJson(`/api/developer-oauth/clients/${encodeURIComponent(id)}/rotate-secret`, {
|
|
7935
|
+
method: "POST"
|
|
7936
|
+
});
|
|
7937
|
+
emit(payload, mode, () => `New client secret: ${payload.data?.client_secret ?? ""}`);
|
|
7938
|
+
} catch (err) {
|
|
7939
|
+
fail(err, mode);
|
|
7940
|
+
}
|
|
7941
|
+
});
|
|
7942
|
+
}
|
|
7943
|
+
function createOAuthClientPayload(options) {
|
|
7944
|
+
const redirectUris = normalizeList(options.redirectUri);
|
|
7945
|
+
if (redirectUris.length === 0) {
|
|
7946
|
+
throw new Error("At least one --redirect-uri is required.");
|
|
7947
|
+
}
|
|
7948
|
+
return {
|
|
7949
|
+
name: options.name,
|
|
7950
|
+
developer_id: options.developerId,
|
|
7951
|
+
developer_app_id: options.developerAppId,
|
|
7952
|
+
description: options.description,
|
|
7953
|
+
client_type: options.public ? "public" : "confidential",
|
|
7954
|
+
redirect_uris: redirectUris,
|
|
7955
|
+
allowed_scopes: normalizeList(options.scope, ["openid", "profile", "email"]),
|
|
7956
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
7957
|
+
homepage_url: options.homepageUrl,
|
|
7958
|
+
logo_url: options.logoUrl
|
|
7959
|
+
};
|
|
7960
|
+
}
|
|
7961
|
+
async function authFetchJson(path4, init = {}) {
|
|
7962
|
+
const environment = selectedEnvironment();
|
|
7963
|
+
const credential = await refreshCredentialIfNeeded(environment);
|
|
7964
|
+
if (!credential) {
|
|
7965
|
+
const err = new Error("Topolo credentials are required.");
|
|
7966
|
+
err.code = "auth_error";
|
|
7967
|
+
throw err;
|
|
7968
|
+
}
|
|
7969
|
+
const config = await readConfig();
|
|
7970
|
+
const profile = resolvedProfileConfig(config, environment);
|
|
7971
|
+
const authBaseUrl = (profile.serviceUrls?.auth ?? "https://auth.topolo.app").replace(/\/+$/, "");
|
|
7972
|
+
const headers = new Headers(init.headers);
|
|
7973
|
+
headers.set("Accept", "application/json");
|
|
7974
|
+
if (init.body && !headers.has("Content-Type")) headers.set("Content-Type", "application/json");
|
|
7975
|
+
if (credential.kind === "api_key") {
|
|
7976
|
+
headers.set("Authorization", `Bearer ${credential.apiKey}`);
|
|
7977
|
+
} else {
|
|
7978
|
+
headers.set("Authorization", `Bearer ${credential.accessToken}`);
|
|
7979
|
+
}
|
|
7980
|
+
const response = await fetch(`${authBaseUrl}${path4}`, { ...init, headers });
|
|
7981
|
+
const text = await response.text();
|
|
7982
|
+
const payload = text ? JSON.parse(text) : {};
|
|
7983
|
+
if (!response.ok || payload.success === false) {
|
|
7984
|
+
const err = new Error(payload.message || payload.error_description || payload.error || "Topolo Auth request failed");
|
|
7985
|
+
err.code = payload.code || payload.error;
|
|
7986
|
+
throw err;
|
|
7987
|
+
}
|
|
7988
|
+
return payload;
|
|
7989
|
+
}
|
|
7990
|
+
function normalizeList(values, fallback = []) {
|
|
7991
|
+
const source = values && values.length > 0 ? values : fallback;
|
|
7992
|
+
return Array.from(new Set(source.flatMap((value) => value.split(/[\s,]+/)).map((value) => value.trim()).filter(Boolean)));
|
|
7993
|
+
}
|
|
7994
|
+
|
|
7995
|
+
// src/commands/integrations.ts
|
|
7996
|
+
import { readdir, readFile as readFile3, stat } from "fs/promises";
|
|
7997
|
+
import { join as join4, relative as relative2 } from "path";
|
|
7998
|
+
function registerIntegrationsCommand(program2) {
|
|
7999
|
+
const integrations = program2.command("integrations").description("Generate and validate Topolo integration handoffs.");
|
|
8000
|
+
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) => {
|
|
8001
|
+
const mode = resolveOutputMode(options.json);
|
|
8002
|
+
try {
|
|
8003
|
+
if (integration !== "login-with-topolo") throw new Error(`Unknown integration prompt: ${integration}`);
|
|
8004
|
+
const framework = normalizeFramework(options.framework);
|
|
8005
|
+
const prompt = buildLoginWithTopoloPrompt(framework);
|
|
8006
|
+
emit({ integration, framework, prompt }, mode, () => prompt);
|
|
8007
|
+
} catch (err) {
|
|
8008
|
+
fail(err, mode);
|
|
8009
|
+
}
|
|
8010
|
+
});
|
|
8011
|
+
integrations.command("check").description("Check whether a repo has the Login with Topolo integration pieces.").argument("<integration>", "Integration check id, currently login-with-topolo").option("--repo-root <path>", "Repository root to inspect", ".").option("--json", "Emit JSON output").action(async (integration, options) => {
|
|
8012
|
+
const mode = resolveOutputMode(options.json);
|
|
8013
|
+
try {
|
|
8014
|
+
if (integration !== "login-with-topolo") throw new Error(`Unknown integration check: ${integration}`);
|
|
8015
|
+
const result = await checkLoginWithTopolo(options.repoRoot ?? ".");
|
|
8016
|
+
emit(result, mode, () => formatCheckResult(result));
|
|
8017
|
+
if (!result.passed) process.exitCode = 1;
|
|
8018
|
+
} catch (err) {
|
|
8019
|
+
fail(err, mode);
|
|
8020
|
+
}
|
|
8021
|
+
});
|
|
8022
|
+
}
|
|
8023
|
+
function buildLoginWithTopoloPrompt(framework = "auto") {
|
|
8024
|
+
return [
|
|
8025
|
+
"You are adding Login with Topolo to an existing third-party application.",
|
|
8026
|
+
"",
|
|
8027
|
+
"Objective:",
|
|
8028
|
+
"- Add Topolo as an additional OAuth/OIDC login provider without replacing existing Google, Facebook, email, SSO, or password login.",
|
|
8029
|
+
"- Use @topolo/login for provider URL generation, token exchange, userinfo, ID-token verification, refresh, and account-link payloads.",
|
|
8030
|
+
"- Use @topolo/sdk only after login when calling Topolo APIs with the Topolo access_token.",
|
|
8031
|
+
"",
|
|
8032
|
+
"Implementation steps:",
|
|
8033
|
+
"1. Inspect the current auth stack, session store, user model, workspace/org model, and callback route conventions.",
|
|
8034
|
+
`2. Select the integration path: ${framework === "auto" ? "auto-detect Auth.js/NextAuth, Express/Passport, or generic OIDC" : framework}.`,
|
|
8035
|
+
"3. Add env vars: TOPOLO_CLIENT_ID, TOPOLO_CLIENT_SECRET when confidential, TOPOLO_AUTH_BASE_URL, and TOPOLO_REDIRECT_URI.",
|
|
8036
|
+
"4. Register the callback URI in TopoloDevelopers or run `topolo oauth clients create`.",
|
|
8037
|
+
"5. Add the Topolo callback route and exchange the authorization code with PKCE.",
|
|
8038
|
+
"6. Fetch userinfo and link it to a local account record with local_user_id, topolo_user_id, topolo_org_id, topolo_context_id, topolo_service_id, connected_at, and last_seen_at.",
|
|
8039
|
+
"7. Never auto-merge solely by matching email unless the user is already signed into the local account or explicitly confirms linking.",
|
|
8040
|
+
"8. Store refresh tokens according to the app\u2019s existing token-security pattern and refresh Topolo tokens when calling Topolo APIs.",
|
|
8041
|
+
"9. Preserve all existing login providers and add tests for callback exchange, account linking, refresh handling, and disconnect/re-login.",
|
|
8042
|
+
"10. Run `topolo integrations check login-with-topolo --repo-root . --json` and fix every failing check."
|
|
8043
|
+
].join("\n");
|
|
8044
|
+
}
|
|
8045
|
+
async function checkLoginWithTopolo(repoRoot) {
|
|
8046
|
+
const files = await collectFiles(repoRoot);
|
|
8047
|
+
const packageJson2 = await readPackageJson2(repoRoot);
|
|
8048
|
+
const haystack = files.map((file) => `${file.path}
|
|
8049
|
+
${file.content}`).join("\n");
|
|
8050
|
+
const framework = detectFramework(packageJson2, haystack);
|
|
8051
|
+
const checks = [
|
|
8052
|
+
makeCheck(
|
|
8053
|
+
"package",
|
|
8054
|
+
"@topolo/login dependency",
|
|
8055
|
+
topoloLoginPackageEvidence(packageJson2, files),
|
|
8056
|
+
true
|
|
8057
|
+
),
|
|
8058
|
+
makeCheck(
|
|
8059
|
+
"env",
|
|
8060
|
+
"Topolo OAuth env vars",
|
|
8061
|
+
matchEvidence(haystack, ["TOPOLO_CLIENT_ID", "TOPOLO_AUTH_BASE_URL", "TOPOLO_REDIRECT_URI"]),
|
|
8062
|
+
true
|
|
8063
|
+
),
|
|
8064
|
+
makeCheck(
|
|
8065
|
+
"callback",
|
|
8066
|
+
"OAuth callback route",
|
|
8067
|
+
matchEvidence(haystack, ["topolo/callback", "auth/topolo/callback", "exchangeTopoloCode", "createTopoloAuthJsProvider"]),
|
|
8068
|
+
true
|
|
8069
|
+
),
|
|
8070
|
+
makeCheck(
|
|
8071
|
+
"account_link",
|
|
8072
|
+
"Account-linking model",
|
|
8073
|
+
matchEvidence(haystack, ["topolo_user_id", "topolo_org_id", "createAccountLinkPayload"]),
|
|
8074
|
+
true
|
|
8075
|
+
),
|
|
8076
|
+
makeCheck(
|
|
8077
|
+
"refresh",
|
|
8078
|
+
"Token refresh handling",
|
|
8079
|
+
matchEvidence(haystack, ["refreshTopoloToken", "refresh_token", "expires_at"]),
|
|
8080
|
+
false
|
|
8081
|
+
),
|
|
8082
|
+
makeCheck(
|
|
8083
|
+
"safety",
|
|
8084
|
+
"Existing auth preserved",
|
|
8085
|
+
matchEvidence(haystack, ["google", "facebook", "email", "password", "credentials"]),
|
|
8086
|
+
false
|
|
8087
|
+
)
|
|
8088
|
+
];
|
|
8089
|
+
return {
|
|
8090
|
+
integration: "login-with-topolo",
|
|
8091
|
+
framework,
|
|
8092
|
+
passed: checks.every((check) => check.status !== "fail"),
|
|
8093
|
+
checks
|
|
8094
|
+
};
|
|
8095
|
+
}
|
|
8096
|
+
function normalizeFramework(value) {
|
|
8097
|
+
if (value === "auto" || value === "authjs" || value === "express" || value === "generic") return value;
|
|
8098
|
+
throw new Error("--framework must be auto, authjs, express, or generic.");
|
|
8099
|
+
}
|
|
8100
|
+
function makeCheck(id, label, evidence, required) {
|
|
8101
|
+
if (evidence.length > 0) return { id, label, status: "pass", evidence };
|
|
8102
|
+
return { id, label, status: required ? "fail" : "warn", evidence: [] };
|
|
8103
|
+
}
|
|
8104
|
+
function matchEvidence(haystack, needles) {
|
|
8105
|
+
return needles.filter((needle) => haystack.toLowerCase().includes(needle.toLowerCase()));
|
|
8106
|
+
}
|
|
8107
|
+
function topoloLoginPackageEvidence(rootPackageJson, files) {
|
|
8108
|
+
const evidence = [];
|
|
8109
|
+
if (declaresTopoloLogin(rootPackageJson)) evidence.push("package.json declares @topolo/login");
|
|
8110
|
+
for (const file of files) {
|
|
8111
|
+
if (!file.path.endsWith("package.json") || file.path === "package.json") continue;
|
|
8112
|
+
try {
|
|
8113
|
+
if (declaresTopoloLogin(JSON.parse(file.content))) {
|
|
8114
|
+
evidence.push(`${file.path} declares @topolo/login`);
|
|
8115
|
+
}
|
|
8116
|
+
} catch {
|
|
8117
|
+
}
|
|
8118
|
+
}
|
|
8119
|
+
return evidence;
|
|
8120
|
+
}
|
|
8121
|
+
function declaresTopoloLogin(packageJson2) {
|
|
8122
|
+
return Boolean(
|
|
8123
|
+
packageJson2?.dependencies?.["@topolo/login"] || packageJson2?.devDependencies?.["@topolo/login"] || packageJson2?.peerDependencies?.["@topolo/login"]
|
|
8124
|
+
);
|
|
8125
|
+
}
|
|
8126
|
+
function detectFramework(packageJson2, haystack) {
|
|
8127
|
+
const deps = {
|
|
8128
|
+
...packageJson2?.dependencies ?? {},
|
|
8129
|
+
...packageJson2?.devDependencies ?? {}
|
|
8130
|
+
};
|
|
8131
|
+
if (deps["next-auth"] || deps["@auth/core"] || deps["@auth/nextjs"] || haystack.includes("NextAuth")) return "authjs";
|
|
8132
|
+
if (deps.express || deps.passport || haystack.includes("passport.use")) return "express";
|
|
8133
|
+
if (haystack.includes("Gemfile") || haystack.includes("omniauth")) return "rails";
|
|
8134
|
+
if (haystack.includes("Laravel") || haystack.includes("Socialite")) return "laravel";
|
|
8135
|
+
if (haystack.includes("django") || haystack.includes("allauth")) return "django";
|
|
8136
|
+
return "generic";
|
|
8137
|
+
}
|
|
8138
|
+
async function readPackageJson2(repoRoot) {
|
|
8139
|
+
try {
|
|
8140
|
+
return JSON.parse(await readFile3(join4(repoRoot, "package.json"), "utf8"));
|
|
8141
|
+
} catch {
|
|
8142
|
+
return null;
|
|
8143
|
+
}
|
|
8144
|
+
}
|
|
8145
|
+
async function collectFiles(repoRoot) {
|
|
8146
|
+
const result = [];
|
|
8147
|
+
const root = repoRoot;
|
|
8148
|
+
async function walk(dir) {
|
|
8149
|
+
if (result.length > 2e3) return;
|
|
8150
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
8151
|
+
for (const entry of entries) {
|
|
8152
|
+
const fullPath = join4(dir, entry.name);
|
|
8153
|
+
if (entry.isDirectory()) {
|
|
8154
|
+
if ([".git", "node_modules", "dist", "build", ".next", "coverage"].includes(entry.name)) continue;
|
|
8155
|
+
await walk(fullPath);
|
|
8156
|
+
continue;
|
|
8157
|
+
}
|
|
8158
|
+
if (!entry.isFile() || !isInspectable(entry.name)) continue;
|
|
8159
|
+
const info = await stat(fullPath);
|
|
8160
|
+
if (info.size > 25e4) continue;
|
|
8161
|
+
result.push({
|
|
8162
|
+
path: relative2(root, fullPath),
|
|
8163
|
+
content: await readFile3(fullPath, "utf8")
|
|
8164
|
+
});
|
|
8165
|
+
}
|
|
8166
|
+
}
|
|
8167
|
+
await walk(root);
|
|
8168
|
+
return result;
|
|
8169
|
+
}
|
|
8170
|
+
function isInspectable(name) {
|
|
8171
|
+
return /\.(ts|tsx|js|jsx|mjs|cjs|json|md|env|py|rb|php|go|java|cs|rs|sql)$/i.test(name) || ["Gemfile", "Procfile", ".env", ".env.example"].includes(name);
|
|
8172
|
+
}
|
|
8173
|
+
function formatCheckResult(result) {
|
|
8174
|
+
const rows = result.checks.map((check) => `${check.status.toUpperCase()} ${check.label}`);
|
|
8175
|
+
return [`Framework: ${result.framework}`, `Passed: ${result.passed ? "yes" : "no"}`, ...rows].join("\n");
|
|
8176
|
+
}
|
|
8177
|
+
|
|
6978
8178
|
// src/index.ts
|
|
6979
8179
|
var program = new Command();
|
|
6980
8180
|
var argv;
|
|
@@ -6998,6 +8198,8 @@ registerWhoamiCommand(program);
|
|
|
6998
8198
|
registerCrmCommands(program);
|
|
6999
8199
|
registerApiCommand(program);
|
|
7000
8200
|
registerAppsCommand(program);
|
|
8201
|
+
registerOAuthCommands(program);
|
|
8202
|
+
registerIntegrationsCommand(program);
|
|
7001
8203
|
registerPlatformCommand(program);
|
|
7002
8204
|
registerServicesCommand(program);
|
|
7003
8205
|
registerSeedCommands(program);
|