@wairon/cli 5.0.2-dev.1 → 5.0.2-dev.10

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.1";
68
+ WAIRON_VERSION = "5.0.2-dev.10";
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, 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";
@@ -907,6 +907,28 @@ var init_specs = __esm({
907
907
  event: import_zod6.z.string().optional(),
908
908
  description: import_zod6.z.string().optional()
909
909
  });
910
+ ExternalLinkTypeSchema = import_zod6.z.enum(["implementation", "informative"]);
911
+ ExternalLinkSchema = import_zod6.z.object({
912
+ url: import_zod6.z.string(),
913
+ /** Defaults to 'informative' so an untyped link never silently satisfies the source requirement. */
914
+ type: ExternalLinkTypeSchema.default("informative"),
915
+ label: import_zod6.z.string().optional()
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
+ });
910
932
  ComponentSpecSchema = import_zod6.z.object({
911
933
  id: SpecIdSchema,
912
934
  name: import_zod6.z.string(),
@@ -920,6 +942,10 @@ var init_specs = __esm({
920
942
  dependsOn: import_zod6.z.array(import_zod6.z.string()).default([]),
921
943
  portalType: PortalTypeSchema.optional(),
922
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(),
923
949
  /** Portal-only: capability → component.method dispatch table (see DispatchBindingSchema). */
924
950
  dispatch: import_zod6.z.array(DispatchBindingSchema).optional(),
925
951
  /** Store-only: whether held state survives restart (see DurabilitySchema). */
@@ -932,6 +958,11 @@ var init_specs = __esm({
932
958
  patterns: import_zod6.z.array(PatternRefSchema).optional(),
933
959
  /** Optional component variant — a declared, base-anchored specialization of this component's stereotype (resolved against the variant registry; UNKNOWN_VARIANT / VARIANT_BASE_MISMATCH). */
934
960
  variant: import_zod6.z.string().optional(),
961
+ /** Opaque external references (see ExternalLinkSchema) — documented URLs wairon does
962
+ * not fetch or validate. An `implementation` link is the external source-of-record and
963
+ * satisfies the source requirement for a source-less implementation (suppresses
964
+ * MISSING_SOURCE_PATH); `informative` links are context only. */
965
+ externalLinks: import_zod6.z.array(ExternalLinkSchema).optional(),
935
966
  /** Per-spec lint suppressions (see LintConfigSchema). */
936
967
  lint: LintConfigSchema.optional(),
937
968
  /** Opaque pack/tool extension data (see ExtDataSchema) — preserved verbatim. */
@@ -1064,6 +1095,18 @@ var init_specs = __esm({
1064
1095
  // Required if type is 'call', references Method name on target interface
1065
1096
  capability: import_zod6.z.string().optional(),
1066
1097
  // Required if type is 'dispatch': the capability routed through the target Portal's dispatch table
1098
+ /**
1099
+ * call/dispatch only: the credential this step presents to an authed callee
1100
+ * Portal, and WHERE it is loaded from (`from`). Two forms: an OPAQUE source
1101
+ * (`env:API_KEY`, a config key, `vault:path`, a free note) — a design note
1102
+ * wairon never resolves; or a MODELED reference `component:<id>` pointing at
1103
+ * the Adapter/Store that provides the secret — validated to resolve, be an
1104
+ * Adapter/Store, and be wired to the presenter (a checked graph edge). The
1105
+ * actual secret is never stored here. Absence on a call into a Portal whose
1106
+ * `auth ≠ none` warns (PORTAL_AUTH_UNMET), so credential loading is never
1107
+ * overlooked.
1108
+ */
1109
+ auth: import_zod6.z.object({ from: import_zod6.z.string(), note: import_zod6.z.string().optional() }).optional(),
1067
1110
  assertsGuarantees: import_zod6.z.array(GuaranteeSchema).optional(),
1068
1111
  /**
1069
1112
  * Declared entity invariants this step upholds, as "<type-id>.<invariant-id>"
@@ -1280,7 +1323,12 @@ var init_specs = __esm({
1280
1323
  dispatch: import_zod6.z.array(DispatchBindingSchema).optional(),
1281
1324
  details: import_zod6.z.string().default(""),
1282
1325
  version: import_zod6.z.string().optional(),
1283
- stability: import_zod6.z.string().optional()
1326
+ stability: import_zod6.z.string().optional(),
1327
+ /** Projected copy of the backing Portal's auth (see PortalAuthSchema) — the codec
1328
+ * emits it as OpenAPI securitySchemes/security. */
1329
+ auth: PortalAuthSchema.optional(),
1330
+ /** The backing Portal's basePath — becomes the per-portal OpenAPI `servers` url. */
1331
+ basePath: import_zod6.z.string().optional()
1284
1332
  });
1285
1333
  SurfaceSnapshotSchema = import_zod6.z.object({
1286
1334
  /** Producing project/system name — the snapshot's resolution identity. */
@@ -1295,6 +1343,11 @@ var init_specs = __esm({
1295
1343
  /** Transitive type closure of every exported signature — self-contained. */
1296
1344
  types: import_zod6.z.array(SurfaceTypeDefSchema).default([])
1297
1345
  });
1346
+ NamedOpenApiSpecSchema = import_zod6.z.object({
1347
+ portalId: import_zod6.z.string(),
1348
+ name: import_zod6.z.string(),
1349
+ document: import_zod6.z.string()
1350
+ });
1298
1351
  GroupSpecSchema = import_zod6.z.object({
1299
1352
  kind: import_zod6.z.literal("group"),
1300
1353
  id: SpecIdSchema,
@@ -2652,6 +2705,7 @@ function buildCanvasModel(issues = []) {
2652
2705
  ...ownerOf.has(comp.id) ? { owner: ownerOf.get(comp.id) } : {},
2653
2706
  owns: comp.owns.filter((o) => componentIds.has(o)),
2654
2707
  dependsOn: comp.dependsOn,
2708
+ ...comp.externalLinks && comp.externalLinks.length ? { externalLinks: comp.externalLinks } : {},
2655
2709
  interfaces: compInterfaces.map((i) => ({
2656
2710
  id: i.id,
2657
2711
  name: i.name,
@@ -3304,12 +3358,21 @@ var MODEL = __MODEL_JSON__;
3304
3358
  var configuredLineStyle = ['bezier', 'straight', 'taxi'].indexOf(diagramConfig.lineStyle) >= 0 ? diagramConfig.lineStyle : 'bezier';
3305
3359
  var defaultViewKind = diagramConfig.defaultView === 'types' ? 'types' : (diagramConfig.defaultView === 'databases' && showDatabaseTab ? 'databases' : 'system');
3306
3360
 
3361
+ // Stage J: a deep link (opts.initialRoute) seeds the initial view directly so a
3362
+ // refresh / shared URL renders the right scope with NO root-first flash. Parsed
3363
+ // by the same resolver as openRoute (hoisted below). No onViewChange fires for
3364
+ // this initial seed.
3365
+ var initialView = { kind: defaultViewKind, id: null };
3366
+ if (typeof opts !== 'undefined' && opts && typeof opts.initialRoute === 'string' && opts.initialRoute.length) {
3367
+ initialView = resolveRoute(opts.initialRoute);
3368
+ }
3369
+
3307
3370
  var state = {
3308
- view: { kind: defaultViewKind, id: null },
3309
- internals: false,
3310
- externals: true,
3311
- dataCoupling: false,
3312
- showIssues: false,
3371
+ view: initialView,
3372
+ internals: typeof saved.internals === 'boolean' ? saved.internals : false,
3373
+ externals: typeof saved.externals === 'boolean' ? saved.externals : true,
3374
+ dataCoupling: typeof saved.dataCoupling === 'boolean' ? saved.dataCoupling : false,
3375
+ showIssues: typeof saved.showIssues === 'boolean' ? saved.showIssues : false,
3313
3376
  query: '',
3314
3377
  selected: null,
3315
3378
  selectedKind: null,
@@ -3324,6 +3387,9 @@ var MODEL = __MODEL_JSON__;
3324
3387
  // Set by buildTypeElements when the ERD is degraded for performance (huge
3325
3388
  // scopes); consumed by renderTypesNotice to explain the level-of-detail.
3326
3389
  var typesNotice = '';
3390
+ // Stage J: true while openRoute is applying a URL-driven view change, so the
3391
+ // onViewChange callback is suppressed and we do not loop URL -> engine -> URL.
3392
+ var applyingRoute = false;
3327
3393
 
3328
3394
  function viewKey() {
3329
3395
  // 'types2' + detail level: table sizes differ per detail, and the prefix
@@ -3343,6 +3409,10 @@ var MODEL = __MODEL_JSON__;
3343
3409
  lineStyle: state.lineStyle,
3344
3410
  panelOpen: state.panelOpen,
3345
3411
  panelWidth: state.panelWidth,
3412
+ internals: state.internals,
3413
+ externals: state.externals,
3414
+ dataCoupling: state.dataCoupling,
3415
+ showIssues: state.showIssues,
3346
3416
  }));
3347
3417
  } catch (e) { /* non-fatal */ }
3348
3418
  }
@@ -4749,6 +4819,78 @@ var MODEL = __MODEL_JSON__;
4749
4819
  state.typesRenderAll = false; // a fresh scope re-evaluates the LOD budget
4750
4820
  rebuild(true);
4751
4821
  renderPanel();
4822
+ notifyViewChange();
4823
+ }
4824
+
4825
+ // ---- Stage J: URL <-> view routing ----------------------------------------
4826
+ // The canvas navigation lives in the URL path (refresh-safe + shareable). A
4827
+ // route is the string AFTER /canvas/<project>: '' = system root; a '/'-joined
4828
+ // NAMESPACE (e.g. 'a/b') that resolves against the model to a subsystem or a
4829
+ // component; or the 'types'/'databases' view modes with an optional scope.
4830
+ // A segment is the '::' namespace with '/' as separator, each part encoded.
4831
+ function encSeg(s) { return encodeURIComponent(String(s)); }
4832
+ function routeOf(view) {
4833
+ if (!view) return '';
4834
+ var k = view.kind;
4835
+ if (k === 'system') return '';
4836
+ if (k === 'types' || k === 'databases') {
4837
+ return view.id ? k + '/' + view.id.split('::').map(encSeg).join('/') : k;
4838
+ }
4839
+ if (k === 'subsystem') {
4840
+ return view.id ? view.id.split('::').map(encSeg).join('/') : '';
4841
+ }
4842
+ if (k === 'component') {
4843
+ // A component's full namespace path IS its id: a chained subproject carries
4844
+ // the subsystem prefix in the id (a::b::comp), a flat project uses the bare
4845
+ // id (comp). Serializing comp.id split on '::' round-trips exactly via the
4846
+ // compById lookup in resolveRoute. Owner-pattern nesting is deliberately NOT
4847
+ // encoded (ownership is a separate axis; comp.id does not embed the owner).
4848
+ var c = compById[view.id];
4849
+ var full = c ? c.id : view.id;
4850
+ return full.split('::').map(encSeg).join('/');
4851
+ }
4852
+ return '';
4853
+ }
4854
+ function resolveRoute(routeStr) {
4855
+ var parts = String(routeStr || '').split('/').filter(function (s) { return s.length > 0; }).map(decodeURIComponent);
4856
+ if (!parts.length) return { kind: 'system', id: null };
4857
+ // NOTE: a subsystem literally named 'types'/'databases' is SHADOWED by these
4858
+ // view-mode routes (acceptable \u2014 the modes own those first segments).
4859
+ if (parts[0] === 'types') {
4860
+ return { kind: 'types', id: parts.length > 1 ? parts.slice(1).join('::') : null };
4861
+ }
4862
+ if (parts[0] === 'databases') {
4863
+ if (!showDatabaseTab) return { kind: 'system', id: null };
4864
+ return { kind: 'databases', id: parts.length > 1 ? parts.slice(1).join('::') : null };
4865
+ }
4866
+ var joined = parts.join('::');
4867
+ if (subById[joined]) return { kind: 'subsystem', id: joined };
4868
+ if (compById[joined]) return { kind: 'component', id: joined };
4869
+ return { kind: 'system', id: null }; // unknown id -> fall back to the root
4870
+ }
4871
+ // Apply a route (URL -> engine) WITHOUT echoing back through onViewChange.
4872
+ function openRoute(routeStr) {
4873
+ var v = resolveRoute(routeStr);
4874
+ if (state.view.kind === v.kind && state.view.id === v.id) return;
4875
+ applyingRoute = true;
4876
+ try {
4877
+ state.view = { kind: v.kind, id: v.id };
4878
+ state.selected = null;
4879
+ state.selectedKind = null;
4880
+ state.typesRenderAll = false;
4881
+ rebuild(true);
4882
+ renderPanel();
4883
+ } finally {
4884
+ applyingRoute = false;
4885
+ }
4886
+ }
4887
+ // Notify the embedder (engine -> URL) of the current view; suppressed while a
4888
+ // route is being applied so the URL is not driven in a loop.
4889
+ function notifyViewChange() {
4890
+ if (applyingRoute) return;
4891
+ if (typeof opts !== 'undefined' && opts && typeof opts.onViewChange === 'function') {
4892
+ try { opts.onViewChange(routeOf(state.view)); } catch (e) { /* ignore */ }
4893
+ }
4752
4894
  }
4753
4895
  // Explain (and offer to override) a performance-degraded ERD.
4754
4896
  function renderTypesNotice() {
@@ -4899,6 +5041,7 @@ var MODEL = __MODEL_JSON__;
4899
5041
  if (t.ghost) {
4900
5042
  state.view = parentViewOf(t.kind, t.id);
4901
5043
  rebuild(true);
5044
+ notifyViewChange();
4902
5045
  select(t.kind, t.id, true);
4903
5046
  return;
4904
5047
  }
@@ -4920,10 +5063,16 @@ var MODEL = __MODEL_JSON__;
4920
5063
  }
4921
5064
 
4922
5065
  document.getElementById('search').addEventListener('input', function (ev) { state.query = ev.target.value.trim(); rebuild(false); });
4923
- document.getElementById('internalsToggle').addEventListener('change', function (ev) { state.internals = ev.target.checked; rebuild(true); });
4924
- document.getElementById('externalsToggle').addEventListener('change', function (ev) { state.externals = ev.target.checked; rebuild(true); });
4925
- document.getElementById('dataCouplingToggle').addEventListener('change', function (ev) { state.dataCoupling = ev.target.checked; renderLegend(); rebuild(true); });
4926
- document.getElementById('issuesToggle').addEventListener('change', function (ev) { state.showIssues = ev.target.checked; rebuild(false); renderPanel(); });
5066
+ // Sync each View toggle's checkbox from the (possibly persisted) state, then
5067
+ // persist on change so the choices survive a refresh (see persist()/saved).
5068
+ document.getElementById('internalsToggle').checked = state.internals;
5069
+ document.getElementById('internalsToggle').addEventListener('change', function (ev) { state.internals = ev.target.checked; persist(); rebuild(true); });
5070
+ document.getElementById('externalsToggle').checked = state.externals;
5071
+ document.getElementById('externalsToggle').addEventListener('change', function (ev) { state.externals = ev.target.checked; persist(); rebuild(true); });
5072
+ document.getElementById('dataCouplingToggle').checked = state.dataCoupling;
5073
+ document.getElementById('dataCouplingToggle').addEventListener('change', function (ev) { state.dataCoupling = ev.target.checked; persist(); renderLegend(); rebuild(true); });
5074
+ document.getElementById('issuesToggle').checked = state.showIssues;
5075
+ document.getElementById('issuesToggle').addEventListener('change', function (ev) { state.showIssues = ev.target.checked; persist(); rebuild(false); renderPanel(); });
4927
5076
  document.getElementById('dragToggle').addEventListener('change', function (ev) { cy.autolock(!ev.target.checked); });
4928
5077
  // Mode seg: Components \u21C4 Types. Entering Types keeps the current subsystem
4929
5078
  // scope, so a subsystem's own types (plus shared ones) show scoped.
@@ -5913,10 +6062,12 @@ var MODEL = __MODEL_JSON__;
5913
6062
  if (kind === 'type' && state.view.kind !== 'types' && state.view.kind !== 'databases') {
5914
6063
  state.view = { kind: 'types', id: typesScopeFromView() };
5915
6064
  rebuild(true);
6065
+ notifyViewChange();
5916
6066
  } else if (kind === 'component' && (state.view.kind === 'types' || state.view.kind === 'databases')) {
5917
6067
  var c = compById[id];
5918
6068
  state.view = { kind: 'subsystem', id: c ? c.subsystem : null };
5919
6069
  rebuild(true);
6070
+ notifyViewChange();
5920
6071
  }
5921
6072
  state.selectedKind = kind;
5922
6073
  state.selected = id;
@@ -5975,6 +6126,14 @@ var MODEL = __MODEL_JSON__;
5975
6126
  if (!exposes || !openApiAllowed()) return '';
5976
6127
  return '<div class="openbtn"><button class="tbtn" data-openapi-tag="' + esc(tag || '') + '">\\u25A4 View OpenAPI \\u2197</button></div>';
5977
6128
  }
6129
+ // "Open in Specs" \u2014 deep-links the focused spec into the hosted Specs value
6130
+ // editor via a host hook (opts.onOpenSpec). Hidden when no host provides it
6131
+ // (standalone file, shared page): those have no editor to open. kind is the
6132
+ // spec layer, id its qualified spec id.
6133
+ function openSpecButton(kind, id) {
6134
+ if (typeof opts === 'undefined' || !opts || typeof opts.onOpenSpec !== 'function') return '';
6135
+ return '<div class="openbtn"><button class="tbtn" data-openspec-kind="' + kind + '" data-openspec-id="' + esc(id) + '">\\u270E Open in Specs \\u2197</button></div>';
6136
+ }
5978
6137
 
5979
6138
  function renderPanel() {
5980
6139
  var head = '', body = '';
@@ -5997,7 +6156,8 @@ var MODEL = __MODEL_JSON__;
5997
6156
  + (scopeFocus ? staticChip('current view') : '')
5998
6157
  + chip(c.subsystem, 'subsystem', c.subsystem)
5999
6158
  + (scopeFocus ? '' : openViewButton('component', c.id, c.owns.length > 0))
6000
- + openApiButton(componentExposesApi(c), c.apiTag);
6159
+ + openApiButton(componentExposesApi(c), c.id)
6160
+ + openSpecButton('component', c.id);
6001
6161
 
6002
6162
  var linkedTypes = MODEL.types.filter(function (t) { return t.componentClass === c.id; });
6003
6163
  if (linkedTypes.length) {
@@ -6007,6 +6167,14 @@ var MODEL = __MODEL_JSON__;
6007
6167
  }
6008
6168
  body += '<p class="desc">' + esc(c.description) + '</p>';
6009
6169
 
6170
+ if (c.externalLinks && c.externalLinks.length) {
6171
+ body += section('External links', c.externalLinks.length, c.externalLinks.map(function (l) {
6172
+ var label = l.label || l.url;
6173
+ var tag = l.type === 'implementation' ? staticChip('source') : '';
6174
+ return '<div class="method">' + tag + '<a href="' + esc(l.url) + '" target="_blank" rel="noopener" style="color:var(--accent);word-break:break-all">' + esc(label) + ' \\u2197</a></div>';
6175
+ }).join(''), true);
6176
+ }
6177
+
6010
6178
  var depInner = (c.dependsOn.length ? c.dependsOn.map(function (d) { return chip(d, 'component', d); }).join('') : '<span class="desc">none</span>')
6011
6179
  + (c.owns.length ? '<div style="margin-top:8px"><b style="font-size:11px">Owns:</b><br>' + c.owns.map(function (d) { return chip(d, 'component', d); }).join('') + '</div>' : '');
6012
6180
  body += section('Dependencies', c.dependsOn.length + c.owns.length, depInner, true);
@@ -6081,7 +6249,8 @@ var MODEL = __MODEL_JSON__;
6081
6249
  MODEL.types.forEach(function (t2) { if (t2.id === focusId) ty = t2; });
6082
6250
  if (ty) {
6083
6251
  head = '<h2>' + esc(ty.name) + '</h2>' + staticChip('\\u00AB' + ty.kind + '\\u00BB')
6084
- + (ty.subsystem ? chip(ty.subsystem, 'subsystem', ty.subsystem) : staticChip('system-level shared'));
6252
+ + (ty.subsystem ? chip(ty.subsystem, 'subsystem', ty.subsystem) : staticChip('system-level shared'))
6253
+ + openSpecButton('type', ty.id);
6085
6254
 
6086
6255
  if (ty.componentClass) {
6087
6256
  head += '<div style="margin-top:6px"><b style="font-size:11px">Class Component:</b> ' + chip(ty.componentClass, 'component', ty.componentClass) + '</div>';
@@ -6157,7 +6326,8 @@ var MODEL = __MODEL_JSON__;
6157
6326
  + (s.status ? staticChip(s.status) : '')
6158
6327
  + (scopeFocus ? staticChip('current view') : '')
6159
6328
  + (scopeFocus ? '' : openViewButton('subsystem', s.id, subKids > 0))
6160
- + openApiButton(subsystemExposesApi(s.id), '');
6329
+ + openApiButton(subsystemExposesApi(s.id), '')
6330
+ + openSpecButton('subsystem', s.id);
6161
6331
  body += '<p class="desc">' + esc(s.description) + '</p>';
6162
6332
  if (s.trustedLinks.length) {
6163
6333
  body += section('Trusted links (fast lanes)', s.trustedLinks.length, s.trustedLinks.map(function (t2) {
@@ -6215,6 +6385,16 @@ var MODEL = __MODEL_JSON__;
6215
6385
  });
6216
6386
  })(oapis[oi]);
6217
6387
  }
6388
+ var ospecs = panel.querySelectorAll('[data-openspec-kind]');
6389
+ for (var si = 0; si < ospecs.length; si++) {
6390
+ (function (b) {
6391
+ b.addEventListener('click', function () {
6392
+ if (typeof opts !== 'undefined' && opts && typeof opts.onOpenSpec === 'function') {
6393
+ opts.onOpenSpec(b.getAttribute('data-openspec-kind'), b.getAttribute('data-openspec-id') || '');
6394
+ }
6395
+ });
6396
+ })(ospecs[si]);
6397
+ }
6218
6398
  var flows = panel.querySelectorAll('[data-flow-comp]');
6219
6399
  for (var j = 0; j < flows.length; j++) {
6220
6400
  (function (b) {
@@ -6227,6 +6407,23 @@ var MODEL = __MODEL_JSON__;
6227
6407
  }
6228
6408
 
6229
6409
  renderPanel();
6410
+ // Stage G: a deep link may focus a component and/or open a method's narrative
6411
+ // modal once the seeded view + DOM + cy graph exist. The host parses the URL hash
6412
+ // into opts.initialSelect / opts.initialFlow \u2014 both carry the component id, so this
6413
+ // works whether the seeded view is the component itself or its parent subsystem
6414
+ // (a leaf component has no meaningful "inside", so Specs deep-links open the parent
6415
+ // and focus the component here).
6416
+ (function () {
6417
+ if (typeof opts === 'undefined' || !opts) return;
6418
+ var f = opts.initialFlow, s = opts.initialSelect;
6419
+ var focusComp = (f && f.comp) || (s && s.comp);
6420
+ if (focusComp && compById[focusComp]) {
6421
+ try { select('component', focusComp, true); } catch (e) { /* ignore */ }
6422
+ }
6423
+ if (f && f.comp && f.method && compById[f.comp]) {
6424
+ try { openFlow(f.comp, f.method, f.mode === 'steps' ? 'steps' : 'flow'); } catch (e) { /* ignore */ }
6425
+ }
6426
+ })();
6230
6427
  })();
6231
6428
  </script>
6232
6429
  </body>
@@ -6764,11 +6961,63 @@ function operationFor(method2, closureIds) {
6764
6961
  }
6765
6962
  return op;
6766
6963
  }
6767
- function toOpenApi(snapshot) {
6768
- const closureIds = new Set(snapshot.types.map((t) => t.id));
6769
- const httpEntries = snapshot.interfaces.filter((e) => e.type === "REST" || e.methods.some((m) => m.endpoint?.transport === "HTTP"));
6964
+ function securitySchemeObject(auth) {
6965
+ if (auth.scheme === "none") return null;
6966
+ const desc = auth.description ? { description: auth.description } : {};
6967
+ switch (auth.scheme) {
6968
+ case "apiKey":
6969
+ return { type: "apiKey", in: auth.in ?? "header", name: auth.name ?? "X-API-Key", ...desc };
6970
+ case "bearer":
6971
+ return { type: "http", scheme: "bearer", ...auth.bearerFormat ? { bearerFormat: auth.bearerFormat } : {}, ...desc };
6972
+ case "basic":
6973
+ return { type: "http", scheme: "basic", ...desc };
6974
+ case "oauth2": {
6975
+ const flow = { scopes: Object.fromEntries((auth.scopes ?? []).map((s) => [s.name, s.description])) };
6976
+ if (auth.authorizationUrl) flow.authorizationUrl = auth.authorizationUrl;
6977
+ if (auth.tokenUrl) flow.tokenUrl = auth.tokenUrl;
6978
+ if (auth.refreshUrl) flow.refreshUrl = auth.refreshUrl;
6979
+ return { type: "oauth2", flows: { [auth.flow ?? "authorizationCode"]: flow }, ...desc };
6980
+ }
6981
+ case "openIdConnect":
6982
+ return { type: "openIdConnect", openIdConnectUrl: auth.openIdConnectUrl ?? "", ...desc };
6983
+ case "custom":
6984
+ return { type: "apiKey", in: auth.in ?? "header", name: auth.name ?? "Authorization", description: auth.description ?? auth.example ?? "Custom authentication scheme." };
6985
+ default:
6986
+ return null;
6987
+ }
6988
+ }
6989
+ function schemeBaseName(scheme) {
6990
+ return { apiKey: "ApiKeyAuth", bearer: "BearerAuth", basic: "BasicAuth", oauth2: "OAuth2", openIdConnect: "OpenIdConnect", custom: "CustomAuth" }[scheme] ?? "Auth";
6991
+ }
6992
+ function buildSecurity(entries) {
6993
+ const schemes = {};
6994
+ const nameByContent = /* @__PURE__ */ new Map();
6995
+ const securityByEntry = /* @__PURE__ */ new Map();
6996
+ for (const entry of entries) {
6997
+ if (!entry.auth) continue;
6998
+ const obj = securitySchemeObject(entry.auth);
6999
+ if (!obj) continue;
7000
+ const content = JSON.stringify(obj);
7001
+ let name = nameByContent.get(content);
7002
+ if (!name) {
7003
+ name = schemeBaseName(entry.auth.scheme);
7004
+ for (let n = 2; schemes[name]; n++) name = schemeBaseName(entry.auth.scheme) + n;
7005
+ schemes[name] = obj;
7006
+ nameByContent.set(content, name);
7007
+ }
7008
+ const scopeNames = entry.auth.scheme === "oauth2" ? (entry.auth.scopes ?? []).map((s) => s.name) : [];
7009
+ securityByEntry.set(entry.id, { [name]: scopeNames });
7010
+ }
7011
+ return { schemes, securityByEntry };
7012
+ }
7013
+ function httpEntriesOf(snapshot) {
7014
+ return snapshot.interfaces.filter((e) => e.type === "REST" || e.methods.some((m) => m.endpoint?.transport === "HTTP"));
7015
+ }
7016
+ function renderDoc(snapshot, entries, closureIds, opts = {}) {
7017
+ const { schemes, securityByEntry } = buildSecurity(entries);
6770
7018
  const paths = {};
6771
- for (const entry of httpEntries) {
7019
+ for (const entry of entries) {
7020
+ const security = securityByEntry.get(entry.id);
6772
7021
  for (const method2 of entry.methods) {
6773
7022
  const endpoint = method2.endpoint;
6774
7023
  if (!endpoint || endpoint.transport !== "HTTP") continue;
@@ -6776,7 +7025,8 @@ function toOpenApi(snapshot) {
6776
7025
  paths[p] = paths[p] ?? {};
6777
7026
  paths[p][endpoint.method.toLowerCase()] = {
6778
7027
  tags: [entry.id],
6779
- ...operationFor(method2, closureIds)
7028
+ ...operationFor(method2, closureIds),
7029
+ ...security ? { security: [security] } : {}
6780
7030
  };
6781
7031
  }
6782
7032
  }
@@ -6789,19 +7039,41 @@ function toOpenApi(snapshot) {
6789
7039
  required: t.fields.filter((f) => !f.optional).map((f) => f.name)
6790
7040
  };
6791
7041
  }
6792
- const doc = {
7042
+ const components = {};
7043
+ if (Object.keys(schemas).length) components.schemas = schemas;
7044
+ if (Object.keys(schemes).length) components.securitySchemes = schemes;
7045
+ const basePaths = [...new Set(entries.map((e) => e.basePath).filter((b) => !!b))];
7046
+ const servers = opts.servers && basePaths.length === 1 ? [{ url: basePaths[0] }] : void 0;
7047
+ return {
6793
7048
  openapi: "3.1.0",
6794
7049
  info: {
6795
- title: snapshot.projectName,
7050
+ title: opts.title ?? snapshot.projectName,
6796
7051
  version: snapshot.version ?? "0.0.0",
6797
7052
  ...snapshot.stateId ? { "x-wairon-state-id": snapshot.stateId } : {},
6798
7053
  "x-wairon-origin": snapshot.origin,
6799
7054
  "x-wairon-generated-at": snapshot.generatedAt
6800
7055
  },
7056
+ ...servers ? { servers } : {},
6801
7057
  paths,
6802
- ...Object.keys(schemas).length ? { components: { schemas } } : {}
7058
+ ...Object.keys(components).length ? { components } : {}
6803
7059
  };
6804
- return JSON.stringify(doc, null, 2);
7060
+ }
7061
+ function toOpenApiSet(snapshot) {
7062
+ const closureIds = new Set(snapshot.types.map((t) => t.id));
7063
+ const byPortal = /* @__PURE__ */ new Map();
7064
+ const order = [];
7065
+ for (const entry of httpEntriesOf(snapshot)) {
7066
+ if (!byPortal.has(entry.component)) {
7067
+ byPortal.set(entry.component, []);
7068
+ order.push(entry.component);
7069
+ }
7070
+ byPortal.get(entry.component).push(entry);
7071
+ }
7072
+ return order.map((portalId) => {
7073
+ const entries = byPortal.get(portalId);
7074
+ const name = entries[0]?.name ?? portalId;
7075
+ return { portalId, name, document: JSON.stringify(renderDoc(snapshot, entries, closureIds, { title: name, servers: true }), null, 2) };
7076
+ });
6805
7077
  }
6806
7078
  function isOpenApiDocument(body) {
6807
7079
  try {
@@ -6823,6 +7095,39 @@ function typeRefFromSchema(schema) {
6823
7095
  if (typeof t === "string" && t !== "object") return t;
6824
7096
  return "json";
6825
7097
  }
7098
+ function authFromSecurityScheme(scheme) {
7099
+ const desc = typeof scheme.description === "string" ? { description: scheme.description } : {};
7100
+ if (scheme.type === "apiKey") {
7101
+ return {
7102
+ scheme: "apiKey",
7103
+ ...scheme.in === "header" || scheme.in === "query" || scheme.in === "cookie" ? { in: scheme.in } : {},
7104
+ ...typeof scheme.name === "string" ? { name: scheme.name } : {},
7105
+ ...desc
7106
+ };
7107
+ }
7108
+ if (scheme.type === "http") {
7109
+ if (scheme.scheme === "bearer") return { scheme: "bearer", ...typeof scheme.bearerFormat === "string" ? { bearerFormat: scheme.bearerFormat } : {}, ...desc };
7110
+ if (scheme.scheme === "basic") return { scheme: "basic", ...desc };
7111
+ }
7112
+ if (scheme.type === "oauth2") {
7113
+ const flows = scheme.flows ?? {};
7114
+ const flowKey = Object.keys(flows)[0];
7115
+ const f = flows[flowKey] ?? {};
7116
+ return {
7117
+ scheme: "oauth2",
7118
+ ...flowKey === "authorizationCode" || flowKey === "clientCredentials" || flowKey === "implicit" || flowKey === "password" ? { flow: flowKey } : {},
7119
+ ...typeof f.authorizationUrl === "string" ? { authorizationUrl: f.authorizationUrl } : {},
7120
+ ...typeof f.tokenUrl === "string" ? { tokenUrl: f.tokenUrl } : {},
7121
+ ...typeof f.refreshUrl === "string" ? { refreshUrl: f.refreshUrl } : {},
7122
+ scopes: Object.entries(f.scopes ?? {}).map(([name, description]) => ({ name, description })),
7123
+ ...desc
7124
+ };
7125
+ }
7126
+ if (scheme.type === "openIdConnect") {
7127
+ return { scheme: "openIdConnect", openIdConnectUrl: typeof scheme.openIdConnectUrl === "string" ? scheme.openIdConnectUrl : "", ...desc };
7128
+ }
7129
+ return void 0;
7130
+ }
6826
7131
  function fromOpenApi(document, projectName) {
6827
7132
  let parsed;
6828
7133
  try {
@@ -6900,6 +7205,9 @@ function fromOpenApi(document, projectName) {
6900
7205
  }))
6901
7206
  });
6902
7207
  }
7208
+ const securitySchemes = parsed.components?.securitySchemes ?? {};
7209
+ const firstScheme = Object.values(securitySchemes)[0];
7210
+ const importedAuth = firstScheme ? authFromSecurityScheme(firstScheme) : void 0;
6903
7211
  const entry = {
6904
7212
  id: `${projectName}-api`,
6905
7213
  name: typeof info.title === "string" ? info.title : projectName,
@@ -6908,7 +7216,8 @@ function fromOpenApi(document, projectName) {
6908
7216
  component: `${projectName}-api`,
6909
7217
  methods,
6910
7218
  details: typeof info.description === "string" ? info.description : `Imported OpenAPI surface of ${projectName}.`,
6911
- ...typeof info.version === "string" ? { version: info.version } : {}
7219
+ ...typeof info.version === "string" ? { version: info.version } : {},
7220
+ ...importedAuth ? { auth: importedAuth } : {}
6912
7221
  };
6913
7222
  return SurfaceSnapshotSchema.parse({
6914
7223
  projectName,
@@ -7023,6 +7332,10 @@ function projectOwnSurface(maxAudience) {
7023
7332
  component: comp.id,
7024
7333
  methods,
7025
7334
  ...comp.dispatch && comp.dispatch.length ? { dispatch: comp.dispatch } : {},
7335
+ // Project the backing Portal's auth + basePath so the codec can emit
7336
+ // OpenAPI security + per-portal servers self-contained from the snapshot.
7337
+ ...comp.auth && comp.auth.scheme !== "none" ? { auth: comp.auth } : {},
7338
+ ...comp.basePath ? { basePath: comp.basePath } : {},
7026
7339
  details: raw.details ?? "",
7027
7340
  ...raw.version ? { version: raw.version } : {},
7028
7341
  ...raw.stability ? { stability: raw.stability } : {}
@@ -7066,20 +7379,28 @@ function saveSnapshot(snapshot, rootDir = getProjectRoot()) {
7066
7379
  function loadSurfaceSnapshots() {
7067
7380
  return listSnapshots();
7068
7381
  }
7382
+ function renderOpenApiForms(snapshot) {
7383
+ const renderedSet = toOpenApiSet(snapshot);
7384
+ return { renderedSet, ...renderedSet.length === 1 ? { rendered: renderedSet[0].document } : {} };
7385
+ }
7386
+ function writeSurfaceFile(outPath, body, snapshot) {
7387
+ const resolved = path10.resolve(outPath);
7388
+ fs9.mkdirSync(path10.dirname(resolved), { recursive: true });
7389
+ if (body !== void 0) fs9.writeFileSync(resolved, body);
7390
+ else writeYamlFile(resolved, snapshot);
7391
+ return resolved;
7392
+ }
7069
7393
  function exportSurface(maxAudience, format, outPath) {
7070
7394
  const snapshot = projectOwnSurface(maxAudience);
7071
- const rendered = format === "openapi" ? toOpenApi(snapshot) : void 0;
7072
- let writtenTo;
7073
- if (outPath) {
7074
- fs9.mkdirSync(path10.dirname(path10.resolve(outPath)), { recursive: true });
7075
- if (rendered !== void 0) {
7076
- fs9.writeFileSync(path10.resolve(outPath), rendered);
7077
- } else {
7078
- writeYamlFile(path10.resolve(outPath), snapshot);
7079
- }
7080
- writtenTo = path10.resolve(outPath);
7081
- }
7082
- return { snapshot, ...rendered !== void 0 ? { rendered } : {}, ...writtenTo ? { writtenTo } : {} };
7395
+ const openapi = format === "openapi" ? renderOpenApiForms(snapshot) : void 0;
7396
+ const body = openapi?.rendered ?? openapi?.renderedSet[0]?.document;
7397
+ const writtenTo = outPath ? writeSurfaceFile(outPath, body, snapshot) : void 0;
7398
+ return {
7399
+ snapshot,
7400
+ ...openapi?.rendered !== void 0 ? { rendered: openapi.rendered } : {},
7401
+ ...openapi ? { renderedSet: openapi.renderedSet } : {},
7402
+ ...writtenTo ? { writtenTo } : {}
7403
+ };
7083
7404
  }
7084
7405
  function importSurface(sourcePath, origin) {
7085
7406
  const resolved = path10.resolve(sourcePath);
@@ -8339,13 +8660,16 @@ var init_conformance = __esm({
8339
8660
  if (isInChainedSubproject(component.subsystem, ctx)) continue;
8340
8661
  const draft = ctx.isImplementationDraft(impl);
8341
8662
  if (!impl.sourcePath) {
8342
- ctx.addIssue(
8343
- "warning",
8344
- "MISSING_SOURCE_PATH",
8345
- `Implementation "${impl.id}" declares no sourcePath \u2014 its contract "${impl.contract}" cannot be structurally checked against code.`,
8346
- impl.id,
8347
- draft
8348
- );
8663
+ const hasExternalSource = (component.externalLinks ?? []).some((l) => l.type === "implementation");
8664
+ if (!hasExternalSource) {
8665
+ ctx.addIssue(
8666
+ "warning",
8667
+ "MISSING_SOURCE_PATH",
8668
+ `Implementation "${impl.id}" declares no sourcePath \u2014 its contract "${impl.contract}" cannot be structurally checked against code.`,
8669
+ impl.id,
8670
+ draft
8671
+ );
8672
+ }
8349
8673
  continue;
8350
8674
  }
8351
8675
  const lookup = lookups.get(normalizeSourcePath(impl.sourcePath));
@@ -8734,13 +9058,14 @@ var init_portals = __esm({
8734
9058
  };
8735
9059
  portalsRule = {
8736
9060
  name: "portal-endpoints",
8737
- description: "A Portal declares its portalType and binds every interface method to a concrete endpoint of the matching transport. Non-Portal components carry no portalType, basePath, or endpoints.",
9061
+ description: "A Portal declares its portalType and binds every interface method to a concrete endpoint of the matching transport. Non-Portal components carry no portalType, basePath, endpoints, or auth (auth is inbound transport auth \u2014 a Gateway carries it on the Portal it owns).",
8738
9062
  codes: [
8739
9063
  { code: "MISSING_PORTAL_TYPE", defaultSeverity: "error", summary: "Portal without a portalType" },
8740
9064
  { code: "MISSING_ENDPOINT", defaultSeverity: "error", summary: "Portal method without a wire endpoint binding" },
8741
9065
  { code: "ENDPOINT_TRANSPORT_MISMATCH", defaultSeverity: "error", summary: "Endpoint transport does not match the Portal portalType" },
8742
9066
  { code: "UNEXPECTED_PORTAL_FIELD", defaultSeverity: "error", summary: "Non-Portal component with portalType/basePath" },
8743
- { code: "ARCHITECTURE_VIOLATION_NON_PORTAL_ENDPOINT", defaultSeverity: "error", summary: "Non-Portal component method declaring an endpoint" }
9067
+ { code: "ARCHITECTURE_VIOLATION_NON_PORTAL_ENDPOINT", defaultSeverity: "error", summary: "Non-Portal component method declaring an endpoint" },
9068
+ { code: "AUTH_ON_NON_PORTAL", defaultSeverity: "warning", summary: "Non-Portal component declaring auth (auth is inbound transport auth, only meaningful on a Portal)" }
8744
9069
  ],
8745
9070
  check(ctx) {
8746
9071
  for (const comp of ctx.components) {
@@ -8792,6 +9117,15 @@ var init_portals = __esm({
8792
9117
  isDraftCtx
8793
9118
  );
8794
9119
  }
9120
+ if (comp.auth !== void 0) {
9121
+ ctx.addIssue(
9122
+ "warning",
9123
+ "AUTH_ON_NON_PORTAL",
9124
+ `Component "${comp.id}" is a ${comp.componentType}, not a Portal, but declares "auth". Auth is inbound transport auth and is only meaningful on a Portal (a Gateway carries it on the Portal it owns). Move it to the exposed Portal, or remove it.`,
9125
+ comp.id,
9126
+ isDraftCtx
9127
+ );
9128
+ }
8795
9129
  const compInterfaces = ctx.interfaces.filter((i) => i.component === comp.id);
8796
9130
  for (const intf of compInterfaces) {
8797
9131
  for (const m of intf.methods) {
@@ -11875,6 +12209,95 @@ var init_lint_allows = __esm({
11875
12209
  }
11876
12210
  });
11877
12211
 
12212
+ // src/core/rules/portal-call-auth.ts
12213
+ var COMPONENT_REF_PREFIX, portalCallAuthRule;
12214
+ var init_portal_call_auth = __esm({
12215
+ "src/core/rules/portal-call-auth.ts"() {
12216
+ "use strict";
12217
+ COMPONENT_REF_PREFIX = "component:";
12218
+ portalCallAuthRule = {
12219
+ name: "portal-call-auth",
12220
+ description: "Hardens authenticated cross-service calls. An OUTBOUND narrative `call` into ANOTHER component's Portal whose auth is not `none` must (a) be made by an Adapter \u2014 the only block that does external I/O \u2014 and (b) declare the credential source it presents via the step's `auth.from`. Absence of a source warns PORTAL_AUTH_UNMET; a non-Adapter presenter warns AUTH_PRESENTER_NOT_ADAPTER. When `auth.from` is a modeled reference (`component:<id>`) it must resolve to an Adapter/Store the presenter is wired to (UNKNOWN_AUTH_SOURCE / AUTH_SOURCE_NOT_PROVIDER / AUTH_SOURCE_UNWIRED). The actual secret is never stored in the spec. Dispatch steps (a portal's OWN inbound routing) and self-calls are not cross-service calls and are excluded.",
12221
+ codes: [
12222
+ { code: "PORTAL_AUTH_UNMET", defaultSeverity: "warning", summary: "A narrative call into another component's authed Portal does not declare where its credential loads from" },
12223
+ { code: "AUTH_PRESENTER_NOT_ADAPTER", defaultSeverity: "warning", summary: "A non-Adapter component authenticates an outbound call to a portal (external I/O must go through an Adapter)" },
12224
+ { code: "UNKNOWN_AUTH_SOURCE", defaultSeverity: "warning", summary: "auth.from references a component: source that does not exist" },
12225
+ { code: "AUTH_SOURCE_NOT_PROVIDER", defaultSeverity: "warning", summary: "auth.from references a component that is not an Adapter or Store" },
12226
+ { code: "AUTH_SOURCE_UNWIRED", defaultSeverity: "warning", summary: "The presenter declares a component: credential source it does not depend on or own" }
12227
+ ],
12228
+ check(ctx) {
12229
+ for (const impl of ctx.implementations) {
12230
+ const ownComponent = ctx.interfaceMap.get(impl.contract)?.component;
12231
+ const presenter = ownComponent ? ctx.componentMap.get(ownComponent) : void 0;
12232
+ const draft = ctx.isImplementationDraft(impl);
12233
+ for (const method2 of impl.methods ?? []) {
12234
+ for (const step of method2.narrative ?? []) {
12235
+ if (step.type !== "call") continue;
12236
+ if (!step.targetComponent || step.targetComponent === ownComponent) continue;
12237
+ const target = ctx.componentMap.get(step.targetComponent);
12238
+ if (!target || target.componentType !== "Portal") continue;
12239
+ if (!target.auth || target.auth.scheme === "none") continue;
12240
+ if (presenter && presenter.componentType !== "Adapter") {
12241
+ ctx.addIssue(
12242
+ "warning",
12243
+ "AUTH_PRESENTER_NOT_ADAPTER",
12244
+ `Narrative step ${step.stepNumber} of "${method2.name}" in "${impl.id}": "${presenter.id}" (${presenter.componentType}) authenticates an outbound call to portal "${target.id}". An authenticated cross-service call is external I/O and must be made by an Adapter (the only block that does external I/O) \u2014 route it through a client Adapter.`,
12245
+ impl.id,
12246
+ draft
12247
+ );
12248
+ }
12249
+ const from = step.auth?.from;
12250
+ if (!from) {
12251
+ ctx.addIssue(
12252
+ "warning",
12253
+ "PORTAL_AUTH_UNMET",
12254
+ `Narrative step ${step.stepNumber} of "${method2.name}" in "${impl.id}" calls the authenticated portal "${target.id}" (auth scheme: ${target.auth.scheme}) but does not declare where its credential is loaded from. Add the credential source (the step's auth.from \u2014 a secret-store component, env var, config key, or vault ref), or drop the portal's auth if it needs none.`,
12255
+ impl.id,
12256
+ draft
12257
+ );
12258
+ continue;
12259
+ }
12260
+ if (from.startsWith(COMPONENT_REF_PREFIX)) {
12261
+ const srcId = from.slice(COMPONENT_REF_PREFIX.length);
12262
+ const src = ctx.componentMap.get(srcId);
12263
+ if (!src) {
12264
+ ctx.addIssue(
12265
+ "warning",
12266
+ "UNKNOWN_AUTH_SOURCE",
12267
+ `Narrative step ${step.stepNumber} of "${method2.name}" in "${impl.id}": auth.from references component "${srcId}", which does not exist.`,
12268
+ impl.id,
12269
+ draft
12270
+ );
12271
+ } else {
12272
+ if (src.componentType !== "Adapter" && src.componentType !== "Store") {
12273
+ ctx.addIssue(
12274
+ "warning",
12275
+ "AUTH_SOURCE_NOT_PROVIDER",
12276
+ `Narrative step ${step.stepNumber} of "${method2.name}" in "${impl.id}": auth.from references "${srcId}" (${src.componentType}); a credential source must be an Adapter (loads the secret via external I/O) or a Store (holds it).`,
12277
+ impl.id,
12278
+ draft
12279
+ );
12280
+ }
12281
+ const wired = (presenter?.dependsOn ?? []).includes(srcId) || (presenter?.owns ?? []).includes(srcId);
12282
+ if (presenter && !wired) {
12283
+ ctx.addIssue(
12284
+ "warning",
12285
+ "AUTH_SOURCE_UNWIRED",
12286
+ `Narrative step ${step.stepNumber} of "${method2.name}" in "${impl.id}": "${presenter.id}" loads its credential from "${srcId}" but neither depends on nor owns it \u2014 declare the dependsOn edge so the credential wiring is real.`,
12287
+ impl.id,
12288
+ draft
12289
+ );
12290
+ }
12291
+ }
12292
+ }
12293
+ }
12294
+ }
12295
+ }
12296
+ }
12297
+ };
12298
+ }
12299
+ });
12300
+
11878
12301
  // src/core/rules/index.ts
11879
12302
  function makeScopeFilter(opts) {
11880
12303
  const { components, interfaces, implementations, types, scopeSubsystem } = opts;
@@ -12120,6 +12543,7 @@ var init_rules = __esm({
12120
12543
  init_hidden_state();
12121
12544
  init_dependency_conformance();
12122
12545
  init_lint_allows();
12546
+ init_portal_call_auth();
12123
12547
  init_source_analysis();
12124
12548
  init_types();
12125
12549
  init_type_analysis();
@@ -12141,6 +12565,9 @@ var init_rules = __esm({
12141
12565
  narrativeAntipatternsRule,
12142
12566
  narrativeDetailRule,
12143
12567
  portalsRule,
12568
+ // Cross-call auth: a narrative call into an authed Portal must name its
12569
+ // credential source (rides with the portal family).
12570
+ portalCallAuthRule,
12144
12571
  stereotypeDepsRule,
12145
12572
  patternsRule,
12146
12573
  // Facade shape rides with pattern ownership: same §7 doctrine, narrative side.
@@ -12203,6 +12630,7 @@ var init_rules = __esm({
12203
12630
  // L4 expectations: implementations and their code linkage.
12204
12631
  MISSING_IMPLEMENTATION_METHOD: "implementations",
12205
12632
  MISSING_SOURCE_PATH: "implementations",
12633
+ PORTAL_AUTH_UNMET: "implementations",
12206
12634
  MISSING_SOURCE_FILE: "implementations",
12207
12635
  SOURCE_PATH_ESCAPES_ROOT: "implementations",
12208
12636
  UNREALIZED_METHOD: "implementations",
@@ -12608,6 +13036,28 @@ function buildGraphModel(level) {
12608
13036
  ...t.subsystem ? { parentId: t.subsystem } : {}
12609
13037
  });
12610
13038
  }
13039
+ const componentByInterface = /* @__PURE__ */ new Map();
13040
+ for (const c of model.components) {
13041
+ for (const intf of c.interfaces) componentByInterface.set(intf.id, c.id);
13042
+ }
13043
+ for (const impl of loadImplementationSpecs()) {
13044
+ const componentId = componentByInterface.get(impl.contract);
13045
+ if (!componentId) continue;
13046
+ nodes.push({
13047
+ id: impl.id,
13048
+ label: impl.name,
13049
+ kind: "implementation",
13050
+ level: 4,
13051
+ parentId: componentId,
13052
+ ...impl.status ? { status: impl.status } : {}
13053
+ });
13054
+ candidates.push({ from: componentId, to: impl.id, edgeKind: "owns" });
13055
+ }
13056
+ for (const c of model.components) {
13057
+ for (const memberId of c.owns) {
13058
+ candidates.push({ from: c.id, to: memberId, edgeKind: "owns" });
13059
+ }
13060
+ }
12611
13061
  for (const e of model.edges) {
12612
13062
  candidates.push({ from: e.from, to: e.to, edgeKind: "depends_on" });
12613
13063
  }
@@ -16744,6 +17194,7 @@ NOTICE:
16744
17194
  type: import_zod9.z.enum(["local", "call", "dispatch", "branch", "switch", "loop", "try", "parallel", "jump", "return", "throw"]),
16745
17195
  targetComponent: import_zod9.z.string().optional().describe("call/dispatch: L2 component id (for dispatch, the Portal routed through)"),
16746
17196
  targetMethod: import_zod9.z.string().optional().describe("call: method name on the target"),
17197
+ auth: import_zod9.z.object({ from: import_zod9.z.string(), note: import_zod9.z.string().optional() }).optional().describe("call/dispatch: the credential this step presents to an AUTHED callee Portal and WHERE it loads from (`from`). Opaque form (env:API_KEY, a config key, vault:path) = a design note wairon never resolves; modeled form `component:<id>` references the Adapter/Store that provides the secret and is validated (must resolve, be an Adapter/Store, and be wired to the presenter). Absence on a call into a Portal whose auth \u2260 none warns (PORTAL_AUTH_UNMET). The authenticated call itself should be made by an Adapter (AUTH_PRESENTER_NOT_ADAPTER)."),
16747
17198
  detach: import_zod9.z.boolean().optional().describe("call/dispatch: fire-and-forget \u2014 issue the call and continue without awaiting the result (no later step consumes it)"),
16748
17199
  capability: import_zod9.z.string().optional().describe("dispatch: the capability routed through the target Portal's dispatch table (validated against it \u2014 UNSERVED_CAPABILITY)"),
16749
17200
  assertsGuarantees: import_zod9.z.array(import_zod9.z.string().min(1)).optional().describe("Semantic guarantees this step relies on \u2014 each must be declared in the called method's L3 guarantees (NARRATIVE_SEMANTIC_UNBACKED otherwise). Builtin tokens: idempotent | atomic | transactional | exactly-once; extension packs may declare more (any other token is UNKNOWN_GUARANTEE)"),
@@ -25456,6 +25907,7 @@ init_validation();
25456
25907
  init_diagram();
25457
25908
  init_loader();
25458
25909
  init_extensions();
25910
+ init_types();
25459
25911
  init_server();
25460
25912
 
25461
25913
  // src/git/config.ts
@@ -25976,7 +26428,11 @@ var hostCore = {
25976
26428
  checkDeclarativePack: (raw) => {
25977
26429
  const result = DeclarativePackSchema.safeParse(raw);
25978
26430
  return result.success ? null : result.error.issues[0]?.message ?? "shape mismatch";
25979
- }
26431
+ },
26432
+ /** The ids of wairon's built-in architectural profiles, read from the core rules
26433
+ * registry's built-in profile set (BUILTIN_PROFILES) — a pure, side-effect-free
26434
+ * read of a bundled constant. */
26435
+ builtinProfileIds: () => [...BUILTIN_PROFILES]
25980
26436
  };
25981
26437
  function validateProjectAsComplete() {
25982
26438
  const config = loadProjectConfig();
@@ -26385,7 +26841,12 @@ function probe(loadRef, baseRoot, scope, displayRef) {
26385
26841
  ref: displayRef,
26386
26842
  profiles: Object.keys(loaded.profiles).length,
26387
26843
  languages: Object.keys(loaded.languages).length,
26388
- rules: loaded.rules.length
26844
+ rules: loaded.rules.length,
26845
+ // Structured id lists paralleling the counts above — a UI can present a
26846
+ // pack's profiles/languages/rules as choices instead of a bare number.
26847
+ profileIds: Object.keys(loaded.profiles),
26848
+ languageIds: Object.keys(loaded.languages),
26849
+ ruleIds: loaded.rules.map((r) => r.name)
26389
26850
  };
26390
26851
  }
26391
26852
  function writeFileAtomic(file, content) {
@@ -26415,6 +26876,31 @@ function storeListGlobalPacks() {
26415
26876
  );
26416
26877
  return [...instance, ...image];
26417
26878
  }
26879
+ function storeListAvailableProfiles() {
26880
+ const out = [];
26881
+ const seen = /* @__PURE__ */ new Set();
26882
+ const emit = (id, source, family) => {
26883
+ const key = JSON.stringify([id, source]);
26884
+ if (seen.has(key)) return;
26885
+ seen.add(key);
26886
+ out.push(family ? { id, source, family } : { id, source });
26887
+ };
26888
+ for (const id of hostCore.builtinProfileIds()) emit(id, "builtin");
26889
+ const scanTier = (dir) => {
26890
+ for (const full of hostCore.discoverPacks(dir)) {
26891
+ try {
26892
+ const loaded = hostCore.loadExtensionPacks([{ ref: full, scope: "global" }], path45.dirname(full));
26893
+ if (loaded.errors.length) continue;
26894
+ const source = loaded.packNames[0] ?? path45.basename(full);
26895
+ for (const [id, def] of Object.entries(loaded.profiles)) emit(id, source, def.family);
26896
+ } catch {
26897
+ }
26898
+ }
26899
+ };
26900
+ scanTier(hostCore.globalPacksDir());
26901
+ scanTier(imagePacksDir());
26902
+ return out;
26903
+ }
26418
26904
  function readPackContent(full) {
26419
26905
  const st = fs36.statSync(full);
26420
26906
  if (st.isFile()) return fs36.readFileSync(full, "utf8");
@@ -26601,6 +27087,23 @@ function installProjectPack(cfg, credential, project2, name, content) {
26601
27087
  requireCap(cfg, credential, "project:admin", "project", project2, "Forbidden \u2014 installing a project pack requires project:admin over the project");
26602
27088
  return executeApprovedInstallProjectPack(cfg, project2, name, content);
26603
27089
  }
27090
+ function listAvailableProfiles(cfg, credential) {
27091
+ requirePrincipal2(cfg, credential);
27092
+ return storeListAvailableProfiles();
27093
+ }
27094
+ function listAdoptableProjectPacks(cfg, credential, project2) {
27095
+ requireCap(cfg, credential, "project:read", "project", project2, "Forbidden \u2014 listing adoptable packs requires project:read over the project");
27096
+ return storeListGlobalPacks();
27097
+ }
27098
+ function adoptProjectPack(cfg, credential, project2, name) {
27099
+ requireCap(cfg, credential, "project:admin", "project", project2, "Forbidden \u2014 adopting a project pack requires project:admin over the project");
27100
+ const resolution = executeApprovedResolveGlobalPacks([name]);
27101
+ if (resolution.resolved.length === 0) {
27102
+ throw new Error(`no such server-global pack "${name}" \u2014 cannot adopt a pack the instance does not carry`);
27103
+ }
27104
+ const resolved = resolution.resolved[0];
27105
+ return executeApprovedInstallProjectPack(cfg, project2, resolved.name, resolved.content);
27106
+ }
26604
27107
  function executeApprovedListProjectPacks(cfg, project2) {
26605
27108
  return runWithProjectRoot(boundProject2(cfg, project2), () => storeListProjectPacks());
26606
27109
  }
@@ -27521,6 +28024,7 @@ function removeIdentityProviderRecord(dataDir, id) {
27521
28024
  }
27522
28025
  var PROJECT_CREATE_CAPABILITY = "project:create";
27523
28026
  var PROJECT_WRITE_CAPABILITY = "project:write";
28027
+ var PROJECT_READ_CAPABILITY = "project:read";
27524
28028
  var POLICY_MANAGE_CAPABILITY = "project:admin";
27525
28029
  function carriesInstancePermission(cfg, principal, capability) {
27526
28030
  return authorize(cfg.dataDir, principal, capability, "instance", "").value === "yes";
@@ -27585,6 +28089,19 @@ function recordProjectProfileSelection(root, selection) {
27585
28089
  writeYamlFile(AI_PATHS.projectConfig(), raw);
27586
28090
  });
27587
28091
  }
28092
+ function readProjectType(root) {
28093
+ return runWithProjectRoot(root, () => {
28094
+ const raw = readYamlFile(AI_PATHS.projectConfig());
28095
+ return raw?.projectType ?? "backend";
28096
+ });
28097
+ }
28098
+ function writeProjectType(root, projectType) {
28099
+ runWithProjectRoot(root, () => {
28100
+ const raw = readYamlFile(AI_PATHS.projectConfig()) ?? {};
28101
+ raw["projectType"] = projectType;
28102
+ writeYamlFile(AI_PATHS.projectConfig(), raw);
28103
+ });
28104
+ }
27588
28105
  function requestPackNames(request) {
27589
28106
  const sel = request.profileSelection;
27590
28107
  if (!sel) return [];
@@ -27779,6 +28296,32 @@ function reconcileProjectPolicy(cfg, credential, projectId) {
27779
28296
  );
27780
28297
  return result;
27781
28298
  }
28299
+ function getProjectConfig(cfg, credential, projectId) {
28300
+ const principal = requirePrincipal4(cfg, credential);
28301
+ if (authorize(cfg.dataDir, principal, PROJECT_READ_CAPABILITY, "project", projectId).value !== "yes") {
28302
+ throw new ForbiddenError(
28303
+ "reading a project's configuration requires project:read over the project"
28304
+ );
28305
+ }
28306
+ const root = resolveProjectRoot(cfg.dataDir, principal, projectId);
28307
+ if (!root) throw new Error(`Unknown project "${projectId}".`);
28308
+ const projectType = readProjectType(root);
28309
+ const locked = runWithProjectRoot(root, () => hostCore.readLockRecord() !== null);
28310
+ return { projectType, locked };
28311
+ }
28312
+ function setProjectType(cfg, credential, projectId, projectType) {
28313
+ const principal = requirePrincipal4(cfg, credential);
28314
+ if (authorize(cfg.dataDir, principal, PROJECT_WRITE_CAPABILITY, "project", projectId).value !== "yes") {
28315
+ throw new ForbiddenError(
28316
+ "changing a project's configuration requires project:write over the project"
28317
+ );
28318
+ }
28319
+ const root = resolveProjectRoot(cfg.dataDir, principal, projectId);
28320
+ if (!root) throw new Error(`Unknown project "${projectId}".`);
28321
+ writeProjectType(root, projectType);
28322
+ const locked = runWithProjectRoot(root, () => hostCore.readLockRecord() !== null);
28323
+ return { projectType, locked };
28324
+ }
27782
28325
  function getPackPolicy(cfg, credential) {
27783
28326
  requirePrincipal4(cfg, credential);
27784
28327
  return getPackPolicyRecord(cfg.dataDir) ?? PERMISSIVE_DEFAULT_POLICY;
@@ -27842,7 +28385,7 @@ function handlePolicyRequest(cfg, credential, req, res, body, url) {
27842
28385
 
27843
28386
  // src/server/identity.ts
27844
28387
  var PROJECT_ADMIN_CAPABILITY2 = "project:admin";
27845
- var PROJECT_READ_CAPABILITY = "project:read";
28388
+ var PROJECT_READ_CAPABILITY2 = "project:read";
27846
28389
  var PROJECT_WRITE_CAPABILITY2 = "project:write";
27847
28390
  function requireInstanceProjectAdmin(cfg, principal, what) {
27848
28391
  if (authorize(cfg.dataDir, principal, PROJECT_ADMIN_CAPABILITY2, "instance", "").value !== "yes") {
@@ -27988,7 +28531,7 @@ function revokeToken(cfg, credential, tokenId) {
27988
28531
  }
27989
28532
  function mintSelfToken(cfg, credential, projectId, write) {
27990
28533
  const principal = requirePrincipal5(cfg, credential);
27991
- if (authorize(cfg.dataDir, principal, PROJECT_READ_CAPABILITY, "project", projectId).value !== "yes") {
28534
+ if (authorize(cfg.dataDir, principal, PROJECT_READ_CAPABILITY2, "project", projectId).value !== "yes") {
27992
28535
  throw new ForbiddenError("caller lacks project:read on the requested project");
27993
28536
  }
27994
28537
  if (write && authorize(cfg.dataDir, principal, PROJECT_WRITE_CAPABILITY2, "project", projectId).value !== "yes") {
@@ -28634,7 +29177,7 @@ function audienceDistance(resolution, targetProjectId) {
28634
29177
  // src/server/landscape.ts
28635
29178
  var yamlLib = __toESM(require("js-yaml"));
28636
29179
  var PROJECT_ADMIN_CAPABILITY3 = "project:admin";
28637
- var PROJECT_READ_CAPABILITY2 = "project:read";
29180
+ var PROJECT_READ_CAPABILITY3 = "project:read";
28638
29181
  var PROJECT_WRITE_CAPABILITY3 = "project:write";
28639
29182
  function requirePrincipal6(cfg, credential) {
28640
29183
  const principal = authenticateCredential(cfg.dataDir, credential);
@@ -28648,7 +29191,7 @@ function readView(cfg, principal) {
28648
29191
  if (isInstanceAdmin(principal)) {
28649
29192
  return { all: true, projectIds: /* @__PURE__ */ new Set(), unitIds: /* @__PURE__ */ new Set(), contextUnitIds: /* @__PURE__ */ new Set() };
28650
29193
  }
28651
- const read = visibleScopes(cfg.dataDir, principal, PROJECT_READ_CAPABILITY2);
29194
+ const read = visibleScopes(cfg.dataDir, principal, PROJECT_READ_CAPABILITY3);
28652
29195
  const admin = visibleScopes(cfg.dataDir, principal, PROJECT_ADMIN_CAPABILITY3);
28653
29196
  const unitIds = /* @__PURE__ */ new Set([...actionableUnitIds(read), ...actionableUnitIds(admin)]);
28654
29197
  const contextUnitIds = new Set(
@@ -28663,13 +29206,13 @@ function readView(cfg, principal) {
28663
29206
  }
28664
29207
  function requireLandscapeReach(cfg, principal, view, what) {
28665
29208
  if (view.all || view.projectIds.size > 0 || view.unitIds.size > 0) return;
28666
- if (permitsCap(cfg, principal, PROJECT_READ_CAPABILITY2, "instance", "") || permitsCap(cfg, principal, PROJECT_ADMIN_CAPABILITY3, "instance", "")) {
29209
+ if (permitsCap(cfg, principal, PROJECT_READ_CAPABILITY3, "instance", "") || permitsCap(cfg, principal, PROJECT_ADMIN_CAPABILITY3, "instance", "")) {
28667
29210
  return;
28668
29211
  }
28669
29212
  throw new ForbiddenError(`${what} requires project:read reach`);
28670
29213
  }
28671
29214
  function requireObserverScope(cfg, principal, observerProjectId) {
28672
- const covered = permitsCap(cfg, principal, PROJECT_READ_CAPABILITY2, "project", observerProjectId) || permitsCap(cfg, principal, PROJECT_WRITE_CAPABILITY3, "project", observerProjectId);
29215
+ const covered = permitsCap(cfg, principal, PROJECT_READ_CAPABILITY3, "project", observerProjectId) || permitsCap(cfg, principal, PROJECT_WRITE_CAPABILITY3, "project", observerProjectId);
28673
29216
  if (!covered) {
28674
29217
  throw new ForbiddenError("caller lacks scope over the current project");
28675
29218
  }
@@ -30706,6 +31249,15 @@ function installGlobalPackArchive2(cfg, credential, archive, name) {
30706
31249
  function installProjectPackArchive2(cfg, credential, project2, archive, name) {
30707
31250
  return installProjectPackArchive(cfg, credential, project2, archive, name);
30708
31251
  }
31252
+ function listAvailableProfiles2(cfg, credential) {
31253
+ return listAvailableProfiles(cfg, credential);
31254
+ }
31255
+ function listAdoptableProjectPacks2(cfg, credential, project2) {
31256
+ return listAdoptableProjectPacks(cfg, credential, project2);
31257
+ }
31258
+ function adoptProjectPack2(cfg, credential, project2, name) {
31259
+ return adoptProjectPack(cfg, credential, project2, name);
31260
+ }
30709
31261
  function getPackPolicy2(cfg, credential) {
30710
31262
  return getPackPolicy(cfg, credential);
30711
31263
  }
@@ -30718,6 +31270,12 @@ function evaluateProjectPolicy2(cfg, credential, projectId) {
30718
31270
  function reconcileProjectPolicy2(cfg, credential, projectId) {
30719
31271
  return reconcileProjectPolicy(cfg, credential, projectId);
30720
31272
  }
31273
+ function getProjectConfig2(cfg, credential, projectId) {
31274
+ return getProjectConfig(cfg, credential, projectId);
31275
+ }
31276
+ function setProjectType2(cfg, credential, projectId, projectType) {
31277
+ return setProjectType(cfg, credential, projectId, projectType);
31278
+ }
30721
31279
  function listProducers2(cfg, credential, project2) {
30722
31280
  return listProducers(cfg, credential, project2);
30723
31281
  }
@@ -31839,7 +32397,7 @@ function getWebProjectCanvasModel(cfg, sessionId, projectId) {
31839
32397
  }
31840
32398
  return runWithProjectRoot(root, () => hostCore.buildCanvasDataModel());
31841
32399
  }
31842
- function getWebProjectOpenApi(cfg, sessionId, projectId) {
32400
+ function getWebProjectOpenApi(cfg, sessionId, projectId, portalId) {
31843
32401
  const principal = authenticateSession(cfg.dataDir, sessionId);
31844
32402
  if (!principal.authenticated) throw new UnauthenticatedError();
31845
32403
  if (!listProjects2(cfg, sessionId).some((r) => r.id === projectId)) {
@@ -31848,7 +32406,19 @@ function getWebProjectOpenApi(cfg, sessionId, projectId) {
31848
32406
  const root = resolveProjectRoot(cfg.dataDir, principal, projectId);
31849
32407
  if (!root) throw new ForbiddenError("project not authorized or unknown");
31850
32408
  const result = runWithProjectRoot(root, () => hostSurfaces.exportBoundSurface("project", "openapi"));
31851
- return swaggerUiPage(result.rendered ?? "{}", projectId);
32409
+ const specs = result.renderedSet ?? (result.rendered ? [{ portalId: "", name: projectId, document: result.rendered }] : []);
32410
+ if (specs.length === 0) return swaggerUiPage("{}", projectId);
32411
+ if (portalId) {
32412
+ const sel = specs.find((s) => s.portalId === portalId) ?? specs[0];
32413
+ return swaggerUiPage(sel.document, sel.name);
32414
+ }
32415
+ if (specs.length === 1) return swaggerUiPage(specs[0].document, specs[0].name);
32416
+ return openApiIndexPage(projectId, specs);
32417
+ }
32418
+ function openApiIndexPage(projectId, specs) {
32419
+ const esc2 = (s) => s.replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
32420
+ 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("");
32421
+ 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>`;
31852
32422
  }
31853
32423
  function reshapeLandscapeGraph(model, level) {
31854
32424
  const unitNodeIds = new Set(model.nodes.filter((n) => n.nodeKind === "orgUnit").map((n) => n.id));
@@ -33999,6 +34569,15 @@ function opsInstallGlobalPackArchive(cfg, sessionId, req, body, res) {
33999
34569
  function opsInstallProjectPackArchive(cfg, sessionId, req, url, body, res) {
34000
34570
  sendJson(res, 200, installProjectPackArchive2(cfg, sessionId, q(url, "projectId") ?? "", archiveBody(body), packNameOverride(req)));
34001
34571
  }
34572
+ function opsListAvailableProfiles(cfg, sessionId, res) {
34573
+ sendJson(res, 200, { profiles: listAvailableProfiles2(cfg, sessionId) });
34574
+ }
34575
+ function opsListAdoptableProjectPacks(cfg, sessionId, url, res) {
34576
+ sendJson(res, 200, { packs: listAdoptableProjectPacks2(cfg, sessionId, q(url, "projectId") ?? "") });
34577
+ }
34578
+ function opsAdoptProjectPack(cfg, sessionId, body, res) {
34579
+ sendJson(res, 200, adoptProjectPack2(cfg, sessionId, String(body?.projectId ?? ""), String(body?.name ?? "")));
34580
+ }
34002
34581
  function opsGetPackPolicy(cfg, sessionId, res) {
34003
34582
  sendJson(res, 200, getPackPolicy2(cfg, sessionId));
34004
34583
  }
@@ -34011,6 +34590,12 @@ function opsPolicyEvaluate(cfg, sessionId, url, res) {
34011
34590
  function opsPolicyReconcile(cfg, sessionId, body, res) {
34012
34591
  sendJson(res, 200, reconcileProjectPolicy2(cfg, sessionId, String(body?.projectId ?? "")));
34013
34592
  }
34593
+ function opsGetProjectConfig(cfg, sessionId, url, res) {
34594
+ sendJson(res, 200, getProjectConfig2(cfg, sessionId, q(url, "projectId") ?? ""));
34595
+ }
34596
+ function opsSetProjectConfig(cfg, sessionId, body, res) {
34597
+ sendJson(res, 200, setProjectType2(cfg, sessionId, String(body?.projectId ?? ""), String(body?.projectType ?? "")));
34598
+ }
34014
34599
  function opsListProducers(cfg, sessionId, url, res) {
34015
34600
  sendJson(res, 200, { producers: listProducers2(cfg, sessionId, q(url, "projectId") ?? "") });
34016
34601
  }
@@ -34195,7 +34780,8 @@ async function handleWebRequest(cfg, req, res, body, url, ctx) {
34195
34780
  }
34196
34781
  if (req.method === "GET" && parts.length === 2 && parts[1] === "openapi") {
34197
34782
  const projectId = url.searchParams.get("projectId") ?? "";
34198
- const html = getWebProjectOpenApi(cfg, sessionId, projectId);
34783
+ const spec = url.searchParams.get("spec") ?? void 0;
34784
+ const html = getWebProjectOpenApi(cfg, sessionId, projectId, spec);
34199
34785
  res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
34200
34786
  res.end(html);
34201
34787
  return;
@@ -34237,6 +34823,18 @@ async function handleWebRequest(cfg, req, res, body, url, ctx) {
34237
34823
  if (req.method === "POST" && parts.length === 4 && parts[2] === "packs" && parts[3] === "remove") {
34238
34824
  return opsRemoveProjectPack(cfg, sessionId, body, res);
34239
34825
  }
34826
+ if (req.method === "GET" && parts.length === 4 && parts[2] === "packs" && parts[3] === "adoptable") {
34827
+ return opsListAdoptableProjectPacks(cfg, sessionId, url, res);
34828
+ }
34829
+ if (req.method === "POST" && parts.length === 4 && parts[2] === "packs" && parts[3] === "adopt") {
34830
+ return opsAdoptProjectPack(cfg, sessionId, body, res);
34831
+ }
34832
+ if (req.method === "GET" && parts.length === 3 && parts[2] === "config") {
34833
+ return opsGetProjectConfig(cfg, sessionId, url, res);
34834
+ }
34835
+ if (req.method === "POST" && parts.length === 3 && parts[2] === "config") {
34836
+ return opsSetProjectConfig(cfg, sessionId, body, res);
34837
+ }
34240
34838
  if (req.method === "GET" && parts.length === 3 && parts[2] === "policy") {
34241
34839
  return opsPolicyEvaluate(cfg, sessionId, url, res);
34242
34840
  }
@@ -34350,6 +34948,9 @@ async function handleWebRequest(cfg, req, res, body, url, ctx) {
34350
34948
  if (req.method === "POST" && parts.length === 4 && parts[2] === "packs" && parts[3] === "remove") {
34351
34949
  return opsRemoveGlobalPack(cfg, sessionId, body, res);
34352
34950
  }
34951
+ if (req.method === "GET" && parts.length === 3 && parts[2] === "profiles") {
34952
+ return opsListAvailableProfiles(cfg, sessionId, res);
34953
+ }
34353
34954
  if (req.method === "GET" && parts.length === 3 && parts[2] === "policy") {
34354
34955
  return opsGetPackPolicy(cfg, sessionId, res);
34355
34956
  }
@@ -35151,6 +35752,7 @@ var WEB_MUTATION_PATHS = /* @__PURE__ */ new Set([
35151
35752
  "/web/projects/packs",
35152
35753
  "/web/projects/packs/upload",
35153
35754
  "/web/projects/packs/remove",
35755
+ "/web/projects/packs/adopt",
35154
35756
  "/web/projects/policy/reconcile",
35155
35757
  "/web/projects/producers",
35156
35758
  "/web/projects/producers/remove",