@topolo/cli 0.1.21 → 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.
Files changed (3) hide show
  1. package/README.md +33 -2
  2. package/dist/index.js +1437 -333
  3. 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": "topoloadmin",
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": "topoloadmin",
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": "pages",
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": "pages",
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": "pages",
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": "pages",
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": "workers",
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": "pages",
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": "workers",
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": "workers",
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": "pages",
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": "topolo-oneclick-sso",
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": null,
1174
- "services": [],
1175
- "deployTargets": []
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": "pages",
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": "pages",
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": "pages",
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": "workers",
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": "pages",
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/auth-middleware is used where applicable",
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 (application.id === "cli" || application.id === "mcp" || application.id === "cloudcontrol") {
1675
+ if (appId === "cli" || appId === "mcp" || appId === "cloudcontrol") {
1687
1676
  scopes.add("tooling");
1688
1677
  }
1689
- if (application.id === "cli" || application.id === "mcp" || hasApiSurface) {
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 (application.id === "cli" || application.id === "mcp" || application.id === "cloudcontrol") {
1813
- evidence.push(`tooling=${application.id}`);
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 TopoloClient = class {
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 = PLATFORM_SERVICE_IDS[opts.service];
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 (application.id === "cli" || application.id === "mcp" || application.id === "cloudcontrol") {
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=${application.id}`],
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 (application.id === "cli" || application.id === "mcp" || application.id === "cloudcontrol") {
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=${application.id}`, ...items],
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 = "2026-05-06";
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 defaultServiceId = target === "platform" ? `srv_topolo_${id}` : id;
3921
- const serviceId = api ? options.service ?? (target === "platform" ? defaultServiceId : catalogApplication?.services[0] ?? defaultServiceId) : null;
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
- distributionModel
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/auth-middleware");
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" ? `PlatformApplications/${application.appDir}` : application.appDir;
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, { browser: options.browser, api: options.api })
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, { api: options.api })
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 PlatformApplications app directory",
4141
- "checks out TopoloDocs under PlatformApplications/TopoloDocs",
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: `PlatformApplications/TopoloDocs/src/content/systems/topolo-${slug}.json`,
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: `PlatformApplications/TopoloDocs/src/content/internal/apps/${slug}.mdx`,
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: `PlatformApplications/TopoloDocs/src/content/public/applications/${slug}.mdx`,
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: `PlatformApplications/TopoloAuth/catalog/service-manifests/${application.serviceId}.json`,
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,6 +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, {
4400
+ audience: options.payload.audience,
4202
4401
  tenancy: options.payload.tenancy
4203
4402
  })
4204
4403
  }
@@ -4219,7 +4418,8 @@ function scaffoldFiles(options) {
4219
4418
  tenancy: options.payload.tenancy,
4220
4419
  surfaceModel: options.payload.surfaceModel,
4221
4420
  containerAppId: options.payload.containerAppId,
4222
- distributionModel: options.payload.distributionModel
4421
+ distributionModel: options.payload.distributionModel,
4422
+ capabilities: options.payload.capabilities
4223
4423
  })
4224
4424
  },
4225
4425
  {
@@ -4234,6 +4434,17 @@ function scaffoldFiles(options) {
4234
4434
  );
4235
4435
  }
4236
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
+ }
4237
4448
  files.push({
4238
4449
  path: `${base}/src/App.tsx`,
4239
4450
  purpose: "Authenticated browser surface.",
@@ -4258,19 +4469,21 @@ function scaffoldFiles(options) {
4258
4469
  "bindings match topolo.cloudcontrol.json or topolo.app.json",
4259
4470
  "secrets are referenced by name only"
4260
4471
  ],
4261
- content: wranglerToml(application, target)
4472
+ content: wranglerToml(application, target, options.payload.resources, options.payload.profile)
4262
4473
  },
4263
4474
  {
4264
4475
  path: `${base}/src/worker.ts`,
4265
4476
  purpose: "Protected service boundary.",
4266
4477
  requiredSignals: [
4267
- "uses @topolo-io/auth-middleware or Auth validation for protected routes",
4478
+ "uses @topolo-io/worker-runtime for Worker auth, gatewaying, headers, and request helpers",
4268
4479
  "derives organization from the credential rather than client-supplied orgId",
4269
4480
  "emits or propagates X-Topolo-Request-Id and X-Topolo-Client"
4270
4481
  ],
4271
4482
  content: workerTs(application, target, {
4272
4483
  audience: options.payload.audience,
4273
- tenancy: options.payload.tenancy
4484
+ tenancy: options.payload.tenancy,
4485
+ profile: options.payload.profile,
4486
+ resources: options.payload.resources
4274
4487
  })
4275
4488
  }
4276
4489
  );
@@ -4290,30 +4503,32 @@ function scaffoldCommands(options) {
4290
4503
  ];
4291
4504
  }
4292
4505
  const portfolioFlags = options.portfolio === "personal" ? " --portfolio personal" : "";
4506
+ const profileFlags = options.profile ? ` --profile ${options.profile}` : "";
4293
4507
  return [
4294
- `topolo platform apps scaffold ${scaffoldId}${portfolioFlags} --write --provision --repo-root /Users/ashes/Projects/topolo --json`,
4508
+ `topolo platform apps scaffold ${scaffoldId}${portfolioFlags}${profileFlags} --write --provision --repo-root /Users/ashes/Projects/Topolo/Platform --json`,
4295
4509
  "topolo apps requirements --json",
4296
- `topolo apps audit ${options.id} --local --repo-root /Users/ashes/Projects/topolo --fail-on needs_review --json`,
4297
- `topolo apps audit ${options.id} --local --repo-root /Users/ashes/Projects/topolo --fail-on partial --json`
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`
4298
4512
  ];
4299
4513
  }
4300
4514
  function topoloConformanceWorkflow(options) {
4515
+ const appPath = firstPartyAppPath(options.appDir, options.audience);
4301
4516
  const docsCheckoutStep = options.appDir === "TopoloDocs" ? [] : [
4302
4517
  "",
4303
4518
  " - name: Checkout TopoloDocs",
4304
4519
  " uses: actions/checkout@v4",
4305
4520
  " with:",
4306
4521
  " repository: Topolo-io/docs",
4307
- " path: PlatformApplications/TopoloDocs",
4522
+ " path: Apps/system/TopoloDocs",
4308
4523
  " token: ${{ secrets.TOPOLO_CI_REPO_TOKEN || github.token }}"
4309
4524
  ];
4310
- const cliCheckoutStep = options.appDir === "TopoloCli" ? [] : [
4525
+ const cliCheckoutStep = options.appDir === "TopoloCLI" || options.appDir === "TopoloCli" ? [] : [
4311
4526
  "",
4312
4527
  " - name: Checkout Topolo CLI",
4313
4528
  " uses: actions/checkout@v4",
4314
4529
  " with:",
4315
4530
  " repository: Topolo-io/TopoloCli",
4316
- " path: PlatformApplications/TopoloCli",
4531
+ " path: Apps/system/TopoloCLI",
4317
4532
  " token: ${{ secrets.TOPOLO_CI_REPO_TOKEN || github.token }}"
4318
4533
  ];
4319
4534
  return [
@@ -4336,14 +4551,14 @@ function topoloConformanceWorkflow(options) {
4336
4551
  " - name: Checkout application",
4337
4552
  " uses: actions/checkout@v4",
4338
4553
  " with:",
4339
- ` path: PlatformApplications/${options.appDir}`,
4554
+ ` path: ${appPath}`,
4340
4555
  ...docsCheckoutStep,
4341
4556
  "",
4342
4557
  " - name: Checkout Topolo SDK",
4343
4558
  " uses: actions/checkout@v4",
4344
4559
  " with:",
4345
4560
  " repository: Topolo-io/topolo-sdk",
4346
- " path: PlatformApplications/packages/topolo-sdk",
4561
+ " path: Apps/packages/topolo-sdk",
4347
4562
  " token: ${{ secrets.TOPOLO_CI_REPO_TOKEN || github.token }}",
4348
4563
  ...cliCheckoutStep,
4349
4564
  "",
@@ -4353,26 +4568,26 @@ function topoloConformanceWorkflow(options) {
4353
4568
  " node-version: 20",
4354
4569
  "",
4355
4570
  " - name: Build Topolo SDK",
4356
- " working-directory: PlatformApplications/packages/topolo-sdk",
4571
+ " working-directory: Apps/packages/topolo-sdk",
4357
4572
  " run: |",
4358
4573
  " npm install --no-audit --no-fund",
4359
4574
  " npm run build",
4360
4575
  "",
4361
4576
  " - name: Build Topolo CLI",
4362
- " working-directory: PlatformApplications/TopoloCli",
4577
+ " working-directory: Apps/system/TopoloCLI",
4363
4578
  " run: |",
4364
4579
  " npm install --no-audit --no-fund",
4365
4580
  " npm run build",
4366
4581
  "",
4367
4582
  " - name: Run Topolo app conformance audit",
4368
4583
  " run: |",
4369
- ` node PlatformApplications/TopoloCli/dist/index.js apps audit ${options.id} --local --repo-root "$GITHUB_WORKSPACE" --fail-on needs_review --json`,
4584
+ ` node Apps/system/TopoloCLI/dist/index.js apps audit ${options.id} --local --repo-root "$GITHUB_WORKSPACE" --fail-on needs_review --json`,
4370
4585
  "",
4371
4586
  " - name: Record green CI evidence",
4372
4587
  ` if: \${{ success() }}`,
4373
- ` working-directory: PlatformApplications/${options.appDir}`,
4588
+ ` working-directory: ${appPath}`,
4374
4589
  " run: |",
4375
- ` node "$GITHUB_WORKSPACE/PlatformApplications/TopoloCli/dist/index.js" apps record-evidence ${options.id} --build --typecheck --test --commit "$GITHUB_SHA"`
4590
+ ` node "$GITHUB_WORKSPACE/Apps/system/TopoloCLI/dist/index.js" apps record-evidence ${options.id} --build --typecheck --test --commit "$GITHUB_SHA"`
4376
4591
  ].join("\n");
4377
4592
  }
4378
4593
  function packageJson(application, target, options) {
@@ -4398,20 +4613,27 @@ function packageJson(application, target, options) {
4398
4613
  dependencies["@topolo-io/auth-client"] = topoloPackageVersion("@topolo-io/auth-client", target);
4399
4614
  dependencies["@topolo-io/design-tokens"] = topoloPackageVersion("@topolo-io/design-tokens", target);
4400
4615
  dependencies["@topolo-io/ui-kit"] = topoloPackageVersion("@topolo-io/ui-kit", target);
4616
+ } else {
4617
+ dependencies["@topolo/login"] = topoloPackageVersion("@topolo/login", target);
4401
4618
  }
4402
4619
  }
4403
4620
  if (options.api) {
4404
4621
  if (target === "platform") {
4405
- dependencies["@topolo-io/auth-middleware"] = topoloPackageVersion("@topolo-io/auth-middleware", target);
4622
+ dependencies["@topolo-io/worker-runtime"] = topoloPackageVersion("@topolo-io/worker-runtime", target);
4623
+ dependencies["@topolo-io/widget-server"] = topoloPackageVersion("@topolo-io/widget-server", target);
4406
4624
  dependencies["@topolo/sdk"] = topoloPackageVersion("@topolo/sdk", target);
4407
4625
  scripts.deploy = `node -e 'console.error("\\nX Bare deploy is disabled. Use deploy:staging or deploy:production.\\n"); process.exit(1)'`;
4408
4626
  scripts["deploy:staging"] = "DEPLOY_ENVIRONMENT=staging VITE_TOPOLO_AUTH_URL=https://auth.stg.topolo.us npm run build && wrangler deploy --env staging";
4409
4627
  scripts["deploy:production"] = "DEPLOY_ENVIRONMENT=production npm run build && wrangler deploy";
4410
4628
  } else {
4629
+ dependencies["@topolo-io/widget-server"] = topoloPackageVersion("@topolo-io/widget-server", target);
4411
4630
  scripts.deploy = "npm run build && wrangler deploy";
4412
4631
  }
4413
4632
  devDependencies.wrangler = "^4.40.2";
4414
4633
  }
4634
+ if (target === "platform" && hasScaffoldResources(options.resources)) {
4635
+ scripts["check:deploy-config"] = "wrangler deploy --dry-run --outdir .wrangler/build";
4636
+ }
4415
4637
  return `${json({
4416
4638
  name: application.packageName,
4417
4639
  version: "0.1.0",
@@ -4425,15 +4647,20 @@ function packageJson(application, target, options) {
4425
4647
  }
4426
4648
  function topoloPackageVersion(packageName, target) {
4427
4649
  if (target === "developer") {
4650
+ if (packageName === "@topolo/login") return "^0.1.0";
4428
4651
  if (packageName === "@topolo-io/auth-client") return "^1.0.0";
4652
+ if (packageName === "@topolo-io/widget-server") return "^0.1.1";
4429
4653
  return "^0.1.0";
4430
4654
  }
4431
4655
  const platformPackageFolders = {
4432
4656
  "@topolo/sdk": "topolo-sdk",
4657
+ "@topolo/login": "topolo-login",
4433
4658
  "@topolo-io/auth-client": "topolo-auth-client",
4434
4659
  "@topolo-io/auth-middleware": "topolo-auth-middleware",
4660
+ "@topolo-io/worker-runtime": "topolo-worker-runtime",
4435
4661
  "@topolo-io/design-tokens": "topolo-design-tokens",
4436
- "@topolo-io/ui-kit": "topolo-ui-kit"
4662
+ "@topolo-io/ui-kit": "topolo-ui-kit",
4663
+ "@topolo-io/widget-server": "topolo-widget-server"
4437
4664
  };
4438
4665
  const folder = platformPackageFolders[packageName];
4439
4666
  if (!folder) {
@@ -4441,6 +4668,49 @@ function topoloPackageVersion(packageName, target) {
4441
4668
  }
4442
4669
  return `file:../packages/${folder}`;
4443
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
+ }
4444
4714
  function readme(application, target, options) {
4445
4715
  const displayName = toTitle(application.name);
4446
4716
  const slug = toSlug(application.id);
@@ -4453,6 +4723,7 @@ function readme(application, target, options) {
4453
4723
  `Audience: ${options.audience}`,
4454
4724
  `Tenancy: ${options.tenancy}`,
4455
4725
  ...target === "platform" && options.portfolio ? [`Portfolio: ${options.portfolio}`] : [],
4726
+ ...options.profile ? [`Profile: ${options.profile}`] : [],
4456
4727
  "",
4457
4728
  "## Development",
4458
4729
  "",
@@ -4474,7 +4745,12 @@ function readme(application, target, options) {
4474
4745
  "- `vite.config.ts` includes the standard React plugin plus React runtime dedupe so linked Topolo packages resolve one browser React runtime."
4475
4746
  );
4476
4747
  if (options.api) {
4477
- lines.push("- `@topolo-io/auth-middleware` validates Worker requests through Topolo Auth.");
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
+ );
4478
4754
  }
4479
4755
  } else {
4480
4756
  lines.push(
@@ -4501,6 +4777,15 @@ function readme(application, target, options) {
4501
4777
  "`topolo.app.json` is the app manifest used to register callback URLs, requested scopes, and local development metadata with Topolo."
4502
4778
  );
4503
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
+ }
4504
4789
  return `${lines.join("\n")}
4505
4790
  `;
4506
4791
  }
@@ -4551,8 +4836,7 @@ function envDts(api) {
4551
4836
  " readonly VITE_TOPOLO_AUTH_URL?: string;",
4552
4837
  " readonly VITE_TOPOLO_CLIENT_ID?: string;",
4553
4838
  " readonly VITE_TOPOLO_REDIRECT_URI?: string;",
4554
- " readonly VITE_TOPOLO_SCOPES?: string;",
4555
- " readonly VITE_TOPOLO_SERVICE_ID?: string;"
4839
+ " readonly VITE_TOPOLO_SCOPES?: string;"
4556
4840
  ];
4557
4841
  if (api) {
4558
4842
  lines.push(" readonly VITE_TOPOLO_API_URL?: string;");
@@ -4567,6 +4851,105 @@ function envDts(api) {
4567
4851
  );
4568
4852
  return lines.join("\n");
4569
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
+ }
4570
4953
  function viteConfig(target) {
4571
4954
  if (target === "developer") {
4572
4955
  return [
@@ -4744,8 +5127,6 @@ function appTsx(application, target, options) {
4744
5127
  return platformAppTsx(application, options);
4745
5128
  }
4746
5129
  function platformAppTsx(application, options) {
4747
- const appName = toTitle(application.name);
4748
- const serviceId = application.serviceId ?? application.id;
4749
5130
  const isPersonal = options.audience === "personal";
4750
5131
  const tagline = isPersonal ? "Topolo personal app" : "Topolo workspace";
4751
5132
  const workspaceTitle = isPersonal ? "Personal app" : "Workspace";
@@ -4755,8 +5136,9 @@ function platformAppTsx(application, options) {
4755
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.";
4756
5137
  return [
4757
5138
  "import { Component, useEffect, useMemo, useState, type ErrorInfo, type ReactNode } from 'react';",
4758
- "import { createBrowserAuthModule, type AuthUser } from '@topolo-io/auth-client';",
5139
+ "import { createBrowserAuthSessionController, type AuthUser } from '@topolo-io/auth-client';",
4759
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';",
4760
5142
  "",
4761
5143
  "// Wire global error/console capture so BugReporter (enabled in TopoloAppShell below)",
4762
5144
  "// can attach the last few errors to any submitted bug report.",
@@ -4764,16 +5146,11 @@ function platformAppTsx(application, options) {
4764
5146
  "",
4765
5147
  "type NavItem = 'home';",
4766
5148
  "type AppRoute = { kind: 'root' } | { kind: 'login' } | { kind: 'callback' };",
5149
+ "type BrowserAuthModule = ReturnType<typeof createBrowserAuthSessionController>;",
4767
5150
  "",
4768
- `const APP_ID = '${application.id}';`,
4769
- `const APP_NAME = '${escapeSingleQuoted(appName)}';`,
4770
- `const DEFAULT_SERVICE_ID = '${serviceId}';`,
4771
- "const DEFAULT_AUTH_URL =",
4772
- " typeof window !== 'undefined' && window.location.hostname.endsWith('.stg.topolo.us')",
4773
- " ? 'https://auth.stg.topolo.us'",
4774
- " : 'https://auth.topolo.app';",
4775
- "const AUTH_URL = import.meta.env.VITE_TOPOLO_AUTH_URL ?? DEFAULT_AUTH_URL;",
4776
- "const SERVICE_ID = import.meta.env.VITE_TOPOLO_SERVICE_ID ?? DEFAULT_SERVICE_ID;",
5151
+ "const APP_ID = SCAFFOLD_APP_ID;",
5152
+ "const APP_NAME = SCAFFOLD_APP_NAME;",
5153
+ "const AUTH_URL = getBrowserAuthBaseUrl();",
4777
5154
  "const LANDING_CONFIG = {",
4778
5155
  " app_name: APP_NAME,",
4779
5156
  ` app_tagline: '${escapeSingleQuoted(tagline)}',`,
@@ -4792,17 +5169,28 @@ function platformAppTsx(application, options) {
4792
5169
  " authenticated_home_path: '/',",
4793
5170
  "};",
4794
5171
  "",
4795
- "const auth = createBrowserAuthModule({",
4796
- " authServiceUrl: AUTH_URL,",
4797
- " serviceId: SERVICE_ID,",
4798
- " devSessionKey: `${SERVICE_ID}_session`,",
4799
- " devSessionStorageEnabled: import.meta.env.DEV,",
4800
- " callbackPath: '/auth/callback',",
4801
- " debug: import.meta.env.DEV,",
4802
- "});",
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
+ "}",
4803
5191
  "",
4804
5192
  "function getAccessToken(): string | null {",
4805
- " return auth.accessTokenMemory.get() ?? auth.accessTokenSession.get();",
5193
+ " return activeAuthModule?.accessTokenMemory.get() ?? activeAuthModule?.accessTokenSession.get() ?? null;",
4806
5194
  "}",
4807
5195
  "",
4808
5196
  "function toShellUser(user: AuthUser | null): TopoloAppShellUser | null {",
@@ -4828,12 +5216,33 @@ function platformAppTsx(application, options) {
4828
5216
  "function AppRoot() {",
4829
5217
  " const route = getRoute();",
4830
5218
  " const [user, setUser] = useState<AuthUser | null>(null);",
5219
+ " const [serviceId, setServiceId] = useState('');",
5220
+ " const [serviceIdentityError, setServiceIdentityError] = useState('');",
4831
5221
  " const [loading, setLoading] = useState(true);",
4832
5222
  " const [authError, setAuthError] = useState('');",
4833
5223
  " const [loginAttempted, setLoginAttempted] = useState(false);",
5224
+ " const auth = useMemo(() => serviceId ? getAuthModule(serviceId) : null, [serviceId]);",
4834
5225
  "",
4835
5226
  " useEffect(() => {",
4836
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
+ " }, []);",
5242
+ "",
5243
+ " useEffect(() => {",
5244
+ " if (!auth) return;",
5245
+ " let active = true;",
4837
5246
  " const syncAuth = async () => {",
4838
5247
  " setLoading(true);",
4839
5248
  " setAuthError('');",
@@ -4865,7 +5274,7 @@ function platformAppTsx(application, options) {
4865
5274
  " return () => {",
4866
5275
  " active = false;",
4867
5276
  " };",
4868
- " }, [route.kind]);",
5277
+ " }, [auth, route.kind]);",
4869
5278
  "",
4870
5279
  " useEffect(() => {",
4871
5280
  " if (!loading && user && route.kind === 'login') {",
@@ -4874,13 +5283,39 @@ function platformAppTsx(application, options) {
4874
5283
  " }, [loading, route.kind, user]);",
4875
5284
  "",
4876
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
+ " }",
4877
5312
  "",
4878
5313
  " if (route.kind === 'callback') {",
4879
5314
  " if (loading) {",
4880
5315
  " return (",
4881
5316
  " <TopoloLoadingScreen",
4882
5317
  " appId={APP_ID}",
4883
- " serviceId={SERVICE_ID}",
5318
+ " serviceId={activeServiceId}",
4884
5319
  " appName={APP_NAME}",
4885
5320
  ' subtitle="Completing secure sign-in..."',
4886
5321
  " />",
@@ -4891,7 +5326,7 @@ function platformAppTsx(application, options) {
4891
5326
  " <TopoloAuthFlowScreen",
4892
5327
  " appName={APP_NAME}",
4893
5328
  " appId={APP_ID}",
4894
- " serviceId={SERVICE_ID}",
5329
+ " serviceId={activeServiceId}",
4895
5330
  ' status="error"',
4896
5331
  ' title="Authentication failed"',
4897
5332
  " message={authError || `We could not complete sign-in for ${APP_NAME}.`}",
@@ -4905,7 +5340,7 @@ function platformAppTsx(application, options) {
4905
5340
  " return (",
4906
5341
  " <TopoloLoadingScreen",
4907
5342
  " appId={APP_ID}",
4908
- " serviceId={SERVICE_ID}",
5343
+ " serviceId={activeServiceId}",
4909
5344
  " appName={APP_NAME}",
4910
5345
  " subtitle={`Loading ${APP_NAME}...`}",
4911
5346
  " />",
@@ -4916,7 +5351,7 @@ function platformAppTsx(application, options) {
4916
5351
  " if (route.kind === 'login') {",
4917
5352
  " return (",
4918
5353
  " <LoginPage",
4919
- " serviceId={SERVICE_ID}",
5354
+ " serviceId={activeServiceId}",
4920
5355
  " authApiUrl={AUTH_URL}",
4921
5356
  " config={LOGIN_CONFIG}",
4922
5357
  " isLoading={loginAttempted}",
@@ -4943,7 +5378,7 @@ function platformAppTsx(application, options) {
4943
5378
  "",
4944
5379
  " return (",
4945
5380
  " <LandingPage",
4946
- " serviceId={SERVICE_ID}",
5381
+ " serviceId={activeServiceId}",
4947
5382
  " authApiUrl={AUTH_URL}",
4948
5383
  " config={LANDING_CONFIG}",
4949
5384
  " onCTAClick={() => window.location.assign('/login')}",
@@ -4955,7 +5390,7 @@ function platformAppTsx(application, options) {
4955
5390
  " return (",
4956
5391
  " <TopoloLoadingScreen",
4957
5392
  " appId={APP_ID}",
4958
- " serviceId={SERVICE_ID}",
5393
+ " serviceId={activeServiceId}",
4959
5394
  " appName={APP_NAME}",
4960
5395
  ` subtitle="Opening your ${escapeDoubleQuoted(signedSurfaceLabel)}..."`,
4961
5396
  " />",
@@ -4965,7 +5400,7 @@ function platformAppTsx(application, options) {
4965
5400
  " return (",
4966
5401
  " <TopoloAppShell<NavItem>",
4967
5402
  " appId={APP_ID}",
4968
- " serviceId={SERVICE_ID}",
5403
+ " serviceId={activeServiceId}",
4969
5404
  " appName={APP_NAME}",
4970
5405
  " user={shellUser}",
4971
5406
  " orgId={user.activeContextType === 'organization' ? user.activeContextId ?? user.orgId ?? null : null}",
@@ -5030,7 +5465,7 @@ function platformAppTsx(application, options) {
5030
5465
  " <TopoloAuthFlowScreen",
5031
5466
  " appName={APP_NAME}",
5032
5467
  " appId={APP_ID}",
5033
- " serviceId={SERVICE_ID}",
5468
+ " serviceId={SCAFFOLD_SERVICE_SLUG}",
5034
5469
  ' status="error"',
5035
5470
  " title={APP_NAME + ' hit an unexpected error'}",
5036
5471
  " message={this.state.error.message}",
@@ -5048,19 +5483,21 @@ function developerAppTsx(application) {
5048
5483
  const defaultClientId = `${toSlug(application.id)}_local_client`;
5049
5484
  return [
5050
5485
  "import { useEffect, useMemo, useState } from 'react';",
5486
+ "import { createAccountLinkPayload, createTopoloAuthorizationUrl, exchangeTopoloCode, fetchTopoloUserInfo, type TopoloUserInfo } from '@topolo/login';",
5051
5487
  "",
5052
5488
  "type AuthState = 'ready' | 'missing_client' | 'authorizing' | 'authenticated' | 'error';",
5053
5489
  "",
5054
5490
  `const APP_NAME = '${escapeSingleQuoted(appName)}';`,
5055
- "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';",
5056
5492
  `const CLIENT_ID = import.meta.env.VITE_TOPOLO_CLIENT_ID || '${defaultClientId}';`,
5057
5493
  "const REDIRECT_URI = import.meta.env.VITE_TOPOLO_REDIRECT_URI || `${window.location.origin}/auth/callback`;",
5058
- "const REQUESTED_SCOPES = (import.meta.env.VITE_TOPOLO_SCOPES || 'identity:read organization:read').trim();",
5494
+ "const REQUESTED_SCOPES = (import.meta.env.VITE_TOPOLO_SCOPES || 'openid profile email organization:read entitlements:read').split(/[\\s,]+/).filter(Boolean);",
5059
5495
  "",
5060
5496
  "export default function App() {",
5061
5497
  " const [authState, setAuthState] = useState<AuthState>(() => CLIENT_ID ? 'ready' : 'missing_client');",
5062
5498
  " const [message, setMessage] = useState<string | null>(null);",
5063
5499
  " const [tokenPreview, setTokenPreview] = useState<string | null>(null);",
5500
+ " const [userInfo, setUserInfo] = useState<TopoloUserInfo | null>(null);",
5064
5501
  "",
5065
5502
  " const callbackCode = useMemo(() => new URLSearchParams(window.location.search).get('code'), []);",
5066
5503
  " const callbackError = useMemo(() => new URLSearchParams(window.location.search).get('error'), []);",
@@ -5075,11 +5512,18 @@ function developerAppTsx(application) {
5075
5512
  "",
5076
5513
  " setAuthState('authorizing');",
5077
5514
  " void exchangeAuthorizationCode(callbackCode)",
5078
- " .then((tokens) => {",
5515
+ " .then(async (tokens) => {",
5079
5516
  " sessionStorage.setItem(`${CLIENT_ID}:topolo_access_token`, tokens.access_token);",
5080
5517
  " if (tokens.refresh_token) {",
5081
5518
  " sessionStorage.setItem(`${CLIENT_ID}:topolo_refresh_token`, tokens.refresh_token);",
5082
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);",
5083
5527
  " setTokenPreview(`${tokens.access_token.slice(0, 18)}...`);",
5084
5528
  " setAuthState('authenticated');",
5085
5529
  " window.history.replaceState({}, document.title, '/');",
@@ -5099,7 +5543,7 @@ function developerAppTsx(application) {
5099
5543
  " <p>Set VITE_TOPOLO_CLIENT_ID after registering this app in Topolo Developers.</p>",
5100
5544
  " )}",
5101
5545
  " {authState === 'authenticated' && (",
5102
- " <p>Topolo sign-in complete. Access token: {tokenPreview}</p>",
5546
+ " <p>Topolo sign-in complete for {userInfo?.email ?? userInfo?.sub}. Access token: {tokenPreview}</p>",
5103
5547
  " )}",
5104
5548
  " {authState === 'error' && <p>{message}</p>}",
5105
5549
  ` <button type="button" disabled={!CLIENT_ID || authState === 'authorizing'} onClick={() => void startTopoloLogin()}>`,
@@ -5111,21 +5555,15 @@ function developerAppTsx(application) {
5111
5555
  "}",
5112
5556
  "",
5113
5557
  "async function startTopoloLogin(): Promise<void> {",
5114
- " const verifier = randomString(64);",
5115
- " const challenge = await pkceChallenge(verifier);",
5116
- " const state = randomString(32);",
5117
- " sessionStorage.setItem(`${CLIENT_ID}:topolo_pkce_verifier`, verifier);",
5118
- " sessionStorage.setItem(`${CLIENT_ID}:topolo_oauth_state`, state);",
5119
- "",
5120
- " const authorizeUrl = new URL('/api/developer-oauth/authorize', AUTH_URL);",
5121
- " authorizeUrl.searchParams.set('response_type', 'code');",
5122
- " authorizeUrl.searchParams.set('client_id', CLIENT_ID);",
5123
- " authorizeUrl.searchParams.set('redirect_uri', REDIRECT_URI);",
5124
- " authorizeUrl.searchParams.set('scope', REQUESTED_SCOPES);",
5125
- " authorizeUrl.searchParams.set('state', state);",
5126
- " authorizeUrl.searchParams.set('code_challenge', challenge);",
5127
- " authorizeUrl.searchParams.set('code_challenge_method', 'S256');",
5128
- " window.location.assign(authorizeUrl.toString());",
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);",
5129
5567
  "}",
5130
5568
  "",
5131
5569
  "async function exchangeAuthorizationCode(code: string): Promise<{ access_token: string; refresh_token?: string }> {",
@@ -5137,49 +5575,23 @@ function developerAppTsx(application) {
5137
5575
  " const verifier = sessionStorage.getItem(`${CLIENT_ID}:topolo_pkce_verifier`);",
5138
5576
  " if (!verifier) throw new Error('Missing PKCE verifier. Restart sign-in.');",
5139
5577
  "",
5140
- " const tokenUrl = new URL('/api/developer-oauth/token', AUTH_URL);",
5141
- " const response = await fetch(tokenUrl, {",
5142
- " method: 'POST',",
5143
- " headers: { 'Content-Type': 'application/json' },",
5144
- " body: JSON.stringify({",
5145
- " grant_type: 'authorization_code',",
5146
- " client_id: CLIENT_ID,",
5578
+ " return exchangeTopoloCode({",
5579
+ " authBaseUrl: AUTH_URL,",
5580
+ " clientId: CLIENT_ID,",
5147
5581
  " code,",
5148
- " redirect_uri: REDIRECT_URI,",
5149
- " code_verifier: verifier,",
5150
- " }),",
5582
+ " redirectUri: REDIRECT_URI,",
5583
+ " codeVerifier: verifier,",
5151
5584
  " });",
5152
- " const payload = await response.json().catch(() => null);",
5153
- " if (!response.ok || !payload?.access_token) {",
5154
- " throw new Error(payload?.error_description || payload?.error || 'Token exchange failed.');",
5155
- " }",
5156
- " return payload;",
5157
- "}",
5158
- "",
5159
- "function randomString(length: number): string {",
5160
- " const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';",
5161
- " const bytes = crypto.getRandomValues(new Uint8Array(length));",
5162
- " return Array.from(bytes, (byte) => alphabet[byte % alphabet.length]).join('');",
5163
- "}",
5164
- "",
5165
- "async function pkceChallenge(verifier: string): Promise<string> {",
5166
- " const data = new TextEncoder().encode(verifier);",
5167
- " const digest = await crypto.subtle.digest('SHA-256', data);",
5168
- " return base64Url(new Uint8Array(digest));",
5169
- "}",
5170
- "",
5171
- "function base64Url(bytes: Uint8Array): string {",
5172
- " let binary = '';",
5173
- " for (const byte of bytes) binary += String.fromCharCode(byte);",
5174
- " return btoa(binary).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/g, '');",
5175
5585
  "}",
5176
5586
  ""
5177
5587
  ].join("\n");
5178
5588
  }
5179
- function wranglerToml(application, target) {
5589
+ function wranglerToml(application, target, resources, profile) {
5180
5590
  const slug = toSlug(application.id);
5181
5591
  const workerName = target === "platform" ? `topolo-${slug}` : application.packageName;
5182
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}"`;
5183
5595
  const lines = [
5184
5596
  `name = "${workerName}"`,
5185
5597
  'main = "src/worker.ts"',
@@ -5215,9 +5627,47 @@ function wranglerToml(application, target) {
5215
5627
  "",
5216
5628
  "[vars]",
5217
5629
  'AUTH_BASE_URL = "https://auth.topolo.app"',
5218
- `SERVICE_ID = "${application.serviceId ?? application.id}"`,
5630
+ runtimeIdentityVar,
5219
5631
  ""
5220
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
+ }
5221
5671
  if (target === "platform") {
5222
5672
  lines.push(
5223
5673
  "# Staging deploys use the separate Topolo Staging Cloudflare account.",
@@ -5231,9 +5681,47 @@ function wranglerToml(application, target) {
5231
5681
  "",
5232
5682
  "[env.staging.vars]",
5233
5683
  'AUTH_BASE_URL = "https://auth.stg.topolo.us"',
5234
- `SERVICE_ID = "${application.serviceId ?? application.id}"`,
5684
+ runtimeIdentityVar,
5235
5685
  ""
5236
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
+ }
5237
5725
  }
5238
5726
  return lines.join("\n");
5239
5727
  }
@@ -5242,12 +5730,29 @@ function workerTs(application, target, options) {
5242
5730
  return platformWorkerTs(application, options);
5243
5731
  }
5244
5732
  function platformWorkerTs(application, options) {
5245
- const serviceId = application.serviceId ?? application.id;
5246
5733
  const serviceName = toTitle(application.name);
5247
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
+ ] : [];
5248
5753
  const bootstrapBody = options.tenancy === "household" ? [
5249
5754
  " const userContext = auth.user && typeof auth.user === 'object' ? (auth.user as unknown as Record<string, unknown>) : {};",
5250
- " return json({",
5755
+ " return rt.json({",
5251
5756
  " user: auth.user,",
5252
5757
  " activeContextType: userContext.activeContextType ?? null,",
5253
5758
  " activeContextId: userContext.activeContextId ?? null,",
@@ -5256,82 +5761,111 @@ function platformWorkerTs(application, options) {
5256
5761
  " selectedHousehold: userContext.selectedHousehold ?? null,",
5257
5762
  " household: userContext.selectedHousehold ?? null,",
5258
5763
  " householdMemberships: userContext.householdMemberships ?? [],",
5259
- " }, { status: 200 }, request);"
5764
+ " }, { status: 200 });"
5260
5765
  ] : [
5261
- " return json({",
5766
+ " return rt.json({",
5262
5767
  " user: auth.user,",
5263
5768
  " organization: auth.organization,",
5264
- " }, { status: 200 }, request);"
5769
+ " }, { status: 200 });"
5265
5770
  ];
5266
5771
  return [
5267
- "import { createTopoloWidgetResponse } from '@topolo/sdk';",
5268
- "import { verifyRequest } from '@topolo-io/auth-middleware';",
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';"] : [],
5269
5776
  "",
5270
5777
  "export interface Env {",
5271
- " AUTH_BASE_URL?: string;",
5272
- " SERVICE_ID?: string;",
5273
- " TOPOLO_SERVICE_API_KEY?: string;",
5778
+ ...envBindings,
5274
5779
  "}",
5275
5780
  "",
5276
- "const CLIENT_HEADER = 'topolo-worker-starter';",
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
+ "});",
5277
5802
  "",
5278
- "export default {",
5279
- " async fetch(request: Request, env: Env): Promise<Response> {",
5280
- " const url = new URL(request.url);",
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
+ "});",
5281
5810
  "",
5282
- " if (url.pathname === '/api/health') {",
5283
- " return json({ ok: true, service: env.SERVICE_ID ?? DEFAULT_SERVICE_ID }, { status: 200 }, request);",
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 });",
5284
5819
  " }",
5285
5820
  "",
5286
- " const auth = await verifyRequest(request, {",
5287
- " authBaseUrl: env.AUTH_BASE_URL,",
5288
- " serviceId: env.SERVICE_ID ?? DEFAULT_SERVICE_ID,",
5289
- " serviceApiKey: env.TOPOLO_SERVICE_API_KEY,",
5290
- " });",
5821
+ " if (url.pathname === '/api/health') {",
5822
+ " return rt.json({ ok: true, service: serviceId, serviceSlug: SCAFFOLD_SERVICE_SLUG }, { status: 200 });",
5823
+ " }",
5291
5824
  "",
5292
- " if (!auth.ok) {",
5293
- " return json({ error: auth.error }, { status: auth.status }, request);",
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 });",
5294
5832
  " }",
5295
5833
  "",
5296
- " if (url.pathname === '/api/widget') {",
5297
- " return json(createTopoloWidgetResponse({",
5298
- " serviceId: env.SERVICE_ID ?? DEFAULT_SERVICE_ID,",
5299
- ` serviceName: '${escapeSingleQuoted(serviceName)}',`,
5300
- " widgets: [",
5301
- " {",
5302
- " type: 'quick-actions',",
5303
- " actions: [",
5304
- ` { id: 'open-workspace', label: '${escapeSingleQuoted(openActionLabel)}', link: '/', primary: true },`,
5305
- " { id: 'view-health', label: 'View health', link: '/api/health' },",
5306
- " ],",
5307
- " },",
5308
- " ],",
5309
- " }), { status: 200 }, request);",
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);",
5310
5842
  " }",
5311
5843
  "",
5312
5844
  " if (url.pathname === '/api/bootstrap') {",
5313
5845
  ...bootstrapBody,
5314
5846
  " }",
5315
5847
  "",
5316
- " return json({ error: 'not_found' }, { status: 404 }, request);",
5317
- " },",
5318
- "};",
5848
+ " return rt.notFound();",
5849
+ "});",
5319
5850
  "",
5320
- `const DEFAULT_SERVICE_ID = '${serviceId}';`,
5851
+ "export default {",
5852
+ " fetch: handleFetch,",
5853
+ "};",
5321
5854
  "",
5322
- "function json(body: unknown, init: ResponseInit, request: Request): Response {",
5323
- " const headers = new Headers(init.headers);",
5324
- " headers.set('Content-Type', 'application/json');",
5325
- " headers.set('X-Topolo-Client', CLIENT_HEADER);",
5326
- " headers.set('X-Topolo-Request-Id', request.headers.get('X-Topolo-Request-Id') ?? crypto.randomUUID());",
5327
- " return new Response(JSON.stringify(body), { ...init, headers });",
5855
+ "function errorMessage(error: unknown): string {",
5856
+ " if (error instanceof Error && error.message) return error.message;",
5857
+ " return 'Unexpected service identity error.';",
5328
5858
  "}",
5859
+ ...workflowExport,
5329
5860
  ""
5330
5861
  ].join("\n");
5331
5862
  }
5332
5863
  function developerWorkerTs(application) {
5333
5864
  const serviceId = application.serviceId ?? application.id;
5865
+ const serviceName = application.name || "Topolo App";
5334
5866
  return [
5867
+ "import { createWidgetHandler } from '@topolo-io/widget-server';",
5868
+ "",
5335
5869
  "export interface Env {",
5336
5870
  " AUTH_BASE_URL?: string;",
5337
5871
  " SERVICE_ID?: string;",
@@ -5339,6 +5873,28 @@ function developerWorkerTs(application) {
5339
5873
  "",
5340
5874
  "const CLIENT_HEADER = 'topolo-developer-starter';",
5341
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
+ "});",
5342
5898
  "",
5343
5899
  "export default {",
5344
5900
  " async fetch(request: Request, env: Env): Promise<Response> {",
@@ -5348,6 +5904,9 @@ function developerWorkerTs(application) {
5348
5904
  " return json({ ok: true, service: env.SERVICE_ID ?? DEFAULT_SERVICE_ID }, { status: 200 }, request);",
5349
5905
  " }",
5350
5906
  "",
5907
+ " // /api/widget delegates fully to @topolo-io/widget-server.",
5908
+ " if (url.pathname === '/api/widget') return widgetHandler(request, env);",
5909
+ "",
5351
5910
  " const auth = await verifyTopoloRequest(request, env);",
5352
5911
  " if (!auth.ok) {",
5353
5912
  " return json({ error: auth.error }, { status: auth.status }, request);",
@@ -5404,8 +5963,108 @@ function developerWorkerTs(application) {
5404
5963
  }
5405
5964
  function cloudcontrolJson(application, options) {
5406
5965
  const slug = toSlug(application.id);
5966
+ const appPath = firstPartyAppPath(application.appDir, options.audience);
5407
5967
  const host = application.productionUrl ? new URL(application.productionUrl).host : `${slug}.topolo.app`;
5408
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
+ }
5409
6068
  const deployTargets = options.api ? [
5410
6069
  {
5411
6070
  kind: "worker",
@@ -5420,7 +6079,7 @@ function cloudcontrolJson(application, options) {
5420
6079
  domains: [host],
5421
6080
  routes: [host],
5422
6081
  api_url: application.productionUrl,
5423
- service_id: application.serviceId
6082
+ service_slug: serviceSlug
5424
6083
  },
5425
6084
  {
5426
6085
  kind: "worker",
@@ -5430,12 +6089,12 @@ function cloudcontrolJson(application, options) {
5430
6089
  config_path: "wrangler.toml",
5431
6090
  build_command: "VITE_TOPOLO_AUTH_URL=https://auth.stg.topolo.us npm run build",
5432
6091
  output_dir: "dist",
5433
- production_branch: "staging",
6092
+ production_branch: "main",
5434
6093
  healthcheck_path: "/api/health",
5435
6094
  domains: [stagingHost],
5436
6095
  routes: [stagingHost],
5437
6096
  api_url: `https://${stagingHost}`,
5438
- service_id: application.serviceId
6097
+ service_slug: serviceSlug
5439
6098
  }
5440
6099
  ] : [
5441
6100
  {
@@ -5455,14 +6114,14 @@ function cloudcontrolJson(application, options) {
5455
6114
  ];
5456
6115
  return `${json({
5457
6116
  name: `topolo-${slug}`,
5458
- description: `Canonical documentation for ${toTitle(application.name)} lives in PlatformApplications/TopoloDocs.`,
6117
+ description: `Canonical documentation for ${toTitle(application.name)} lives in Apps/system/TopoloDocs.`,
5459
6118
  deploy_method: options.api ? "wrangler" : "cloudflare_pages",
5460
6119
  ci_provider: "github_actions",
5461
6120
  repo_identity: {
5462
6121
  provider: "github",
5463
6122
  owner: "Topolo-io",
5464
- repo: `topolo-${slug}`,
5465
- remote_url: `git@github.com:Topolo-io/topolo-${slug}.git`,
6123
+ repo: application.appDir,
6124
+ remote_url: `git@github.com:Topolo-io/${application.appDir}.git`,
5466
6125
  default_branch: "main"
5467
6126
  },
5468
6127
  environments: application.productionUrl ? [
@@ -5475,12 +6134,12 @@ function cloudcontrolJson(application, options) {
5475
6134
  {
5476
6135
  name: "staging",
5477
6136
  url: `https://${stagingHost}`,
5478
- branch: "staging",
6137
+ branch: "main",
5479
6138
  status: "unknown"
5480
6139
  }
5481
6140
  ] : [],
5482
6141
  deploy_targets: deployTargets,
5483
- resource_bindings: [],
6142
+ resource_bindings: resourceBindings,
5484
6143
  ci_workflows: [
5485
6144
  {
5486
6145
  provider: "github_actions",
@@ -5490,8 +6149,25 @@ function cloudcontrolJson(application, options) {
5490
6149
  required: true
5491
6150
  }
5492
6151
  ],
5493
- ci_targets: [],
5494
- tags: ["application", "cloudflare", "typescript"],
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],
5495
6171
  cloudflare_account_id: CLOUDFLARE_ACCOUNT_ID
5496
6172
  })}
5497
6173
  `;
@@ -5500,7 +6176,9 @@ function systemJson(application, options) {
5500
6176
  const slug = toSlug(application.id);
5501
6177
  const suiteLabel = options.audience === "personal" ? "personal" : "platform";
5502
6178
  const summaryLabel = options.audience === "personal" ? "first-party Topolo personal application" : "first-party Topolo platform application";
5503
- 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." : "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." : "Browser-only application surface using shared Topolo Auth landing/login/callback routes, shared shell packages, and React runtime dedupe.";
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.";
5504
6182
  const sensitiveData = options.tenancy === "household" ? ["identity", "personal_data", "household_data"] : ["identity", "org_data"];
5505
6183
  const privacySensitiveData = options.tenancy === "household" ? ["identity", "personal", "household"] : ["identity", "organization"];
5506
6184
  return `${json({
@@ -5512,19 +6190,19 @@ function systemJson(application, options) {
5512
6190
  lifecycle: "inventory",
5513
6191
  last_verified: CURRENT_VERIFICATION_DATE,
5514
6192
  owners: ["platform-engineering"],
5515
- repo_paths: [`PlatformApplications/${application.appDir}`],
6193
+ repo_paths: [appPath],
5516
6194
  service_ids: options.api && application.serviceId ? [application.serviceId] : [],
5517
6195
  visibility: "internal",
5518
6196
  api_contract: {
5519
6197
  type: options.api ? "curated" : "none",
5520
- source: options.api ? `PlatformApplications/${application.appDir}/src/worker.ts` : `PlatformApplications/${application.appDir}/src/App.tsx`,
6198
+ source: options.api ? `${appPath}/src/worker.ts` : `${appPath}/src/App.tsx`,
5521
6199
  notes: apiNotes
5522
6200
  },
5523
6201
  primary_hosts: application.productionUrl ? [application.productionUrl] : [],
5524
6202
  doc_paths: [`internal/apps/${slug}`, `public/applications/${slug}`],
5525
6203
  security_assurance: {
5526
6204
  risk_tier: "internal",
5527
- auth_boundary: "Topolo Auth through @topolo-io/auth-client for browser sessions and @topolo-io/auth-middleware for Worker requests.",
6205
+ auth_boundary: "Topolo Auth through @topolo-io/auth-client for browser sessions and @topolo-io/worker-runtime for Worker requests.",
5528
6206
  tenant_isolation: options.tenancy === "household" ? "household_scoped" : "organization_scoped",
5529
6207
  external_inputs: ["browser", ...options.api ? ["api"] : []],
5530
6208
  sensitive_data: sensitiveData,
@@ -5536,7 +6214,13 @@ function systemJson(application, options) {
5536
6214
  data_privacy: {
5537
6215
  classification: "confidential",
5538
6216
  sensitive_data_classes: privacySensitiveData,
5539
- storage_locations: ["source_control"],
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
+ ],
5540
6224
  encryption_at_rest: "platform_managed",
5541
6225
  encryption_in_transit: "https_only",
5542
6226
  key_management: "cloudflare_managed",
@@ -5554,11 +6238,24 @@ function systemJson(application, options) {
5554
6238
  }
5555
6239
  function internalDocs(application, options) {
5556
6240
  const slug = toSlug(application.id);
6241
+ const serviceSlug = application.serviceSlug ?? `topolo-${slug}`;
5557
6242
  const title = toTitle(application.name);
6243
+ const appPath = firstPartyAppPath(application.appDir, options.audience);
5558
6244
  const suiteLabel = options.audience === "personal" ? "personal" : "platform";
5559
6245
  const tenantLabel = options.tenancy === "household" ? "household" : "organization";
5560
6246
  const apiContextLabel = options.tenancy === "household" ? "active personal context and selected household context" : "organization context";
5561
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
+ ] : [];
5562
6259
  return [
5563
6260
  "---",
5564
6261
  `title: "${escapeFrontmatter(title)} Handbook"`,
@@ -5567,8 +6264,8 @@ function internalDocs(application, options) {
5567
6264
  'status: "draft"',
5568
6265
  `last_verified: "${CURRENT_VERIFICATION_DATE}"`,
5569
6266
  "source_repos:",
5570
- ` - "PlatformApplications/${application.appDir}"`,
5571
- ' - "PlatformApplications/TopoloDocs"',
6267
+ ` - "${appPath}"`,
6268
+ ' - "Apps/system/TopoloDocs"',
5572
6269
  'audience: "internal"',
5573
6270
  "tags:",
5574
6271
  ` - "${slug}"`,
@@ -5588,16 +6285,18 @@ function internalDocs(application, options) {
5588
6285
  "## Architecture",
5589
6286
  "",
5590
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.`,
5591
6289
  "",
5592
6290
  "## Runtime Surfaces",
5593
6291
  "",
5594
- `- \`PlatformApplications/${application.appDir}/src/App.tsx\``,
6292
+ `- \`${appPath}/src/App.tsx\``,
6293
+ `- \`${appPath}/src/service-identity.ts\``,
5595
6294
  "- public landing route: `/`",
5596
6295
  "- public sign-in route: `/login`",
5597
6296
  "- auth callback route: `/auth/callback`",
5598
- ...options.api ? [`- \`PlatformApplications/${application.appDir}/src/worker.ts\``] : [],
5599
- `- \`PlatformApplications/${application.appDir}/topolo.cloudcontrol.json\``,
5600
- ...options.api && application.serviceId ? [`- \`PlatformApplications/TopoloAuth/catalog/service-manifests/${application.serviceId}.json\``] : [],
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\``] : [],
5601
6300
  "",
5602
6301
  "## API Reference",
5603
6302
  "",
@@ -5605,11 +6304,12 @@ function internalDocs(application, options) {
5605
6304
  "",
5606
6305
  "## Auth and Permissions",
5607
6306
  "",
5608
- options.api ? `Browser sessions and Worker requests must be validated through Topolo Auth under service id \`${application.serviceId}\`. Worker routes derive ${apiContextLabel} from the verified credential.` : "Browser sessions must be established through Topolo Auth and rendered inside the shared Topolo shell.",
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.",
5609
6308
  "",
5610
6309
  "## Data Ownership",
5611
6310
  "",
5612
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,
5613
6313
  "",
5614
6314
  "## Deployments",
5615
6315
  "",
@@ -5617,7 +6317,7 @@ function internalDocs(application, options) {
5617
6317
  "",
5618
6318
  "## Failure Modes",
5619
6319
  "",
5620
- "- Developers provisioning drifts from the generated Worker `SERVICE_ID`.",
6320
+ `- the Worker \`SERVICE_ID\` binding drifts from the Auth service manifest for \`${serviceSlug}\`.`,
5621
6321
  "- CloudControl metadata does not match the eventual Cloudflare project or route.",
5622
6322
  "- linked Topolo packages resolve a second React runtime and the browser shell renders blank instead of mounting the shared surfaces.",
5623
6323
  `- ${boundaryFailureText}`,
@@ -5625,9 +6325,9 @@ function internalDocs(application, options) {
5625
6325
  "## Debugging",
5626
6326
  "",
5627
6327
  "- run `npm run typecheck` and `npm run build` in the application directory",
5628
- "- run `topolo apps audit <app> --local --repo-root /Users/ashes/Projects/topolo --fail-on needs_review --json` after SDK catalog registration",
6328
+ "- run `topolo apps audit <app> --local --repo-root /Users/ashes/Projects/Topolo/Platform --fail-on needs_review --json` after SDK catalog registration",
5629
6329
  "- inspect `topolo.cloudcontrol.json` before deployment changes",
5630
- ...options.api && application.serviceId ? [`- inspect \`PlatformApplications/TopoloAuth/catalog/service-manifests/${application.serviceId}.json\` when permission grants fail`] : [],
6330
+ ...options.api ? ["- inspect the Worker `SERVICE_ID` binding and matching Auth service manifest when permission grants fail"] : [],
5631
6331
  ...options.api && application.serviceId ? [`- re-run \`topolo platform apps scaffold ${slug} --write --provision\` when embedded app login, launcher visibility, or callback origins drift`] : [],
5632
6332
  "",
5633
6333
  "## Change Log / Verification",
@@ -5641,7 +6341,9 @@ function internalDocs(application, options) {
5641
6341
  }
5642
6342
  function publicDocs(application, options) {
5643
6343
  const slug = toSlug(application.id);
6344
+ const serviceSlug = application.serviceSlug ?? `topolo-${slug}`;
5644
6345
  const title = toTitle(application.name);
6346
+ const appPath = firstPartyAppPath(application.appDir, options.audience);
5645
6347
  const suiteLabel = options.audience === "personal" ? "personal" : "business";
5646
6348
  const tenantLabel = options.tenancy === "household" ? "household" : "organization";
5647
6349
  const apiRouteLabel = options.tenancy === "household" ? "authenticated personal and household routes" : "authenticated workspace routes";
@@ -5654,7 +6356,7 @@ function publicDocs(application, options) {
5654
6356
  'status: "draft"',
5655
6357
  `last_verified: "${CURRENT_VERIFICATION_DATE}"`,
5656
6358
  "source_repos:",
5657
- ` - "PlatformApplications/${application.appDir}"`,
6359
+ ` - "${appPath}"`,
5658
6360
  'audience: "public"',
5659
6361
  "tags:",
5660
6362
  ` - "${slug}"`,
@@ -5685,11 +6387,11 @@ function publicDocs(application, options) {
5685
6387
  "",
5686
6388
  "## Auth and Permissions",
5687
6389
  "",
5688
- options.api && application.serviceId ? `Signed browser and API requests use Topolo Auth through service id \`${application.serviceId}\`.` : "Signed browser requests use Topolo Auth and the shared Topolo shell.",
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.",
5689
6391
  "",
5690
6392
  "## Data Ownership",
5691
6393
  "",
5692
- `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.`,
5693
6395
  "",
5694
6396
  "## Deployments",
5695
6397
  "",
@@ -5714,16 +6416,20 @@ function publicDocs(application, options) {
5714
6416
  function serviceManifestJson(application, options) {
5715
6417
  const slug = toSlug(application.id);
5716
6418
  const title = toTitle(application.name);
6419
+ const appPath = firstPartyAppPath(application.appDir, options.audience);
5717
6420
  const permissionPrefix = application.id.replace(/[^a-z0-9_]/g, "_");
5718
6421
  const resourceLabel = options.tenancy === "household" ? "household" : "workspace";
5719
- const launcherSettings = options.tenancy === "household" ? {
6422
+ const launcherSettings = {
5720
6423
  settings: {
5721
6424
  launcher: {
5722
- supported_contexts: ["personal"],
5723
- household_capable: true
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 } : {}
5724
6430
  }
5725
6431
  }
5726
- } : {};
6432
+ };
5727
6433
  return `${json({
5728
6434
  serviceId: application.serviceId,
5729
6435
  ...launcherSettings,
@@ -5731,7 +6437,7 @@ function serviceManifestJson(application, options) {
5731
6437
  {
5732
6438
  systemId: `topolo-${slug}`,
5733
6439
  name: title,
5734
- repoPaths: [`PlatformApplications/${application.appDir}`],
6440
+ repoPaths: [appPath],
5735
6441
  docPaths: [`applications/${slug}`, `internal/apps/${slug}`]
5736
6442
  }
5737
6443
  ],
@@ -5773,6 +6479,10 @@ function serviceManifestJson(application, options) {
5773
6479
  `;
5774
6480
  }
5775
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"];
5776
6486
  return `${json({
5777
6487
  schema: "https://topolo.app/schemas/topolo-app-manifest.v1.json",
5778
6488
  id: application.id,
@@ -5785,17 +6495,49 @@ function developerManifest(application, options) {
5785
6495
  container_app_id: options.containerAppId,
5786
6496
  package_name: application.packageName,
5787
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
+ },
5788
6512
  auth: {
5789
6513
  provider: "topolo",
5790
6514
  flow: "authorization_code_pkce",
5791
- callback_path: "/auth/callback",
5792
- local_callback_url: "http://localhost:5173/auth/callback",
5793
- production_callback_url: application.productionUrl ? `${application.productionUrl.replace(/\/$/, "")}/auth/callback` : null,
5794
- requested_scopes: options.api ? ["identity:read", "organization:read", `${application.id}:read`] : ["identity:read", "organization:read"]
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
+ }
5795
6532
  },
5796
6533
  runtime: {
6534
+ strategy: "external",
5797
6535
  browser: true,
5798
- 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"
5799
6541
  }
5800
6542
  })}
5801
6543
  `;
@@ -5803,10 +6545,10 @@ function developerManifest(application, options) {
5803
6545
  function envExample(application) {
5804
6546
  return [
5805
6547
  "VITE_TOPOLO_AUTH_URL=https://auth.topolo.app",
6548
+ "VITE_TOPOLO_AUTH_BASE_URL=https://auth.topolo.app",
5806
6549
  "VITE_TOPOLO_CLIENT_ID=",
5807
6550
  "VITE_TOPOLO_REDIRECT_URI=http://localhost:5173/auth/callback",
5808
- "VITE_TOPOLO_SCOPES=identity:read organization:read",
5809
- `VITE_TOPOLO_SERVICE_ID=${application.serviceId ?? application.id}`,
6551
+ "VITE_TOPOLO_SCOPES=openid profile email organization:read entitlements:read",
5810
6552
  ""
5811
6553
  ].join("\n");
5812
6554
  }
@@ -5851,6 +6593,21 @@ function normalizeSurfaceModel(value) {
5851
6593
  "Invalid scaffold surface model. Use web_app, mobile_container, mobile_module, or messaging_surface."
5852
6594
  );
5853
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
+ }
5854
6611
  function normalizeContainerAppId(value, surfaceModel) {
5855
6612
  const trimmed = String(value || "").trim();
5856
6613
  if (surfaceModel === "mobile_module") {
@@ -5887,7 +6644,8 @@ function toSlug(value) {
5887
6644
  return value.replace(/_/g, "-");
5888
6645
  }
5889
6646
  function toPascalCase(value) {
5890
- return value.split(/[-_]/).filter(Boolean).map((part) => part.slice(0, 1).toUpperCase() + part.slice(1)).join("");
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("");
5891
6649
  }
5892
6650
  function toTitle(value) {
5893
6651
  return value.replace(/^topolo[-_\s]*/i, "Topolo ").split(/[-_\s]+/).filter(Boolean).map((part) => part.slice(0, 1).toUpperCase() + part.slice(1)).join(" ");
@@ -5925,7 +6683,30 @@ function escapeFrontmatter(value) {
5925
6683
  }
5926
6684
 
5927
6685
  // src/commands/app-scaffold-provision.ts
5928
- var DEVELOPERS_SERVICE_ID = "svc_topolo_developers";
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
+ }
5929
6710
  async function provisionApplicationScaffold(payload) {
5930
6711
  if (!payload.application.productionUrl) {
5931
6712
  throw new Error(
@@ -5937,6 +6718,7 @@ async function provisionApplicationScaffold(payload) {
5937
6718
  "Platform scaffold provisioning requires a service id. Re-run without --no-api or pass --service."
5938
6719
  );
5939
6720
  }
6721
+ assertProvisioningPayloadComplete(payload);
5940
6722
  const environment = selectedEnvironment();
5941
6723
  const credential = await refreshCredentialIfNeeded(environment);
5942
6724
  if (!credential) {
@@ -5953,12 +6735,20 @@ async function provisionApplicationScaffold(payload) {
5953
6735
  const identity = await client.client.introspect();
5954
6736
  const config = await readConfig();
5955
6737
  const profile = resolvedProfileConfig(config, environment);
6738
+ const authOrigin = sanitizeOrigin(profile.serviceUrls?.auth ?? defaultAuthOrigin(environment));
6739
+ assertServiceUrlForEnvironment("auth", authOrigin, environment);
5956
6740
  const developersOrigin = sanitizeOrigin(profile.serviceUrls?.developers ?? "https://developers.topolo.app");
5957
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
+ }
5958
6748
  const endpoint = payload.target === "platform" ? "/api/developer-console/admin/first-party-app-scaffolds" : "/api/developer-console/apps/scaffold";
5959
6749
  const headers = new Headers({
5960
6750
  "Content-Type": "application/json",
5961
- "X-Service-ID": DEVELOPERS_SERVICE_ID,
6751
+ "X-Service-ID": developersServiceId,
5962
6752
  "X-Topolo-Client": `topolo-cli/${CLI_VERSION}`
5963
6753
  });
5964
6754
  const agentName = resolveAgentName(profile.agentName);
@@ -6030,11 +6820,21 @@ function buildProvisioningRequest(payload) {
6030
6820
  surfaceModel: payload.surfaceModel,
6031
6821
  containerAppId: payload.containerAppId,
6032
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",
6033
6832
  applicationId: payload.application.id,
6034
6833
  appDir: payload.application.appDir,
6035
6834
  packageName: payload.application.packageName,
6036
6835
  name: payload.application.name,
6037
6836
  productionUrl: payload.application.productionUrl,
6837
+ serviceSlug: payload.application.serviceSlug,
6038
6838
  serviceId: payload.application.serviceId
6039
6839
  };
6040
6840
  }
@@ -6051,6 +6851,19 @@ function resolveProvisioningError(envelope, status, target) {
6051
6851
  function sanitizeOrigin(value) {
6052
6852
  return value.replace(/\/+$/, "");
6053
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
+ }
6054
6867
  function pickRecord(value) {
6055
6868
  return value && typeof value === "object" ? value : null;
6056
6869
  }
@@ -6147,7 +6960,7 @@ function registerAppsCommand(program2) {
6147
6960
  "Developer app surface model: web_app, mobile_container, mobile_module, or messaging_surface"
6148
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(
6149
6962
  "--provision",
6150
- "Provision the generated app into the current Topolo developer workspace using the authenticated session"
6963
+ "Register the app in the current Topolo developer workspace using the authenticated session"
6151
6964
  ).action(async (application, opts) => {
6152
6965
  const mode = opts.json === false || process.argv.includes("--no-json") ? "pretty" : resolveOutputMode(opts.json);
6153
6966
  try {
@@ -6166,9 +6979,6 @@ function registerAppsCommand(program2) {
6166
6979
  surfaceModel: resolveScaffoldSurfaceModel(opts.surfaceModel),
6167
6980
  containerAppId: opts.containerAppId
6168
6981
  });
6169
- if (opts.provision && !opts.write) {
6170
- throw new Error("--provision requires --write so remote developer records match written scaffold files.");
6171
- }
6172
6982
  const writeResult = opts.write ? await writeApplicationScaffold(payload, { repoRoot: opts.repoRoot, force: opts.force }) : null;
6173
6983
  const provisionResult = opts.provision ? await provisionApplicationScaffold(payload) : null;
6174
6984
  const output = {
@@ -6534,7 +7344,7 @@ function registerPlatformCommand(program2) {
6534
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(
6535
7345
  "--surface-model <surfaceModel>",
6536
7346
  "First-party surface model: web_app, mobile_container, mobile_module, or messaging_surface"
6537
- ).option("--container-app-id <appId>", "Container app id when --surface-model mobile_module is used").option("--write", "Write the generated scaffold files to disk").option("--repo-root <path>", "Directory where scaffold files should be written").option("--force", "Overwrite existing scaffold files when --write is set").option("--with-api", "Include the API/service baseline").option("--no-api", "Do not include API/service requirements").option("--no-browser", "Do not include browser-shell requirements").option(
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(
6538
7348
  "--provision",
6539
7349
  "Provision the generated first-party app into Topolo Developers and Auth using the authenticated Topolo admin session"
6540
7350
  ).option(
@@ -6543,10 +7353,14 @@ function registerPlatformCommand(program2) {
6543
7353
  ).option("--workflow-only", "Output only the generated GitHub Actions workflow in human mode").action(async (application, opts) => {
6544
7354
  const mode = opts.json === false || process.argv.includes("--no-json") ? "pretty" : resolveOutputMode(opts.json);
6545
7355
  try {
6546
- await requirePlatformAdminSession();
6547
7356
  if (opts.workflowOnly && opts.write) {
6548
7357
  throw new Error("--workflow-only cannot be used with --write.");
6549
7358
  }
7359
+ const provisionRequested = process.argv.includes("--provision");
7360
+ const shouldProvision = Boolean(opts.write) && opts.provision !== false;
7361
+ if (shouldProvision) {
7362
+ await requirePlatformAdminSession();
7363
+ }
6550
7364
  const api = resolveScaffoldApiOption(opts.api, opts.withApi);
6551
7365
  const payload = applicationScaffoldPayload(application, {
6552
7366
  appDir: opts.appDir,
@@ -6559,13 +7373,17 @@ function registerPlatformCommand(program2) {
6559
7373
  target: "platform",
6560
7374
  portfolio: resolvePlatformPortfolio(opts.portfolio),
6561
7375
  surfaceModel: resolvePlatformSurfaceModel(opts.surfaceModel),
6562
- 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
6563
7383
  });
6564
7384
  if (opts.workflowOnly && !payload.ciWorkflow) {
6565
7385
  throw new Error("--workflow-only is only available for first-party scaffolds.");
6566
7386
  }
6567
- const provisionRequested = process.argv.includes("--provision");
6568
- const shouldProvision = Boolean(opts.write) && opts.provision !== false;
6569
7387
  if (provisionRequested && !opts.write) {
6570
7388
  throw new Error("--provision requires --write so remote first-party records match written scaffold files.");
6571
7389
  }
@@ -6698,6 +7516,14 @@ function isPlatformAdminIdentity(identity) {
6698
7516
  const orgSlug = String(identity?.organization?.slug || "").trim().toLowerCase();
6699
7517
  return orgSlug === "admin" && (role === "platform_super_admin" || role === "platform_admin");
6700
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
+ }
6701
7527
  async function requirePlatformAdminSession() {
6702
7528
  const client = await buildClient();
6703
7529
  if (!client) {
@@ -6706,9 +7532,9 @@ async function requirePlatformAdminSession() {
6706
7532
  );
6707
7533
  }
6708
7534
  const identity = await client.client.introspect();
6709
- if (!isPlatformAdminIdentity(identity)) {
7535
+ if (!canProvisionFirstPartyScaffoldIdentity(identity)) {
6710
7536
  throw new Error(
6711
- "`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."
6712
7538
  );
6713
7539
  }
6714
7540
  }
@@ -6975,6 +7801,282 @@ function formatManifestsTable(manifests) {
6975
7801
  return lines.join("\n");
6976
7802
  }
6977
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
+
6978
8080
  // src/index.ts
6979
8081
  var program = new Command();
6980
8082
  var argv;
@@ -6998,6 +8100,8 @@ registerWhoamiCommand(program);
6998
8100
  registerCrmCommands(program);
6999
8101
  registerApiCommand(program);
7000
8102
  registerAppsCommand(program);
8103
+ registerOAuthCommands(program);
8104
+ registerIntegrationsCommand(program);
7001
8105
  registerPlatformCommand(program);
7002
8106
  registerServicesCommand(program);
7003
8107
  registerSeedCommands(program);