@topolo/cli 0.1.20 → 0.1.22
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 +1440 -414
- package/package.json +5 -3
package/dist/index.js
CHANGED
|
@@ -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"
|
|
@@ -1534,12 +1522,12 @@ var APPLICATION_REQUIREMENTS = [
|
|
|
1534
1522
|
summary: "Applications must derive identity, organization, scopes, and service entitlement from TopoloAuth rather than inventing parallel auth state.",
|
|
1535
1523
|
appliesTo: ["browser", "api", "tooling", "agent_surface"],
|
|
1536
1524
|
evidence: [
|
|
1537
|
-
"@topolo/auth-client or @topolo/
|
|
1525
|
+
"@topolo/auth-client or @topolo-io/worker-runtime is used where applicable",
|
|
1538
1526
|
"No public command, tool, or SDK method accepts orgId",
|
|
1539
1527
|
"Service slug and Auth service registration are documented for protected services"
|
|
1540
1528
|
],
|
|
1541
1529
|
implementation: [
|
|
1542
|
-
"Use shared auth packages instead of copying token parsing or browser storage flows",
|
|
1530
|
+
"Use shared auth/runtime packages instead of copying token parsing, gateway, or browser storage flows",
|
|
1543
1531
|
"Derive organization from the credential on every request",
|
|
1544
1532
|
"Route OAuth, refresh, logout, and SSO paths according to the shared Auth contract"
|
|
1545
1533
|
]
|
|
@@ -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,115 @@ 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
|
+
}
|
|
2071
|
+
/**
|
|
2072
|
+
* List the platform service catalog from the live registry
|
|
2073
|
+
* (`GET /api/services/catalog` on TopoloAuth). Credential-aware: the
|
|
2074
|
+
* caller's credential is attached, so an authenticated caller in an
|
|
2075
|
+
* entitled org also receives the `developer_only` services its role
|
|
2076
|
+
* permits; an unauthenticated/agent caller receives `active` only.
|
|
2077
|
+
*
|
|
2078
|
+
* Resilient: on any network/HTTP failure this falls back to the
|
|
2079
|
+
* build-time generated `APPLICATIONS` snapshot so offline/degraded
|
|
2080
|
+
* callers still get a usable (if static) catalog. The live result is
|
|
2081
|
+
* authoritative; the snapshot is a floor, not a cap.
|
|
2082
|
+
*/
|
|
2083
|
+
async listServices() {
|
|
2084
|
+
try {
|
|
2085
|
+
const res = await this.request({ service: "auth", method: "GET", path: "/api/services/catalog" });
|
|
2086
|
+
const raw = res.data?.services ?? res.services;
|
|
2087
|
+
if (Array.isArray(raw) && raw.length > 0) {
|
|
2088
|
+
const entries = [];
|
|
2089
|
+
for (const item of raw) {
|
|
2090
|
+
const s = item;
|
|
2091
|
+
const serviceId = typeof s.service_id === "string" ? s.service_id : typeof s.id === "string" ? s.id : null;
|
|
2092
|
+
if (!serviceId) continue;
|
|
2093
|
+
entries.push({
|
|
2094
|
+
serviceId,
|
|
2095
|
+
slug: typeof s.app_slug === "string" ? s.app_slug : null,
|
|
2096
|
+
name: typeof s.name === "string" ? s.name : null,
|
|
2097
|
+
description: typeof s.description === "string" ? s.description : null,
|
|
2098
|
+
baseUrl: typeof s.base_url === "string" ? s.base_url : null,
|
|
2099
|
+
status: typeof s.status === "string" ? s.status : null,
|
|
2100
|
+
surfaceKind: typeof s.surface_kind === "string" ? s.surface_kind : null,
|
|
2101
|
+
launchable: s.launchable === true
|
|
2102
|
+
});
|
|
2103
|
+
}
|
|
2104
|
+
if (entries.length > 0) return entries;
|
|
2105
|
+
}
|
|
2106
|
+
} catch {
|
|
2107
|
+
}
|
|
2108
|
+
return this.generatedCatalogFallback();
|
|
2109
|
+
}
|
|
2110
|
+
generatedCatalogFallback() {
|
|
2111
|
+
const apps = Object.values(APPLICATIONS);
|
|
2112
|
+
return apps.map((app) => ({
|
|
2113
|
+
serviceId: app.id,
|
|
2114
|
+
slug: app.id,
|
|
2115
|
+
name: app.name ?? app.id,
|
|
2116
|
+
description: null,
|
|
2117
|
+
baseUrl: app.productionUrl ?? null,
|
|
2118
|
+
status: "active",
|
|
2119
|
+
surfaceKind: "application",
|
|
2120
|
+
launchable: Boolean(app.productionUrl)
|
|
2121
|
+
}));
|
|
2122
|
+
}
|
|
2006
2123
|
/**
|
|
2007
2124
|
* Introspect the current credential. Returns the caller's resolved identity,
|
|
2008
2125
|
* organization, and permission set. Used by CLI `whoami` and by MCP to gate
|
|
@@ -2039,6 +2156,26 @@ var TopoloClient = class {
|
|
|
2039
2156
|
};
|
|
2040
2157
|
}
|
|
2041
2158
|
};
|
|
2159
|
+
function resolveConfiguredServiceId(slug, serviceIds) {
|
|
2160
|
+
for (const serviceKey of serviceKeyCandidates(slug)) {
|
|
2161
|
+
const configured = serviceIds?.[serviceKey];
|
|
2162
|
+
if (typeof configured === "string" && configured.trim()) return configured.trim();
|
|
2163
|
+
const envKey = `TOPOLO_SERVICE_ID_${serviceKey.replace(/\./g, "_").toUpperCase()}`;
|
|
2164
|
+
const envValue = typeof process !== "undefined" ? process.env?.[envKey] : void 0;
|
|
2165
|
+
if (typeof envValue === "string" && envValue.trim()) return envValue.trim();
|
|
2166
|
+
}
|
|
2167
|
+
return null;
|
|
2168
|
+
}
|
|
2169
|
+
function serviceKeyCandidates(slug) {
|
|
2170
|
+
const normalized = slug.trim().toLowerCase();
|
|
2171
|
+
const withoutTopolo = normalized.startsWith("topolo-") ? normalized.slice("topolo-".length) : normalized;
|
|
2172
|
+
return [.../* @__PURE__ */ new Set([
|
|
2173
|
+
normalized,
|
|
2174
|
+
normalized.replace(/-/g, "_"),
|
|
2175
|
+
withoutTopolo,
|
|
2176
|
+
withoutTopolo.replace(/-/g, "_")
|
|
2177
|
+
])];
|
|
2178
|
+
}
|
|
2042
2179
|
function describeError(body, fallback) {
|
|
2043
2180
|
if (body && typeof body === "object") {
|
|
2044
2181
|
const maybe = body;
|
|
@@ -3150,13 +3287,14 @@ function enrichFinding(application, finding2, evidence) {
|
|
|
3150
3287
|
});
|
|
3151
3288
|
}
|
|
3152
3289
|
case "service-registration-and-scopes": {
|
|
3290
|
+
const applicationId = String(application.id);
|
|
3153
3291
|
const declaredServices = application.services ?? [];
|
|
3154
3292
|
if (declaredServices.length === 0) {
|
|
3155
|
-
if (
|
|
3293
|
+
if (applicationId === "cli" || applicationId === "mcp" || applicationId === "cloudcontrol") {
|
|
3156
3294
|
return mergeFinding(finding2, {
|
|
3157
3295
|
status: "met",
|
|
3158
3296
|
reason: "This application is itself a tooling/agent surface and does not register a callable platform service.",
|
|
3159
|
-
evidence: [`tooling=${
|
|
3297
|
+
evidence: [`tooling=${applicationId}`],
|
|
3160
3298
|
nextActions: []
|
|
3161
3299
|
});
|
|
3162
3300
|
}
|
|
@@ -3316,6 +3454,7 @@ function enrichFinding(application, finding2, evidence) {
|
|
|
3316
3454
|
});
|
|
3317
3455
|
}
|
|
3318
3456
|
case "agent-and-operator-surfaces": {
|
|
3457
|
+
const applicationId = String(application.id);
|
|
3319
3458
|
const declaredServices = application.services ?? [];
|
|
3320
3459
|
const text = evidence.source.text;
|
|
3321
3460
|
const items = [];
|
|
@@ -3325,11 +3464,11 @@ function enrichFinding(application, finding2, evidence) {
|
|
|
3325
3464
|
items.push("source=confirm-write-gate");
|
|
3326
3465
|
}
|
|
3327
3466
|
if (text.includes("@topolo/sdk")) items.push("source=@topolo/sdk");
|
|
3328
|
-
if (
|
|
3467
|
+
if (applicationId === "cli" || applicationId === "mcp" || applicationId === "cloudcontrol") {
|
|
3329
3468
|
return mergeFinding(finding2, {
|
|
3330
3469
|
status: "met",
|
|
3331
3470
|
reason: "This application is itself an agent/operator surface (CLI / MCP / CloudControl).",
|
|
3332
|
-
evidence: [`tooling=${
|
|
3471
|
+
evidence: [`tooling=${applicationId}`, ...items],
|
|
3333
3472
|
nextActions: []
|
|
3334
3473
|
});
|
|
3335
3474
|
}
|
|
@@ -3889,13 +4028,17 @@ function isFile(filePath) {
|
|
|
3889
4028
|
}
|
|
3890
4029
|
|
|
3891
4030
|
// src/commands/app-scaffold.ts
|
|
4031
|
+
import { randomBytes } from "crypto";
|
|
3892
4032
|
import { constants } from "fs";
|
|
3893
4033
|
import { access, mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
|
|
3894
4034
|
import { dirname as dirname3, relative, resolve } from "path";
|
|
3895
4035
|
var CLOUDFLARE_ACCOUNT_ID = "49ef1ba682ad8cfd720c86699ae17521";
|
|
3896
4036
|
var STAGING_CLOUDFLARE_ACCOUNT_ID = "4f4e1c69a3830946f9fea7b1eb7531ac";
|
|
3897
4037
|
var CLOUDFLARE_COMPATIBILITY_DATE = "2026-05-05";
|
|
3898
|
-
var CURRENT_VERIFICATION_DATE =
|
|
4038
|
+
var CURRENT_VERIFICATION_DATE = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
4039
|
+
function generateOpaqueServiceId() {
|
|
4040
|
+
return `srv_${randomBytes(9).toString("base64url")}`;
|
|
4041
|
+
}
|
|
3899
4042
|
function applicationScaffoldPayload(application, options = {}) {
|
|
3900
4043
|
const target = normalizeTarget(options.target);
|
|
3901
4044
|
const portfolio = target === "platform" ? normalizePortfolio(options.portfolio) : null;
|
|
@@ -3912,23 +4055,28 @@ function applicationScaffoldPayload(application, options = {}) {
|
|
|
3912
4055
|
const tenancy = normalizeTenancy(options.tenancy, audience);
|
|
3913
4056
|
const surfaceModel = normalizeSurfaceModel(options.surfaceModel);
|
|
3914
4057
|
const containerAppId = normalizeContainerAppId(options.containerAppId, surfaceModel);
|
|
4058
|
+
const profile = normalizeScaffoldProfile(options.profile);
|
|
4059
|
+
const resources = resolveScaffoldResources(profile, options);
|
|
3915
4060
|
const ownerType = target === "platform" ? "topolo_first_party" : "third_party";
|
|
3916
4061
|
const distributionModel = target === "platform" ? "marketplace" : "organization_internal";
|
|
3917
4062
|
const appDir = options.appDir ?? catalogApplication?.appDir ?? (target === "platform" ? `Topolo${toPascalCase(id)}` : `topolo-${slug}`);
|
|
3918
4063
|
const name = options.name ?? catalogApplication?.name ?? `topolo-${slug}`;
|
|
3919
4064
|
const packageName = options.packageName ?? catalogApplication?.packageName ?? `topolo-${slug}`;
|
|
3920
|
-
const
|
|
3921
|
-
const
|
|
4065
|
+
const serviceSlug = api ? target === "platform" ? `topolo-${slug}` : id : null;
|
|
4066
|
+
const defaultServiceId = target === "platform" ? generateOpaqueServiceId() : catalogApplication?.services[0] ?? id;
|
|
4067
|
+
const serviceId = api ? options.service ?? defaultServiceId : null;
|
|
3922
4068
|
const productionUrl = browser ? options.productionUrl ?? catalogApplication?.productionUrl ?? `https://${slug}.topolo.app` : null;
|
|
3923
4069
|
const scopes = scaffoldScopes({ browser, api });
|
|
4070
|
+
const capabilities = scaffoldCapabilities({ browser, api, distributionModel });
|
|
3924
4071
|
const ciWorkflowPath = target === "platform" ? ".github/workflows/topolo-conformance.yml" : null;
|
|
3925
|
-
const ciWorkflow = target === "platform" ? topoloConformanceWorkflow({ id, appDir }) : null;
|
|
4072
|
+
const ciWorkflow = target === "platform" ? topoloConformanceWorkflow({ id, appDir, audience }) : null;
|
|
3926
4073
|
const applicationPayload = {
|
|
3927
4074
|
id,
|
|
3928
4075
|
name,
|
|
3929
4076
|
appDir,
|
|
3930
4077
|
packageName,
|
|
3931
4078
|
productionUrl,
|
|
4079
|
+
serviceSlug,
|
|
3932
4080
|
serviceId
|
|
3933
4081
|
};
|
|
3934
4082
|
return {
|
|
@@ -3940,7 +4088,10 @@ function applicationScaffoldPayload(application, options = {}) {
|
|
|
3940
4088
|
tenancy,
|
|
3941
4089
|
surfaceModel,
|
|
3942
4090
|
containerAppId,
|
|
4091
|
+
profile,
|
|
4092
|
+
resources,
|
|
3943
4093
|
distributionModel,
|
|
4094
|
+
capabilities,
|
|
3944
4095
|
application: applicationPayload,
|
|
3945
4096
|
scopes,
|
|
3946
4097
|
requirements: APPLICATION_REQUIREMENTS.filter(
|
|
@@ -3955,7 +4106,10 @@ function applicationScaffoldPayload(application, options = {}) {
|
|
|
3955
4106
|
tenancy,
|
|
3956
4107
|
surfaceModel,
|
|
3957
4108
|
containerAppId,
|
|
3958
|
-
|
|
4109
|
+
profile,
|
|
4110
|
+
resources,
|
|
4111
|
+
distributionModel,
|
|
4112
|
+
capabilities
|
|
3959
4113
|
},
|
|
3960
4114
|
application: applicationPayload,
|
|
3961
4115
|
target,
|
|
@@ -3968,7 +4122,8 @@ function applicationScaffoldPayload(application, options = {}) {
|
|
|
3968
4122
|
target,
|
|
3969
4123
|
audience,
|
|
3970
4124
|
tenancy,
|
|
3971
|
-
portfolio
|
|
4125
|
+
portfolio,
|
|
4126
|
+
profile
|
|
3972
4127
|
}),
|
|
3973
4128
|
ciWorkflowPath,
|
|
3974
4129
|
ciWorkflow
|
|
@@ -4017,7 +4172,7 @@ function scaffoldScopes(options) {
|
|
|
4017
4172
|
}
|
|
4018
4173
|
function scaffoldPackages(options) {
|
|
4019
4174
|
if (options.target === "developer") {
|
|
4020
|
-
return [];
|
|
4175
|
+
return options.browser ? ["@topolo/login"] : [];
|
|
4021
4176
|
}
|
|
4022
4177
|
const packages = /* @__PURE__ */ new Set();
|
|
4023
4178
|
if (options.browser) {
|
|
@@ -4025,15 +4180,30 @@ function scaffoldPackages(options) {
|
|
|
4025
4180
|
packages.add("@topolo-io/auth-client");
|
|
4026
4181
|
}
|
|
4027
4182
|
if (options.api) {
|
|
4028
|
-
packages.add("@topolo-io/
|
|
4183
|
+
packages.add("@topolo-io/worker-runtime");
|
|
4029
4184
|
packages.add("@topolo/sdk");
|
|
4030
4185
|
}
|
|
4031
4186
|
return Array.from(packages);
|
|
4032
4187
|
}
|
|
4188
|
+
function scaffoldCapabilities(options) {
|
|
4189
|
+
const capabilities = [];
|
|
4190
|
+
if (options.browser) {
|
|
4191
|
+
capabilities.push("login", "launcher");
|
|
4192
|
+
}
|
|
4193
|
+
if (options.distributionModel === "marketplace") capabilities.push("marketplace");
|
|
4194
|
+
if (options.api) capabilities.push("api", "widget");
|
|
4195
|
+
return capabilities;
|
|
4196
|
+
}
|
|
4197
|
+
function firstPartyAppPath(appDir, audience) {
|
|
4198
|
+
if (appDir === "TopoloCli" || appDir === "TopoloCLI") return "Apps/system/TopoloCLI";
|
|
4199
|
+
if (appDir === "TopoloDocs") return "Apps/system/TopoloDocs";
|
|
4200
|
+
if (appDir === "TopoloAuth") return "Apps/system/TopoloAuth";
|
|
4201
|
+
return `${audience === "personal" ? "Apps/personal" : "Apps/business"}/${appDir}`;
|
|
4202
|
+
}
|
|
4033
4203
|
function scaffoldFiles(options) {
|
|
4034
4204
|
const { application, target } = options;
|
|
4035
4205
|
const slug = toSlug(application.id);
|
|
4036
|
-
const base = target === "platform" ?
|
|
4206
|
+
const base = target === "platform" ? firstPartyAppPath(application.appDir, options.payload.audience) : application.appDir;
|
|
4037
4207
|
const files = [
|
|
4038
4208
|
{
|
|
4039
4209
|
path: `${base}/package.json`,
|
|
@@ -4042,7 +4212,11 @@ function scaffoldFiles(options) {
|
|
|
4042
4212
|
"name matches the packageName in the SDK application catalog",
|
|
4043
4213
|
"scripts include build plus typecheck where the stack supports them"
|
|
4044
4214
|
],
|
|
4045
|
-
content: packageJson(application, target, {
|
|
4215
|
+
content: packageJson(application, target, {
|
|
4216
|
+
browser: options.browser,
|
|
4217
|
+
api: options.api,
|
|
4218
|
+
resources: options.payload.resources
|
|
4219
|
+
})
|
|
4046
4220
|
},
|
|
4047
4221
|
{
|
|
4048
4222
|
path: `${base}/README.md`,
|
|
@@ -4055,7 +4229,9 @@ function scaffoldFiles(options) {
|
|
|
4055
4229
|
api: options.api,
|
|
4056
4230
|
audience: options.payload.audience,
|
|
4057
4231
|
tenancy: options.payload.tenancy,
|
|
4058
|
-
portfolio: options.payload.portfolio
|
|
4232
|
+
portfolio: options.payload.portfolio,
|
|
4233
|
+
profile: options.payload.profile,
|
|
4234
|
+
resources: options.payload.resources
|
|
4059
4235
|
})
|
|
4060
4236
|
},
|
|
4061
4237
|
{
|
|
@@ -4112,6 +4288,17 @@ function scaffoldFiles(options) {
|
|
|
4112
4288
|
],
|
|
4113
4289
|
content: envDts(options.api)
|
|
4114
4290
|
},
|
|
4291
|
+
...options.payload.resources.workflow ? [
|
|
4292
|
+
{
|
|
4293
|
+
path: `${base}/src/cloudflare-workers.d.ts`,
|
|
4294
|
+
purpose: "Minimal Cloudflare Workflow entrypoint typing for scaffolded Workers.",
|
|
4295
|
+
requiredSignals: [
|
|
4296
|
+
"declares cloudflare:workers WorkflowEntrypoint for TypeScript",
|
|
4297
|
+
"keeps Workflow imports deployable without adding a new dependency"
|
|
4298
|
+
],
|
|
4299
|
+
content: cloudflareWorkersDts()
|
|
4300
|
+
}
|
|
4301
|
+
] : [],
|
|
4115
4302
|
{
|
|
4116
4303
|
path: `${base}/src/styles.css`,
|
|
4117
4304
|
purpose: "Minimal app-local styles outside the shared Topolo shell.",
|
|
@@ -4131,21 +4318,26 @@ function scaffoldFiles(options) {
|
|
|
4131
4318
|
"production URLs and Cloudflare targets are explicit",
|
|
4132
4319
|
"resource bindings and required secrets are declared without secret values"
|
|
4133
4320
|
],
|
|
4134
|
-
content: cloudcontrolJson(application, {
|
|
4321
|
+
content: cloudcontrolJson(application, {
|
|
4322
|
+
api: options.api,
|
|
4323
|
+
audience: options.payload.audience,
|
|
4324
|
+
profile: options.payload.profile,
|
|
4325
|
+
resources: options.payload.resources
|
|
4326
|
+
})
|
|
4135
4327
|
},
|
|
4136
4328
|
{
|
|
4137
4329
|
path: `${base}/.github/workflows/topolo-conformance.yml`,
|
|
4138
4330
|
purpose: "PR/push gate that runs the local Topolo app audit with docs evidence.",
|
|
4139
4331
|
requiredSignals: [
|
|
4140
|
-
"checks out this repo under its
|
|
4141
|
-
"checks out TopoloDocs under
|
|
4332
|
+
"checks out this repo under its Apps lane directory",
|
|
4333
|
+
"checks out TopoloDocs under Apps/system/TopoloDocs",
|
|
4142
4334
|
"checks out topolo-sdk and TopoloCli from main so the gate does not wait for an npm publish",
|
|
4143
4335
|
"runs topolo apps audit with --local and --fail-on needs_review"
|
|
4144
4336
|
],
|
|
4145
4337
|
content: options.ciWorkflow ?? ""
|
|
4146
4338
|
},
|
|
4147
4339
|
{
|
|
4148
|
-
path: `
|
|
4340
|
+
path: `Apps/system/TopoloDocs/src/content/systems/topolo-${slug}.json`,
|
|
4149
4341
|
purpose: "Canonical system registry entry.",
|
|
4150
4342
|
requiredSignals: [
|
|
4151
4343
|
`repo_paths includes ${base}`,
|
|
@@ -4156,11 +4348,13 @@ function scaffoldFiles(options) {
|
|
|
4156
4348
|
api: options.api,
|
|
4157
4349
|
audience: options.payload.audience,
|
|
4158
4350
|
tenancy: options.payload.tenancy,
|
|
4159
|
-
portfolio: options.payload.portfolio
|
|
4351
|
+
portfolio: options.payload.portfolio,
|
|
4352
|
+
profile: options.payload.profile,
|
|
4353
|
+
resources: options.payload.resources
|
|
4160
4354
|
})
|
|
4161
4355
|
},
|
|
4162
4356
|
{
|
|
4163
|
-
path: `
|
|
4357
|
+
path: `Apps/system/TopoloDocs/src/content/internal/apps/${slug}.mdx`,
|
|
4164
4358
|
purpose: "Internal operator handbook.",
|
|
4165
4359
|
requiredSignals: [
|
|
4166
4360
|
"documents ownership, routes, auth boundary, deployment, operations, and verification",
|
|
@@ -4170,11 +4364,13 @@ function scaffoldFiles(options) {
|
|
|
4170
4364
|
api: options.api,
|
|
4171
4365
|
audience: options.payload.audience,
|
|
4172
4366
|
tenancy: options.payload.tenancy,
|
|
4173
|
-
portfolio: options.payload.portfolio
|
|
4367
|
+
portfolio: options.payload.portfolio,
|
|
4368
|
+
profile: options.payload.profile,
|
|
4369
|
+
resources: options.payload.resources
|
|
4174
4370
|
})
|
|
4175
4371
|
},
|
|
4176
4372
|
{
|
|
4177
|
-
path: `
|
|
4373
|
+
path: `Apps/system/TopoloDocs/src/content/public/applications/${slug}.mdx`,
|
|
4178
4374
|
purpose: "Public application overview.",
|
|
4179
4375
|
requiredSignals: [
|
|
4180
4376
|
"documents the product surface without internal-only details",
|
|
@@ -4184,14 +4380,16 @@ function scaffoldFiles(options) {
|
|
|
4184
4380
|
api: options.api,
|
|
4185
4381
|
audience: options.payload.audience,
|
|
4186
4382
|
tenancy: options.payload.tenancy,
|
|
4187
|
-
portfolio: options.payload.portfolio
|
|
4383
|
+
portfolio: options.payload.portfolio,
|
|
4384
|
+
profile: options.payload.profile,
|
|
4385
|
+
resources: options.payload.resources
|
|
4188
4386
|
})
|
|
4189
4387
|
}
|
|
4190
4388
|
);
|
|
4191
4389
|
if (options.api && application.serviceId) {
|
|
4192
4390
|
files.push(
|
|
4193
4391
|
{
|
|
4194
|
-
path: `
|
|
4392
|
+
path: `Apps/system/TopoloAuth/catalog/service-manifests/${application.serviceId}.json`,
|
|
4195
4393
|
purpose: "Auth service manifest for first-party permissions, role bundles, and API-key scopes.",
|
|
4196
4394
|
requiredSignals: [
|
|
4197
4395
|
"serviceId matches the generated Worker service id",
|
|
@@ -4199,18 +4397,7 @@ function scaffoldFiles(options) {
|
|
|
4199
4397
|
"systems metadata maps the service back to the generated TopoloDocs entries"
|
|
4200
4398
|
],
|
|
4201
4399
|
content: serviceManifestJson(application, {
|
|
4202
|
-
|
|
4203
|
-
})
|
|
4204
|
-
},
|
|
4205
|
-
{
|
|
4206
|
-
path: `PlatformApplications/TopoloAuth/scripts/add-topolo-${slug}-service-production.sql`,
|
|
4207
|
-
purpose: "Auth service catalog registration for embedded first-party login, launcher visibility, and callback origins.",
|
|
4208
|
-
requiredSignals: [
|
|
4209
|
-
"services.id matches the generated Worker service id",
|
|
4210
|
-
"settings.launcher.category is first_party so embedded password login is allowed",
|
|
4211
|
-
"base_url and auth_domain define the production callback origin"
|
|
4212
|
-
],
|
|
4213
|
-
content: serviceRegistrationSql(application, {
|
|
4400
|
+
audience: options.payload.audience,
|
|
4214
4401
|
tenancy: options.payload.tenancy
|
|
4215
4402
|
})
|
|
4216
4403
|
}
|
|
@@ -4231,7 +4418,8 @@ function scaffoldFiles(options) {
|
|
|
4231
4418
|
tenancy: options.payload.tenancy,
|
|
4232
4419
|
surfaceModel: options.payload.surfaceModel,
|
|
4233
4420
|
containerAppId: options.payload.containerAppId,
|
|
4234
|
-
distributionModel: options.payload.distributionModel
|
|
4421
|
+
distributionModel: options.payload.distributionModel,
|
|
4422
|
+
capabilities: options.payload.capabilities
|
|
4235
4423
|
})
|
|
4236
4424
|
},
|
|
4237
4425
|
{
|
|
@@ -4246,6 +4434,17 @@ function scaffoldFiles(options) {
|
|
|
4246
4434
|
);
|
|
4247
4435
|
}
|
|
4248
4436
|
if (options.browser) {
|
|
4437
|
+
if (target === "platform") {
|
|
4438
|
+
files.push({
|
|
4439
|
+
path: `${base}/src/service-identity.ts`,
|
|
4440
|
+
purpose: "Central service identity resolver shared by browser and Worker runtime surfaces.",
|
|
4441
|
+
requiredSignals: [
|
|
4442
|
+
"exports the generated app id, display name, and stable Auth service slug",
|
|
4443
|
+
"requires the provisioned Auth service id from the Worker deployment config"
|
|
4444
|
+
],
|
|
4445
|
+
content: serviceIdentityTs(application)
|
|
4446
|
+
});
|
|
4447
|
+
}
|
|
4249
4448
|
files.push({
|
|
4250
4449
|
path: `${base}/src/App.tsx`,
|
|
4251
4450
|
purpose: "Authenticated browser surface.",
|
|
@@ -4270,19 +4469,21 @@ function scaffoldFiles(options) {
|
|
|
4270
4469
|
"bindings match topolo.cloudcontrol.json or topolo.app.json",
|
|
4271
4470
|
"secrets are referenced by name only"
|
|
4272
4471
|
],
|
|
4273
|
-
content: wranglerToml(application, target)
|
|
4472
|
+
content: wranglerToml(application, target, options.payload.resources, options.payload.profile)
|
|
4274
4473
|
},
|
|
4275
4474
|
{
|
|
4276
4475
|
path: `${base}/src/worker.ts`,
|
|
4277
4476
|
purpose: "Protected service boundary.",
|
|
4278
4477
|
requiredSignals: [
|
|
4279
|
-
"uses @topolo-io/
|
|
4478
|
+
"uses @topolo-io/worker-runtime for Worker auth, gatewaying, headers, and request helpers",
|
|
4280
4479
|
"derives organization from the credential rather than client-supplied orgId",
|
|
4281
4480
|
"emits or propagates X-Topolo-Request-Id and X-Topolo-Client"
|
|
4282
4481
|
],
|
|
4283
4482
|
content: workerTs(application, target, {
|
|
4284
4483
|
audience: options.payload.audience,
|
|
4285
|
-
tenancy: options.payload.tenancy
|
|
4484
|
+
tenancy: options.payload.tenancy,
|
|
4485
|
+
profile: options.payload.profile,
|
|
4486
|
+
resources: options.payload.resources
|
|
4286
4487
|
})
|
|
4287
4488
|
}
|
|
4288
4489
|
);
|
|
@@ -4302,30 +4503,32 @@ function scaffoldCommands(options) {
|
|
|
4302
4503
|
];
|
|
4303
4504
|
}
|
|
4304
4505
|
const portfolioFlags = options.portfolio === "personal" ? " --portfolio personal" : "";
|
|
4506
|
+
const profileFlags = options.profile ? ` --profile ${options.profile}` : "";
|
|
4305
4507
|
return [
|
|
4306
|
-
`topolo platform apps scaffold ${scaffoldId}${portfolioFlags} --write --provision --repo-root /Users/ashes/Projects/
|
|
4508
|
+
`topolo platform apps scaffold ${scaffoldId}${portfolioFlags}${profileFlags} --write --provision --repo-root /Users/ashes/Projects/Topolo/Platform --json`,
|
|
4307
4509
|
"topolo apps requirements --json",
|
|
4308
|
-
`topolo apps audit ${options.id} --local --repo-root /Users/ashes/Projects/
|
|
4309
|
-
`topolo apps audit ${options.id} --local --repo-root /Users/ashes/Projects/
|
|
4510
|
+
`topolo apps audit ${options.id} --local --repo-root /Users/ashes/Projects/Topolo/Platform --fail-on needs_review --json`,
|
|
4511
|
+
`topolo apps audit ${options.id} --local --repo-root /Users/ashes/Projects/Topolo/Platform --fail-on partial --json`
|
|
4310
4512
|
];
|
|
4311
4513
|
}
|
|
4312
4514
|
function topoloConformanceWorkflow(options) {
|
|
4515
|
+
const appPath = firstPartyAppPath(options.appDir, options.audience);
|
|
4313
4516
|
const docsCheckoutStep = options.appDir === "TopoloDocs" ? [] : [
|
|
4314
4517
|
"",
|
|
4315
4518
|
" - name: Checkout TopoloDocs",
|
|
4316
4519
|
" uses: actions/checkout@v4",
|
|
4317
4520
|
" with:",
|
|
4318
4521
|
" repository: Topolo-io/docs",
|
|
4319
|
-
" path:
|
|
4522
|
+
" path: Apps/system/TopoloDocs",
|
|
4320
4523
|
" token: ${{ secrets.TOPOLO_CI_REPO_TOKEN || github.token }}"
|
|
4321
4524
|
];
|
|
4322
|
-
const cliCheckoutStep = options.appDir === "TopoloCli" ? [] : [
|
|
4525
|
+
const cliCheckoutStep = options.appDir === "TopoloCLI" || options.appDir === "TopoloCli" ? [] : [
|
|
4323
4526
|
"",
|
|
4324
4527
|
" - name: Checkout Topolo CLI",
|
|
4325
4528
|
" uses: actions/checkout@v4",
|
|
4326
4529
|
" with:",
|
|
4327
4530
|
" repository: Topolo-io/TopoloCli",
|
|
4328
|
-
" path:
|
|
4531
|
+
" path: Apps/system/TopoloCLI",
|
|
4329
4532
|
" token: ${{ secrets.TOPOLO_CI_REPO_TOKEN || github.token }}"
|
|
4330
4533
|
];
|
|
4331
4534
|
return [
|
|
@@ -4348,14 +4551,14 @@ function topoloConformanceWorkflow(options) {
|
|
|
4348
4551
|
" - name: Checkout application",
|
|
4349
4552
|
" uses: actions/checkout@v4",
|
|
4350
4553
|
" with:",
|
|
4351
|
-
` path:
|
|
4554
|
+
` path: ${appPath}`,
|
|
4352
4555
|
...docsCheckoutStep,
|
|
4353
4556
|
"",
|
|
4354
4557
|
" - name: Checkout Topolo SDK",
|
|
4355
4558
|
" uses: actions/checkout@v4",
|
|
4356
4559
|
" with:",
|
|
4357
4560
|
" repository: Topolo-io/topolo-sdk",
|
|
4358
|
-
" path:
|
|
4561
|
+
" path: Apps/packages/topolo-sdk",
|
|
4359
4562
|
" token: ${{ secrets.TOPOLO_CI_REPO_TOKEN || github.token }}",
|
|
4360
4563
|
...cliCheckoutStep,
|
|
4361
4564
|
"",
|
|
@@ -4365,26 +4568,26 @@ function topoloConformanceWorkflow(options) {
|
|
|
4365
4568
|
" node-version: 20",
|
|
4366
4569
|
"",
|
|
4367
4570
|
" - name: Build Topolo SDK",
|
|
4368
|
-
" working-directory:
|
|
4571
|
+
" working-directory: Apps/packages/topolo-sdk",
|
|
4369
4572
|
" run: |",
|
|
4370
4573
|
" npm install --no-audit --no-fund",
|
|
4371
4574
|
" npm run build",
|
|
4372
4575
|
"",
|
|
4373
4576
|
" - name: Build Topolo CLI",
|
|
4374
|
-
" working-directory:
|
|
4577
|
+
" working-directory: Apps/system/TopoloCLI",
|
|
4375
4578
|
" run: |",
|
|
4376
4579
|
" npm install --no-audit --no-fund",
|
|
4377
4580
|
" npm run build",
|
|
4378
4581
|
"",
|
|
4379
4582
|
" - name: Run Topolo app conformance audit",
|
|
4380
4583
|
" run: |",
|
|
4381
|
-
` node
|
|
4584
|
+
` node Apps/system/TopoloCLI/dist/index.js apps audit ${options.id} --local --repo-root "$GITHUB_WORKSPACE" --fail-on needs_review --json`,
|
|
4382
4585
|
"",
|
|
4383
4586
|
" - name: Record green CI evidence",
|
|
4384
4587
|
` if: \${{ success() }}`,
|
|
4385
|
-
` working-directory:
|
|
4588
|
+
` working-directory: ${appPath}`,
|
|
4386
4589
|
" run: |",
|
|
4387
|
-
` node "$GITHUB_WORKSPACE/
|
|
4590
|
+
` node "$GITHUB_WORKSPACE/Apps/system/TopoloCLI/dist/index.js" apps record-evidence ${options.id} --build --typecheck --test --commit "$GITHUB_SHA"`
|
|
4388
4591
|
].join("\n");
|
|
4389
4592
|
}
|
|
4390
4593
|
function packageJson(application, target, options) {
|
|
@@ -4410,20 +4613,27 @@ function packageJson(application, target, options) {
|
|
|
4410
4613
|
dependencies["@topolo-io/auth-client"] = topoloPackageVersion("@topolo-io/auth-client", target);
|
|
4411
4614
|
dependencies["@topolo-io/design-tokens"] = topoloPackageVersion("@topolo-io/design-tokens", target);
|
|
4412
4615
|
dependencies["@topolo-io/ui-kit"] = topoloPackageVersion("@topolo-io/ui-kit", target);
|
|
4616
|
+
} else {
|
|
4617
|
+
dependencies["@topolo/login"] = topoloPackageVersion("@topolo/login", target);
|
|
4413
4618
|
}
|
|
4414
4619
|
}
|
|
4415
4620
|
if (options.api) {
|
|
4416
4621
|
if (target === "platform") {
|
|
4417
|
-
dependencies["@topolo-io/
|
|
4622
|
+
dependencies["@topolo-io/worker-runtime"] = topoloPackageVersion("@topolo-io/worker-runtime", target);
|
|
4623
|
+
dependencies["@topolo-io/widget-server"] = topoloPackageVersion("@topolo-io/widget-server", target);
|
|
4418
4624
|
dependencies["@topolo/sdk"] = topoloPackageVersion("@topolo/sdk", target);
|
|
4419
4625
|
scripts.deploy = `node -e 'console.error("\\nX Bare deploy is disabled. Use deploy:staging or deploy:production.\\n"); process.exit(1)'`;
|
|
4420
4626
|
scripts["deploy:staging"] = "DEPLOY_ENVIRONMENT=staging VITE_TOPOLO_AUTH_URL=https://auth.stg.topolo.us npm run build && wrangler deploy --env staging";
|
|
4421
4627
|
scripts["deploy:production"] = "DEPLOY_ENVIRONMENT=production npm run build && wrangler deploy";
|
|
4422
4628
|
} else {
|
|
4629
|
+
dependencies["@topolo-io/widget-server"] = topoloPackageVersion("@topolo-io/widget-server", target);
|
|
4423
4630
|
scripts.deploy = "npm run build && wrangler deploy";
|
|
4424
4631
|
}
|
|
4425
4632
|
devDependencies.wrangler = "^4.40.2";
|
|
4426
4633
|
}
|
|
4634
|
+
if (target === "platform" && hasScaffoldResources(options.resources)) {
|
|
4635
|
+
scripts["check:deploy-config"] = "wrangler deploy --dry-run --outdir .wrangler/build";
|
|
4636
|
+
}
|
|
4427
4637
|
return `${json({
|
|
4428
4638
|
name: application.packageName,
|
|
4429
4639
|
version: "0.1.0",
|
|
@@ -4437,15 +4647,20 @@ function packageJson(application, target, options) {
|
|
|
4437
4647
|
}
|
|
4438
4648
|
function topoloPackageVersion(packageName, target) {
|
|
4439
4649
|
if (target === "developer") {
|
|
4650
|
+
if (packageName === "@topolo/login") return "^0.1.0";
|
|
4440
4651
|
if (packageName === "@topolo-io/auth-client") return "^1.0.0";
|
|
4652
|
+
if (packageName === "@topolo-io/widget-server") return "^0.1.1";
|
|
4441
4653
|
return "^0.1.0";
|
|
4442
4654
|
}
|
|
4443
4655
|
const platformPackageFolders = {
|
|
4444
4656
|
"@topolo/sdk": "topolo-sdk",
|
|
4657
|
+
"@topolo/login": "topolo-login",
|
|
4445
4658
|
"@topolo-io/auth-client": "topolo-auth-client",
|
|
4446
4659
|
"@topolo-io/auth-middleware": "topolo-auth-middleware",
|
|
4660
|
+
"@topolo-io/worker-runtime": "topolo-worker-runtime",
|
|
4447
4661
|
"@topolo-io/design-tokens": "topolo-design-tokens",
|
|
4448
|
-
"@topolo-io/ui-kit": "topolo-ui-kit"
|
|
4662
|
+
"@topolo-io/ui-kit": "topolo-ui-kit",
|
|
4663
|
+
"@topolo-io/widget-server": "topolo-widget-server"
|
|
4449
4664
|
};
|
|
4450
4665
|
const folder = platformPackageFolders[packageName];
|
|
4451
4666
|
if (!folder) {
|
|
@@ -4453,8 +4668,52 @@ function topoloPackageVersion(packageName, target) {
|
|
|
4453
4668
|
}
|
|
4454
4669
|
return `file:../packages/${folder}`;
|
|
4455
4670
|
}
|
|
4671
|
+
function hasScaffoldResources(resources) {
|
|
4672
|
+
return resources.d1 || resources.r2 || resources.queue || resources.workflow || resources.containerProcessing;
|
|
4673
|
+
}
|
|
4674
|
+
function resourceConfig(application, profile) {
|
|
4675
|
+
const slug = toSlug(application.id);
|
|
4676
|
+
const pascal = toPascalCase(application.id);
|
|
4677
|
+
if (profile === "spatial-assets") {
|
|
4678
|
+
return {
|
|
4679
|
+
d1Binding: "APP_DB",
|
|
4680
|
+
d1ProductionName: `topolo-${slug}-production-db`,
|
|
4681
|
+
d1StagingName: `topolo-${slug}-staging-db`,
|
|
4682
|
+
r2Binding: "SPACES_ASSETS",
|
|
4683
|
+
r2ProductionName: `topolo-${slug}-assets`,
|
|
4684
|
+
r2StagingName: `topolo-${slug}-assets-staging`,
|
|
4685
|
+
queueBinding: "SPACES_PROCESSING_QUEUE",
|
|
4686
|
+
queueProductionName: `topolo-${slug}-processing`,
|
|
4687
|
+
queueStagingName: `topolo-${slug}-processing-staging`,
|
|
4688
|
+
workflowBinding: "SPACES_IMPORT_WORKFLOW",
|
|
4689
|
+
workflowClassName: `${pascal}ImportWorkflow`,
|
|
4690
|
+
workflowProductionName: `topolo-${slug}-import`,
|
|
4691
|
+
workflowStagingName: `topolo-${slug}-import-staging`,
|
|
4692
|
+
containerProductionName: `topolo-${slug}-processor`,
|
|
4693
|
+
containerStagingName: `topolo-${slug}-processor-staging`
|
|
4694
|
+
};
|
|
4695
|
+
}
|
|
4696
|
+
return {
|
|
4697
|
+
d1Binding: "APP_DB",
|
|
4698
|
+
d1ProductionName: `topolo-${slug}-prod`,
|
|
4699
|
+
d1StagingName: `topolo-${slug}-staging`,
|
|
4700
|
+
r2Binding: "APP_ASSETS",
|
|
4701
|
+
r2ProductionName: `topolo-${slug}-assets`,
|
|
4702
|
+
r2StagingName: `topolo-${slug}-assets-staging`,
|
|
4703
|
+
queueBinding: "APP_QUEUE",
|
|
4704
|
+
queueProductionName: `topolo-${slug}-queue`,
|
|
4705
|
+
queueStagingName: `topolo-${slug}-queue-staging`,
|
|
4706
|
+
workflowBinding: "APP_WORKFLOW",
|
|
4707
|
+
workflowClassName: `${pascal}Workflow`,
|
|
4708
|
+
workflowProductionName: `topolo-${slug}-workflow`,
|
|
4709
|
+
workflowStagingName: `topolo-${slug}-workflow-staging`,
|
|
4710
|
+
containerProductionName: `topolo-${slug}-processor`,
|
|
4711
|
+
containerStagingName: `topolo-${slug}-processor-staging`
|
|
4712
|
+
};
|
|
4713
|
+
}
|
|
4456
4714
|
function readme(application, target, options) {
|
|
4457
4715
|
const displayName = toTitle(application.name);
|
|
4716
|
+
const slug = toSlug(application.id);
|
|
4458
4717
|
const targetLabel = target === "platform" ? options.portfolio === "personal" ? "first-party Topolo personal application" : "first-party Topolo platform application" : options.audience === "personal" ? "third-party Topolo personal application" : "third-party Topolo business application";
|
|
4459
4718
|
const lines = [
|
|
4460
4719
|
`# ${displayName}`,
|
|
@@ -4464,6 +4723,7 @@ function readme(application, target, options) {
|
|
|
4464
4723
|
`Audience: ${options.audience}`,
|
|
4465
4724
|
`Tenancy: ${options.tenancy}`,
|
|
4466
4725
|
...target === "platform" && options.portfolio ? [`Portfolio: ${options.portfolio}`] : [],
|
|
4726
|
+
...options.profile ? [`Profile: ${options.profile}`] : [],
|
|
4467
4727
|
"",
|
|
4468
4728
|
"## Development",
|
|
4469
4729
|
"",
|
|
@@ -4485,7 +4745,12 @@ function readme(application, target, options) {
|
|
|
4485
4745
|
"- `vite.config.ts` includes the standard React plugin plus React runtime dedupe so linked Topolo packages resolve one browser React runtime."
|
|
4486
4746
|
);
|
|
4487
4747
|
if (options.api) {
|
|
4488
|
-
lines.push("- `@topolo-io/
|
|
4748
|
+
lines.push("- `@topolo-io/worker-runtime` owns Worker auth validation, `/api/auth/*` gatewaying, security headers, request ids, and BugFix reporting.");
|
|
4749
|
+
}
|
|
4750
|
+
if (hasScaffoldResources(options.resources)) {
|
|
4751
|
+
lines.push(
|
|
4752
|
+
"- Generated resource bindings are declared in both `wrangler.toml` and `topolo.cloudcontrol.json`."
|
|
4753
|
+
);
|
|
4489
4754
|
}
|
|
4490
4755
|
} else {
|
|
4491
4756
|
lines.push(
|
|
@@ -4502,7 +4767,7 @@ function readme(application, target, options) {
|
|
|
4502
4767
|
"",
|
|
4503
4768
|
"## Platform Registration",
|
|
4504
4769
|
"",
|
|
4505
|
-
|
|
4770
|
+
`This scaffold includes CloudControl metadata, TopoloDocs system coverage, and a Topolo app conformance workflow. Run \`topolo platform apps scaffold ${slug} --write --provision\` with an authenticated platform-admin session to register the app through Topolo Developers and project the service into Auth.`
|
|
4506
4771
|
);
|
|
4507
4772
|
} else {
|
|
4508
4773
|
lines.push(
|
|
@@ -4512,6 +4777,15 @@ function readme(application, target, options) {
|
|
|
4512
4777
|
"`topolo.app.json` is the app manifest used to register callback URLs, requested scopes, and local development metadata with Topolo."
|
|
4513
4778
|
);
|
|
4514
4779
|
}
|
|
4780
|
+
if (hasScaffoldResources(options.resources)) {
|
|
4781
|
+
lines.push(
|
|
4782
|
+
"",
|
|
4783
|
+
"## Resource Verification",
|
|
4784
|
+
"",
|
|
4785
|
+
"- Keep CloudControl resource bindings aligned with Wrangler bindings before deploy.",
|
|
4786
|
+
"- Run `npm run check:deploy-config` after changing Worker bindings."
|
|
4787
|
+
);
|
|
4788
|
+
}
|
|
4515
4789
|
return `${lines.join("\n")}
|
|
4516
4790
|
`;
|
|
4517
4791
|
}
|
|
@@ -4562,8 +4836,7 @@ function envDts(api) {
|
|
|
4562
4836
|
" readonly VITE_TOPOLO_AUTH_URL?: string;",
|
|
4563
4837
|
" readonly VITE_TOPOLO_CLIENT_ID?: string;",
|
|
4564
4838
|
" readonly VITE_TOPOLO_REDIRECT_URI?: string;",
|
|
4565
|
-
" readonly VITE_TOPOLO_SCOPES?: string;"
|
|
4566
|
-
" readonly VITE_TOPOLO_SERVICE_ID?: string;"
|
|
4839
|
+
" readonly VITE_TOPOLO_SCOPES?: string;"
|
|
4567
4840
|
];
|
|
4568
4841
|
if (api) {
|
|
4569
4842
|
lines.push(" readonly VITE_TOPOLO_API_URL?: string;");
|
|
@@ -4578,6 +4851,105 @@ function envDts(api) {
|
|
|
4578
4851
|
);
|
|
4579
4852
|
return lines.join("\n");
|
|
4580
4853
|
}
|
|
4854
|
+
function serviceIdentityTs(application) {
|
|
4855
|
+
const appName = toTitle(application.name);
|
|
4856
|
+
const serviceSlug = application.serviceSlug ?? `topolo-${toSlug(application.id)}`;
|
|
4857
|
+
return [
|
|
4858
|
+
`export const SCAFFOLD_APP_ID = '${application.id}';`,
|
|
4859
|
+
`export const SCAFFOLD_APP_NAME = '${escapeSingleQuoted(appName)}';`,
|
|
4860
|
+
`export const SCAFFOLD_SERVICE_SLUG = '${escapeSingleQuoted(serviceSlug)}';`,
|
|
4861
|
+
"",
|
|
4862
|
+
"interface ServiceIdentityEnv {",
|
|
4863
|
+
" SERVICE_ID?: string;",
|
|
4864
|
+
"}",
|
|
4865
|
+
"",
|
|
4866
|
+
"let cachedBrowserServiceId = '';",
|
|
4867
|
+
"",
|
|
4868
|
+
"declare global {",
|
|
4869
|
+
" interface Window {",
|
|
4870
|
+
" __TOPOLO_SCAFFOLD_SERVICE_ID__?: string;",
|
|
4871
|
+
" }",
|
|
4872
|
+
"}",
|
|
4873
|
+
"",
|
|
4874
|
+
"export function getBrowserAuthBaseUrl(): string {",
|
|
4875
|
+
" const defaultAuthUrl =",
|
|
4876
|
+
" typeof window !== 'undefined' && window.location.hostname.endsWith('.stg.topolo.us')",
|
|
4877
|
+
" ? 'https://auth.stg.topolo.us'",
|
|
4878
|
+
" : 'https://auth.topolo.app';",
|
|
4879
|
+
" return (import.meta.env.VITE_TOPOLO_AUTH_URL || defaultAuthUrl).replace(/\\/+$/, '');",
|
|
4880
|
+
"}",
|
|
4881
|
+
"",
|
|
4882
|
+
"export function getAppServiceId(): string {",
|
|
4883
|
+
" const runtimeServiceId =",
|
|
4884
|
+
" typeof window !== 'undefined'",
|
|
4885
|
+
" ? window.__TOPOLO_SCAFFOLD_SERVICE_ID__",
|
|
4886
|
+
" : undefined;",
|
|
4887
|
+
" return (cachedBrowserServiceId || runtimeServiceId || '').trim();",
|
|
4888
|
+
"}",
|
|
4889
|
+
"",
|
|
4890
|
+
"export async function resolveAppServiceId(): Promise<string> {",
|
|
4891
|
+
" const current = getAppServiceId();",
|
|
4892
|
+
" if (current) {",
|
|
4893
|
+
" cachedBrowserServiceId = current;",
|
|
4894
|
+
" return current;",
|
|
4895
|
+
" }",
|
|
4896
|
+
"",
|
|
4897
|
+
" const response = await fetchServiceIdentity();",
|
|
4898
|
+
" const payload = await response.json().catch(() => null);",
|
|
4899
|
+
" const serviceId = payload?.serviceId || payload?.data?.serviceId || payload?.data?.service_id;",
|
|
4900
|
+
" if (typeof serviceId !== 'string' || !serviceId.trim()) {",
|
|
4901
|
+
" throw new Error(`Missing Auth service id for ${SCAFFOLD_SERVICE_SLUG}`);",
|
|
4902
|
+
" }",
|
|
4903
|
+
" cachedBrowserServiceId = serviceId.trim();",
|
|
4904
|
+
" if (typeof window !== 'undefined') {",
|
|
4905
|
+
" window.__TOPOLO_SCAFFOLD_SERVICE_ID__ = cachedBrowserServiceId;",
|
|
4906
|
+
" }",
|
|
4907
|
+
" return cachedBrowserServiceId;",
|
|
4908
|
+
"}",
|
|
4909
|
+
"",
|
|
4910
|
+
"async function fetchServiceIdentity(): Promise<Response> {",
|
|
4911
|
+
" const response = await fetch('/api/service-identity', {",
|
|
4912
|
+
" headers: { Accept: 'application/json' },",
|
|
4913
|
+
" );",
|
|
4914
|
+
" if (!response.ok) {",
|
|
4915
|
+
" throw new Error(`Unable to resolve ${SCAFFOLD_SERVICE_SLUG} service identity`);",
|
|
4916
|
+
" }",
|
|
4917
|
+
" return response;",
|
|
4918
|
+
"}",
|
|
4919
|
+
"",
|
|
4920
|
+
"function requireServiceId(value: unknown): string {",
|
|
4921
|
+
" const serviceId = typeof value === 'string' ? value.trim() : '';",
|
|
4922
|
+
" if (!serviceId) {",
|
|
4923
|
+
" throw new Error(`SERVICE_ID is required for ${SCAFFOLD_SERVICE_SLUG}`);",
|
|
4924
|
+
" }",
|
|
4925
|
+
" return serviceId;",
|
|
4926
|
+
"}",
|
|
4927
|
+
"",
|
|
4928
|
+
"export function resolveRuntimeServiceId(env: ServiceIdentityEnv): Promise<string> {",
|
|
4929
|
+
" return Promise.resolve(requireServiceId(env.SERVICE_ID));",
|
|
4930
|
+
"}",
|
|
4931
|
+
""
|
|
4932
|
+
].join("\n");
|
|
4933
|
+
}
|
|
4934
|
+
function cloudflareWorkersDts() {
|
|
4935
|
+
return [
|
|
4936
|
+
"declare module 'cloudflare:workers' {",
|
|
4937
|
+
" export interface WorkflowEvent<T = unknown> {",
|
|
4938
|
+
" payload: T;",
|
|
4939
|
+
" }",
|
|
4940
|
+
"",
|
|
4941
|
+
" export interface WorkflowStep {",
|
|
4942
|
+
" do<T>(name: string, callback: () => Promise<T> | T): Promise<T>;",
|
|
4943
|
+
" }",
|
|
4944
|
+
"",
|
|
4945
|
+
" export abstract class WorkflowEntrypoint<Env = unknown, Params = unknown> {",
|
|
4946
|
+
" env: Env;",
|
|
4947
|
+
" abstract run(event: Readonly<WorkflowEvent<Params>>, step: WorkflowStep): Promise<unknown>;",
|
|
4948
|
+
" }",
|
|
4949
|
+
"}",
|
|
4950
|
+
""
|
|
4951
|
+
].join("\n");
|
|
4952
|
+
}
|
|
4581
4953
|
function viteConfig(target) {
|
|
4582
4954
|
if (target === "developer") {
|
|
4583
4955
|
return [
|
|
@@ -4755,8 +5127,6 @@ function appTsx(application, target, options) {
|
|
|
4755
5127
|
return platformAppTsx(application, options);
|
|
4756
5128
|
}
|
|
4757
5129
|
function platformAppTsx(application, options) {
|
|
4758
|
-
const appName = toTitle(application.name);
|
|
4759
|
-
const serviceId = application.serviceId ?? application.id;
|
|
4760
5130
|
const isPersonal = options.audience === "personal";
|
|
4761
5131
|
const tagline = isPersonal ? "Topolo personal app" : "Topolo workspace";
|
|
4762
5132
|
const workspaceTitle = isPersonal ? "Personal app" : "Workspace";
|
|
@@ -4766,8 +5136,9 @@ function platformAppTsx(application, options) {
|
|
|
4766
5136
|
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.";
|
|
4767
5137
|
return [
|
|
4768
5138
|
"import { Component, useEffect, useMemo, useState, type ErrorInfo, type ReactNode } from 'react';",
|
|
4769
|
-
"import {
|
|
5139
|
+
"import { createBrowserAuthSessionController, type AuthUser } from '@topolo-io/auth-client';",
|
|
4770
5140
|
"import { LandingPage, LoginPage, TopoloAppShell, TopoloAuthFlowScreen, TopoloLoadingScreen, setupBugReporterErrorCapture, type TopoloAppShellUser } from '@topolo-io/ui-kit';",
|
|
5141
|
+
"import { SCAFFOLD_APP_ID, SCAFFOLD_APP_NAME, SCAFFOLD_SERVICE_SLUG, getBrowserAuthBaseUrl, resolveAppServiceId } from './service-identity';",
|
|
4771
5142
|
"",
|
|
4772
5143
|
"// Wire global error/console capture so BugReporter (enabled in TopoloAppShell below)",
|
|
4773
5144
|
"// can attach the last few errors to any submitted bug report.",
|
|
@@ -4775,16 +5146,11 @@ function platformAppTsx(application, options) {
|
|
|
4775
5146
|
"",
|
|
4776
5147
|
"type NavItem = 'home';",
|
|
4777
5148
|
"type AppRoute = { kind: 'root' } | { kind: 'login' } | { kind: 'callback' };",
|
|
5149
|
+
"type BrowserAuthModule = ReturnType<typeof createBrowserAuthSessionController>;",
|
|
4778
5150
|
"",
|
|
4779
|
-
|
|
4780
|
-
|
|
4781
|
-
|
|
4782
|
-
"const DEFAULT_AUTH_URL =",
|
|
4783
|
-
" typeof window !== 'undefined' && window.location.hostname.endsWith('.stg.topolo.us')",
|
|
4784
|
-
" ? 'https://auth.stg.topolo.us'",
|
|
4785
|
-
" : 'https://auth.topolo.app';",
|
|
4786
|
-
"const AUTH_URL = import.meta.env.VITE_TOPOLO_AUTH_URL ?? DEFAULT_AUTH_URL;",
|
|
4787
|
-
"const SERVICE_ID = import.meta.env.VITE_TOPOLO_SERVICE_ID ?? DEFAULT_SERVICE_ID;",
|
|
5151
|
+
"const APP_ID = SCAFFOLD_APP_ID;",
|
|
5152
|
+
"const APP_NAME = SCAFFOLD_APP_NAME;",
|
|
5153
|
+
"const AUTH_URL = getBrowserAuthBaseUrl();",
|
|
4788
5154
|
"const LANDING_CONFIG = {",
|
|
4789
5155
|
" app_name: APP_NAME,",
|
|
4790
5156
|
` app_tagline: '${escapeSingleQuoted(tagline)}',`,
|
|
@@ -4803,17 +5169,28 @@ function platformAppTsx(application, options) {
|
|
|
4803
5169
|
" authenticated_home_path: '/',",
|
|
4804
5170
|
"};",
|
|
4805
5171
|
"",
|
|
4806
|
-
"
|
|
4807
|
-
"
|
|
4808
|
-
"
|
|
4809
|
-
"
|
|
4810
|
-
"
|
|
4811
|
-
"
|
|
4812
|
-
"
|
|
4813
|
-
"
|
|
5172
|
+
"let activeAuthServiceId = '';",
|
|
5173
|
+
"let activeAuthModule: BrowserAuthModule | null = null;",
|
|
5174
|
+
"",
|
|
5175
|
+
"function getAuthModule(serviceId: string): BrowserAuthModule {",
|
|
5176
|
+
" const normalizedServiceId = serviceId.trim();",
|
|
5177
|
+
" if (!normalizedServiceId) throw new Error(`Missing Auth service id for ${SCAFFOLD_SERVICE_SLUG}`);",
|
|
5178
|
+
" if (!activeAuthModule || activeAuthServiceId !== normalizedServiceId) {",
|
|
5179
|
+
" activeAuthServiceId = normalizedServiceId;",
|
|
5180
|
+
" activeAuthModule = createBrowserAuthSessionController({",
|
|
5181
|
+
" authServiceUrl: AUTH_URL,",
|
|
5182
|
+
" serviceId: normalizedServiceId,",
|
|
5183
|
+
" devSessionKey: `${normalizedServiceId}_session`,",
|
|
5184
|
+
" devSessionStorageEnabled: import.meta.env.DEV,",
|
|
5185
|
+
" callbackPath: '/auth/callback',",
|
|
5186
|
+
" debug: import.meta.env.DEV,",
|
|
5187
|
+
" });",
|
|
5188
|
+
" }",
|
|
5189
|
+
" return activeAuthModule;",
|
|
5190
|
+
"}",
|
|
4814
5191
|
"",
|
|
4815
5192
|
"function getAccessToken(): string | null {",
|
|
4816
|
-
" return
|
|
5193
|
+
" return activeAuthModule?.accessTokenMemory.get() ?? activeAuthModule?.accessTokenSession.get() ?? null;",
|
|
4817
5194
|
"}",
|
|
4818
5195
|
"",
|
|
4819
5196
|
"function toShellUser(user: AuthUser | null): TopoloAppShellUser | null {",
|
|
@@ -4839,11 +5216,32 @@ function platformAppTsx(application, options) {
|
|
|
4839
5216
|
"function AppRoot() {",
|
|
4840
5217
|
" const route = getRoute();",
|
|
4841
5218
|
" const [user, setUser] = useState<AuthUser | null>(null);",
|
|
5219
|
+
" const [serviceId, setServiceId] = useState('');",
|
|
5220
|
+
" const [serviceIdentityError, setServiceIdentityError] = useState('');",
|
|
4842
5221
|
" const [loading, setLoading] = useState(true);",
|
|
4843
5222
|
" const [authError, setAuthError] = useState('');",
|
|
4844
5223
|
" const [loginAttempted, setLoginAttempted] = useState(false);",
|
|
5224
|
+
" const auth = useMemo(() => serviceId ? getAuthModule(serviceId) : null, [serviceId]);",
|
|
5225
|
+
"",
|
|
5226
|
+
" useEffect(() => {",
|
|
5227
|
+
" let active = true;",
|
|
5228
|
+
" setServiceIdentityError('');",
|
|
5229
|
+
" void resolveAppServiceId()",
|
|
5230
|
+
" .then((nextServiceId) => {",
|
|
5231
|
+
" if (active) setServiceId(nextServiceId);",
|
|
5232
|
+
" })",
|
|
5233
|
+
" .catch((error) => {",
|
|
5234
|
+
" if (!active) return;",
|
|
5235
|
+
" setServiceIdentityError(errorMessage(error));",
|
|
5236
|
+
" setLoading(false);",
|
|
5237
|
+
" });",
|
|
5238
|
+
" return () => {",
|
|
5239
|
+
" active = false;",
|
|
5240
|
+
" };",
|
|
5241
|
+
" }, []);",
|
|
4845
5242
|
"",
|
|
4846
5243
|
" useEffect(() => {",
|
|
5244
|
+
" if (!auth) return;",
|
|
4847
5245
|
" let active = true;",
|
|
4848
5246
|
" const syncAuth = async () => {",
|
|
4849
5247
|
" setLoading(true);",
|
|
@@ -4876,7 +5274,7 @@ function platformAppTsx(application, options) {
|
|
|
4876
5274
|
" return () => {",
|
|
4877
5275
|
" active = false;",
|
|
4878
5276
|
" };",
|
|
4879
|
-
" }, [route.kind]);",
|
|
5277
|
+
" }, [auth, route.kind]);",
|
|
4880
5278
|
"",
|
|
4881
5279
|
" useEffect(() => {",
|
|
4882
5280
|
" if (!loading && user && route.kind === 'login') {",
|
|
@@ -4885,13 +5283,39 @@ function platformAppTsx(application, options) {
|
|
|
4885
5283
|
" }, [loading, route.kind, user]);",
|
|
4886
5284
|
"",
|
|
4887
5285
|
" const shellUser = useMemo(() => toShellUser(user), [user]);",
|
|
5286
|
+
" const activeServiceId = serviceId || SCAFFOLD_SERVICE_SLUG;",
|
|
5287
|
+
"",
|
|
5288
|
+
" if (!auth) {",
|
|
5289
|
+
" if (serviceIdentityError) {",
|
|
5290
|
+
" return (",
|
|
5291
|
+
" <TopoloAuthFlowScreen",
|
|
5292
|
+
" appName={APP_NAME}",
|
|
5293
|
+
" appId={APP_ID}",
|
|
5294
|
+
" serviceId={SCAFFOLD_SERVICE_SLUG}",
|
|
5295
|
+
' status="error"',
|
|
5296
|
+
' title="Service identity unavailable"',
|
|
5297
|
+
" message={serviceIdentityError}",
|
|
5298
|
+
' actionLabel="Retry"',
|
|
5299
|
+
" onAction={() => window.location.reload()}",
|
|
5300
|
+
" />",
|
|
5301
|
+
" );",
|
|
5302
|
+
" }",
|
|
5303
|
+
" return (",
|
|
5304
|
+
" <TopoloLoadingScreen",
|
|
5305
|
+
" appId={APP_ID}",
|
|
5306
|
+
" serviceId={SCAFFOLD_SERVICE_SLUG}",
|
|
5307
|
+
" appName={APP_NAME}",
|
|
5308
|
+
" subtitle={`Loading ${APP_NAME}...`}",
|
|
5309
|
+
" />",
|
|
5310
|
+
" );",
|
|
5311
|
+
" }",
|
|
4888
5312
|
"",
|
|
4889
5313
|
" if (route.kind === 'callback') {",
|
|
4890
5314
|
" if (loading) {",
|
|
4891
5315
|
" return (",
|
|
4892
5316
|
" <TopoloLoadingScreen",
|
|
4893
5317
|
" appId={APP_ID}",
|
|
4894
|
-
" serviceId={
|
|
5318
|
+
" serviceId={activeServiceId}",
|
|
4895
5319
|
" appName={APP_NAME}",
|
|
4896
5320
|
' subtitle="Completing secure sign-in..."',
|
|
4897
5321
|
" />",
|
|
@@ -4902,7 +5326,7 @@ function platformAppTsx(application, options) {
|
|
|
4902
5326
|
" <TopoloAuthFlowScreen",
|
|
4903
5327
|
" appName={APP_NAME}",
|
|
4904
5328
|
" appId={APP_ID}",
|
|
4905
|
-
" serviceId={
|
|
5329
|
+
" serviceId={activeServiceId}",
|
|
4906
5330
|
' status="error"',
|
|
4907
5331
|
' title="Authentication failed"',
|
|
4908
5332
|
" message={authError || `We could not complete sign-in for ${APP_NAME}.`}",
|
|
@@ -4916,7 +5340,7 @@ function platformAppTsx(application, options) {
|
|
|
4916
5340
|
" return (",
|
|
4917
5341
|
" <TopoloLoadingScreen",
|
|
4918
5342
|
" appId={APP_ID}",
|
|
4919
|
-
" serviceId={
|
|
5343
|
+
" serviceId={activeServiceId}",
|
|
4920
5344
|
" appName={APP_NAME}",
|
|
4921
5345
|
" subtitle={`Loading ${APP_NAME}...`}",
|
|
4922
5346
|
" />",
|
|
@@ -4927,7 +5351,7 @@ function platformAppTsx(application, options) {
|
|
|
4927
5351
|
" if (route.kind === 'login') {",
|
|
4928
5352
|
" return (",
|
|
4929
5353
|
" <LoginPage",
|
|
4930
|
-
" serviceId={
|
|
5354
|
+
" serviceId={activeServiceId}",
|
|
4931
5355
|
" authApiUrl={AUTH_URL}",
|
|
4932
5356
|
" config={LOGIN_CONFIG}",
|
|
4933
5357
|
" isLoading={loginAttempted}",
|
|
@@ -4954,7 +5378,7 @@ function platformAppTsx(application, options) {
|
|
|
4954
5378
|
"",
|
|
4955
5379
|
" return (",
|
|
4956
5380
|
" <LandingPage",
|
|
4957
|
-
" serviceId={
|
|
5381
|
+
" serviceId={activeServiceId}",
|
|
4958
5382
|
" authApiUrl={AUTH_URL}",
|
|
4959
5383
|
" config={LANDING_CONFIG}",
|
|
4960
5384
|
" onCTAClick={() => window.location.assign('/login')}",
|
|
@@ -4966,7 +5390,7 @@ function platformAppTsx(application, options) {
|
|
|
4966
5390
|
" return (",
|
|
4967
5391
|
" <TopoloLoadingScreen",
|
|
4968
5392
|
" appId={APP_ID}",
|
|
4969
|
-
" serviceId={
|
|
5393
|
+
" serviceId={activeServiceId}",
|
|
4970
5394
|
" appName={APP_NAME}",
|
|
4971
5395
|
` subtitle="Opening your ${escapeDoubleQuoted(signedSurfaceLabel)}..."`,
|
|
4972
5396
|
" />",
|
|
@@ -4976,7 +5400,7 @@ function platformAppTsx(application, options) {
|
|
|
4976
5400
|
" return (",
|
|
4977
5401
|
" <TopoloAppShell<NavItem>",
|
|
4978
5402
|
" appId={APP_ID}",
|
|
4979
|
-
" serviceId={
|
|
5403
|
+
" serviceId={activeServiceId}",
|
|
4980
5404
|
" appName={APP_NAME}",
|
|
4981
5405
|
" user={shellUser}",
|
|
4982
5406
|
" orgId={user.activeContextType === 'organization' ? user.activeContextId ?? user.orgId ?? null : null}",
|
|
@@ -5041,7 +5465,7 @@ function platformAppTsx(application, options) {
|
|
|
5041
5465
|
" <TopoloAuthFlowScreen",
|
|
5042
5466
|
" appName={APP_NAME}",
|
|
5043
5467
|
" appId={APP_ID}",
|
|
5044
|
-
" serviceId={
|
|
5468
|
+
" serviceId={SCAFFOLD_SERVICE_SLUG}",
|
|
5045
5469
|
' status="error"',
|
|
5046
5470
|
" title={APP_NAME + ' hit an unexpected error'}",
|
|
5047
5471
|
" message={this.state.error.message}",
|
|
@@ -5059,19 +5483,21 @@ function developerAppTsx(application) {
|
|
|
5059
5483
|
const defaultClientId = `${toSlug(application.id)}_local_client`;
|
|
5060
5484
|
return [
|
|
5061
5485
|
"import { useEffect, useMemo, useState } from 'react';",
|
|
5486
|
+
"import { createAccountLinkPayload, createTopoloAuthorizationUrl, exchangeTopoloCode, fetchTopoloUserInfo, type TopoloUserInfo } from '@topolo/login';",
|
|
5062
5487
|
"",
|
|
5063
5488
|
"type AuthState = 'ready' | 'missing_client' | 'authorizing' | 'authenticated' | 'error';",
|
|
5064
5489
|
"",
|
|
5065
5490
|
`const APP_NAME = '${escapeSingleQuoted(appName)}';`,
|
|
5066
|
-
"const AUTH_URL = import.meta.env.VITE_TOPOLO_AUTH_URL ?? 'https://auth.topolo.app';",
|
|
5491
|
+
"const AUTH_URL = import.meta.env.VITE_TOPOLO_AUTH_URL ?? import.meta.env.VITE_TOPOLO_AUTH_BASE_URL ?? 'https://auth.topolo.app';",
|
|
5067
5492
|
`const CLIENT_ID = import.meta.env.VITE_TOPOLO_CLIENT_ID || '${defaultClientId}';`,
|
|
5068
5493
|
"const REDIRECT_URI = import.meta.env.VITE_TOPOLO_REDIRECT_URI || `${window.location.origin}/auth/callback`;",
|
|
5069
|
-
"const REQUESTED_SCOPES = (import.meta.env.VITE_TOPOLO_SCOPES || '
|
|
5494
|
+
"const REQUESTED_SCOPES = (import.meta.env.VITE_TOPOLO_SCOPES || 'openid profile email organization:read entitlements:read').split(/[\\s,]+/).filter(Boolean);",
|
|
5070
5495
|
"",
|
|
5071
5496
|
"export default function App() {",
|
|
5072
5497
|
" const [authState, setAuthState] = useState<AuthState>(() => CLIENT_ID ? 'ready' : 'missing_client');",
|
|
5073
5498
|
" const [message, setMessage] = useState<string | null>(null);",
|
|
5074
5499
|
" const [tokenPreview, setTokenPreview] = useState<string | null>(null);",
|
|
5500
|
+
" const [userInfo, setUserInfo] = useState<TopoloUserInfo | null>(null);",
|
|
5075
5501
|
"",
|
|
5076
5502
|
" const callbackCode = useMemo(() => new URLSearchParams(window.location.search).get('code'), []);",
|
|
5077
5503
|
" const callbackError = useMemo(() => new URLSearchParams(window.location.search).get('error'), []);",
|
|
@@ -5086,11 +5512,18 @@ function developerAppTsx(application) {
|
|
|
5086
5512
|
"",
|
|
5087
5513
|
" setAuthState('authorizing');",
|
|
5088
5514
|
" void exchangeAuthorizationCode(callbackCode)",
|
|
5089
|
-
" .then((tokens) => {",
|
|
5515
|
+
" .then(async (tokens) => {",
|
|
5090
5516
|
" sessionStorage.setItem(`${CLIENT_ID}:topolo_access_token`, tokens.access_token);",
|
|
5091
5517
|
" if (tokens.refresh_token) {",
|
|
5092
5518
|
" sessionStorage.setItem(`${CLIENT_ID}:topolo_refresh_token`, tokens.refresh_token);",
|
|
5093
5519
|
" }",
|
|
5520
|
+
" const profile = await fetchTopoloUserInfo({ authBaseUrl: AUTH_URL, accessToken: tokens.access_token });",
|
|
5521
|
+
" const link = createAccountLinkPayload({",
|
|
5522
|
+
" localUserId: profile.email ? `local:${profile.email}` : `local:${profile.sub}`,",
|
|
5523
|
+
" userInfo: profile,",
|
|
5524
|
+
" });",
|
|
5525
|
+
" sessionStorage.setItem(`${CLIENT_ID}:topolo_account_link`, JSON.stringify(link));",
|
|
5526
|
+
" setUserInfo(profile);",
|
|
5094
5527
|
" setTokenPreview(`${tokens.access_token.slice(0, 18)}...`);",
|
|
5095
5528
|
" setAuthState('authenticated');",
|
|
5096
5529
|
" window.history.replaceState({}, document.title, '/');",
|
|
@@ -5110,7 +5543,7 @@ function developerAppTsx(application) {
|
|
|
5110
5543
|
" <p>Set VITE_TOPOLO_CLIENT_ID after registering this app in Topolo Developers.</p>",
|
|
5111
5544
|
" )}",
|
|
5112
5545
|
" {authState === 'authenticated' && (",
|
|
5113
|
-
" <p>Topolo sign-in complete. Access token: {tokenPreview}</p>",
|
|
5546
|
+
" <p>Topolo sign-in complete for {userInfo?.email ?? userInfo?.sub}. Access token: {tokenPreview}</p>",
|
|
5114
5547
|
" )}",
|
|
5115
5548
|
" {authState === 'error' && <p>{message}</p>}",
|
|
5116
5549
|
` <button type="button" disabled={!CLIENT_ID || authState === 'authorizing'} onClick={() => void startTopoloLogin()}>`,
|
|
@@ -5122,21 +5555,15 @@ function developerAppTsx(application) {
|
|
|
5122
5555
|
"}",
|
|
5123
5556
|
"",
|
|
5124
5557
|
"async function startTopoloLogin(): Promise<void> {",
|
|
5125
|
-
" const
|
|
5126
|
-
"
|
|
5127
|
-
"
|
|
5128
|
-
"
|
|
5129
|
-
"
|
|
5130
|
-
"",
|
|
5131
|
-
"
|
|
5132
|
-
"
|
|
5133
|
-
"
|
|
5134
|
-
" authorizeUrl.searchParams.set('redirect_uri', REDIRECT_URI);",
|
|
5135
|
-
" authorizeUrl.searchParams.set('scope', REQUESTED_SCOPES);",
|
|
5136
|
-
" authorizeUrl.searchParams.set('state', state);",
|
|
5137
|
-
" authorizeUrl.searchParams.set('code_challenge', challenge);",
|
|
5138
|
-
" authorizeUrl.searchParams.set('code_challenge_method', 'S256');",
|
|
5139
|
-
" window.location.assign(authorizeUrl.toString());",
|
|
5558
|
+
" const authorization = await createTopoloAuthorizationUrl({",
|
|
5559
|
+
" authBaseUrl: AUTH_URL,",
|
|
5560
|
+
" clientId: CLIENT_ID,",
|
|
5561
|
+
" redirectUri: REDIRECT_URI,",
|
|
5562
|
+
" scopes: REQUESTED_SCOPES,",
|
|
5563
|
+
" });",
|
|
5564
|
+
" sessionStorage.setItem(`${CLIENT_ID}:topolo_pkce_verifier`, authorization.codeVerifier);",
|
|
5565
|
+
" sessionStorage.setItem(`${CLIENT_ID}:topolo_oauth_state`, authorization.state);",
|
|
5566
|
+
" window.location.assign(authorization.url);",
|
|
5140
5567
|
"}",
|
|
5141
5568
|
"",
|
|
5142
5569
|
"async function exchangeAuthorizationCode(code: string): Promise<{ access_token: string; refresh_token?: string }> {",
|
|
@@ -5148,49 +5575,23 @@ function developerAppTsx(application) {
|
|
|
5148
5575
|
" const verifier = sessionStorage.getItem(`${CLIENT_ID}:topolo_pkce_verifier`);",
|
|
5149
5576
|
" if (!verifier) throw new Error('Missing PKCE verifier. Restart sign-in.');",
|
|
5150
5577
|
"",
|
|
5151
|
-
"
|
|
5152
|
-
"
|
|
5153
|
-
"
|
|
5154
|
-
" headers: { 'Content-Type': 'application/json' },",
|
|
5155
|
-
" body: JSON.stringify({",
|
|
5156
|
-
" grant_type: 'authorization_code',",
|
|
5157
|
-
" client_id: CLIENT_ID,",
|
|
5578
|
+
" return exchangeTopoloCode({",
|
|
5579
|
+
" authBaseUrl: AUTH_URL,",
|
|
5580
|
+
" clientId: CLIENT_ID,",
|
|
5158
5581
|
" code,",
|
|
5159
|
-
"
|
|
5160
|
-
"
|
|
5161
|
-
" }),",
|
|
5582
|
+
" redirectUri: REDIRECT_URI,",
|
|
5583
|
+
" codeVerifier: verifier,",
|
|
5162
5584
|
" });",
|
|
5163
|
-
" const payload = await response.json().catch(() => null);",
|
|
5164
|
-
" if (!response.ok || !payload?.access_token) {",
|
|
5165
|
-
" throw new Error(payload?.error_description || payload?.error || 'Token exchange failed.');",
|
|
5166
|
-
" }",
|
|
5167
|
-
" return payload;",
|
|
5168
|
-
"}",
|
|
5169
|
-
"",
|
|
5170
|
-
"function randomString(length: number): string {",
|
|
5171
|
-
" const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';",
|
|
5172
|
-
" const bytes = crypto.getRandomValues(new Uint8Array(length));",
|
|
5173
|
-
" return Array.from(bytes, (byte) => alphabet[byte % alphabet.length]).join('');",
|
|
5174
|
-
"}",
|
|
5175
|
-
"",
|
|
5176
|
-
"async function pkceChallenge(verifier: string): Promise<string> {",
|
|
5177
|
-
" const data = new TextEncoder().encode(verifier);",
|
|
5178
|
-
" const digest = await crypto.subtle.digest('SHA-256', data);",
|
|
5179
|
-
" return base64Url(new Uint8Array(digest));",
|
|
5180
|
-
"}",
|
|
5181
|
-
"",
|
|
5182
|
-
"function base64Url(bytes: Uint8Array): string {",
|
|
5183
|
-
" let binary = '';",
|
|
5184
|
-
" for (const byte of bytes) binary += String.fromCharCode(byte);",
|
|
5185
|
-
" return btoa(binary).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/g, '');",
|
|
5186
5585
|
"}",
|
|
5187
5586
|
""
|
|
5188
5587
|
].join("\n");
|
|
5189
5588
|
}
|
|
5190
|
-
function wranglerToml(application, target) {
|
|
5589
|
+
function wranglerToml(application, target, resources, profile) {
|
|
5191
5590
|
const slug = toSlug(application.id);
|
|
5192
5591
|
const workerName = target === "platform" ? `topolo-${slug}` : application.packageName;
|
|
5193
5592
|
const routeHost = application.productionUrl ? new URL(application.productionUrl).host : null;
|
|
5593
|
+
const resource = resourceConfig(application, profile);
|
|
5594
|
+
const runtimeIdentityVar = target === "platform" ? `SERVICE_ID = "${application.serviceId ?? ""}"` : `SERVICE_ID = "${application.serviceId ?? application.id}"`;
|
|
5194
5595
|
const lines = [
|
|
5195
5596
|
`name = "${workerName}"`,
|
|
5196
5597
|
'main = "src/worker.ts"',
|
|
@@ -5226,9 +5627,47 @@ function wranglerToml(application, target) {
|
|
|
5226
5627
|
"",
|
|
5227
5628
|
"[vars]",
|
|
5228
5629
|
'AUTH_BASE_URL = "https://auth.topolo.app"',
|
|
5229
|
-
|
|
5630
|
+
runtimeIdentityVar,
|
|
5230
5631
|
""
|
|
5231
5632
|
);
|
|
5633
|
+
if (resources.d1) {
|
|
5634
|
+
lines.push(
|
|
5635
|
+
"[[d1_databases]]",
|
|
5636
|
+
`binding = "${resource.d1Binding}"`,
|
|
5637
|
+
`database_name = "${resource.d1ProductionName}"`,
|
|
5638
|
+
'database_id = "provision-after-scaffold"',
|
|
5639
|
+
'migrations_dir = "migrations"',
|
|
5640
|
+
""
|
|
5641
|
+
);
|
|
5642
|
+
}
|
|
5643
|
+
if (resources.r2) {
|
|
5644
|
+
lines.push(
|
|
5645
|
+
"[[r2_buckets]]",
|
|
5646
|
+
`binding = "${resource.r2Binding}"`,
|
|
5647
|
+
`bucket_name = "${resource.r2ProductionName}"`,
|
|
5648
|
+
""
|
|
5649
|
+
);
|
|
5650
|
+
}
|
|
5651
|
+
if (resources.queue) {
|
|
5652
|
+
lines.push(
|
|
5653
|
+
"[[queues.producers]]",
|
|
5654
|
+
`binding = "${resource.queueBinding}"`,
|
|
5655
|
+
`queue = "${resource.queueProductionName}"`,
|
|
5656
|
+
"",
|
|
5657
|
+
"[[queues.consumers]]",
|
|
5658
|
+
`queue = "${resource.queueProductionName}"`,
|
|
5659
|
+
""
|
|
5660
|
+
);
|
|
5661
|
+
}
|
|
5662
|
+
if (resources.workflow) {
|
|
5663
|
+
lines.push(
|
|
5664
|
+
"[[workflows]]",
|
|
5665
|
+
`binding = "${resource.workflowBinding}"`,
|
|
5666
|
+
`name = "${resource.workflowProductionName}"`,
|
|
5667
|
+
`class_name = "${resource.workflowClassName}"`,
|
|
5668
|
+
""
|
|
5669
|
+
);
|
|
5670
|
+
}
|
|
5232
5671
|
if (target === "platform") {
|
|
5233
5672
|
lines.push(
|
|
5234
5673
|
"# Staging deploys use the separate Topolo Staging Cloudflare account.",
|
|
@@ -5242,9 +5681,47 @@ function wranglerToml(application, target) {
|
|
|
5242
5681
|
"",
|
|
5243
5682
|
"[env.staging.vars]",
|
|
5244
5683
|
'AUTH_BASE_URL = "https://auth.stg.topolo.us"',
|
|
5245
|
-
|
|
5684
|
+
runtimeIdentityVar,
|
|
5246
5685
|
""
|
|
5247
5686
|
);
|
|
5687
|
+
if (resources.d1) {
|
|
5688
|
+
lines.push(
|
|
5689
|
+
"[[env.staging.d1_databases]]",
|
|
5690
|
+
`binding = "${resource.d1Binding}"`,
|
|
5691
|
+
`database_name = "${resource.d1StagingName}"`,
|
|
5692
|
+
'database_id = "provision-after-scaffold"',
|
|
5693
|
+
'migrations_dir = "migrations"',
|
|
5694
|
+
""
|
|
5695
|
+
);
|
|
5696
|
+
}
|
|
5697
|
+
if (resources.r2) {
|
|
5698
|
+
lines.push(
|
|
5699
|
+
"[[env.staging.r2_buckets]]",
|
|
5700
|
+
`binding = "${resource.r2Binding}"`,
|
|
5701
|
+
`bucket_name = "${resource.r2StagingName}"`,
|
|
5702
|
+
""
|
|
5703
|
+
);
|
|
5704
|
+
}
|
|
5705
|
+
if (resources.queue) {
|
|
5706
|
+
lines.push(
|
|
5707
|
+
"[[env.staging.queues.producers]]",
|
|
5708
|
+
`binding = "${resource.queueBinding}"`,
|
|
5709
|
+
`queue = "${resource.queueStagingName}"`,
|
|
5710
|
+
"",
|
|
5711
|
+
"[[env.staging.queues.consumers]]",
|
|
5712
|
+
`queue = "${resource.queueStagingName}"`,
|
|
5713
|
+
""
|
|
5714
|
+
);
|
|
5715
|
+
}
|
|
5716
|
+
if (resources.workflow) {
|
|
5717
|
+
lines.push(
|
|
5718
|
+
"[[env.staging.workflows]]",
|
|
5719
|
+
`binding = "${resource.workflowBinding}"`,
|
|
5720
|
+
`name = "${resource.workflowStagingName}"`,
|
|
5721
|
+
`class_name = "${resource.workflowClassName}"`,
|
|
5722
|
+
""
|
|
5723
|
+
);
|
|
5724
|
+
}
|
|
5248
5725
|
}
|
|
5249
5726
|
return lines.join("\n");
|
|
5250
5727
|
}
|
|
@@ -5253,12 +5730,29 @@ function workerTs(application, target, options) {
|
|
|
5253
5730
|
return platformWorkerTs(application, options);
|
|
5254
5731
|
}
|
|
5255
5732
|
function platformWorkerTs(application, options) {
|
|
5256
|
-
const serviceId = application.serviceId ?? application.id;
|
|
5257
5733
|
const serviceName = toTitle(application.name);
|
|
5258
5734
|
const openActionLabel = options.audience === "personal" ? "Open app" : "Open workspace";
|
|
5735
|
+
const resource = resourceConfig(application, options.profile);
|
|
5736
|
+
const envBindings = [
|
|
5737
|
+
" AUTH_BASE_URL?: string;",
|
|
5738
|
+
" SERVICE_ID?: string;",
|
|
5739
|
+
" TOPOLO_SERVICE_API_KEY?: string;",
|
|
5740
|
+
...options.resources.d1 ? [` ${resource.d1Binding}?: unknown;`] : [],
|
|
5741
|
+
...options.resources.r2 ? [` ${resource.r2Binding}?: unknown;`] : [],
|
|
5742
|
+
...options.resources.queue ? [` ${resource.queueBinding}?: { send(message: unknown): Promise<void> };`] : [],
|
|
5743
|
+
...options.resources.workflow ? [` ${resource.workflowBinding}?: unknown;`] : []
|
|
5744
|
+
];
|
|
5745
|
+
const workflowExport = options.resources.workflow ? [
|
|
5746
|
+
"",
|
|
5747
|
+
`export class ${resource.workflowClassName} extends WorkflowEntrypoint<Env, unknown> {`,
|
|
5748
|
+
" async run(_event: Readonly<WorkflowEvent<unknown>>, step: WorkflowStep): Promise<unknown> {",
|
|
5749
|
+
" return step.do('scaffold no-op', async () => ({ status: 'pending_implementation' }));",
|
|
5750
|
+
" }",
|
|
5751
|
+
"}"
|
|
5752
|
+
] : [];
|
|
5259
5753
|
const bootstrapBody = options.tenancy === "household" ? [
|
|
5260
5754
|
" const userContext = auth.user && typeof auth.user === 'object' ? (auth.user as unknown as Record<string, unknown>) : {};",
|
|
5261
|
-
" return json({",
|
|
5755
|
+
" return rt.json({",
|
|
5262
5756
|
" user: auth.user,",
|
|
5263
5757
|
" activeContextType: userContext.activeContextType ?? null,",
|
|
5264
5758
|
" activeContextId: userContext.activeContextId ?? null,",
|
|
@@ -5267,82 +5761,111 @@ function platformWorkerTs(application, options) {
|
|
|
5267
5761
|
" selectedHousehold: userContext.selectedHousehold ?? null,",
|
|
5268
5762
|
" household: userContext.selectedHousehold ?? null,",
|
|
5269
5763
|
" householdMemberships: userContext.householdMemberships ?? [],",
|
|
5270
|
-
" }, { status: 200 }
|
|
5764
|
+
" }, { status: 200 });"
|
|
5271
5765
|
] : [
|
|
5272
|
-
" return json({",
|
|
5766
|
+
" return rt.json({",
|
|
5273
5767
|
" user: auth.user,",
|
|
5274
5768
|
" organization: auth.organization,",
|
|
5275
|
-
" }, { status: 200 }
|
|
5769
|
+
" }, { status: 200 });"
|
|
5276
5770
|
];
|
|
5277
5771
|
return [
|
|
5278
|
-
"import {
|
|
5279
|
-
"import {
|
|
5772
|
+
"import { createWidgetHandler } from '@topolo-io/widget-server';",
|
|
5773
|
+
"import { createTopoloWorkerRuntime } from '@topolo-io/worker-runtime';",
|
|
5774
|
+
"import { SCAFFOLD_APP_ID, SCAFFOLD_APP_NAME, SCAFFOLD_SERVICE_SLUG, resolveRuntimeServiceId } from './service-identity';",
|
|
5775
|
+
...options.resources.workflow ? ["import { WorkflowEntrypoint, type WorkflowEvent, type WorkflowStep } from 'cloudflare:workers';"] : [],
|
|
5280
5776
|
"",
|
|
5281
5777
|
"export interface Env {",
|
|
5282
|
-
|
|
5283
|
-
" SERVICE_ID?: string;",
|
|
5284
|
-
" TOPOLO_SERVICE_API_KEY?: string;",
|
|
5778
|
+
...envBindings,
|
|
5285
5779
|
"}",
|
|
5286
5780
|
"",
|
|
5287
|
-
"
|
|
5781
|
+
"// Required by the platform `native-dashboard-widget` requirement \u2014",
|
|
5782
|
+
"// TopoloOne renders this in the user's live workspace whenever the app",
|
|
5783
|
+
"// is installed. Replace the quick-actions placeholder with org-scoped",
|
|
5784
|
+
"// stats once your data model is ready: ctx.user / ctx.orgId / ctx.orgSlug",
|
|
5785
|
+
"// / ctx.isPlatformAdmin / ctx.contextType / ctx.householdId are all",
|
|
5786
|
+
"// pre-resolved. Builder exceptions degrade to empty widgets, not 500s.",
|
|
5787
|
+
"const widgetHandler = createWidgetHandler<Env>({",
|
|
5788
|
+
" serviceId: SCAFFOLD_SERVICE_SLUG,",
|
|
5789
|
+
` serviceName: '${escapeSingleQuoted(serviceName)}',`,
|
|
5790
|
+
" authBaseUrl: (env) => env.AUTH_BASE_URL,",
|
|
5791
|
+
" serviceApiKey: (env) => env.TOPOLO_SERVICE_API_KEY,",
|
|
5792
|
+
" build: () => [",
|
|
5793
|
+
" {",
|
|
5794
|
+
" type: 'quick-actions',",
|
|
5795
|
+
" actions: [",
|
|
5796
|
+
` { id: 'open-workspace', label: '${escapeSingleQuoted(openActionLabel)}', link: '/', primary: true },`,
|
|
5797
|
+
" { id: 'view-health', label: 'View health', link: '/api/health' },",
|
|
5798
|
+
" ],",
|
|
5799
|
+
" },",
|
|
5800
|
+
" ],",
|
|
5801
|
+
"});",
|
|
5288
5802
|
"",
|
|
5289
|
-
"
|
|
5290
|
-
"
|
|
5291
|
-
"
|
|
5803
|
+
"const runtime = createTopoloWorkerRuntime<Env>({",
|
|
5804
|
+
" appName: SCAFFOLD_APP_NAME,",
|
|
5805
|
+
" serviceSlug: SCAFFOLD_SERVICE_SLUG,",
|
|
5806
|
+
" resolveServiceId: (env) => resolveRuntimeServiceId(env),",
|
|
5807
|
+
" authBaseUrl: (env) => env.AUTH_BASE_URL,",
|
|
5808
|
+
" serviceApiKey: (env) => env.TOPOLO_SERVICE_API_KEY,",
|
|
5809
|
+
"});",
|
|
5292
5810
|
"",
|
|
5293
|
-
"
|
|
5294
|
-
"
|
|
5811
|
+
"const handleFetch = runtime.worker(async (request, env, _ctx, rt): Promise<Response> => {",
|
|
5812
|
+
" const url = rt.url;",
|
|
5813
|
+
"",
|
|
5814
|
+
" let serviceId: string;",
|
|
5815
|
+
" try {",
|
|
5816
|
+
" serviceId = await resolveRuntimeServiceId(env);",
|
|
5817
|
+
" } catch (error) {",
|
|
5818
|
+
" return rt.json({ error: 'service_identity_unavailable', message: errorMessage(error) }, { status: 503 });",
|
|
5295
5819
|
" }",
|
|
5296
5820
|
"",
|
|
5297
|
-
"
|
|
5298
|
-
"
|
|
5299
|
-
"
|
|
5300
|
-
" serviceApiKey: env.TOPOLO_SERVICE_API_KEY,",
|
|
5301
|
-
" });",
|
|
5821
|
+
" if (url.pathname === '/api/health') {",
|
|
5822
|
+
" return rt.json({ ok: true, service: serviceId, serviceSlug: SCAFFOLD_SERVICE_SLUG }, { status: 200 });",
|
|
5823
|
+
" }",
|
|
5302
5824
|
"",
|
|
5303
|
-
" if (
|
|
5304
|
-
" return json({
|
|
5825
|
+
" if (url.pathname === '/api/service-identity') {",
|
|
5826
|
+
" return rt.json({",
|
|
5827
|
+
" appId: SCAFFOLD_APP_ID,",
|
|
5828
|
+
" appName: SCAFFOLD_APP_NAME,",
|
|
5829
|
+
" serviceSlug: SCAFFOLD_SERVICE_SLUG,",
|
|
5830
|
+
" serviceId,",
|
|
5831
|
+
" }, { status: 200 });",
|
|
5305
5832
|
" }",
|
|
5306
5833
|
"",
|
|
5307
|
-
"
|
|
5308
|
-
"
|
|
5309
|
-
"
|
|
5310
|
-
|
|
5311
|
-
"
|
|
5312
|
-
"
|
|
5313
|
-
"
|
|
5314
|
-
"
|
|
5315
|
-
` { id: 'open-workspace', label: '${escapeSingleQuoted(openActionLabel)}', link: '/', primary: true },`,
|
|
5316
|
-
" { id: 'view-health', label: 'View health', link: '/api/health' },",
|
|
5317
|
-
" ],",
|
|
5318
|
-
" },",
|
|
5319
|
-
" ],",
|
|
5320
|
-
" }), { status: 200 }, request);",
|
|
5834
|
+
" // /api/widget delegates fully to @topolo-io/widget-server \u2014 it owns",
|
|
5835
|
+
" // auth, context parsing, response wrapping, and error degradation.",
|
|
5836
|
+
" if (url.pathname === '/api/widget') return widgetHandler(request, env);",
|
|
5837
|
+
"",
|
|
5838
|
+
" const auth = await rt.verifyAuth({ serviceId });",
|
|
5839
|
+
"",
|
|
5840
|
+
" if (!auth.ok) {",
|
|
5841
|
+
" return rt.authError(auth);",
|
|
5321
5842
|
" }",
|
|
5322
5843
|
"",
|
|
5323
5844
|
" if (url.pathname === '/api/bootstrap') {",
|
|
5324
5845
|
...bootstrapBody,
|
|
5325
5846
|
" }",
|
|
5326
5847
|
"",
|
|
5327
|
-
" return
|
|
5328
|
-
"
|
|
5329
|
-
"};",
|
|
5848
|
+
" return rt.notFound();",
|
|
5849
|
+
"});",
|
|
5330
5850
|
"",
|
|
5331
|
-
|
|
5851
|
+
"export default {",
|
|
5852
|
+
" fetch: handleFetch,",
|
|
5853
|
+
"};",
|
|
5332
5854
|
"",
|
|
5333
|
-
"function
|
|
5334
|
-
"
|
|
5335
|
-
"
|
|
5336
|
-
" headers.set('X-Topolo-Client', CLIENT_HEADER);",
|
|
5337
|
-
" headers.set('X-Topolo-Request-Id', request.headers.get('X-Topolo-Request-Id') ?? crypto.randomUUID());",
|
|
5338
|
-
" return new Response(JSON.stringify(body), { ...init, headers });",
|
|
5855
|
+
"function errorMessage(error: unknown): string {",
|
|
5856
|
+
" if (error instanceof Error && error.message) return error.message;",
|
|
5857
|
+
" return 'Unexpected service identity error.';",
|
|
5339
5858
|
"}",
|
|
5859
|
+
...workflowExport,
|
|
5340
5860
|
""
|
|
5341
5861
|
].join("\n");
|
|
5342
5862
|
}
|
|
5343
5863
|
function developerWorkerTs(application) {
|
|
5344
5864
|
const serviceId = application.serviceId ?? application.id;
|
|
5865
|
+
const serviceName = application.name || "Topolo App";
|
|
5345
5866
|
return [
|
|
5867
|
+
"import { createWidgetHandler } from '@topolo-io/widget-server';",
|
|
5868
|
+
"",
|
|
5346
5869
|
"export interface Env {",
|
|
5347
5870
|
" AUTH_BASE_URL?: string;",
|
|
5348
5871
|
" SERVICE_ID?: string;",
|
|
@@ -5350,6 +5873,28 @@ function developerWorkerTs(application) {
|
|
|
5350
5873
|
"",
|
|
5351
5874
|
"const CLIENT_HEADER = 'topolo-developer-starter';",
|
|
5352
5875
|
`const DEFAULT_SERVICE_ID = '${serviceId}';`,
|
|
5876
|
+
`const SERVICE_NAME = '${escapeSingleQuoted(serviceName)}';`,
|
|
5877
|
+
"",
|
|
5878
|
+
"// Required by the platform `native-dashboard-widget` requirement \u2014",
|
|
5879
|
+
"// TopoloOne renders this in the user's live workspace whenever the app",
|
|
5880
|
+
"// is installed. Replace the quick-actions placeholder with org-scoped",
|
|
5881
|
+
"// stats once your data model is ready: ctx.user / ctx.orgId / ctx.orgSlug",
|
|
5882
|
+
"// / ctx.isPlatformAdmin / ctx.contextType / ctx.householdId are all",
|
|
5883
|
+
"// pre-resolved. Builder exceptions degrade to empty widgets, not 500s.",
|
|
5884
|
+
"const widgetHandler = createWidgetHandler<Env>({",
|
|
5885
|
+
" serviceId: DEFAULT_SERVICE_ID,",
|
|
5886
|
+
" serviceName: SERVICE_NAME,",
|
|
5887
|
+
" authBaseUrl: (env) => env.AUTH_BASE_URL,",
|
|
5888
|
+
" build: () => [",
|
|
5889
|
+
" {",
|
|
5890
|
+
" type: 'quick-actions',",
|
|
5891
|
+
" actions: [",
|
|
5892
|
+
" { id: 'open-app', label: `Open ${SERVICE_NAME}`, link: '/', primary: true },",
|
|
5893
|
+
" { id: 'view-health', label: 'View health', link: '/api/health' },",
|
|
5894
|
+
" ],",
|
|
5895
|
+
" },",
|
|
5896
|
+
" ],",
|
|
5897
|
+
"});",
|
|
5353
5898
|
"",
|
|
5354
5899
|
"export default {",
|
|
5355
5900
|
" async fetch(request: Request, env: Env): Promise<Response> {",
|
|
@@ -5359,6 +5904,9 @@ function developerWorkerTs(application) {
|
|
|
5359
5904
|
" return json({ ok: true, service: env.SERVICE_ID ?? DEFAULT_SERVICE_ID }, { status: 200 }, request);",
|
|
5360
5905
|
" }",
|
|
5361
5906
|
"",
|
|
5907
|
+
" // /api/widget delegates fully to @topolo-io/widget-server.",
|
|
5908
|
+
" if (url.pathname === '/api/widget') return widgetHandler(request, env);",
|
|
5909
|
+
"",
|
|
5362
5910
|
" const auth = await verifyTopoloRequest(request, env);",
|
|
5363
5911
|
" if (!auth.ok) {",
|
|
5364
5912
|
" return json({ error: auth.error }, { status: auth.status }, request);",
|
|
@@ -5415,8 +5963,108 @@ function developerWorkerTs(application) {
|
|
|
5415
5963
|
}
|
|
5416
5964
|
function cloudcontrolJson(application, options) {
|
|
5417
5965
|
const slug = toSlug(application.id);
|
|
5966
|
+
const appPath = firstPartyAppPath(application.appDir, options.audience);
|
|
5418
5967
|
const host = application.productionUrl ? new URL(application.productionUrl).host : `${slug}.topolo.app`;
|
|
5419
5968
|
const stagingHost = `${slug}.stg.topolo.us`;
|
|
5969
|
+
const serviceSlug = application.serviceSlug ?? `topolo-${slug}`;
|
|
5970
|
+
const resources = resourceConfig(application, options.profile);
|
|
5971
|
+
const resourceBindings = [];
|
|
5972
|
+
if (options.resources.d1) {
|
|
5973
|
+
resourceBindings.push(
|
|
5974
|
+
{
|
|
5975
|
+
resource_type: "d1",
|
|
5976
|
+
binding: resources.d1Binding,
|
|
5977
|
+
environment: "production",
|
|
5978
|
+
account_id: CLOUDFLARE_ACCOUNT_ID,
|
|
5979
|
+
resource_name: resources.d1ProductionName,
|
|
5980
|
+
resource_id: "provision-after-scaffold",
|
|
5981
|
+
migrations_dir: "migrations"
|
|
5982
|
+
},
|
|
5983
|
+
{
|
|
5984
|
+
resource_type: "d1",
|
|
5985
|
+
binding: resources.d1Binding,
|
|
5986
|
+
environment: "staging",
|
|
5987
|
+
account_id: STAGING_CLOUDFLARE_ACCOUNT_ID,
|
|
5988
|
+
resource_name: resources.d1StagingName,
|
|
5989
|
+
resource_id: "provision-after-scaffold",
|
|
5990
|
+
migrations_dir: "migrations"
|
|
5991
|
+
}
|
|
5992
|
+
);
|
|
5993
|
+
}
|
|
5994
|
+
if (options.resources.r2) {
|
|
5995
|
+
resourceBindings.push(
|
|
5996
|
+
{
|
|
5997
|
+
resource_type: "r2",
|
|
5998
|
+
binding: resources.r2Binding,
|
|
5999
|
+
environment: "production",
|
|
6000
|
+
account_id: CLOUDFLARE_ACCOUNT_ID,
|
|
6001
|
+
resource_name: resources.r2ProductionName
|
|
6002
|
+
},
|
|
6003
|
+
{
|
|
6004
|
+
resource_type: "r2",
|
|
6005
|
+
binding: resources.r2Binding,
|
|
6006
|
+
environment: "staging",
|
|
6007
|
+
account_id: STAGING_CLOUDFLARE_ACCOUNT_ID,
|
|
6008
|
+
resource_name: resources.r2StagingName
|
|
6009
|
+
}
|
|
6010
|
+
);
|
|
6011
|
+
}
|
|
6012
|
+
if (options.resources.queue) {
|
|
6013
|
+
resourceBindings.push(
|
|
6014
|
+
{
|
|
6015
|
+
resource_type: "queue",
|
|
6016
|
+
binding: resources.queueBinding,
|
|
6017
|
+
environment: "production",
|
|
6018
|
+
account_id: CLOUDFLARE_ACCOUNT_ID,
|
|
6019
|
+
resource_name: resources.queueProductionName
|
|
6020
|
+
},
|
|
6021
|
+
{
|
|
6022
|
+
resource_type: "queue",
|
|
6023
|
+
binding: resources.queueBinding,
|
|
6024
|
+
environment: "staging",
|
|
6025
|
+
account_id: STAGING_CLOUDFLARE_ACCOUNT_ID,
|
|
6026
|
+
resource_name: resources.queueStagingName
|
|
6027
|
+
}
|
|
6028
|
+
);
|
|
6029
|
+
}
|
|
6030
|
+
if (options.resources.workflow) {
|
|
6031
|
+
resourceBindings.push(
|
|
6032
|
+
{
|
|
6033
|
+
resource_type: "workflow",
|
|
6034
|
+
binding: resources.workflowBinding,
|
|
6035
|
+
environment: "production",
|
|
6036
|
+
account_id: CLOUDFLARE_ACCOUNT_ID,
|
|
6037
|
+
resource_name: resources.workflowProductionName,
|
|
6038
|
+
class_name: resources.workflowClassName
|
|
6039
|
+
},
|
|
6040
|
+
{
|
|
6041
|
+
resource_type: "workflow",
|
|
6042
|
+
binding: resources.workflowBinding,
|
|
6043
|
+
environment: "staging",
|
|
6044
|
+
account_id: STAGING_CLOUDFLARE_ACCOUNT_ID,
|
|
6045
|
+
resource_name: resources.workflowStagingName,
|
|
6046
|
+
class_name: resources.workflowClassName
|
|
6047
|
+
}
|
|
6048
|
+
);
|
|
6049
|
+
}
|
|
6050
|
+
if (options.resources.containerProcessing) {
|
|
6051
|
+
resourceBindings.push(
|
|
6052
|
+
{
|
|
6053
|
+
resource_type: "container",
|
|
6054
|
+
environment: "production",
|
|
6055
|
+
account_id: CLOUDFLARE_ACCOUNT_ID,
|
|
6056
|
+
resource_name: resources.containerProductionName,
|
|
6057
|
+
purpose: "processing"
|
|
6058
|
+
},
|
|
6059
|
+
{
|
|
6060
|
+
resource_type: "container",
|
|
6061
|
+
environment: "staging",
|
|
6062
|
+
account_id: STAGING_CLOUDFLARE_ACCOUNT_ID,
|
|
6063
|
+
resource_name: resources.containerStagingName,
|
|
6064
|
+
purpose: "processing"
|
|
6065
|
+
}
|
|
6066
|
+
);
|
|
6067
|
+
}
|
|
5420
6068
|
const deployTargets = options.api ? [
|
|
5421
6069
|
{
|
|
5422
6070
|
kind: "worker",
|
|
@@ -5431,7 +6079,7 @@ function cloudcontrolJson(application, options) {
|
|
|
5431
6079
|
domains: [host],
|
|
5432
6080
|
routes: [host],
|
|
5433
6081
|
api_url: application.productionUrl,
|
|
5434
|
-
|
|
6082
|
+
service_slug: serviceSlug
|
|
5435
6083
|
},
|
|
5436
6084
|
{
|
|
5437
6085
|
kind: "worker",
|
|
@@ -5441,12 +6089,12 @@ function cloudcontrolJson(application, options) {
|
|
|
5441
6089
|
config_path: "wrangler.toml",
|
|
5442
6090
|
build_command: "VITE_TOPOLO_AUTH_URL=https://auth.stg.topolo.us npm run build",
|
|
5443
6091
|
output_dir: "dist",
|
|
5444
|
-
production_branch: "
|
|
6092
|
+
production_branch: "main",
|
|
5445
6093
|
healthcheck_path: "/api/health",
|
|
5446
6094
|
domains: [stagingHost],
|
|
5447
6095
|
routes: [stagingHost],
|
|
5448
6096
|
api_url: `https://${stagingHost}`,
|
|
5449
|
-
|
|
6097
|
+
service_slug: serviceSlug
|
|
5450
6098
|
}
|
|
5451
6099
|
] : [
|
|
5452
6100
|
{
|
|
@@ -5466,14 +6114,14 @@ function cloudcontrolJson(application, options) {
|
|
|
5466
6114
|
];
|
|
5467
6115
|
return `${json({
|
|
5468
6116
|
name: `topolo-${slug}`,
|
|
5469
|
-
description: `Canonical documentation for ${toTitle(application.name)} lives in
|
|
6117
|
+
description: `Canonical documentation for ${toTitle(application.name)} lives in Apps/system/TopoloDocs.`,
|
|
5470
6118
|
deploy_method: options.api ? "wrangler" : "cloudflare_pages",
|
|
5471
6119
|
ci_provider: "github_actions",
|
|
5472
6120
|
repo_identity: {
|
|
5473
6121
|
provider: "github",
|
|
5474
6122
|
owner: "Topolo-io",
|
|
5475
|
-
repo:
|
|
5476
|
-
remote_url: `git@github.com:Topolo-io
|
|
6123
|
+
repo: application.appDir,
|
|
6124
|
+
remote_url: `git@github.com:Topolo-io/${application.appDir}.git`,
|
|
5477
6125
|
default_branch: "main"
|
|
5478
6126
|
},
|
|
5479
6127
|
environments: application.productionUrl ? [
|
|
@@ -5486,12 +6134,12 @@ function cloudcontrolJson(application, options) {
|
|
|
5486
6134
|
{
|
|
5487
6135
|
name: "staging",
|
|
5488
6136
|
url: `https://${stagingHost}`,
|
|
5489
|
-
branch: "
|
|
6137
|
+
branch: "main",
|
|
5490
6138
|
status: "unknown"
|
|
5491
6139
|
}
|
|
5492
6140
|
] : [],
|
|
5493
6141
|
deploy_targets: deployTargets,
|
|
5494
|
-
resource_bindings:
|
|
6142
|
+
resource_bindings: resourceBindings,
|
|
5495
6143
|
ci_workflows: [
|
|
5496
6144
|
{
|
|
5497
6145
|
provider: "github_actions",
|
|
@@ -5501,8 +6149,25 @@ function cloudcontrolJson(application, options) {
|
|
|
5501
6149
|
required: true
|
|
5502
6150
|
}
|
|
5503
6151
|
],
|
|
5504
|
-
ci_targets: [
|
|
5505
|
-
|
|
6152
|
+
ci_targets: [
|
|
6153
|
+
{
|
|
6154
|
+
name: "typecheck",
|
|
6155
|
+
command: "npm run typecheck",
|
|
6156
|
+
required: true
|
|
6157
|
+
},
|
|
6158
|
+
{
|
|
6159
|
+
name: "test",
|
|
6160
|
+
command: "npm run test",
|
|
6161
|
+
required: true
|
|
6162
|
+
},
|
|
6163
|
+
{
|
|
6164
|
+
name: "build",
|
|
6165
|
+
command: "npm run build",
|
|
6166
|
+
required: true
|
|
6167
|
+
}
|
|
6168
|
+
],
|
|
6169
|
+
tags: ["application", "cloudflare", "typescript", ...hasScaffoldResources(options.resources) ? ["resource-backed"] : []],
|
|
6170
|
+
source_paths: [appPath],
|
|
5506
6171
|
cloudflare_account_id: CLOUDFLARE_ACCOUNT_ID
|
|
5507
6172
|
})}
|
|
5508
6173
|
`;
|
|
@@ -5511,7 +6176,9 @@ function systemJson(application, options) {
|
|
|
5511
6176
|
const slug = toSlug(application.id);
|
|
5512
6177
|
const suiteLabel = options.audience === "personal" ? "personal" : "platform";
|
|
5513
6178
|
const summaryLabel = options.audience === "personal" ? "first-party Topolo personal application" : "first-party Topolo platform application";
|
|
5514
|
-
const
|
|
6179
|
+
const appPath = firstPartyAppPath(application.appDir, options.audience);
|
|
6180
|
+
const resourceNotes = hasScaffoldResources(options.resources) ? " Generated resource bindings are declared in Wrangler and CloudControl for durable platform-owned state and processing." : "";
|
|
6181
|
+
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.";
|
|
5515
6182
|
const sensitiveData = options.tenancy === "household" ? ["identity", "personal_data", "household_data"] : ["identity", "org_data"];
|
|
5516
6183
|
const privacySensitiveData = options.tenancy === "household" ? ["identity", "personal", "household"] : ["identity", "organization"];
|
|
5517
6184
|
return `${json({
|
|
@@ -5523,19 +6190,19 @@ function systemJson(application, options) {
|
|
|
5523
6190
|
lifecycle: "inventory",
|
|
5524
6191
|
last_verified: CURRENT_VERIFICATION_DATE,
|
|
5525
6192
|
owners: ["platform-engineering"],
|
|
5526
|
-
repo_paths: [
|
|
6193
|
+
repo_paths: [appPath],
|
|
5527
6194
|
service_ids: options.api && application.serviceId ? [application.serviceId] : [],
|
|
5528
6195
|
visibility: "internal",
|
|
5529
6196
|
api_contract: {
|
|
5530
6197
|
type: options.api ? "curated" : "none",
|
|
5531
|
-
source: options.api ?
|
|
6198
|
+
source: options.api ? `${appPath}/src/worker.ts` : `${appPath}/src/App.tsx`,
|
|
5532
6199
|
notes: apiNotes
|
|
5533
6200
|
},
|
|
5534
6201
|
primary_hosts: application.productionUrl ? [application.productionUrl] : [],
|
|
5535
6202
|
doc_paths: [`internal/apps/${slug}`, `public/applications/${slug}`],
|
|
5536
6203
|
security_assurance: {
|
|
5537
6204
|
risk_tier: "internal",
|
|
5538
|
-
auth_boundary: "Topolo Auth through @topolo-io/auth-client for browser sessions and @topolo-io/
|
|
6205
|
+
auth_boundary: "Topolo Auth through @topolo-io/auth-client for browser sessions and @topolo-io/worker-runtime for Worker requests.",
|
|
5539
6206
|
tenant_isolation: options.tenancy === "household" ? "household_scoped" : "organization_scoped",
|
|
5540
6207
|
external_inputs: ["browser", ...options.api ? ["api"] : []],
|
|
5541
6208
|
sensitive_data: sensitiveData,
|
|
@@ -5547,7 +6214,13 @@ function systemJson(application, options) {
|
|
|
5547
6214
|
data_privacy: {
|
|
5548
6215
|
classification: "confidential",
|
|
5549
6216
|
sensitive_data_classes: privacySensitiveData,
|
|
5550
|
-
storage_locations: [
|
|
6217
|
+
storage_locations: [
|
|
6218
|
+
"source_control",
|
|
6219
|
+
...options.resources.d1 ? ["d1"] : [],
|
|
6220
|
+
...options.resources.r2 ? ["r2"] : [],
|
|
6221
|
+
...options.resources.queue ? ["queue"] : [],
|
|
6222
|
+
...options.resources.workflow ? ["workflow"] : []
|
|
6223
|
+
],
|
|
5551
6224
|
encryption_at_rest: "platform_managed",
|
|
5552
6225
|
encryption_in_transit: "https_only",
|
|
5553
6226
|
key_management: "cloudflare_managed",
|
|
@@ -5565,11 +6238,24 @@ function systemJson(application, options) {
|
|
|
5565
6238
|
}
|
|
5566
6239
|
function internalDocs(application, options) {
|
|
5567
6240
|
const slug = toSlug(application.id);
|
|
6241
|
+
const serviceSlug = application.serviceSlug ?? `topolo-${slug}`;
|
|
5568
6242
|
const title = toTitle(application.name);
|
|
6243
|
+
const appPath = firstPartyAppPath(application.appDir, options.audience);
|
|
5569
6244
|
const suiteLabel = options.audience === "personal" ? "personal" : "platform";
|
|
5570
6245
|
const tenantLabel = options.tenancy === "household" ? "household" : "organization";
|
|
5571
6246
|
const apiContextLabel = options.tenancy === "household" ? "active personal context and selected household context" : "organization context";
|
|
5572
6247
|
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.";
|
|
6248
|
+
const resource = resourceConfig(application, options.profile);
|
|
6249
|
+
const resourceLines = hasScaffoldResources(options.resources) ? [
|
|
6250
|
+
"",
|
|
6251
|
+
"## Resource Bindings",
|
|
6252
|
+
"",
|
|
6253
|
+
...options.resources.d1 ? [`- D1 binding \`${resource.d1Binding}\` for durable metadata.`] : [],
|
|
6254
|
+
...options.resources.r2 ? [`- R2 binding \`${resource.r2Binding}\` for object assets.`] : [],
|
|
6255
|
+
...options.resources.queue ? [`- Queue binding \`${resource.queueBinding}\` for asynchronous work intake.`] : [],
|
|
6256
|
+
...options.resources.workflow ? [`- Workflow binding \`${resource.workflowBinding}\` for import or processing orchestration.`] : [],
|
|
6257
|
+
...options.resources.containerProcessing ? ["- Container processing metadata is declared in CloudControl for CPU/file-heavy jobs."] : []
|
|
6258
|
+
] : [];
|
|
5573
6259
|
return [
|
|
5574
6260
|
"---",
|
|
5575
6261
|
`title: "${escapeFrontmatter(title)} Handbook"`,
|
|
@@ -5578,8 +6264,8 @@ function internalDocs(application, options) {
|
|
|
5578
6264
|
'status: "draft"',
|
|
5579
6265
|
`last_verified: "${CURRENT_VERIFICATION_DATE}"`,
|
|
5580
6266
|
"source_repos:",
|
|
5581
|
-
` - "
|
|
5582
|
-
' - "
|
|
6267
|
+
` - "${appPath}"`,
|
|
6268
|
+
' - "Apps/system/TopoloDocs"',
|
|
5583
6269
|
'audience: "internal"',
|
|
5584
6270
|
"tags:",
|
|
5585
6271
|
` - "${slug}"`,
|
|
@@ -5599,17 +6285,18 @@ function internalDocs(application, options) {
|
|
|
5599
6285
|
"## Architecture",
|
|
5600
6286
|
"",
|
|
5601
6287
|
"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.",
|
|
6288
|
+
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.`,
|
|
5602
6289
|
"",
|
|
5603
6290
|
"## Runtime Surfaces",
|
|
5604
6291
|
"",
|
|
5605
|
-
`-
|
|
6292
|
+
`- \`${appPath}/src/App.tsx\``,
|
|
6293
|
+
`- \`${appPath}/src/service-identity.ts\``,
|
|
5606
6294
|
"- public landing route: `/`",
|
|
5607
6295
|
"- public sign-in route: `/login`",
|
|
5608
6296
|
"- auth callback route: `/auth/callback`",
|
|
5609
|
-
...options.api ? [`-
|
|
5610
|
-
`-
|
|
5611
|
-
...options.api && application.serviceId ? [`- \`
|
|
5612
|
-
...options.api && application.serviceId ? [`- \`PlatformApplications/TopoloAuth/scripts/add-topolo-${slug}-service-production.sql\``] : [],
|
|
6297
|
+
...options.api ? [`- \`${appPath}/src/worker.ts\``] : [],
|
|
6298
|
+
`- \`${appPath}/topolo.cloudcontrol.json\``,
|
|
6299
|
+
...options.api && application.serviceId ? [`- \`Apps/system/TopoloAuth/catalog/service-manifests/${application.serviceId}.json\``] : [],
|
|
5613
6300
|
"",
|
|
5614
6301
|
"## API Reference",
|
|
5615
6302
|
"",
|
|
@@ -5617,11 +6304,12 @@ function internalDocs(application, options) {
|
|
|
5617
6304
|
"",
|
|
5618
6305
|
"## Auth and Permissions",
|
|
5619
6306
|
"",
|
|
5620
|
-
options.api ? `Browser sessions and Worker requests must be validated through Topolo Auth
|
|
6307
|
+
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.",
|
|
5621
6308
|
"",
|
|
5622
6309
|
"## Data Ownership",
|
|
5623
6310
|
"",
|
|
5624
6311
|
`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.`,
|
|
6312
|
+
...resourceLines,
|
|
5625
6313
|
"",
|
|
5626
6314
|
"## Deployments",
|
|
5627
6315
|
"",
|
|
@@ -5629,7 +6317,7 @@ function internalDocs(application, options) {
|
|
|
5629
6317
|
"",
|
|
5630
6318
|
"## Failure Modes",
|
|
5631
6319
|
"",
|
|
5632
|
-
|
|
6320
|
+
`- the Worker \`SERVICE_ID\` binding drifts from the Auth service manifest for \`${serviceSlug}\`.`,
|
|
5633
6321
|
"- CloudControl metadata does not match the eventual Cloudflare project or route.",
|
|
5634
6322
|
"- linked Topolo packages resolve a second React runtime and the browser shell renders blank instead of mounting the shared surfaces.",
|
|
5635
6323
|
`- ${boundaryFailureText}`,
|
|
@@ -5637,10 +6325,10 @@ function internalDocs(application, options) {
|
|
|
5637
6325
|
"## Debugging",
|
|
5638
6326
|
"",
|
|
5639
6327
|
"- run `npm run typecheck` and `npm run build` in the application directory",
|
|
5640
|
-
"- run `topolo apps audit <app> --local --repo-root /Users/ashes/Projects/
|
|
6328
|
+
"- run `topolo apps audit <app> --local --repo-root /Users/ashes/Projects/Topolo/Platform --fail-on needs_review --json` after SDK catalog registration",
|
|
5641
6329
|
"- inspect `topolo.cloudcontrol.json` before deployment changes",
|
|
5642
|
-
...options.api
|
|
5643
|
-
...options.api && application.serviceId ? [`-
|
|
6330
|
+
...options.api ? ["- inspect the Worker `SERVICE_ID` binding and matching Auth service manifest when permission grants fail"] : [],
|
|
6331
|
+
...options.api && application.serviceId ? [`- re-run \`topolo platform apps scaffold ${slug} --write --provision\` when embedded app login, launcher visibility, or callback origins drift`] : [],
|
|
5644
6332
|
"",
|
|
5645
6333
|
"## Change Log / Verification",
|
|
5646
6334
|
"",
|
|
@@ -5653,7 +6341,9 @@ function internalDocs(application, options) {
|
|
|
5653
6341
|
}
|
|
5654
6342
|
function publicDocs(application, options) {
|
|
5655
6343
|
const slug = toSlug(application.id);
|
|
6344
|
+
const serviceSlug = application.serviceSlug ?? `topolo-${slug}`;
|
|
5656
6345
|
const title = toTitle(application.name);
|
|
6346
|
+
const appPath = firstPartyAppPath(application.appDir, options.audience);
|
|
5657
6347
|
const suiteLabel = options.audience === "personal" ? "personal" : "business";
|
|
5658
6348
|
const tenantLabel = options.tenancy === "household" ? "household" : "organization";
|
|
5659
6349
|
const apiRouteLabel = options.tenancy === "household" ? "authenticated personal and household routes" : "authenticated workspace routes";
|
|
@@ -5666,7 +6356,7 @@ function publicDocs(application, options) {
|
|
|
5666
6356
|
'status: "draft"',
|
|
5667
6357
|
`last_verified: "${CURRENT_VERIFICATION_DATE}"`,
|
|
5668
6358
|
"source_repos:",
|
|
5669
|
-
` - "
|
|
6359
|
+
` - "${appPath}"`,
|
|
5670
6360
|
'audience: "public"',
|
|
5671
6361
|
"tags:",
|
|
5672
6362
|
` - "${slug}"`,
|
|
@@ -5697,11 +6387,11 @@ function publicDocs(application, options) {
|
|
|
5697
6387
|
"",
|
|
5698
6388
|
"## Auth and Permissions",
|
|
5699
6389
|
"",
|
|
5700
|
-
options.api
|
|
6390
|
+
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.",
|
|
5701
6391
|
"",
|
|
5702
6392
|
"## Data Ownership",
|
|
5703
6393
|
"",
|
|
5704
|
-
`The generated baseline has no durable domain data yet. Future data must be ${tenantLabel}-scoped at the backend boundary.`,
|
|
6394
|
+
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.`,
|
|
5705
6395
|
"",
|
|
5706
6396
|
"## Deployments",
|
|
5707
6397
|
"",
|
|
@@ -5726,16 +6416,20 @@ function publicDocs(application, options) {
|
|
|
5726
6416
|
function serviceManifestJson(application, options) {
|
|
5727
6417
|
const slug = toSlug(application.id);
|
|
5728
6418
|
const title = toTitle(application.name);
|
|
6419
|
+
const appPath = firstPartyAppPath(application.appDir, options.audience);
|
|
5729
6420
|
const permissionPrefix = application.id.replace(/[^a-z0-9_]/g, "_");
|
|
5730
6421
|
const resourceLabel = options.tenancy === "household" ? "household" : "workspace";
|
|
5731
|
-
const launcherSettings =
|
|
6422
|
+
const launcherSettings = {
|
|
5732
6423
|
settings: {
|
|
5733
6424
|
launcher: {
|
|
5734
|
-
|
|
5735
|
-
|
|
6425
|
+
category: "first_party",
|
|
6426
|
+
source: "first_party",
|
|
6427
|
+
app_id: slug,
|
|
6428
|
+
supported_contexts: options.tenancy === "household" ? ["personal"] : ["organization"],
|
|
6429
|
+
...options.tenancy === "household" ? { household_capable: true } : {}
|
|
5736
6430
|
}
|
|
5737
6431
|
}
|
|
5738
|
-
}
|
|
6432
|
+
};
|
|
5739
6433
|
return `${json({
|
|
5740
6434
|
serviceId: application.serviceId,
|
|
5741
6435
|
...launcherSettings,
|
|
@@ -5743,7 +6437,7 @@ function serviceManifestJson(application, options) {
|
|
|
5743
6437
|
{
|
|
5744
6438
|
systemId: `topolo-${slug}`,
|
|
5745
6439
|
name: title,
|
|
5746
|
-
repoPaths: [
|
|
6440
|
+
repoPaths: [appPath],
|
|
5747
6441
|
docPaths: [`applications/${slug}`, `internal/apps/${slug}`]
|
|
5748
6442
|
}
|
|
5749
6443
|
],
|
|
@@ -5784,63 +6478,11 @@ function serviceManifestJson(application, options) {
|
|
|
5784
6478
|
})}
|
|
5785
6479
|
`;
|
|
5786
6480
|
}
|
|
5787
|
-
function serviceRegistrationSql(application, options) {
|
|
5788
|
-
const slug = toSlug(application.id);
|
|
5789
|
-
const title = toTitle(application.name);
|
|
5790
|
-
const serviceId = application.serviceId ?? application.id;
|
|
5791
|
-
const productionUrl = application.productionUrl ?? `https://${slug}.topolo.app`;
|
|
5792
|
-
const authDomain = hostnameFromUrl(productionUrl);
|
|
5793
|
-
const surface = options.tenancy === "household" ? "personal-workspace" : "workspace";
|
|
5794
|
-
const settings = JSON.stringify({
|
|
5795
|
-
app_slug: `topolo-${slug}`,
|
|
5796
|
-
surface,
|
|
5797
|
-
auth_domain: authDomain,
|
|
5798
|
-
launcher: {
|
|
5799
|
-
category: "first_party",
|
|
5800
|
-
app_id: slug
|
|
5801
|
-
}
|
|
5802
|
-
});
|
|
5803
|
-
return [
|
|
5804
|
-
`-- Register ${title} in the Topolo Auth service catalog.`,
|
|
5805
|
-
"-- Safe to rerun.",
|
|
5806
|
-
"",
|
|
5807
|
-
"INSERT INTO services (",
|
|
5808
|
-
" id,",
|
|
5809
|
-
" name,",
|
|
5810
|
-
" description,",
|
|
5811
|
-
" base_url,",
|
|
5812
|
-
" version,",
|
|
5813
|
-
" status,",
|
|
5814
|
-
" settings,",
|
|
5815
|
-
" created_at,",
|
|
5816
|
-
" updated_at,",
|
|
5817
|
-
" show_in_app_switcher",
|
|
5818
|
-
")",
|
|
5819
|
-
"VALUES (",
|
|
5820
|
-
` '${sqlString(serviceId)}',`,
|
|
5821
|
-
` '${sqlString(title)}',`,
|
|
5822
|
-
` 'First-party ${sqlString(title)} ${sqlString(surface)} application.',`,
|
|
5823
|
-
` '${sqlString(productionUrl)}',`,
|
|
5824
|
-
" '1.0.0',",
|
|
5825
|
-
" 'active',",
|
|
5826
|
-
` '${sqlString(settings)}',`,
|
|
5827
|
-
" strftime('%s', 'now'),",
|
|
5828
|
-
" strftime('%s', 'now'),",
|
|
5829
|
-
" 1",
|
|
5830
|
-
")",
|
|
5831
|
-
"ON CONFLICT(id) DO UPDATE SET",
|
|
5832
|
-
" name = excluded.name,",
|
|
5833
|
-
" description = excluded.description,",
|
|
5834
|
-
" base_url = excluded.base_url,",
|
|
5835
|
-
" version = excluded.version,",
|
|
5836
|
-
" status = excluded.status,",
|
|
5837
|
-
" settings = excluded.settings,",
|
|
5838
|
-
" updated_at = strftime('%s', 'now'),",
|
|
5839
|
-
" show_in_app_switcher = excluded.show_in_app_switcher;",
|
|
5840
|
-
""
|
|
5841
|
-
].join("\n");
|
|
5842
|
-
}
|
|
5843
6481
|
function developerManifest(application, options) {
|
|
6482
|
+
const callbackPath = "/auth/callback";
|
|
6483
|
+
const localCallbackUrl = "http://localhost:5173/auth/callback";
|
|
6484
|
+
const productionCallbackUrl = application.productionUrl ? `${application.productionUrl.replace(/\/$/, "")}${callbackPath}` : null;
|
|
6485
|
+
const requestedScopes = options.api ? ["openid", "profile", "email", "organization:read", "entitlements:read", `${application.id}:read`] : ["openid", "profile", "email", "organization:read"];
|
|
5844
6486
|
return `${json({
|
|
5845
6487
|
schema: "https://topolo.app/schemas/topolo-app-manifest.v1.json",
|
|
5846
6488
|
id: application.id,
|
|
@@ -5853,17 +6495,49 @@ function developerManifest(application, options) {
|
|
|
5853
6495
|
container_app_id: options.containerAppId,
|
|
5854
6496
|
package_name: application.packageName,
|
|
5855
6497
|
service_id: options.api ? application.serviceId : null,
|
|
6498
|
+
capabilities: {
|
|
6499
|
+
selected: options.capabilities,
|
|
6500
|
+
supported_v1: [
|
|
6501
|
+
"login",
|
|
6502
|
+
"launcher",
|
|
6503
|
+
"marketplace",
|
|
6504
|
+
"api",
|
|
6505
|
+
"widget",
|
|
6506
|
+
"billing",
|
|
6507
|
+
"nexus_ai",
|
|
6508
|
+
"agent_access",
|
|
6509
|
+
"hosting"
|
|
6510
|
+
]
|
|
6511
|
+
},
|
|
5856
6512
|
auth: {
|
|
5857
6513
|
provider: "topolo",
|
|
5858
6514
|
flow: "authorization_code_pkce",
|
|
5859
|
-
callback_path:
|
|
5860
|
-
|
|
5861
|
-
|
|
5862
|
-
|
|
6515
|
+
callback_path: callbackPath,
|
|
6516
|
+
callback_urls: [localCallbackUrl, productionCallbackUrl].filter(Boolean),
|
|
6517
|
+
local_callback_url: localCallbackUrl,
|
|
6518
|
+
production_callback_url: productionCallbackUrl,
|
|
6519
|
+
requested_scopes: requestedScopes,
|
|
6520
|
+
account_linking: {
|
|
6521
|
+
required_columns: [
|
|
6522
|
+
"local_user_id",
|
|
6523
|
+
"topolo_user_id",
|
|
6524
|
+
"topolo_org_id",
|
|
6525
|
+
"topolo_context_id",
|
|
6526
|
+
"topolo_service_id",
|
|
6527
|
+
"connected_at",
|
|
6528
|
+
"last_seen_at"
|
|
6529
|
+
],
|
|
6530
|
+
email_match_policy: "confirm_or_current_session_only"
|
|
6531
|
+
}
|
|
5863
6532
|
},
|
|
5864
6533
|
runtime: {
|
|
6534
|
+
strategy: "external",
|
|
5865
6535
|
browser: true,
|
|
5866
|
-
api: options.api
|
|
6536
|
+
api: options.api,
|
|
6537
|
+
health_endpoint: options.api ? "/api/health" : null
|
|
6538
|
+
},
|
|
6539
|
+
marketplace: {
|
|
6540
|
+
intent: options.distributionModel === "marketplace" ? "submit_when_ready" : "not_requested"
|
|
5867
6541
|
}
|
|
5868
6542
|
})}
|
|
5869
6543
|
`;
|
|
@@ -5871,10 +6545,10 @@ function developerManifest(application, options) {
|
|
|
5871
6545
|
function envExample(application) {
|
|
5872
6546
|
return [
|
|
5873
6547
|
"VITE_TOPOLO_AUTH_URL=https://auth.topolo.app",
|
|
6548
|
+
"VITE_TOPOLO_AUTH_BASE_URL=https://auth.topolo.app",
|
|
5874
6549
|
"VITE_TOPOLO_CLIENT_ID=",
|
|
5875
6550
|
"VITE_TOPOLO_REDIRECT_URI=http://localhost:5173/auth/callback",
|
|
5876
|
-
"VITE_TOPOLO_SCOPES=
|
|
5877
|
-
`VITE_TOPOLO_SERVICE_ID=${application.serviceId ?? application.id}`,
|
|
6551
|
+
"VITE_TOPOLO_SCOPES=openid profile email organization:read entitlements:read",
|
|
5878
6552
|
""
|
|
5879
6553
|
].join("\n");
|
|
5880
6554
|
}
|
|
@@ -5919,6 +6593,21 @@ function normalizeSurfaceModel(value) {
|
|
|
5919
6593
|
"Invalid scaffold surface model. Use web_app, mobile_container, mobile_module, or messaging_surface."
|
|
5920
6594
|
);
|
|
5921
6595
|
}
|
|
6596
|
+
function normalizeScaffoldProfile(value) {
|
|
6597
|
+
if (!value) return null;
|
|
6598
|
+
if (value === "spatial-assets") return value;
|
|
6599
|
+
throw new Error("Invalid scaffold profile. Use spatial-assets.");
|
|
6600
|
+
}
|
|
6601
|
+
function resolveScaffoldResources(profile, options) {
|
|
6602
|
+
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 };
|
|
6603
|
+
return {
|
|
6604
|
+
d1: Boolean(profileResources.d1 || options.withD1),
|
|
6605
|
+
r2: Boolean(profileResources.r2 || options.withR2),
|
|
6606
|
+
queue: Boolean(profileResources.queue || options.withQueue),
|
|
6607
|
+
workflow: Boolean(profileResources.workflow || options.withWorkflow),
|
|
6608
|
+
containerProcessing: Boolean(profileResources.containerProcessing || options.withContainerProcessing)
|
|
6609
|
+
};
|
|
6610
|
+
}
|
|
5922
6611
|
function normalizeContainerAppId(value, surfaceModel) {
|
|
5923
6612
|
const trimmed = String(value || "").trim();
|
|
5924
6613
|
if (surfaceModel === "mobile_module") {
|
|
@@ -5955,7 +6644,8 @@ function toSlug(value) {
|
|
|
5955
6644
|
return value.replace(/_/g, "-");
|
|
5956
6645
|
}
|
|
5957
6646
|
function toPascalCase(value) {
|
|
5958
|
-
|
|
6647
|
+
const acronyms = /* @__PURE__ */ new Set(["api", "cli", "crm", "mcp", "mdm", "p2p"]);
|
|
6648
|
+
return value.split(/[-_]/).filter(Boolean).map((part) => acronyms.has(part.toLowerCase()) ? part.toUpperCase() : part.slice(0, 1).toUpperCase() + part.slice(1)).join("");
|
|
5959
6649
|
}
|
|
5960
6650
|
function toTitle(value) {
|
|
5961
6651
|
return value.replace(/^topolo[-_\s]*/i, "Topolo ").split(/[-_\s]+/).filter(Boolean).map((part) => part.slice(0, 1).toUpperCase() + part.slice(1)).join(" ");
|
|
@@ -5963,13 +6653,6 @@ function toTitle(value) {
|
|
|
5963
6653
|
function json(value) {
|
|
5964
6654
|
return JSON.stringify(value, null, 2);
|
|
5965
6655
|
}
|
|
5966
|
-
function hostnameFromUrl(value) {
|
|
5967
|
-
try {
|
|
5968
|
-
return new URL(value).hostname;
|
|
5969
|
-
} catch {
|
|
5970
|
-
return value.replace(/^https?:\/\//, "").replace(/\/.*$/, "");
|
|
5971
|
-
}
|
|
5972
|
-
}
|
|
5973
6656
|
async function pathExists(path4) {
|
|
5974
6657
|
try {
|
|
5975
6658
|
await access(path4, constants.F_OK);
|
|
@@ -5992,9 +6675,6 @@ function escapeHtml(value) {
|
|
|
5992
6675
|
function escapeSingleQuoted(value) {
|
|
5993
6676
|
return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
5994
6677
|
}
|
|
5995
|
-
function sqlString(value) {
|
|
5996
|
-
return value.replace(/'/g, "''");
|
|
5997
|
-
}
|
|
5998
6678
|
function escapeDoubleQuoted(value) {
|
|
5999
6679
|
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
6000
6680
|
}
|
|
@@ -6003,7 +6683,30 @@ function escapeFrontmatter(value) {
|
|
|
6003
6683
|
}
|
|
6004
6684
|
|
|
6005
6685
|
// src/commands/app-scaffold-provision.ts
|
|
6006
|
-
|
|
6686
|
+
import { readFile as readFile2, writeFile as writeFile3 } from "fs/promises";
|
|
6687
|
+
var DEVELOPERS_SERVICE_SLUG = "topolo-developers";
|
|
6688
|
+
var SERVICE_ID_PLACEHOLDER_RE = /TODO|FILL[_-]?IN|PLACEHOLDER|CHANGE[_-]?ME|XXX/i;
|
|
6689
|
+
function assertProvisioningPayloadComplete(payload) {
|
|
6690
|
+
const app = payload.application;
|
|
6691
|
+
const missing = [];
|
|
6692
|
+
if (!app.name?.trim()) missing.push("name");
|
|
6693
|
+
if (!app.serviceSlug?.trim()) missing.push("serviceSlug");
|
|
6694
|
+
if (!payload.audience) missing.push("audience");
|
|
6695
|
+
if (!payload.tenancy) missing.push("tenancy");
|
|
6696
|
+
if (!payload.surfaceModel) missing.push("surfaceModel");
|
|
6697
|
+
if (!payload.ownerType) missing.push("ownerType");
|
|
6698
|
+
if (missing.length > 0) {
|
|
6699
|
+
throw new Error(
|
|
6700
|
+
`Scaffold provisioning is missing required fields: ${missing.join(", ")}. An app must meet the minimum standard before it can be projected into the platform.`
|
|
6701
|
+
);
|
|
6702
|
+
}
|
|
6703
|
+
const serviceId = app.serviceId?.trim();
|
|
6704
|
+
if (serviceId && SERVICE_ID_PLACEHOLDER_RE.test(serviceId)) {
|
|
6705
|
+
throw new Error(
|
|
6706
|
+
`Refusing to provision with a placeholder service id (${serviceId}). Remove the placeholder and let provisioning mint/assign the canonical srv_ id.`
|
|
6707
|
+
);
|
|
6708
|
+
}
|
|
6709
|
+
}
|
|
6007
6710
|
async function provisionApplicationScaffold(payload) {
|
|
6008
6711
|
if (!payload.application.productionUrl) {
|
|
6009
6712
|
throw new Error(
|
|
@@ -6015,6 +6718,7 @@ async function provisionApplicationScaffold(payload) {
|
|
|
6015
6718
|
"Platform scaffold provisioning requires a service id. Re-run without --no-api or pass --service."
|
|
6016
6719
|
);
|
|
6017
6720
|
}
|
|
6721
|
+
assertProvisioningPayloadComplete(payload);
|
|
6018
6722
|
const environment = selectedEnvironment();
|
|
6019
6723
|
const credential = await refreshCredentialIfNeeded(environment);
|
|
6020
6724
|
if (!credential) {
|
|
@@ -6031,12 +6735,20 @@ async function provisionApplicationScaffold(payload) {
|
|
|
6031
6735
|
const identity = await client.client.introspect();
|
|
6032
6736
|
const config = await readConfig();
|
|
6033
6737
|
const profile = resolvedProfileConfig(config, environment);
|
|
6738
|
+
const authOrigin = sanitizeOrigin(profile.serviceUrls?.auth ?? defaultAuthOrigin(environment));
|
|
6739
|
+
assertServiceUrlForEnvironment("auth", authOrigin, environment);
|
|
6034
6740
|
const developersOrigin = sanitizeOrigin(profile.serviceUrls?.developers ?? "https://developers.topolo.app");
|
|
6035
6741
|
assertServiceUrlForEnvironment("developers", developersOrigin, environment);
|
|
6742
|
+
const developersServiceId = await resolveServiceIdBySlug(authOrigin, DEVELOPERS_SERVICE_SLUG);
|
|
6743
|
+
if (!developersServiceId) {
|
|
6744
|
+
throw new Error(
|
|
6745
|
+
`Could not resolve Developers service id from Auth slug ${DEVELOPERS_SERVICE_SLUG}.`
|
|
6746
|
+
);
|
|
6747
|
+
}
|
|
6036
6748
|
const endpoint = payload.target === "platform" ? "/api/developer-console/admin/first-party-app-scaffolds" : "/api/developer-console/apps/scaffold";
|
|
6037
6749
|
const headers = new Headers({
|
|
6038
6750
|
"Content-Type": "application/json",
|
|
6039
|
-
"X-Service-ID":
|
|
6751
|
+
"X-Service-ID": developersServiceId,
|
|
6040
6752
|
"X-Topolo-Client": `topolo-cli/${CLI_VERSION}`
|
|
6041
6753
|
});
|
|
6042
6754
|
const agentName = resolveAgentName(profile.agentName);
|
|
@@ -6108,11 +6820,21 @@ function buildProvisioningRequest(payload) {
|
|
|
6108
6820
|
surfaceModel: payload.surfaceModel,
|
|
6109
6821
|
containerAppId: payload.containerAppId,
|
|
6110
6822
|
distributionModel: payload.distributionModel,
|
|
6823
|
+
capabilities: payload.capabilities,
|
|
6824
|
+
callbackUrls: [
|
|
6825
|
+
"http://localhost:5173/auth/callback",
|
|
6826
|
+
payload.application.productionUrl ? `${payload.application.productionUrl.replace(/\/$/, "")}/auth/callback` : null
|
|
6827
|
+
].filter(Boolean),
|
|
6828
|
+
healthEndpoint: payload.application.productionUrl ? `${payload.application.productionUrl.replace(/\/$/, "")}/api/health` : null,
|
|
6829
|
+
runtimeStrategy: "external",
|
|
6830
|
+
requestedScopes: payload.application.serviceId ? ["openid", "profile", "email", "organization:read", "entitlements:read", `${payload.application.id}:read`] : ["openid", "profile", "email", "organization:read"],
|
|
6831
|
+
marketplaceIntent: payload.distributionModel === "marketplace" ? "submit_when_ready" : "not_requested",
|
|
6111
6832
|
applicationId: payload.application.id,
|
|
6112
6833
|
appDir: payload.application.appDir,
|
|
6113
6834
|
packageName: payload.application.packageName,
|
|
6114
6835
|
name: payload.application.name,
|
|
6115
6836
|
productionUrl: payload.application.productionUrl,
|
|
6837
|
+
serviceSlug: payload.application.serviceSlug,
|
|
6116
6838
|
serviceId: payload.application.serviceId
|
|
6117
6839
|
};
|
|
6118
6840
|
}
|
|
@@ -6129,6 +6851,19 @@ function resolveProvisioningError(envelope, status, target) {
|
|
|
6129
6851
|
function sanitizeOrigin(value) {
|
|
6130
6852
|
return value.replace(/\/+$/, "");
|
|
6131
6853
|
}
|
|
6854
|
+
function defaultAuthOrigin(environment) {
|
|
6855
|
+
return environment === "staging" ? "https://auth.stg.topolo.us" : "https://auth.topolo.app";
|
|
6856
|
+
}
|
|
6857
|
+
async function resolveServiceIdBySlug(authOrigin, slug) {
|
|
6858
|
+
const response = await fetch(
|
|
6859
|
+
`${authOrigin}/api/services/by-slug/${encodeURIComponent(slug)}`,
|
|
6860
|
+
{ headers: { Accept: "application/json" } }
|
|
6861
|
+
).catch(() => null);
|
|
6862
|
+
if (!response?.ok) return null;
|
|
6863
|
+
const payload = await response.json().catch(() => null);
|
|
6864
|
+
const serviceId = payload?.data?.service_id;
|
|
6865
|
+
return typeof serviceId === "string" && serviceId.trim() ? serviceId.trim() : null;
|
|
6866
|
+
}
|
|
6132
6867
|
function pickRecord(value) {
|
|
6133
6868
|
return value && typeof value === "object" ? value : null;
|
|
6134
6869
|
}
|
|
@@ -6225,7 +6960,7 @@ function registerAppsCommand(program2) {
|
|
|
6225
6960
|
"Developer app surface model: web_app, mobile_container, mobile_module, or messaging_surface"
|
|
6226
6961
|
).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(
|
|
6227
6962
|
"--provision",
|
|
6228
|
-
"
|
|
6963
|
+
"Register the app in the current Topolo developer workspace using the authenticated session"
|
|
6229
6964
|
).action(async (application, opts) => {
|
|
6230
6965
|
const mode = opts.json === false || process.argv.includes("--no-json") ? "pretty" : resolveOutputMode(opts.json);
|
|
6231
6966
|
try {
|
|
@@ -6244,9 +6979,6 @@ function registerAppsCommand(program2) {
|
|
|
6244
6979
|
surfaceModel: resolveScaffoldSurfaceModel(opts.surfaceModel),
|
|
6245
6980
|
containerAppId: opts.containerAppId
|
|
6246
6981
|
});
|
|
6247
|
-
if (opts.provision && !opts.write) {
|
|
6248
|
-
throw new Error("--provision requires --write so remote developer records match written scaffold files.");
|
|
6249
|
-
}
|
|
6250
6982
|
const writeResult = opts.write ? await writeApplicationScaffold(payload, { repoRoot: opts.repoRoot, force: opts.force }) : null;
|
|
6251
6983
|
const provisionResult = opts.provision ? await provisionApplicationScaffold(payload) : null;
|
|
6252
6984
|
const output = {
|
|
@@ -6612,7 +7344,7 @@ function registerPlatformCommand(program2) {
|
|
|
6612
7344
|
).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(
|
|
6613
7345
|
"--surface-model <surfaceModel>",
|
|
6614
7346
|
"First-party surface model: web_app, mobile_container, mobile_module, or messaging_surface"
|
|
6615
|
-
).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(
|
|
7347
|
+
).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(
|
|
6616
7348
|
"--provision",
|
|
6617
7349
|
"Provision the generated first-party app into Topolo Developers and Auth using the authenticated Topolo admin session"
|
|
6618
7350
|
).option(
|
|
@@ -6621,10 +7353,14 @@ function registerPlatformCommand(program2) {
|
|
|
6621
7353
|
).option("--workflow-only", "Output only the generated GitHub Actions workflow in human mode").action(async (application, opts) => {
|
|
6622
7354
|
const mode = opts.json === false || process.argv.includes("--no-json") ? "pretty" : resolveOutputMode(opts.json);
|
|
6623
7355
|
try {
|
|
6624
|
-
await requirePlatformAdminSession();
|
|
6625
7356
|
if (opts.workflowOnly && opts.write) {
|
|
6626
7357
|
throw new Error("--workflow-only cannot be used with --write.");
|
|
6627
7358
|
}
|
|
7359
|
+
const provisionRequested = process.argv.includes("--provision");
|
|
7360
|
+
const shouldProvision = Boolean(opts.write) && opts.provision !== false;
|
|
7361
|
+
if (shouldProvision) {
|
|
7362
|
+
await requirePlatformAdminSession();
|
|
7363
|
+
}
|
|
6628
7364
|
const api = resolveScaffoldApiOption(opts.api, opts.withApi);
|
|
6629
7365
|
const payload = applicationScaffoldPayload(application, {
|
|
6630
7366
|
appDir: opts.appDir,
|
|
@@ -6637,13 +7373,17 @@ function registerPlatformCommand(program2) {
|
|
|
6637
7373
|
target: "platform",
|
|
6638
7374
|
portfolio: resolvePlatformPortfolio(opts.portfolio),
|
|
6639
7375
|
surfaceModel: resolvePlatformSurfaceModel(opts.surfaceModel),
|
|
6640
|
-
containerAppId: opts.containerAppId
|
|
7376
|
+
containerAppId: opts.containerAppId,
|
|
7377
|
+
profile: opts.profile,
|
|
7378
|
+
withD1: opts.withD1,
|
|
7379
|
+
withR2: opts.withR2,
|
|
7380
|
+
withQueue: opts.withQueue,
|
|
7381
|
+
withWorkflow: opts.withWorkflow,
|
|
7382
|
+
withContainerProcessing: opts.withContainerProcessing
|
|
6641
7383
|
});
|
|
6642
7384
|
if (opts.workflowOnly && !payload.ciWorkflow) {
|
|
6643
7385
|
throw new Error("--workflow-only is only available for first-party scaffolds.");
|
|
6644
7386
|
}
|
|
6645
|
-
const provisionRequested = process.argv.includes("--provision");
|
|
6646
|
-
const shouldProvision = Boolean(opts.write) && opts.provision !== false;
|
|
6647
7387
|
if (provisionRequested && !opts.write) {
|
|
6648
7388
|
throw new Error("--provision requires --write so remote first-party records match written scaffold files.");
|
|
6649
7389
|
}
|
|
@@ -6776,6 +7516,14 @@ function isPlatformAdminIdentity(identity) {
|
|
|
6776
7516
|
const orgSlug = String(identity?.organization?.slug || "").trim().toLowerCase();
|
|
6777
7517
|
return orgSlug === "admin" && (role === "platform_super_admin" || role === "platform_admin");
|
|
6778
7518
|
}
|
|
7519
|
+
function isTopoloWorkspaceAdminIdentity(identity) {
|
|
7520
|
+
const role = String(identity?.user?.role || "").trim().toLowerCase();
|
|
7521
|
+
const orgSlug = String(identity?.organization?.slug || "").trim().toLowerCase();
|
|
7522
|
+
return orgSlug === "topolo" && (role === "owner" || role === "admin" || role === "super_admin");
|
|
7523
|
+
}
|
|
7524
|
+
function canProvisionFirstPartyScaffoldIdentity(identity) {
|
|
7525
|
+
return isPlatformAdminIdentity(identity) || isTopoloWorkspaceAdminIdentity(identity);
|
|
7526
|
+
}
|
|
6779
7527
|
async function requirePlatformAdminSession() {
|
|
6780
7528
|
const client = await buildClient();
|
|
6781
7529
|
if (!client) {
|
|
@@ -6784,9 +7532,9 @@ async function requirePlatformAdminSession() {
|
|
|
6784
7532
|
);
|
|
6785
7533
|
}
|
|
6786
7534
|
const identity = await client.client.introspect();
|
|
6787
|
-
if (!
|
|
7535
|
+
if (!canProvisionFirstPartyScaffoldIdentity(identity)) {
|
|
6788
7536
|
throw new Error(
|
|
6789
|
-
"`topolo platform ...` is restricted to Topolo platform admins in the Auth `admin` organization."
|
|
7537
|
+
"`topolo platform ...` is restricted to Topolo platform admins in the Auth `admin` organization or owner/admin users in the Topolo developer workspace."
|
|
6790
7538
|
);
|
|
6791
7539
|
}
|
|
6792
7540
|
}
|
|
@@ -7053,6 +7801,282 @@ function formatManifestsTable(manifests) {
|
|
|
7053
7801
|
return lines.join("\n");
|
|
7054
7802
|
}
|
|
7055
7803
|
|
|
7804
|
+
// src/commands/oauth.ts
|
|
7805
|
+
function registerOAuthCommands(program2) {
|
|
7806
|
+
const oauth = program2.command("oauth").description("Manage Topolo OAuth resources.");
|
|
7807
|
+
const clients = oauth.command("clients").description("Manage Login with Topolo OAuth clients.");
|
|
7808
|
+
clients.command("list").description("List OAuth clients owned by the authenticated developer organization.").option("--json", "Emit JSON output").action(async (options) => {
|
|
7809
|
+
const mode = resolveOutputMode(options.json);
|
|
7810
|
+
try {
|
|
7811
|
+
const payload = await authFetchJson("/api/developer-oauth/clients");
|
|
7812
|
+
emit(payload, mode, () => {
|
|
7813
|
+
const clients2 = Array.isArray(payload.data?.clients) ? payload.data.clients : [];
|
|
7814
|
+
if (clients2.length === 0) return "No OAuth clients found.";
|
|
7815
|
+
return clients2.map((client) => `${client.client_id} ${client.name} ${client.status}`).join("\n");
|
|
7816
|
+
});
|
|
7817
|
+
} catch (err) {
|
|
7818
|
+
fail(err, mode);
|
|
7819
|
+
}
|
|
7820
|
+
});
|
|
7821
|
+
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) => {
|
|
7822
|
+
const mode = resolveOutputMode(options.json);
|
|
7823
|
+
try {
|
|
7824
|
+
const payload = createOAuthClientPayload(options);
|
|
7825
|
+
const response = await authFetchJson("/api/developer-oauth/clients", {
|
|
7826
|
+
method: "POST",
|
|
7827
|
+
body: JSON.stringify(payload)
|
|
7828
|
+
});
|
|
7829
|
+
emit(response, mode, () => {
|
|
7830
|
+
const client = response.data?.client;
|
|
7831
|
+
const secret = response.data?.client_secret;
|
|
7832
|
+
return [
|
|
7833
|
+
`Created OAuth client ${client?.client_id ?? ""}`,
|
|
7834
|
+
secret ? `Client secret: ${secret}` : "Public client: no secret issued"
|
|
7835
|
+
].join("\n");
|
|
7836
|
+
});
|
|
7837
|
+
} catch (err) {
|
|
7838
|
+
fail(err, mode);
|
|
7839
|
+
}
|
|
7840
|
+
});
|
|
7841
|
+
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) => {
|
|
7842
|
+
const mode = resolveOutputMode(options.json);
|
|
7843
|
+
try {
|
|
7844
|
+
const payload = await authFetchJson(`/api/developer-oauth/clients/${encodeURIComponent(id)}`, {
|
|
7845
|
+
method: "DELETE"
|
|
7846
|
+
});
|
|
7847
|
+
emit(payload, mode, () => `Revoked OAuth client ${id}`);
|
|
7848
|
+
} catch (err) {
|
|
7849
|
+
fail(err, mode);
|
|
7850
|
+
}
|
|
7851
|
+
});
|
|
7852
|
+
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) => {
|
|
7853
|
+
const mode = resolveOutputMode(options.json);
|
|
7854
|
+
try {
|
|
7855
|
+
const payload = await authFetchJson(`/api/developer-oauth/clients/${encodeURIComponent(id)}/rotate-secret`, {
|
|
7856
|
+
method: "POST"
|
|
7857
|
+
});
|
|
7858
|
+
emit(payload, mode, () => `New client secret: ${payload.data?.client_secret ?? ""}`);
|
|
7859
|
+
} catch (err) {
|
|
7860
|
+
fail(err, mode);
|
|
7861
|
+
}
|
|
7862
|
+
});
|
|
7863
|
+
}
|
|
7864
|
+
function createOAuthClientPayload(options) {
|
|
7865
|
+
const redirectUris = normalizeList(options.redirectUri);
|
|
7866
|
+
if (redirectUris.length === 0) {
|
|
7867
|
+
throw new Error("At least one --redirect-uri is required.");
|
|
7868
|
+
}
|
|
7869
|
+
return {
|
|
7870
|
+
name: options.name,
|
|
7871
|
+
developer_id: options.developerId,
|
|
7872
|
+
developer_app_id: options.developerAppId,
|
|
7873
|
+
description: options.description,
|
|
7874
|
+
client_type: options.public ? "public" : "confidential",
|
|
7875
|
+
redirect_uris: redirectUris,
|
|
7876
|
+
allowed_scopes: normalizeList(options.scope, ["openid", "profile", "email"]),
|
|
7877
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
7878
|
+
homepage_url: options.homepageUrl,
|
|
7879
|
+
logo_url: options.logoUrl
|
|
7880
|
+
};
|
|
7881
|
+
}
|
|
7882
|
+
async function authFetchJson(path4, init = {}) {
|
|
7883
|
+
const environment = selectedEnvironment();
|
|
7884
|
+
const credential = await refreshCredentialIfNeeded(environment);
|
|
7885
|
+
if (!credential) {
|
|
7886
|
+
const err = new Error("Topolo credentials are required.");
|
|
7887
|
+
err.code = "auth_error";
|
|
7888
|
+
throw err;
|
|
7889
|
+
}
|
|
7890
|
+
const config = await readConfig();
|
|
7891
|
+
const profile = resolvedProfileConfig(config, environment);
|
|
7892
|
+
const authBaseUrl = (profile.serviceUrls?.auth ?? "https://auth.topolo.app").replace(/\/+$/, "");
|
|
7893
|
+
const headers = new Headers(init.headers);
|
|
7894
|
+
headers.set("Accept", "application/json");
|
|
7895
|
+
if (init.body && !headers.has("Content-Type")) headers.set("Content-Type", "application/json");
|
|
7896
|
+
if (credential.kind === "api_key") {
|
|
7897
|
+
headers.set("Authorization", `Bearer ${credential.apiKey}`);
|
|
7898
|
+
} else {
|
|
7899
|
+
headers.set("Authorization", `Bearer ${credential.accessToken}`);
|
|
7900
|
+
}
|
|
7901
|
+
const response = await fetch(`${authBaseUrl}${path4}`, { ...init, headers });
|
|
7902
|
+
const text = await response.text();
|
|
7903
|
+
const payload = text ? JSON.parse(text) : {};
|
|
7904
|
+
if (!response.ok || payload.success === false) {
|
|
7905
|
+
const err = new Error(payload.message || payload.error_description || payload.error || "Topolo Auth request failed");
|
|
7906
|
+
err.code = payload.code || payload.error;
|
|
7907
|
+
throw err;
|
|
7908
|
+
}
|
|
7909
|
+
return payload;
|
|
7910
|
+
}
|
|
7911
|
+
function normalizeList(values, fallback = []) {
|
|
7912
|
+
const source = values && values.length > 0 ? values : fallback;
|
|
7913
|
+
return Array.from(new Set(source.flatMap((value) => value.split(/[\s,]+/)).map((value) => value.trim()).filter(Boolean)));
|
|
7914
|
+
}
|
|
7915
|
+
|
|
7916
|
+
// src/commands/integrations.ts
|
|
7917
|
+
import { readdir, readFile as readFile3, stat } from "fs/promises";
|
|
7918
|
+
import { join as join3, relative as relative2 } from "path";
|
|
7919
|
+
function registerIntegrationsCommand(program2) {
|
|
7920
|
+
const integrations = program2.command("integrations").description("Generate and validate Topolo integration handoffs.");
|
|
7921
|
+
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) => {
|
|
7922
|
+
const mode = resolveOutputMode(options.json);
|
|
7923
|
+
try {
|
|
7924
|
+
if (integration !== "login-with-topolo") throw new Error(`Unknown integration prompt: ${integration}`);
|
|
7925
|
+
const framework = normalizeFramework(options.framework);
|
|
7926
|
+
const prompt = buildLoginWithTopoloPrompt(framework);
|
|
7927
|
+
emit({ integration, framework, prompt }, mode, () => prompt);
|
|
7928
|
+
} catch (err) {
|
|
7929
|
+
fail(err, mode);
|
|
7930
|
+
}
|
|
7931
|
+
});
|
|
7932
|
+
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) => {
|
|
7933
|
+
const mode = resolveOutputMode(options.json);
|
|
7934
|
+
try {
|
|
7935
|
+
if (integration !== "login-with-topolo") throw new Error(`Unknown integration check: ${integration}`);
|
|
7936
|
+
const result = await checkLoginWithTopolo(options.repoRoot ?? ".");
|
|
7937
|
+
emit(result, mode, () => formatCheckResult(result));
|
|
7938
|
+
if (!result.passed) process.exitCode = 1;
|
|
7939
|
+
} catch (err) {
|
|
7940
|
+
fail(err, mode);
|
|
7941
|
+
}
|
|
7942
|
+
});
|
|
7943
|
+
}
|
|
7944
|
+
function buildLoginWithTopoloPrompt(framework = "auto") {
|
|
7945
|
+
return [
|
|
7946
|
+
"You are adding Login with Topolo to an existing third-party application.",
|
|
7947
|
+
"",
|
|
7948
|
+
"Objective:",
|
|
7949
|
+
"- Add Topolo as an additional OAuth/OIDC login provider without replacing existing Google, Facebook, email, SSO, or password login.",
|
|
7950
|
+
"- Use @topolo/login for provider URL generation, token exchange, userinfo, ID-token verification, refresh, and account-link payloads.",
|
|
7951
|
+
"- Use @topolo/sdk only after login when calling Topolo APIs with the Topolo access_token.",
|
|
7952
|
+
"",
|
|
7953
|
+
"Implementation steps:",
|
|
7954
|
+
"1. Inspect the current auth stack, session store, user model, workspace/org model, and callback route conventions.",
|
|
7955
|
+
`2. Select the integration path: ${framework === "auto" ? "auto-detect Auth.js/NextAuth, Express/Passport, or generic OIDC" : framework}.`,
|
|
7956
|
+
"3. Add env vars: TOPOLO_CLIENT_ID, TOPOLO_CLIENT_SECRET when confidential, TOPOLO_AUTH_BASE_URL, and TOPOLO_REDIRECT_URI.",
|
|
7957
|
+
"4. Register the callback URI in TopoloDevelopers or run `topolo oauth clients create`.",
|
|
7958
|
+
"5. Add the Topolo callback route and exchange the authorization code with PKCE.",
|
|
7959
|
+
"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.",
|
|
7960
|
+
"7. Never auto-merge solely by matching email unless the user is already signed into the local account or explicitly confirms linking.",
|
|
7961
|
+
"8. Store refresh tokens according to the app\u2019s existing token-security pattern and refresh Topolo tokens when calling Topolo APIs.",
|
|
7962
|
+
"9. Preserve all existing login providers and add tests for callback exchange, account linking, refresh handling, and disconnect/re-login.",
|
|
7963
|
+
"10. Run `topolo integrations check login-with-topolo --repo-root . --json` and fix every failing check."
|
|
7964
|
+
].join("\n");
|
|
7965
|
+
}
|
|
7966
|
+
async function checkLoginWithTopolo(repoRoot) {
|
|
7967
|
+
const files = await collectFiles(repoRoot);
|
|
7968
|
+
const packageJson2 = await readPackageJson2(repoRoot);
|
|
7969
|
+
const haystack = files.map((file) => `${file.path}
|
|
7970
|
+
${file.content}`).join("\n");
|
|
7971
|
+
const framework = detectFramework(packageJson2, haystack);
|
|
7972
|
+
const checks = [
|
|
7973
|
+
makeCheck(
|
|
7974
|
+
"package",
|
|
7975
|
+
"@topolo/login dependency",
|
|
7976
|
+
packageJson2?.dependencies?.["@topolo/login"] || packageJson2?.devDependencies?.["@topolo/login"] ? ["package.json declares @topolo/login"] : [],
|
|
7977
|
+
true
|
|
7978
|
+
),
|
|
7979
|
+
makeCheck(
|
|
7980
|
+
"env",
|
|
7981
|
+
"Topolo OAuth env vars",
|
|
7982
|
+
matchEvidence(haystack, ["TOPOLO_CLIENT_ID", "TOPOLO_AUTH_BASE_URL", "TOPOLO_REDIRECT_URI"]),
|
|
7983
|
+
true
|
|
7984
|
+
),
|
|
7985
|
+
makeCheck(
|
|
7986
|
+
"callback",
|
|
7987
|
+
"OAuth callback route",
|
|
7988
|
+
matchEvidence(haystack, ["topolo/callback", "auth/topolo/callback", "exchangeTopoloCode", "createTopoloAuthJsProvider"]),
|
|
7989
|
+
true
|
|
7990
|
+
),
|
|
7991
|
+
makeCheck(
|
|
7992
|
+
"account_link",
|
|
7993
|
+
"Account-linking model",
|
|
7994
|
+
matchEvidence(haystack, ["topolo_user_id", "topolo_org_id", "createAccountLinkPayload"]),
|
|
7995
|
+
true
|
|
7996
|
+
),
|
|
7997
|
+
makeCheck(
|
|
7998
|
+
"refresh",
|
|
7999
|
+
"Token refresh handling",
|
|
8000
|
+
matchEvidence(haystack, ["refreshTopoloToken", "refresh_token", "expires_at"]),
|
|
8001
|
+
false
|
|
8002
|
+
),
|
|
8003
|
+
makeCheck(
|
|
8004
|
+
"safety",
|
|
8005
|
+
"Existing auth preserved",
|
|
8006
|
+
matchEvidence(haystack, ["google", "facebook", "email", "password", "credentials"]),
|
|
8007
|
+
false
|
|
8008
|
+
)
|
|
8009
|
+
];
|
|
8010
|
+
return {
|
|
8011
|
+
integration: "login-with-topolo",
|
|
8012
|
+
framework,
|
|
8013
|
+
passed: checks.every((check) => check.status !== "fail"),
|
|
8014
|
+
checks
|
|
8015
|
+
};
|
|
8016
|
+
}
|
|
8017
|
+
function normalizeFramework(value) {
|
|
8018
|
+
if (value === "auto" || value === "authjs" || value === "express" || value === "generic") return value;
|
|
8019
|
+
throw new Error("--framework must be auto, authjs, express, or generic.");
|
|
8020
|
+
}
|
|
8021
|
+
function makeCheck(id, label, evidence, required) {
|
|
8022
|
+
if (evidence.length > 0) return { id, label, status: "pass", evidence };
|
|
8023
|
+
return { id, label, status: required ? "fail" : "warn", evidence: [] };
|
|
8024
|
+
}
|
|
8025
|
+
function matchEvidence(haystack, needles) {
|
|
8026
|
+
return needles.filter((needle) => haystack.toLowerCase().includes(needle.toLowerCase()));
|
|
8027
|
+
}
|
|
8028
|
+
function detectFramework(packageJson2, haystack) {
|
|
8029
|
+
const deps = {
|
|
8030
|
+
...packageJson2?.dependencies ?? {},
|
|
8031
|
+
...packageJson2?.devDependencies ?? {}
|
|
8032
|
+
};
|
|
8033
|
+
if (deps["next-auth"] || deps["@auth/core"] || deps["@auth/nextjs"] || haystack.includes("NextAuth")) return "authjs";
|
|
8034
|
+
if (deps.express || deps.passport || haystack.includes("passport.use")) return "express";
|
|
8035
|
+
if (haystack.includes("Gemfile") || haystack.includes("omniauth")) return "rails";
|
|
8036
|
+
if (haystack.includes("Laravel") || haystack.includes("Socialite")) return "laravel";
|
|
8037
|
+
if (haystack.includes("django") || haystack.includes("allauth")) return "django";
|
|
8038
|
+
return "generic";
|
|
8039
|
+
}
|
|
8040
|
+
async function readPackageJson2(repoRoot) {
|
|
8041
|
+
try {
|
|
8042
|
+
return JSON.parse(await readFile3(join3(repoRoot, "package.json"), "utf8"));
|
|
8043
|
+
} catch {
|
|
8044
|
+
return null;
|
|
8045
|
+
}
|
|
8046
|
+
}
|
|
8047
|
+
async function collectFiles(repoRoot) {
|
|
8048
|
+
const result = [];
|
|
8049
|
+
const root = repoRoot;
|
|
8050
|
+
async function walk(dir) {
|
|
8051
|
+
if (result.length > 2e3) return;
|
|
8052
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
8053
|
+
for (const entry of entries) {
|
|
8054
|
+
const fullPath = join3(dir, entry.name);
|
|
8055
|
+
if (entry.isDirectory()) {
|
|
8056
|
+
if ([".git", "node_modules", "dist", "build", ".next", "coverage"].includes(entry.name)) continue;
|
|
8057
|
+
await walk(fullPath);
|
|
8058
|
+
continue;
|
|
8059
|
+
}
|
|
8060
|
+
if (!entry.isFile() || !isInspectable(entry.name)) continue;
|
|
8061
|
+
const info = await stat(fullPath);
|
|
8062
|
+
if (info.size > 25e4) continue;
|
|
8063
|
+
result.push({
|
|
8064
|
+
path: relative2(root, fullPath),
|
|
8065
|
+
content: await readFile3(fullPath, "utf8")
|
|
8066
|
+
});
|
|
8067
|
+
}
|
|
8068
|
+
}
|
|
8069
|
+
await walk(root);
|
|
8070
|
+
return result;
|
|
8071
|
+
}
|
|
8072
|
+
function isInspectable(name) {
|
|
8073
|
+
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);
|
|
8074
|
+
}
|
|
8075
|
+
function formatCheckResult(result) {
|
|
8076
|
+
const rows = result.checks.map((check) => `${check.status.toUpperCase()} ${check.label}`);
|
|
8077
|
+
return [`Framework: ${result.framework}`, `Passed: ${result.passed ? "yes" : "no"}`, ...rows].join("\n");
|
|
8078
|
+
}
|
|
8079
|
+
|
|
7056
8080
|
// src/index.ts
|
|
7057
8081
|
var program = new Command();
|
|
7058
8082
|
var argv;
|
|
@@ -7076,6 +8100,8 @@ registerWhoamiCommand(program);
|
|
|
7076
8100
|
registerCrmCommands(program);
|
|
7077
8101
|
registerApiCommand(program);
|
|
7078
8102
|
registerAppsCommand(program);
|
|
8103
|
+
registerOAuthCommands(program);
|
|
8104
|
+
registerIntegrationsCommand(program);
|
|
7079
8105
|
registerPlatformCommand(program);
|
|
7080
8106
|
registerServicesCommand(program);
|
|
7081
8107
|
registerSeedCommands(program);
|