@wairon/cli 5.0.2-dev.7 → 5.0.2-dev.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -65,7 +65,7 @@ var init_defaults = __esm({
65
65
  copilot: ".github/prompts",
66
66
  codex: ".codex/agents"
67
67
  };
68
- WAIRON_VERSION = "5.0.2-dev.7";
68
+ WAIRON_VERSION = "5.0.2-dev.8";
69
69
  GITHUB_REPO = "SYW-Apps/Waffle-AIron";
70
70
  ARCHITECT_AGENT_ID = "agent-architect";
71
71
  ARCHITECT_TEMPLATE_ID = "architect";
@@ -703,7 +703,7 @@ var init_template = __esm({
703
703
  });
704
704
 
705
705
  // src/models/specs.ts
706
- var import_zod6, SpecIdSchema, SpecStatusSchema, BoundaryItemSchema, RequirementItemSchema, DatabaseSpecSchema, DiagramConfigSchema, SURFACE_AUDIENCES, SurfaceAudienceSchema, SystemPublicInterfaceSchema, SystemSpecSchema, PublicInterfaceTypeSchema, PublicInterfaceSchema, TrustedLinkSchema, LintAllowSchema, LintConfigSchema, ExtDataSchema, LifecycleEntrypointSchema, SubsystemSpecSchema, ComponentTypeSchema, PATTERN_TYPES, PortalTypeSchema, DispatchBindingSchema, DurabilitySchema, PatternRefSchema, EventBindingSchema, ExternalLinkTypeSchema, ExternalLinkSchema, ComponentSpecSchema, HttpMethodSchema, TransportSchema, EndpointSchema, SEMANTIC_GUARANTEES, GuaranteeSchema, MethodParamSchema, MethodSignatureSchema, InterfaceSpecSchema, NarrativeStepTypeSchema, LoopKindSchema, SwitchCaseSchema, CatchClauseSchema, ParallelBranchSchema, NarrativeStepSchema, NarrativeDetailSchema, ConformanceTierSchema, MethodImplementationSchema, ImplementationSpecSchema, TypeKindSchema, TypeFieldSchema, TypeMethodSchema, InvariantSchema, TypeSpecSchema, SurfaceOriginSchema, SurfaceTypeDefSchema, SurfaceContractEntrySchema, SurfaceSnapshotSchema, GroupSpecSchema;
706
+ var import_zod6, SpecIdSchema, SpecStatusSchema, BoundaryItemSchema, RequirementItemSchema, DatabaseSpecSchema, DiagramConfigSchema, SURFACE_AUDIENCES, SurfaceAudienceSchema, SystemPublicInterfaceSchema, SystemSpecSchema, PublicInterfaceTypeSchema, PublicInterfaceSchema, TrustedLinkSchema, LintAllowSchema, LintConfigSchema, ExtDataSchema, LifecycleEntrypointSchema, SubsystemSpecSchema, ComponentTypeSchema, PATTERN_TYPES, PortalTypeSchema, DispatchBindingSchema, DurabilitySchema, PatternRefSchema, EventBindingSchema, ExternalLinkTypeSchema, ExternalLinkSchema, PortalAuthSchemeSchema, PortalAuthSchema, ComponentSpecSchema, HttpMethodSchema, TransportSchema, EndpointSchema, SEMANTIC_GUARANTEES, GuaranteeSchema, MethodParamSchema, MethodSignatureSchema, InterfaceSpecSchema, NarrativeStepTypeSchema, LoopKindSchema, SwitchCaseSchema, CatchClauseSchema, ParallelBranchSchema, NarrativeStepSchema, NarrativeDetailSchema, ConformanceTierSchema, MethodImplementationSchema, ImplementationSpecSchema, TypeKindSchema, TypeFieldSchema, TypeMethodSchema, InvariantSchema, TypeSpecSchema, SurfaceOriginSchema, SurfaceTypeDefSchema, SurfaceContractEntrySchema, SurfaceSnapshotSchema, NamedOpenApiSpecSchema, GroupSpecSchema;
707
707
  var init_specs = __esm({
708
708
  "src/models/specs.ts"() {
709
709
  "use strict";
@@ -914,6 +914,21 @@ var init_specs = __esm({
914
914
  type: ExternalLinkTypeSchema.default("informative"),
915
915
  label: import_zod6.z.string().optional()
916
916
  });
917
+ PortalAuthSchemeSchema = import_zod6.z.enum(["none", "apiKey", "bearer", "basic", "oauth2", "openIdConnect", "custom"]);
918
+ PortalAuthSchema = import_zod6.z.object({
919
+ scheme: PortalAuthSchemeSchema,
920
+ in: import_zod6.z.enum(["header", "query", "cookie"]).optional(),
921
+ name: import_zod6.z.string().optional(),
922
+ bearerFormat: import_zod6.z.string().optional(),
923
+ authorizationUrl: import_zod6.z.string().optional(),
924
+ tokenUrl: import_zod6.z.string().optional(),
925
+ refreshUrl: import_zod6.z.string().optional(),
926
+ scopes: import_zod6.z.array(import_zod6.z.object({ name: import_zod6.z.string(), description: import_zod6.z.string() })).optional(),
927
+ flow: import_zod6.z.enum(["authorizationCode", "clientCredentials", "implicit", "password"]).optional(),
928
+ openIdConnectUrl: import_zod6.z.string().optional(),
929
+ description: import_zod6.z.string().optional(),
930
+ example: import_zod6.z.string().optional()
931
+ });
917
932
  ComponentSpecSchema = import_zod6.z.object({
918
933
  id: SpecIdSchema,
919
934
  name: import_zod6.z.string(),
@@ -927,6 +942,10 @@ var init_specs = __esm({
927
942
  dependsOn: import_zod6.z.array(import_zod6.z.string()).default([]),
928
943
  portalType: PortalTypeSchema.optional(),
929
944
  basePath: import_zod6.z.string().optional(),
945
+ /** Portal-only: the API's authentication scheme (see PortalAuthSchema) — projected
946
+ * into the generated OpenAPI's securitySchemes/security. Portals with different auth
947
+ * must be separate components (one auth per portal ⇒ one OpenAPI spec per portal). */
948
+ auth: PortalAuthSchema.optional(),
930
949
  /** Portal-only: capability → component.method dispatch table (see DispatchBindingSchema). */
931
950
  dispatch: import_zod6.z.array(DispatchBindingSchema).optional(),
932
951
  /** Store-only: whether held state survives restart (see DurabilitySchema). */
@@ -1292,7 +1311,12 @@ var init_specs = __esm({
1292
1311
  dispatch: import_zod6.z.array(DispatchBindingSchema).optional(),
1293
1312
  details: import_zod6.z.string().default(""),
1294
1313
  version: import_zod6.z.string().optional(),
1295
- stability: import_zod6.z.string().optional()
1314
+ stability: import_zod6.z.string().optional(),
1315
+ /** Projected copy of the backing Portal's auth (see PortalAuthSchema) — the codec
1316
+ * emits it as OpenAPI securitySchemes/security. */
1317
+ auth: PortalAuthSchema.optional(),
1318
+ /** The backing Portal's basePath — becomes the per-portal OpenAPI `servers` url. */
1319
+ basePath: import_zod6.z.string().optional()
1296
1320
  });
1297
1321
  SurfaceSnapshotSchema = import_zod6.z.object({
1298
1322
  /** Producing project/system name — the snapshot's resolution identity. */
@@ -1307,6 +1331,11 @@ var init_specs = __esm({
1307
1331
  /** Transitive type closure of every exported signature — self-contained. */
1308
1332
  types: import_zod6.z.array(SurfaceTypeDefSchema).default([])
1309
1333
  });
1334
+ NamedOpenApiSpecSchema = import_zod6.z.object({
1335
+ portalId: import_zod6.z.string(),
1336
+ name: import_zod6.z.string(),
1337
+ document: import_zod6.z.string()
1338
+ });
1310
1339
  GroupSpecSchema = import_zod6.z.object({
1311
1340
  kind: import_zod6.z.literal("group"),
1312
1341
  id: SpecIdSchema,
@@ -6115,7 +6144,7 @@ var MODEL = __MODEL_JSON__;
6115
6144
  + (scopeFocus ? staticChip('current view') : '')
6116
6145
  + chip(c.subsystem, 'subsystem', c.subsystem)
6117
6146
  + (scopeFocus ? '' : openViewButton('component', c.id, c.owns.length > 0))
6118
- + openApiButton(componentExposesApi(c), c.apiTag)
6147
+ + openApiButton(componentExposesApi(c), c.id)
6119
6148
  + openSpecButton('component', c.id);
6120
6149
 
6121
6150
  var linkedTypes = MODEL.types.filter(function (t) { return t.componentClass === c.id; });
@@ -6920,11 +6949,63 @@ function operationFor(method2, closureIds) {
6920
6949
  }
6921
6950
  return op;
6922
6951
  }
6923
- function toOpenApi(snapshot) {
6924
- const closureIds = new Set(snapshot.types.map((t) => t.id));
6925
- const httpEntries = snapshot.interfaces.filter((e) => e.type === "REST" || e.methods.some((m) => m.endpoint?.transport === "HTTP"));
6952
+ function securitySchemeObject(auth) {
6953
+ if (auth.scheme === "none") return null;
6954
+ const desc = auth.description ? { description: auth.description } : {};
6955
+ switch (auth.scheme) {
6956
+ case "apiKey":
6957
+ return { type: "apiKey", in: auth.in ?? "header", name: auth.name ?? "X-API-Key", ...desc };
6958
+ case "bearer":
6959
+ return { type: "http", scheme: "bearer", ...auth.bearerFormat ? { bearerFormat: auth.bearerFormat } : {}, ...desc };
6960
+ case "basic":
6961
+ return { type: "http", scheme: "basic", ...desc };
6962
+ case "oauth2": {
6963
+ const flow = { scopes: Object.fromEntries((auth.scopes ?? []).map((s) => [s.name, s.description])) };
6964
+ if (auth.authorizationUrl) flow.authorizationUrl = auth.authorizationUrl;
6965
+ if (auth.tokenUrl) flow.tokenUrl = auth.tokenUrl;
6966
+ if (auth.refreshUrl) flow.refreshUrl = auth.refreshUrl;
6967
+ return { type: "oauth2", flows: { [auth.flow ?? "authorizationCode"]: flow }, ...desc };
6968
+ }
6969
+ case "openIdConnect":
6970
+ return { type: "openIdConnect", openIdConnectUrl: auth.openIdConnectUrl ?? "", ...desc };
6971
+ case "custom":
6972
+ return { type: "apiKey", in: auth.in ?? "header", name: auth.name ?? "Authorization", description: auth.description ?? auth.example ?? "Custom authentication scheme." };
6973
+ default:
6974
+ return null;
6975
+ }
6976
+ }
6977
+ function schemeBaseName(scheme) {
6978
+ return { apiKey: "ApiKeyAuth", bearer: "BearerAuth", basic: "BasicAuth", oauth2: "OAuth2", openIdConnect: "OpenIdConnect", custom: "CustomAuth" }[scheme] ?? "Auth";
6979
+ }
6980
+ function buildSecurity(entries) {
6981
+ const schemes = {};
6982
+ const nameByContent = /* @__PURE__ */ new Map();
6983
+ const securityByEntry = /* @__PURE__ */ new Map();
6984
+ for (const entry of entries) {
6985
+ if (!entry.auth) continue;
6986
+ const obj = securitySchemeObject(entry.auth);
6987
+ if (!obj) continue;
6988
+ const content = JSON.stringify(obj);
6989
+ let name = nameByContent.get(content);
6990
+ if (!name) {
6991
+ name = schemeBaseName(entry.auth.scheme);
6992
+ for (let n = 2; schemes[name]; n++) name = schemeBaseName(entry.auth.scheme) + n;
6993
+ schemes[name] = obj;
6994
+ nameByContent.set(content, name);
6995
+ }
6996
+ const scopeNames = entry.auth.scheme === "oauth2" ? (entry.auth.scopes ?? []).map((s) => s.name) : [];
6997
+ securityByEntry.set(entry.id, { [name]: scopeNames });
6998
+ }
6999
+ return { schemes, securityByEntry };
7000
+ }
7001
+ function httpEntriesOf(snapshot) {
7002
+ return snapshot.interfaces.filter((e) => e.type === "REST" || e.methods.some((m) => m.endpoint?.transport === "HTTP"));
7003
+ }
7004
+ function renderDoc(snapshot, entries, closureIds, opts = {}) {
7005
+ const { schemes, securityByEntry } = buildSecurity(entries);
6926
7006
  const paths = {};
6927
- for (const entry of httpEntries) {
7007
+ for (const entry of entries) {
7008
+ const security = securityByEntry.get(entry.id);
6928
7009
  for (const method2 of entry.methods) {
6929
7010
  const endpoint = method2.endpoint;
6930
7011
  if (!endpoint || endpoint.transport !== "HTTP") continue;
@@ -6932,7 +7013,8 @@ function toOpenApi(snapshot) {
6932
7013
  paths[p] = paths[p] ?? {};
6933
7014
  paths[p][endpoint.method.toLowerCase()] = {
6934
7015
  tags: [entry.id],
6935
- ...operationFor(method2, closureIds)
7016
+ ...operationFor(method2, closureIds),
7017
+ ...security ? { security: [security] } : {}
6936
7018
  };
6937
7019
  }
6938
7020
  }
@@ -6945,19 +7027,41 @@ function toOpenApi(snapshot) {
6945
7027
  required: t.fields.filter((f) => !f.optional).map((f) => f.name)
6946
7028
  };
6947
7029
  }
6948
- const doc = {
7030
+ const components = {};
7031
+ if (Object.keys(schemas).length) components.schemas = schemas;
7032
+ if (Object.keys(schemes).length) components.securitySchemes = schemes;
7033
+ const basePaths = [...new Set(entries.map((e) => e.basePath).filter((b) => !!b))];
7034
+ const servers = opts.servers && basePaths.length === 1 ? [{ url: basePaths[0] }] : void 0;
7035
+ return {
6949
7036
  openapi: "3.1.0",
6950
7037
  info: {
6951
- title: snapshot.projectName,
7038
+ title: opts.title ?? snapshot.projectName,
6952
7039
  version: snapshot.version ?? "0.0.0",
6953
7040
  ...snapshot.stateId ? { "x-wairon-state-id": snapshot.stateId } : {},
6954
7041
  "x-wairon-origin": snapshot.origin,
6955
7042
  "x-wairon-generated-at": snapshot.generatedAt
6956
7043
  },
7044
+ ...servers ? { servers } : {},
6957
7045
  paths,
6958
- ...Object.keys(schemas).length ? { components: { schemas } } : {}
7046
+ ...Object.keys(components).length ? { components } : {}
6959
7047
  };
6960
- return JSON.stringify(doc, null, 2);
7048
+ }
7049
+ function toOpenApiSet(snapshot) {
7050
+ const closureIds = new Set(snapshot.types.map((t) => t.id));
7051
+ const byPortal = /* @__PURE__ */ new Map();
7052
+ const order = [];
7053
+ for (const entry of httpEntriesOf(snapshot)) {
7054
+ if (!byPortal.has(entry.component)) {
7055
+ byPortal.set(entry.component, []);
7056
+ order.push(entry.component);
7057
+ }
7058
+ byPortal.get(entry.component).push(entry);
7059
+ }
7060
+ return order.map((portalId) => {
7061
+ const entries = byPortal.get(portalId);
7062
+ const name = entries[0]?.name ?? portalId;
7063
+ return { portalId, name, document: JSON.stringify(renderDoc(snapshot, entries, closureIds, { title: name, servers: true }), null, 2) };
7064
+ });
6961
7065
  }
6962
7066
  function isOpenApiDocument(body) {
6963
7067
  try {
@@ -6979,6 +7083,39 @@ function typeRefFromSchema(schema) {
6979
7083
  if (typeof t === "string" && t !== "object") return t;
6980
7084
  return "json";
6981
7085
  }
7086
+ function authFromSecurityScheme(scheme) {
7087
+ const desc = typeof scheme.description === "string" ? { description: scheme.description } : {};
7088
+ if (scheme.type === "apiKey") {
7089
+ return {
7090
+ scheme: "apiKey",
7091
+ ...scheme.in === "header" || scheme.in === "query" || scheme.in === "cookie" ? { in: scheme.in } : {},
7092
+ ...typeof scheme.name === "string" ? { name: scheme.name } : {},
7093
+ ...desc
7094
+ };
7095
+ }
7096
+ if (scheme.type === "http") {
7097
+ if (scheme.scheme === "bearer") return { scheme: "bearer", ...typeof scheme.bearerFormat === "string" ? { bearerFormat: scheme.bearerFormat } : {}, ...desc };
7098
+ if (scheme.scheme === "basic") return { scheme: "basic", ...desc };
7099
+ }
7100
+ if (scheme.type === "oauth2") {
7101
+ const flows = scheme.flows ?? {};
7102
+ const flowKey = Object.keys(flows)[0];
7103
+ const f = flows[flowKey] ?? {};
7104
+ return {
7105
+ scheme: "oauth2",
7106
+ ...flowKey === "authorizationCode" || flowKey === "clientCredentials" || flowKey === "implicit" || flowKey === "password" ? { flow: flowKey } : {},
7107
+ ...typeof f.authorizationUrl === "string" ? { authorizationUrl: f.authorizationUrl } : {},
7108
+ ...typeof f.tokenUrl === "string" ? { tokenUrl: f.tokenUrl } : {},
7109
+ ...typeof f.refreshUrl === "string" ? { refreshUrl: f.refreshUrl } : {},
7110
+ scopes: Object.entries(f.scopes ?? {}).map(([name, description]) => ({ name, description })),
7111
+ ...desc
7112
+ };
7113
+ }
7114
+ if (scheme.type === "openIdConnect") {
7115
+ return { scheme: "openIdConnect", openIdConnectUrl: typeof scheme.openIdConnectUrl === "string" ? scheme.openIdConnectUrl : "", ...desc };
7116
+ }
7117
+ return void 0;
7118
+ }
6982
7119
  function fromOpenApi(document, projectName) {
6983
7120
  let parsed;
6984
7121
  try {
@@ -7056,6 +7193,9 @@ function fromOpenApi(document, projectName) {
7056
7193
  }))
7057
7194
  });
7058
7195
  }
7196
+ const securitySchemes = parsed.components?.securitySchemes ?? {};
7197
+ const firstScheme = Object.values(securitySchemes)[0];
7198
+ const importedAuth = firstScheme ? authFromSecurityScheme(firstScheme) : void 0;
7059
7199
  const entry = {
7060
7200
  id: `${projectName}-api`,
7061
7201
  name: typeof info.title === "string" ? info.title : projectName,
@@ -7064,7 +7204,8 @@ function fromOpenApi(document, projectName) {
7064
7204
  component: `${projectName}-api`,
7065
7205
  methods,
7066
7206
  details: typeof info.description === "string" ? info.description : `Imported OpenAPI surface of ${projectName}.`,
7067
- ...typeof info.version === "string" ? { version: info.version } : {}
7207
+ ...typeof info.version === "string" ? { version: info.version } : {},
7208
+ ...importedAuth ? { auth: importedAuth } : {}
7068
7209
  };
7069
7210
  return SurfaceSnapshotSchema.parse({
7070
7211
  projectName,
@@ -7179,6 +7320,10 @@ function projectOwnSurface(maxAudience) {
7179
7320
  component: comp.id,
7180
7321
  methods,
7181
7322
  ...comp.dispatch && comp.dispatch.length ? { dispatch: comp.dispatch } : {},
7323
+ // Project the backing Portal's auth + basePath so the codec can emit
7324
+ // OpenAPI security + per-portal servers self-contained from the snapshot.
7325
+ ...comp.auth && comp.auth.scheme !== "none" ? { auth: comp.auth } : {},
7326
+ ...comp.basePath ? { basePath: comp.basePath } : {},
7182
7327
  details: raw.details ?? "",
7183
7328
  ...raw.version ? { version: raw.version } : {},
7184
7329
  ...raw.stability ? { stability: raw.stability } : {}
@@ -7222,20 +7367,28 @@ function saveSnapshot(snapshot, rootDir = getProjectRoot()) {
7222
7367
  function loadSurfaceSnapshots() {
7223
7368
  return listSnapshots();
7224
7369
  }
7370
+ function renderOpenApiForms(snapshot) {
7371
+ const renderedSet = toOpenApiSet(snapshot);
7372
+ return { renderedSet, ...renderedSet.length === 1 ? { rendered: renderedSet[0].document } : {} };
7373
+ }
7374
+ function writeSurfaceFile(outPath, body, snapshot) {
7375
+ const resolved = path10.resolve(outPath);
7376
+ fs9.mkdirSync(path10.dirname(resolved), { recursive: true });
7377
+ if (body !== void 0) fs9.writeFileSync(resolved, body);
7378
+ else writeYamlFile(resolved, snapshot);
7379
+ return resolved;
7380
+ }
7225
7381
  function exportSurface(maxAudience, format, outPath) {
7226
7382
  const snapshot = projectOwnSurface(maxAudience);
7227
- const rendered = format === "openapi" ? toOpenApi(snapshot) : void 0;
7228
- let writtenTo;
7229
- if (outPath) {
7230
- fs9.mkdirSync(path10.dirname(path10.resolve(outPath)), { recursive: true });
7231
- if (rendered !== void 0) {
7232
- fs9.writeFileSync(path10.resolve(outPath), rendered);
7233
- } else {
7234
- writeYamlFile(path10.resolve(outPath), snapshot);
7235
- }
7236
- writtenTo = path10.resolve(outPath);
7237
- }
7238
- return { snapshot, ...rendered !== void 0 ? { rendered } : {}, ...writtenTo ? { writtenTo } : {} };
7383
+ const openapi = format === "openapi" ? renderOpenApiForms(snapshot) : void 0;
7384
+ const body = openapi?.rendered ?? openapi?.renderedSet[0]?.document;
7385
+ const writtenTo = outPath ? writeSurfaceFile(outPath, body, snapshot) : void 0;
7386
+ return {
7387
+ snapshot,
7388
+ ...openapi?.rendered !== void 0 ? { rendered: openapi.rendered } : {},
7389
+ ...openapi ? { renderedSet: openapi.renderedSet } : {},
7390
+ ...writtenTo ? { writtenTo } : {}
7391
+ };
7239
7392
  }
7240
7393
  function importSurface(sourcePath, origin) {
7241
7394
  const resolved = path10.resolve(sourcePath);
@@ -32127,7 +32280,7 @@ function getWebProjectCanvasModel(cfg, sessionId, projectId) {
32127
32280
  }
32128
32281
  return runWithProjectRoot(root, () => hostCore.buildCanvasDataModel());
32129
32282
  }
32130
- function getWebProjectOpenApi(cfg, sessionId, projectId) {
32283
+ function getWebProjectOpenApi(cfg, sessionId, projectId, portalId) {
32131
32284
  const principal = authenticateSession(cfg.dataDir, sessionId);
32132
32285
  if (!principal.authenticated) throw new UnauthenticatedError();
32133
32286
  if (!listProjects2(cfg, sessionId).some((r) => r.id === projectId)) {
@@ -32136,7 +32289,19 @@ function getWebProjectOpenApi(cfg, sessionId, projectId) {
32136
32289
  const root = resolveProjectRoot(cfg.dataDir, principal, projectId);
32137
32290
  if (!root) throw new ForbiddenError("project not authorized or unknown");
32138
32291
  const result = runWithProjectRoot(root, () => hostSurfaces.exportBoundSurface("project", "openapi"));
32139
- return swaggerUiPage(result.rendered ?? "{}", projectId);
32292
+ const specs = result.renderedSet ?? (result.rendered ? [{ portalId: "", name: projectId, document: result.rendered }] : []);
32293
+ if (specs.length === 0) return swaggerUiPage("{}", projectId);
32294
+ if (portalId) {
32295
+ const sel = specs.find((s) => s.portalId === portalId) ?? specs[0];
32296
+ return swaggerUiPage(sel.document, sel.name);
32297
+ }
32298
+ if (specs.length === 1) return swaggerUiPage(specs[0].document, specs[0].name);
32299
+ return openApiIndexPage(projectId, specs);
32300
+ }
32301
+ function openApiIndexPage(projectId, specs) {
32302
+ const esc2 = (s) => s.replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
32303
+ const items = specs.map((s) => `<li><a href="/web/openapi?projectId=${encodeURIComponent(projectId)}&spec=${encodeURIComponent(s.portalId)}">${esc2(s.name)}</a> <code>${esc2(s.portalId)}</code></li>`).join("");
32304
+ return `<!doctype html><html><head><meta charset="utf-8"><title>${esc2(projectId)} \u2014 API specs</title><style>body{font-family:system-ui,sans-serif;max-width:680px;margin:48px auto;padding:0 20px;color:#e6e6e6;background:#161616}h1{font-size:20px}a{color:#6ea8fe;text-decoration:none}a:hover{text-decoration:underline}li{margin:10px 0}code{color:#8a94a6;font-size:12px;margin-left:8px}</style></head><body><h1>${esc2(projectId)} \u2014 API specs</h1><p>This project exposes ${specs.length} separate public APIs, each with its own OpenAPI document and auth:</p><ul>${items}</ul></body></html>`;
32140
32305
  }
32141
32306
  function reshapeLandscapeGraph(model, level) {
32142
32307
  const unitNodeIds = new Set(model.nodes.filter((n) => n.nodeKind === "orgUnit").map((n) => n.id));
@@ -34498,7 +34663,8 @@ async function handleWebRequest(cfg, req, res, body, url, ctx) {
34498
34663
  }
34499
34664
  if (req.method === "GET" && parts.length === 2 && parts[1] === "openapi") {
34500
34665
  const projectId = url.searchParams.get("projectId") ?? "";
34501
- const html = getWebProjectOpenApi(cfg, sessionId, projectId);
34666
+ const spec = url.searchParams.get("spec") ?? void 0;
34667
+ const html = getWebProjectOpenApi(cfg, sessionId, projectId, spec);
34502
34668
  res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
34503
34669
  res.end(html);
34504
34670
  return;