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

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.11";
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>
@@ -6662,6 +6859,16 @@ var init_type_references = __esm({
6662
6859
  }
6663
6860
  });
6664
6861
 
6862
+ // src/utils/filenames.ts
6863
+ function safeFilenamePart(value) {
6864
+ return value.replace(/[^a-zA-Z0-9._-]/g, "-");
6865
+ }
6866
+ var init_filenames = __esm({
6867
+ "src/utils/filenames.ts"() {
6868
+ "use strict";
6869
+ }
6870
+ });
6871
+
6665
6872
  // src/core/statehash.ts
6666
6873
  function computeStateId() {
6667
6874
  const tree = {
@@ -6764,11 +6971,63 @@ function operationFor(method2, closureIds) {
6764
6971
  }
6765
6972
  return op;
6766
6973
  }
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"));
6974
+ function securitySchemeObject(auth) {
6975
+ if (auth.scheme === "none") return null;
6976
+ const desc = auth.description ? { description: auth.description } : {};
6977
+ switch (auth.scheme) {
6978
+ case "apiKey":
6979
+ return { type: "apiKey", in: auth.in ?? "header", name: auth.name ?? "X-API-Key", ...desc };
6980
+ case "bearer":
6981
+ return { type: "http", scheme: "bearer", ...auth.bearerFormat ? { bearerFormat: auth.bearerFormat } : {}, ...desc };
6982
+ case "basic":
6983
+ return { type: "http", scheme: "basic", ...desc };
6984
+ case "oauth2": {
6985
+ const flow = { scopes: Object.fromEntries((auth.scopes ?? []).map((s) => [s.name, s.description])) };
6986
+ if (auth.authorizationUrl) flow.authorizationUrl = auth.authorizationUrl;
6987
+ if (auth.tokenUrl) flow.tokenUrl = auth.tokenUrl;
6988
+ if (auth.refreshUrl) flow.refreshUrl = auth.refreshUrl;
6989
+ return { type: "oauth2", flows: { [auth.flow ?? "authorizationCode"]: flow }, ...desc };
6990
+ }
6991
+ case "openIdConnect":
6992
+ return { type: "openIdConnect", openIdConnectUrl: auth.openIdConnectUrl ?? "", ...desc };
6993
+ case "custom":
6994
+ return { type: "apiKey", in: auth.in ?? "header", name: auth.name ?? "Authorization", description: auth.description ?? auth.example ?? "Custom authentication scheme." };
6995
+ default:
6996
+ return null;
6997
+ }
6998
+ }
6999
+ function schemeBaseName(scheme) {
7000
+ return { apiKey: "ApiKeyAuth", bearer: "BearerAuth", basic: "BasicAuth", oauth2: "OAuth2", openIdConnect: "OpenIdConnect", custom: "CustomAuth" }[scheme] ?? "Auth";
7001
+ }
7002
+ function buildSecurity(entries) {
7003
+ const schemes = {};
7004
+ const nameByContent = /* @__PURE__ */ new Map();
7005
+ const securityByEntry = /* @__PURE__ */ new Map();
7006
+ for (const entry of entries) {
7007
+ if (!entry.auth) continue;
7008
+ const obj = securitySchemeObject(entry.auth);
7009
+ if (!obj) continue;
7010
+ const content = JSON.stringify(obj);
7011
+ let name = nameByContent.get(content);
7012
+ if (!name) {
7013
+ name = schemeBaseName(entry.auth.scheme);
7014
+ for (let n = 2; schemes[name]; n++) name = schemeBaseName(entry.auth.scheme) + n;
7015
+ schemes[name] = obj;
7016
+ nameByContent.set(content, name);
7017
+ }
7018
+ const scopeNames = entry.auth.scheme === "oauth2" ? (entry.auth.scopes ?? []).map((s) => s.name) : [];
7019
+ securityByEntry.set(entry.id, { [name]: scopeNames });
7020
+ }
7021
+ return { schemes, securityByEntry };
7022
+ }
7023
+ function httpEntriesOf(snapshot) {
7024
+ return snapshot.interfaces.filter((e) => e.type === "REST" || e.methods.some((m) => m.endpoint?.transport === "HTTP"));
7025
+ }
7026
+ function renderDoc(snapshot, entries, closureIds, opts = {}) {
7027
+ const { schemes, securityByEntry } = buildSecurity(entries);
6770
7028
  const paths = {};
6771
- for (const entry of httpEntries) {
7029
+ for (const entry of entries) {
7030
+ const security = securityByEntry.get(entry.id);
6772
7031
  for (const method2 of entry.methods) {
6773
7032
  const endpoint = method2.endpoint;
6774
7033
  if (!endpoint || endpoint.transport !== "HTTP") continue;
@@ -6776,7 +7035,8 @@ function toOpenApi(snapshot) {
6776
7035
  paths[p] = paths[p] ?? {};
6777
7036
  paths[p][endpoint.method.toLowerCase()] = {
6778
7037
  tags: [entry.id],
6779
- ...operationFor(method2, closureIds)
7038
+ ...operationFor(method2, closureIds),
7039
+ ...security ? { security: [security] } : {}
6780
7040
  };
6781
7041
  }
6782
7042
  }
@@ -6789,19 +7049,41 @@ function toOpenApi(snapshot) {
6789
7049
  required: t.fields.filter((f) => !f.optional).map((f) => f.name)
6790
7050
  };
6791
7051
  }
6792
- const doc = {
7052
+ const components = {};
7053
+ if (Object.keys(schemas).length) components.schemas = schemas;
7054
+ if (Object.keys(schemes).length) components.securitySchemes = schemes;
7055
+ const basePaths = [...new Set(entries.map((e) => e.basePath).filter((b) => !!b))];
7056
+ const servers = opts.servers && basePaths.length === 1 ? [{ url: basePaths[0] }] : void 0;
7057
+ return {
6793
7058
  openapi: "3.1.0",
6794
7059
  info: {
6795
- title: snapshot.projectName,
7060
+ title: opts.title ?? snapshot.projectName,
6796
7061
  version: snapshot.version ?? "0.0.0",
6797
7062
  ...snapshot.stateId ? { "x-wairon-state-id": snapshot.stateId } : {},
6798
7063
  "x-wairon-origin": snapshot.origin,
6799
7064
  "x-wairon-generated-at": snapshot.generatedAt
6800
7065
  },
7066
+ ...servers ? { servers } : {},
6801
7067
  paths,
6802
- ...Object.keys(schemas).length ? { components: { schemas } } : {}
7068
+ ...Object.keys(components).length ? { components } : {}
6803
7069
  };
6804
- return JSON.stringify(doc, null, 2);
7070
+ }
7071
+ function toOpenApiSet(snapshot) {
7072
+ const closureIds = new Set(snapshot.types.map((t) => t.id));
7073
+ const byPortal = /* @__PURE__ */ new Map();
7074
+ const order = [];
7075
+ for (const entry of httpEntriesOf(snapshot)) {
7076
+ if (!byPortal.has(entry.component)) {
7077
+ byPortal.set(entry.component, []);
7078
+ order.push(entry.component);
7079
+ }
7080
+ byPortal.get(entry.component).push(entry);
7081
+ }
7082
+ return order.map((portalId) => {
7083
+ const entries = byPortal.get(portalId);
7084
+ const name = entries[0]?.name ?? portalId;
7085
+ return { portalId, name, document: JSON.stringify(renderDoc(snapshot, entries, closureIds, { title: name, servers: true }), null, 2) };
7086
+ });
6805
7087
  }
6806
7088
  function isOpenApiDocument(body) {
6807
7089
  try {
@@ -6823,6 +7105,39 @@ function typeRefFromSchema(schema) {
6823
7105
  if (typeof t === "string" && t !== "object") return t;
6824
7106
  return "json";
6825
7107
  }
7108
+ function authFromSecurityScheme(scheme) {
7109
+ const desc = typeof scheme.description === "string" ? { description: scheme.description } : {};
7110
+ if (scheme.type === "apiKey") {
7111
+ return {
7112
+ scheme: "apiKey",
7113
+ ...scheme.in === "header" || scheme.in === "query" || scheme.in === "cookie" ? { in: scheme.in } : {},
7114
+ ...typeof scheme.name === "string" ? { name: scheme.name } : {},
7115
+ ...desc
7116
+ };
7117
+ }
7118
+ if (scheme.type === "http") {
7119
+ if (scheme.scheme === "bearer") return { scheme: "bearer", ...typeof scheme.bearerFormat === "string" ? { bearerFormat: scheme.bearerFormat } : {}, ...desc };
7120
+ if (scheme.scheme === "basic") return { scheme: "basic", ...desc };
7121
+ }
7122
+ if (scheme.type === "oauth2") {
7123
+ const flows = scheme.flows ?? {};
7124
+ const flowKey = Object.keys(flows)[0];
7125
+ const f = flows[flowKey] ?? {};
7126
+ return {
7127
+ scheme: "oauth2",
7128
+ ...flowKey === "authorizationCode" || flowKey === "clientCredentials" || flowKey === "implicit" || flowKey === "password" ? { flow: flowKey } : {},
7129
+ ...typeof f.authorizationUrl === "string" ? { authorizationUrl: f.authorizationUrl } : {},
7130
+ ...typeof f.tokenUrl === "string" ? { tokenUrl: f.tokenUrl } : {},
7131
+ ...typeof f.refreshUrl === "string" ? { refreshUrl: f.refreshUrl } : {},
7132
+ scopes: Object.entries(f.scopes ?? {}).map(([name, description]) => ({ name, description })),
7133
+ ...desc
7134
+ };
7135
+ }
7136
+ if (scheme.type === "openIdConnect") {
7137
+ return { scheme: "openIdConnect", openIdConnectUrl: typeof scheme.openIdConnectUrl === "string" ? scheme.openIdConnectUrl : "", ...desc };
7138
+ }
7139
+ return void 0;
7140
+ }
6826
7141
  function fromOpenApi(document, projectName) {
6827
7142
  let parsed;
6828
7143
  try {
@@ -6900,6 +7215,9 @@ function fromOpenApi(document, projectName) {
6900
7215
  }))
6901
7216
  });
6902
7217
  }
7218
+ const securitySchemes = parsed.components?.securitySchemes ?? {};
7219
+ const firstScheme = Object.values(securitySchemes)[0];
7220
+ const importedAuth = firstScheme ? authFromSecurityScheme(firstScheme) : void 0;
6903
7221
  const entry = {
6904
7222
  id: `${projectName}-api`,
6905
7223
  name: typeof info.title === "string" ? info.title : projectName,
@@ -6908,7 +7226,8 @@ function fromOpenApi(document, projectName) {
6908
7226
  component: `${projectName}-api`,
6909
7227
  methods,
6910
7228
  details: typeof info.description === "string" ? info.description : `Imported OpenAPI surface of ${projectName}.`,
6911
- ...typeof info.version === "string" ? { version: info.version } : {}
7229
+ ...typeof info.version === "string" ? { version: info.version } : {},
7230
+ ...importedAuth ? { auth: importedAuth } : {}
6912
7231
  };
6913
7232
  return SurfaceSnapshotSchema.parse({
6914
7233
  projectName,
@@ -7023,6 +7342,10 @@ function projectOwnSurface(maxAudience) {
7023
7342
  component: comp.id,
7024
7343
  methods,
7025
7344
  ...comp.dispatch && comp.dispatch.length ? { dispatch: comp.dispatch } : {},
7345
+ // Project the backing Portal's auth + basePath so the codec can emit
7346
+ // OpenAPI security + per-portal servers self-contained from the snapshot.
7347
+ ...comp.auth && comp.auth.scheme !== "none" ? { auth: comp.auth } : {},
7348
+ ...comp.basePath ? { basePath: comp.basePath } : {},
7026
7349
  details: raw.details ?? "",
7027
7350
  ...raw.version ? { version: raw.version } : {},
7028
7351
  ...raw.stability ? { stability: raw.stability } : {}
@@ -7066,20 +7389,52 @@ function saveSnapshot(snapshot, rootDir = getProjectRoot()) {
7066
7389
  function loadSurfaceSnapshots() {
7067
7390
  return listSnapshots();
7068
7391
  }
7069
- function exportSurface(maxAudience, format, outPath) {
7392
+ function selectPortalSpec(renderedSet, portalId) {
7393
+ const hit = renderedSet.find((spec) => spec.portalId === portalId);
7394
+ if (!hit) {
7395
+ const known = renderedSet.map((s) => s.portalId).join(", ");
7396
+ throw new Error(`Unknown portal "${portalId}" \u2014 this surface renders: ${known || "(no portals)"}.`);
7397
+ }
7398
+ return [hit];
7399
+ }
7400
+ function perPortalPath(resolvedOut, portalId) {
7401
+ const ext = path10.extname(resolvedOut);
7402
+ const stem2 = ext ? resolvedOut.slice(0, -ext.length) : resolvedOut;
7403
+ return `${stem2}.${safeFilenamePart(portalId)}${ext}`;
7404
+ }
7405
+ function writeSurfaceFile(outPath, snapshot, renderedSet) {
7406
+ const resolved = path10.resolve(outPath);
7407
+ fs9.mkdirSync(path10.dirname(resolved), { recursive: true });
7408
+ if (!renderedSet || renderedSet.length === 0) {
7409
+ writeYamlFile(resolved, snapshot);
7410
+ return [resolved];
7411
+ }
7412
+ if (renderedSet.length === 1) {
7413
+ fs9.writeFileSync(resolved, renderedSet[0].document);
7414
+ return [resolved];
7415
+ }
7416
+ return renderedSet.map((spec) => {
7417
+ const target = perPortalPath(resolved, spec.portalId);
7418
+ fs9.writeFileSync(target, spec.document);
7419
+ return target;
7420
+ });
7421
+ }
7422
+ function exportResult(snapshot, renderedSet, writtenPaths) {
7423
+ const rendered = renderedSet?.length === 1 ? renderedSet[0].document : void 0;
7424
+ return {
7425
+ snapshot,
7426
+ ...rendered !== void 0 ? { rendered } : {},
7427
+ ...renderedSet ? { renderedSet } : {},
7428
+ ...writtenPaths.length === 1 ? { writtenTo: writtenPaths[0] } : {},
7429
+ ...writtenPaths.length ? { writtenPaths } : {}
7430
+ };
7431
+ }
7432
+ function exportSurface(maxAudience, format, outPath, portalId) {
7070
7433
  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 } : {} };
7434
+ let renderedSet = format === "openapi" ? toOpenApiSet(snapshot) : void 0;
7435
+ if (renderedSet && portalId) renderedSet = selectPortalSpec(renderedSet, portalId);
7436
+ const writtenPaths = outPath ? writeSurfaceFile(outPath, snapshot, renderedSet) : [];
7437
+ return exportResult(snapshot, renderedSet, writtenPaths);
7083
7438
  }
7084
7439
  function importSurface(sourcePath, origin) {
7085
7440
  const resolved = path10.resolve(sourcePath);
@@ -7145,6 +7500,7 @@ var init_surfaces = __esm({
7145
7500
  path10 = __toESM(require("path"));
7146
7501
  init_fs();
7147
7502
  init_yaml();
7503
+ init_filenames();
7148
7504
  init_models();
7149
7505
  init_specs2();
7150
7506
  init_statehash();
@@ -8339,13 +8695,16 @@ var init_conformance = __esm({
8339
8695
  if (isInChainedSubproject(component.subsystem, ctx)) continue;
8340
8696
  const draft = ctx.isImplementationDraft(impl);
8341
8697
  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
- );
8698
+ const hasExternalSource = (component.externalLinks ?? []).some((l) => l.type === "implementation");
8699
+ if (!hasExternalSource) {
8700
+ ctx.addIssue(
8701
+ "warning",
8702
+ "MISSING_SOURCE_PATH",
8703
+ `Implementation "${impl.id}" declares no sourcePath \u2014 its contract "${impl.contract}" cannot be structurally checked against code.`,
8704
+ impl.id,
8705
+ draft
8706
+ );
8707
+ }
8349
8708
  continue;
8350
8709
  }
8351
8710
  const lookup = lookups.get(normalizeSourcePath(impl.sourcePath));
@@ -8734,13 +9093,14 @@ var init_portals = __esm({
8734
9093
  };
8735
9094
  portalsRule = {
8736
9095
  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.",
9096
+ 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
9097
  codes: [
8739
9098
  { code: "MISSING_PORTAL_TYPE", defaultSeverity: "error", summary: "Portal without a portalType" },
8740
9099
  { code: "MISSING_ENDPOINT", defaultSeverity: "error", summary: "Portal method without a wire endpoint binding" },
8741
9100
  { code: "ENDPOINT_TRANSPORT_MISMATCH", defaultSeverity: "error", summary: "Endpoint transport does not match the Portal portalType" },
8742
9101
  { 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" }
9102
+ { code: "ARCHITECTURE_VIOLATION_NON_PORTAL_ENDPOINT", defaultSeverity: "error", summary: "Non-Portal component method declaring an endpoint" },
9103
+ { code: "AUTH_ON_NON_PORTAL", defaultSeverity: "warning", summary: "Non-Portal component declaring auth (auth is inbound transport auth, only meaningful on a Portal)" }
8744
9104
  ],
8745
9105
  check(ctx) {
8746
9106
  for (const comp of ctx.components) {
@@ -8792,6 +9152,15 @@ var init_portals = __esm({
8792
9152
  isDraftCtx
8793
9153
  );
8794
9154
  }
9155
+ if (comp.auth !== void 0) {
9156
+ ctx.addIssue(
9157
+ "warning",
9158
+ "AUTH_ON_NON_PORTAL",
9159
+ `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.`,
9160
+ comp.id,
9161
+ isDraftCtx
9162
+ );
9163
+ }
8795
9164
  const compInterfaces = ctx.interfaces.filter((i) => i.component === comp.id);
8796
9165
  for (const intf of compInterfaces) {
8797
9166
  for (const m of intf.methods) {
@@ -11875,6 +12244,95 @@ var init_lint_allows = __esm({
11875
12244
  }
11876
12245
  });
11877
12246
 
12247
+ // src/core/rules/portal-call-auth.ts
12248
+ var COMPONENT_REF_PREFIX, portalCallAuthRule;
12249
+ var init_portal_call_auth = __esm({
12250
+ "src/core/rules/portal-call-auth.ts"() {
12251
+ "use strict";
12252
+ COMPONENT_REF_PREFIX = "component:";
12253
+ portalCallAuthRule = {
12254
+ name: "portal-call-auth",
12255
+ 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.",
12256
+ codes: [
12257
+ { code: "PORTAL_AUTH_UNMET", defaultSeverity: "warning", summary: "A narrative call into another component's authed Portal does not declare where its credential loads from" },
12258
+ { 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)" },
12259
+ { code: "UNKNOWN_AUTH_SOURCE", defaultSeverity: "warning", summary: "auth.from references a component: source that does not exist" },
12260
+ { code: "AUTH_SOURCE_NOT_PROVIDER", defaultSeverity: "warning", summary: "auth.from references a component that is not an Adapter or Store" },
12261
+ { code: "AUTH_SOURCE_UNWIRED", defaultSeverity: "warning", summary: "The presenter declares a component: credential source it does not depend on or own" }
12262
+ ],
12263
+ check(ctx) {
12264
+ for (const impl of ctx.implementations) {
12265
+ const ownComponent = ctx.interfaceMap.get(impl.contract)?.component;
12266
+ const presenter = ownComponent ? ctx.componentMap.get(ownComponent) : void 0;
12267
+ const draft = ctx.isImplementationDraft(impl);
12268
+ for (const method2 of impl.methods ?? []) {
12269
+ for (const step of method2.narrative ?? []) {
12270
+ if (step.type !== "call") continue;
12271
+ if (!step.targetComponent || step.targetComponent === ownComponent) continue;
12272
+ const target = ctx.componentMap.get(step.targetComponent);
12273
+ if (!target || target.componentType !== "Portal") continue;
12274
+ if (!target.auth || target.auth.scheme === "none") continue;
12275
+ if (presenter && presenter.componentType !== "Adapter") {
12276
+ ctx.addIssue(
12277
+ "warning",
12278
+ "AUTH_PRESENTER_NOT_ADAPTER",
12279
+ `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.`,
12280
+ impl.id,
12281
+ draft
12282
+ );
12283
+ }
12284
+ const from = step.auth?.from;
12285
+ if (!from) {
12286
+ ctx.addIssue(
12287
+ "warning",
12288
+ "PORTAL_AUTH_UNMET",
12289
+ `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.`,
12290
+ impl.id,
12291
+ draft
12292
+ );
12293
+ continue;
12294
+ }
12295
+ if (from.startsWith(COMPONENT_REF_PREFIX)) {
12296
+ const srcId = from.slice(COMPONENT_REF_PREFIX.length);
12297
+ const src = ctx.componentMap.get(srcId);
12298
+ if (!src) {
12299
+ ctx.addIssue(
12300
+ "warning",
12301
+ "UNKNOWN_AUTH_SOURCE",
12302
+ `Narrative step ${step.stepNumber} of "${method2.name}" in "${impl.id}": auth.from references component "${srcId}", which does not exist.`,
12303
+ impl.id,
12304
+ draft
12305
+ );
12306
+ } else {
12307
+ if (src.componentType !== "Adapter" && src.componentType !== "Store") {
12308
+ ctx.addIssue(
12309
+ "warning",
12310
+ "AUTH_SOURCE_NOT_PROVIDER",
12311
+ `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).`,
12312
+ impl.id,
12313
+ draft
12314
+ );
12315
+ }
12316
+ const wired = (presenter?.dependsOn ?? []).includes(srcId) || (presenter?.owns ?? []).includes(srcId);
12317
+ if (presenter && !wired) {
12318
+ ctx.addIssue(
12319
+ "warning",
12320
+ "AUTH_SOURCE_UNWIRED",
12321
+ `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.`,
12322
+ impl.id,
12323
+ draft
12324
+ );
12325
+ }
12326
+ }
12327
+ }
12328
+ }
12329
+ }
12330
+ }
12331
+ }
12332
+ };
12333
+ }
12334
+ });
12335
+
11878
12336
  // src/core/rules/index.ts
11879
12337
  function makeScopeFilter(opts) {
11880
12338
  const { components, interfaces, implementations, types, scopeSubsystem } = opts;
@@ -12120,6 +12578,7 @@ var init_rules = __esm({
12120
12578
  init_hidden_state();
12121
12579
  init_dependency_conformance();
12122
12580
  init_lint_allows();
12581
+ init_portal_call_auth();
12123
12582
  init_source_analysis();
12124
12583
  init_types();
12125
12584
  init_type_analysis();
@@ -12141,6 +12600,9 @@ var init_rules = __esm({
12141
12600
  narrativeAntipatternsRule,
12142
12601
  narrativeDetailRule,
12143
12602
  portalsRule,
12603
+ // Cross-call auth: a narrative call into an authed Portal must name its
12604
+ // credential source (rides with the portal family).
12605
+ portalCallAuthRule,
12144
12606
  stereotypeDepsRule,
12145
12607
  patternsRule,
12146
12608
  // Facade shape rides with pattern ownership: same §7 doctrine, narrative side.
@@ -12203,6 +12665,7 @@ var init_rules = __esm({
12203
12665
  // L4 expectations: implementations and their code linkage.
12204
12666
  MISSING_IMPLEMENTATION_METHOD: "implementations",
12205
12667
  MISSING_SOURCE_PATH: "implementations",
12668
+ PORTAL_AUTH_UNMET: "implementations",
12206
12669
  MISSING_SOURCE_FILE: "implementations",
12207
12670
  SOURCE_PATH_ESCAPES_ROOT: "implementations",
12208
12671
  UNREALIZED_METHOD: "implementations",
@@ -12608,6 +13071,28 @@ function buildGraphModel(level) {
12608
13071
  ...t.subsystem ? { parentId: t.subsystem } : {}
12609
13072
  });
12610
13073
  }
13074
+ const componentByInterface = /* @__PURE__ */ new Map();
13075
+ for (const c of model.components) {
13076
+ for (const intf of c.interfaces) componentByInterface.set(intf.id, c.id);
13077
+ }
13078
+ for (const impl of loadImplementationSpecs()) {
13079
+ const componentId = componentByInterface.get(impl.contract);
13080
+ if (!componentId) continue;
13081
+ nodes.push({
13082
+ id: impl.id,
13083
+ label: impl.name,
13084
+ kind: "implementation",
13085
+ level: 4,
13086
+ parentId: componentId,
13087
+ ...impl.status ? { status: impl.status } : {}
13088
+ });
13089
+ candidates.push({ from: componentId, to: impl.id, edgeKind: "owns" });
13090
+ }
13091
+ for (const c of model.components) {
13092
+ for (const memberId of c.owns) {
13093
+ candidates.push({ from: c.id, to: memberId, edgeKind: "owns" });
13094
+ }
13095
+ }
12611
13096
  for (const e of model.edges) {
12612
13097
  candidates.push({ from: e.from, to: e.to, edgeKind: "depends_on" });
12613
13098
  }
@@ -16744,6 +17229,7 @@ NOTICE:
16744
17229
  type: import_zod9.z.enum(["local", "call", "dispatch", "branch", "switch", "loop", "try", "parallel", "jump", "return", "throw"]),
16745
17230
  targetComponent: import_zod9.z.string().optional().describe("call/dispatch: L2 component id (for dispatch, the Portal routed through)"),
16746
17231
  targetMethod: import_zod9.z.string().optional().describe("call: method name on the target"),
17232
+ 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
17233
  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
17234
  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
17235
  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 +25942,7 @@ init_validation();
25456
25942
  init_diagram();
25457
25943
  init_loader();
25458
25944
  init_extensions();
25945
+ init_types();
25459
25946
  init_server();
25460
25947
 
25461
25948
  // src/git/config.ts
@@ -25976,7 +26463,11 @@ var hostCore = {
25976
26463
  checkDeclarativePack: (raw) => {
25977
26464
  const result = DeclarativePackSchema.safeParse(raw);
25978
26465
  return result.success ? null : result.error.issues[0]?.message ?? "shape mismatch";
25979
- }
26466
+ },
26467
+ /** The ids of wairon's built-in architectural profiles, read from the core rules
26468
+ * registry's built-in profile set (BUILTIN_PROFILES) — a pure, side-effect-free
26469
+ * read of a bundled constant. */
26470
+ builtinProfileIds: () => [...BUILTIN_PROFILES]
25980
26471
  };
25981
26472
  function validateProjectAsComplete() {
25982
26473
  const config = loadProjectConfig();
@@ -26385,7 +26876,12 @@ function probe(loadRef, baseRoot, scope, displayRef) {
26385
26876
  ref: displayRef,
26386
26877
  profiles: Object.keys(loaded.profiles).length,
26387
26878
  languages: Object.keys(loaded.languages).length,
26388
- rules: loaded.rules.length
26879
+ rules: loaded.rules.length,
26880
+ // Structured id lists paralleling the counts above — a UI can present a
26881
+ // pack's profiles/languages/rules as choices instead of a bare number.
26882
+ profileIds: Object.keys(loaded.profiles),
26883
+ languageIds: Object.keys(loaded.languages),
26884
+ ruleIds: loaded.rules.map((r) => r.name)
26389
26885
  };
26390
26886
  }
26391
26887
  function writeFileAtomic(file, content) {
@@ -26415,6 +26911,31 @@ function storeListGlobalPacks() {
26415
26911
  );
26416
26912
  return [...instance, ...image];
26417
26913
  }
26914
+ function storeListAvailableProfiles() {
26915
+ const out = [];
26916
+ const seen = /* @__PURE__ */ new Set();
26917
+ const emit = (id, source, family) => {
26918
+ const key = JSON.stringify([id, source]);
26919
+ if (seen.has(key)) return;
26920
+ seen.add(key);
26921
+ out.push(family ? { id, source, family } : { id, source });
26922
+ };
26923
+ for (const id of hostCore.builtinProfileIds()) emit(id, "builtin");
26924
+ const scanTier = (dir) => {
26925
+ for (const full of hostCore.discoverPacks(dir)) {
26926
+ try {
26927
+ const loaded = hostCore.loadExtensionPacks([{ ref: full, scope: "global" }], path45.dirname(full));
26928
+ if (loaded.errors.length) continue;
26929
+ const source = loaded.packNames[0] ?? path45.basename(full);
26930
+ for (const [id, def] of Object.entries(loaded.profiles)) emit(id, source, def.family);
26931
+ } catch {
26932
+ }
26933
+ }
26934
+ };
26935
+ scanTier(hostCore.globalPacksDir());
26936
+ scanTier(imagePacksDir());
26937
+ return out;
26938
+ }
26418
26939
  function readPackContent(full) {
26419
26940
  const st = fs36.statSync(full);
26420
26941
  if (st.isFile()) return fs36.readFileSync(full, "utf8");
@@ -26601,6 +27122,23 @@ function installProjectPack(cfg, credential, project2, name, content) {
26601
27122
  requireCap(cfg, credential, "project:admin", "project", project2, "Forbidden \u2014 installing a project pack requires project:admin over the project");
26602
27123
  return executeApprovedInstallProjectPack(cfg, project2, name, content);
26603
27124
  }
27125
+ function listAvailableProfiles(cfg, credential) {
27126
+ requirePrincipal2(cfg, credential);
27127
+ return storeListAvailableProfiles();
27128
+ }
27129
+ function listAdoptableProjectPacks(cfg, credential, project2) {
27130
+ requireCap(cfg, credential, "project:read", "project", project2, "Forbidden \u2014 listing adoptable packs requires project:read over the project");
27131
+ return storeListGlobalPacks();
27132
+ }
27133
+ function adoptProjectPack(cfg, credential, project2, name) {
27134
+ requireCap(cfg, credential, "project:admin", "project", project2, "Forbidden \u2014 adopting a project pack requires project:admin over the project");
27135
+ const resolution = executeApprovedResolveGlobalPacks([name]);
27136
+ if (resolution.resolved.length === 0) {
27137
+ throw new Error(`no such server-global pack "${name}" \u2014 cannot adopt a pack the instance does not carry`);
27138
+ }
27139
+ const resolved = resolution.resolved[0];
27140
+ return executeApprovedInstallProjectPack(cfg, project2, resolved.name, resolved.content);
27141
+ }
26604
27142
  function executeApprovedListProjectPacks(cfg, project2) {
26605
27143
  return runWithProjectRoot(boundProject2(cfg, project2), () => storeListProjectPacks());
26606
27144
  }
@@ -27521,6 +28059,7 @@ function removeIdentityProviderRecord(dataDir, id) {
27521
28059
  }
27522
28060
  var PROJECT_CREATE_CAPABILITY = "project:create";
27523
28061
  var PROJECT_WRITE_CAPABILITY = "project:write";
28062
+ var PROJECT_READ_CAPABILITY = "project:read";
27524
28063
  var POLICY_MANAGE_CAPABILITY = "project:admin";
27525
28064
  function carriesInstancePermission(cfg, principal, capability) {
27526
28065
  return authorize(cfg.dataDir, principal, capability, "instance", "").value === "yes";
@@ -27585,6 +28124,19 @@ function recordProjectProfileSelection(root, selection) {
27585
28124
  writeYamlFile(AI_PATHS.projectConfig(), raw);
27586
28125
  });
27587
28126
  }
28127
+ function readProjectType(root) {
28128
+ return runWithProjectRoot(root, () => {
28129
+ const raw = readYamlFile(AI_PATHS.projectConfig());
28130
+ return raw?.projectType ?? "backend";
28131
+ });
28132
+ }
28133
+ function writeProjectType(root, projectType) {
28134
+ runWithProjectRoot(root, () => {
28135
+ const raw = readYamlFile(AI_PATHS.projectConfig()) ?? {};
28136
+ raw["projectType"] = projectType;
28137
+ writeYamlFile(AI_PATHS.projectConfig(), raw);
28138
+ });
28139
+ }
27588
28140
  function requestPackNames(request) {
27589
28141
  const sel = request.profileSelection;
27590
28142
  if (!sel) return [];
@@ -27779,6 +28331,32 @@ function reconcileProjectPolicy(cfg, credential, projectId) {
27779
28331
  );
27780
28332
  return result;
27781
28333
  }
28334
+ function getProjectConfig(cfg, credential, projectId) {
28335
+ const principal = requirePrincipal4(cfg, credential);
28336
+ if (authorize(cfg.dataDir, principal, PROJECT_READ_CAPABILITY, "project", projectId).value !== "yes") {
28337
+ throw new ForbiddenError(
28338
+ "reading a project's configuration requires project:read over the project"
28339
+ );
28340
+ }
28341
+ const root = resolveProjectRoot(cfg.dataDir, principal, projectId);
28342
+ if (!root) throw new Error(`Unknown project "${projectId}".`);
28343
+ const projectType = readProjectType(root);
28344
+ const locked = runWithProjectRoot(root, () => hostCore.readLockRecord() !== null);
28345
+ return { projectType, locked };
28346
+ }
28347
+ function setProjectType(cfg, credential, projectId, projectType) {
28348
+ const principal = requirePrincipal4(cfg, credential);
28349
+ if (authorize(cfg.dataDir, principal, PROJECT_WRITE_CAPABILITY, "project", projectId).value !== "yes") {
28350
+ throw new ForbiddenError(
28351
+ "changing a project's configuration requires project:write over the project"
28352
+ );
28353
+ }
28354
+ const root = resolveProjectRoot(cfg.dataDir, principal, projectId);
28355
+ if (!root) throw new Error(`Unknown project "${projectId}".`);
28356
+ writeProjectType(root, projectType);
28357
+ const locked = runWithProjectRoot(root, () => hostCore.readLockRecord() !== null);
28358
+ return { projectType, locked };
28359
+ }
27782
28360
  function getPackPolicy(cfg, credential) {
27783
28361
  requirePrincipal4(cfg, credential);
27784
28362
  return getPackPolicyRecord(cfg.dataDir) ?? PERMISSIVE_DEFAULT_POLICY;
@@ -27842,7 +28420,7 @@ function handlePolicyRequest(cfg, credential, req, res, body, url) {
27842
28420
 
27843
28421
  // src/server/identity.ts
27844
28422
  var PROJECT_ADMIN_CAPABILITY2 = "project:admin";
27845
- var PROJECT_READ_CAPABILITY = "project:read";
28423
+ var PROJECT_READ_CAPABILITY2 = "project:read";
27846
28424
  var PROJECT_WRITE_CAPABILITY2 = "project:write";
27847
28425
  function requireInstanceProjectAdmin(cfg, principal, what) {
27848
28426
  if (authorize(cfg.dataDir, principal, PROJECT_ADMIN_CAPABILITY2, "instance", "").value !== "yes") {
@@ -27988,7 +28566,7 @@ function revokeToken(cfg, credential, tokenId) {
27988
28566
  }
27989
28567
  function mintSelfToken(cfg, credential, projectId, write) {
27990
28568
  const principal = requirePrincipal5(cfg, credential);
27991
- if (authorize(cfg.dataDir, principal, PROJECT_READ_CAPABILITY, "project", projectId).value !== "yes") {
28569
+ if (authorize(cfg.dataDir, principal, PROJECT_READ_CAPABILITY2, "project", projectId).value !== "yes") {
27992
28570
  throw new ForbiddenError("caller lacks project:read on the requested project");
27993
28571
  }
27994
28572
  if (write && authorize(cfg.dataDir, principal, PROJECT_WRITE_CAPABILITY2, "project", projectId).value !== "yes") {
@@ -28633,8 +29211,9 @@ function audienceDistance(resolution, targetProjectId) {
28633
29211
 
28634
29212
  // src/server/landscape.ts
28635
29213
  var yamlLib = __toESM(require("js-yaml"));
29214
+ init_filenames();
28636
29215
  var PROJECT_ADMIN_CAPABILITY3 = "project:admin";
28637
- var PROJECT_READ_CAPABILITY2 = "project:read";
29216
+ var PROJECT_READ_CAPABILITY3 = "project:read";
28638
29217
  var PROJECT_WRITE_CAPABILITY3 = "project:write";
28639
29218
  function requirePrincipal6(cfg, credential) {
28640
29219
  const principal = authenticateCredential(cfg.dataDir, credential);
@@ -28648,7 +29227,7 @@ function readView(cfg, principal) {
28648
29227
  if (isInstanceAdmin(principal)) {
28649
29228
  return { all: true, projectIds: /* @__PURE__ */ new Set(), unitIds: /* @__PURE__ */ new Set(), contextUnitIds: /* @__PURE__ */ new Set() };
28650
29229
  }
28651
- const read = visibleScopes(cfg.dataDir, principal, PROJECT_READ_CAPABILITY2);
29230
+ const read = visibleScopes(cfg.dataDir, principal, PROJECT_READ_CAPABILITY3);
28652
29231
  const admin = visibleScopes(cfg.dataDir, principal, PROJECT_ADMIN_CAPABILITY3);
28653
29232
  const unitIds = /* @__PURE__ */ new Set([...actionableUnitIds(read), ...actionableUnitIds(admin)]);
28654
29233
  const contextUnitIds = new Set(
@@ -28663,13 +29242,13 @@ function readView(cfg, principal) {
28663
29242
  }
28664
29243
  function requireLandscapeReach(cfg, principal, view, what) {
28665
29244
  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", "")) {
29245
+ if (permitsCap(cfg, principal, PROJECT_READ_CAPABILITY3, "instance", "") || permitsCap(cfg, principal, PROJECT_ADMIN_CAPABILITY3, "instance", "")) {
28667
29246
  return;
28668
29247
  }
28669
29248
  throw new ForbiddenError(`${what} requires project:read reach`);
28670
29249
  }
28671
29250
  function requireObserverScope(cfg, principal, observerProjectId) {
28672
- const covered = permitsCap(cfg, principal, PROJECT_READ_CAPABILITY2, "project", observerProjectId) || permitsCap(cfg, principal, PROJECT_WRITE_CAPABILITY3, "project", observerProjectId);
29251
+ const covered = permitsCap(cfg, principal, PROJECT_READ_CAPABILITY3, "project", observerProjectId) || permitsCap(cfg, principal, PROJECT_WRITE_CAPABILITY3, "project", observerProjectId);
28673
29252
  if (!covered) {
28674
29253
  throw new ForbiddenError("caller lacks scope over the current project");
28675
29254
  }
@@ -29119,7 +29698,7 @@ function getProjectSurfaceForMcp(cfg, credential, currentProjectId, targetProjec
29119
29698
  const result = runWithProjectRoot(record2.rootPath, () => hostSurfaces.exportBoundSurface(maxAudience, "native"));
29120
29699
  return { ...result.snapshot, origin: "exchanged" };
29121
29700
  }
29122
- function exportProjectSurface(cfg, credential, projectId, format, maxAudience) {
29701
+ function exportProjectSurface(cfg, credential, projectId, format, maxAudience, portalId) {
29123
29702
  const principal = requirePrincipal6(cfg, credential);
29124
29703
  if (!permitsCap(cfg, principal, PROJECT_ADMIN_CAPABILITY3, "project", projectId)) {
29125
29704
  throw new ForbiddenError(
@@ -29133,16 +29712,41 @@ function exportProjectSurface(cfg, credential, projectId, format, maxAudience) {
29133
29712
  if (!root) throw new Error(`Unknown project "${projectId}".`);
29134
29713
  const result = runWithProjectRoot(root, () => hostSurfaces.exportBoundSurface(maxAudience, format));
29135
29714
  if (format === "openapi") {
29715
+ const specs = result.renderedSet ?? [];
29716
+ if (portalId) {
29717
+ const hit = specs.find((s) => s.portalId === portalId);
29718
+ if (!hit) {
29719
+ throw new Error(
29720
+ `Unknown portal "${portalId}" in project "${projectId}" (published: ${specs.map((s) => s.portalId).join(", ") || "none"}).`
29721
+ );
29722
+ }
29723
+ return {
29724
+ body: hit.document,
29725
+ contentType: "application/json",
29726
+ filename: `${safeFilenamePart(projectId)}-${safeFilenamePart(portalId)}-surface.openapi.json`
29727
+ };
29728
+ }
29729
+ if (specs.length === 1) {
29730
+ return {
29731
+ body: specs[0].document,
29732
+ contentType: "application/json",
29733
+ filename: `${safeFilenamePart(projectId)}-surface.openapi.json`
29734
+ };
29735
+ }
29136
29736
  return {
29137
- body: result.rendered ?? "{}",
29737
+ body: JSON.stringify(
29738
+ { openapiIndex: true, specs: specs.map((s) => ({ portalId: s.portalId, name: s.name })) },
29739
+ null,
29740
+ 2
29741
+ ),
29138
29742
  contentType: "application/json",
29139
- filename: `${projectId}-surface.openapi.json`
29743
+ filename: `${safeFilenamePart(projectId)}-surface.openapi.index.json`
29140
29744
  };
29141
29745
  }
29142
29746
  return {
29143
29747
  body: yamlLib.dump(result.snapshot),
29144
29748
  contentType: "application/yaml",
29145
- filename: `${projectId}-surface.yaml`
29749
+ filename: `${safeFilenamePart(projectId)}-surface.yaml`
29146
29750
  };
29147
29751
  }
29148
29752
  function removeRelation(cfg, credential, id) {
@@ -29186,7 +29790,8 @@ function handleLandscapeRequest(cfg, credential, req, res, body, url) {
29186
29790
  credential,
29187
29791
  parts[2],
29188
29792
  url.searchParams.get("format") ?? "native",
29189
- url.searchParams.get("audience") ?? "instance"
29793
+ url.searchParams.get("audience") ?? "instance",
29794
+ url.searchParams.get("spec") ?? void 0
29190
29795
  );
29191
29796
  res.writeHead(200, {
29192
29797
  "content-type": artifact.contentType,
@@ -30706,6 +31311,15 @@ function installGlobalPackArchive2(cfg, credential, archive, name) {
30706
31311
  function installProjectPackArchive2(cfg, credential, project2, archive, name) {
30707
31312
  return installProjectPackArchive(cfg, credential, project2, archive, name);
30708
31313
  }
31314
+ function listAvailableProfiles2(cfg, credential) {
31315
+ return listAvailableProfiles(cfg, credential);
31316
+ }
31317
+ function listAdoptableProjectPacks2(cfg, credential, project2) {
31318
+ return listAdoptableProjectPacks(cfg, credential, project2);
31319
+ }
31320
+ function adoptProjectPack2(cfg, credential, project2, name) {
31321
+ return adoptProjectPack(cfg, credential, project2, name);
31322
+ }
30709
31323
  function getPackPolicy2(cfg, credential) {
30710
31324
  return getPackPolicy(cfg, credential);
30711
31325
  }
@@ -30718,6 +31332,12 @@ function evaluateProjectPolicy2(cfg, credential, projectId) {
30718
31332
  function reconcileProjectPolicy2(cfg, credential, projectId) {
30719
31333
  return reconcileProjectPolicy(cfg, credential, projectId);
30720
31334
  }
31335
+ function getProjectConfig2(cfg, credential, projectId) {
31336
+ return getProjectConfig(cfg, credential, projectId);
31337
+ }
31338
+ function setProjectType2(cfg, credential, projectId, projectType) {
31339
+ return setProjectType(cfg, credential, projectId, projectType);
31340
+ }
30721
31341
  function listProducers2(cfg, credential, project2) {
30722
31342
  return listProducers(cfg, credential, project2);
30723
31343
  }
@@ -31246,6 +31866,26 @@ var ShareSnapshotRegistry = class {
31246
31866
  return stored;
31247
31867
  }
31248
31868
  };
31869
+ function openApiIndexDocument(specs) {
31870
+ return JSON.stringify(
31871
+ { openapiIndex: true, specs: specs.map((s) => ({ portalId: s.portalId, name: s.name })) },
31872
+ null,
31873
+ 2
31874
+ );
31875
+ }
31876
+ function selectCapturedOpenApi(snap, portalId) {
31877
+ const specs = snap.openapiSet;
31878
+ if (!specs || specs.length === 0) {
31879
+ if (portalId) return null;
31880
+ return snap.openapi ?? null;
31881
+ }
31882
+ if (portalId) {
31883
+ const hit = specs.find((s) => s.portalId === portalId);
31884
+ return hit ? hit.document : null;
31885
+ }
31886
+ if (specs.length === 1) return specs[0].document;
31887
+ return openApiIndexDocument(specs);
31888
+ }
31249
31889
  var ShareSnapshotIndex = class {
31250
31890
  constructor(store) {
31251
31891
  this.store = store;
@@ -31253,12 +31893,12 @@ var ShareSnapshotIndex = class {
31253
31893
  get(snapshotId) {
31254
31894
  return this.store.read(snapshotId);
31255
31895
  }
31256
- getArtifact(snapshotId, kind) {
31896
+ getArtifact(snapshotId, kind, portalId) {
31257
31897
  const snap = this.store.read(snapshotId);
31258
31898
  if (!snap) return null;
31259
31899
  if (kind === "canvas") return snap.canvasModel ?? null;
31260
31900
  if (kind === "html") return snap.html ?? null;
31261
- if (kind === "openapi") return snap.openapi ?? null;
31901
+ if (kind === "openapi") return selectCapturedOpenApi(snap, portalId);
31262
31902
  return null;
31263
31903
  }
31264
31904
  };
@@ -31268,8 +31908,8 @@ function putSnapshot(dataDir, snapshot) {
31268
31908
  function getSnapshot2(dataDir, snapshotId) {
31269
31909
  return new ShareSnapshotIndex(new ShareSnapshotStore(dataDir)).get(snapshotId);
31270
31910
  }
31271
- function getSnapshotArtifact(dataDir, snapshotId, kind) {
31272
- return new ShareSnapshotIndex(new ShareSnapshotStore(dataDir)).getArtifact(snapshotId, kind);
31911
+ function getSnapshotArtifact(dataDir, snapshotId, kind, portalId) {
31912
+ return new ShareSnapshotIndex(new ShareSnapshotStore(dataDir)).getArtifact(snapshotId, kind, portalId);
31273
31913
  }
31274
31914
  function captureSnapshot(dataDir, principal, projectId, view, artifacts) {
31275
31915
  const root = resolveProjectRoot(dataDir, principal, projectId);
@@ -31288,7 +31928,11 @@ function captureSnapshot(dataDir, principal, projectId, view, artifacts) {
31288
31928
  }
31289
31929
  if (artifacts.includes("openapi")) {
31290
31930
  const result = hostSurfaces.exportBoundSurface("project", "openapi");
31291
- snapshot.openapi = result.rendered ?? "{}";
31931
+ const specs = result.renderedSet ?? [];
31932
+ if (specs.length) {
31933
+ snapshot.openapiSet = specs;
31934
+ if (specs.length === 1) snapshot.openapi = specs[0].document;
31935
+ }
31292
31936
  }
31293
31937
  return snapshot;
31294
31938
  });
@@ -31839,7 +32483,7 @@ function getWebProjectCanvasModel(cfg, sessionId, projectId) {
31839
32483
  }
31840
32484
  return runWithProjectRoot(root, () => hostCore.buildCanvasDataModel());
31841
32485
  }
31842
- function getWebProjectOpenApi(cfg, sessionId, projectId) {
32486
+ function getWebProjectOpenApi(cfg, sessionId, projectId, portalId) {
31843
32487
  const principal = authenticateSession(cfg.dataDir, sessionId);
31844
32488
  if (!principal.authenticated) throw new UnauthenticatedError();
31845
32489
  if (!listProjects2(cfg, sessionId).some((r) => r.id === projectId)) {
@@ -31848,7 +32492,19 @@ function getWebProjectOpenApi(cfg, sessionId, projectId) {
31848
32492
  const root = resolveProjectRoot(cfg.dataDir, principal, projectId);
31849
32493
  if (!root) throw new ForbiddenError("project not authorized or unknown");
31850
32494
  const result = runWithProjectRoot(root, () => hostSurfaces.exportBoundSurface("project", "openapi"));
31851
- return swaggerUiPage(result.rendered ?? "{}", projectId);
32495
+ const specs = result.renderedSet ?? (result.rendered ? [{ portalId: "", name: projectId, document: result.rendered }] : []);
32496
+ if (specs.length === 0) return openApiIndexPage(projectId, []);
32497
+ if (portalId) {
32498
+ const sel = specs.find((s) => s.portalId === portalId);
32499
+ return sel ? swaggerUiPage(sel.document, sel.name) : openApiIndexPage(projectId, specs);
32500
+ }
32501
+ if (specs.length === 1) return swaggerUiPage(specs[0].document, specs[0].name);
32502
+ return openApiIndexPage(projectId, specs);
32503
+ }
32504
+ function openApiIndexPage(projectId, specs) {
32505
+ const esc2 = (s) => s.replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
32506
+ 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("");
32507
+ 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>` + (specs.length ? `<p>This project exposes ${specs.length} separate public APIs, each with its own OpenAPI document and auth:</p><ul>${items}</ul>` : `<p>This project publishes no HTTP API \u2014 no public portal exposes one.</p>`) + `</body></html>`;
31852
32508
  }
31853
32509
  function reshapeLandscapeGraph(model, level) {
31854
32510
  const unitNodeIds = new Set(model.nodes.filter((n) => n.nodeKind === "orgUnit").map((n) => n.id));
@@ -33999,6 +34655,15 @@ function opsInstallGlobalPackArchive(cfg, sessionId, req, body, res) {
33999
34655
  function opsInstallProjectPackArchive(cfg, sessionId, req, url, body, res) {
34000
34656
  sendJson(res, 200, installProjectPackArchive2(cfg, sessionId, q(url, "projectId") ?? "", archiveBody(body), packNameOverride(req)));
34001
34657
  }
34658
+ function opsListAvailableProfiles(cfg, sessionId, res) {
34659
+ sendJson(res, 200, { profiles: listAvailableProfiles2(cfg, sessionId) });
34660
+ }
34661
+ function opsListAdoptableProjectPacks(cfg, sessionId, url, res) {
34662
+ sendJson(res, 200, { packs: listAdoptableProjectPacks2(cfg, sessionId, q(url, "projectId") ?? "") });
34663
+ }
34664
+ function opsAdoptProjectPack(cfg, sessionId, body, res) {
34665
+ sendJson(res, 200, adoptProjectPack2(cfg, sessionId, String(body?.projectId ?? ""), String(body?.name ?? "")));
34666
+ }
34002
34667
  function opsGetPackPolicy(cfg, sessionId, res) {
34003
34668
  sendJson(res, 200, getPackPolicy2(cfg, sessionId));
34004
34669
  }
@@ -34011,6 +34676,12 @@ function opsPolicyEvaluate(cfg, sessionId, url, res) {
34011
34676
  function opsPolicyReconcile(cfg, sessionId, body, res) {
34012
34677
  sendJson(res, 200, reconcileProjectPolicy2(cfg, sessionId, String(body?.projectId ?? "")));
34013
34678
  }
34679
+ function opsGetProjectConfig(cfg, sessionId, url, res) {
34680
+ sendJson(res, 200, getProjectConfig2(cfg, sessionId, q(url, "projectId") ?? ""));
34681
+ }
34682
+ function opsSetProjectConfig(cfg, sessionId, body, res) {
34683
+ sendJson(res, 200, setProjectType2(cfg, sessionId, String(body?.projectId ?? ""), String(body?.projectType ?? "")));
34684
+ }
34014
34685
  function opsListProducers(cfg, sessionId, url, res) {
34015
34686
  sendJson(res, 200, { producers: listProducers2(cfg, sessionId, q(url, "projectId") ?? "") });
34016
34687
  }
@@ -34195,7 +34866,8 @@ async function handleWebRequest(cfg, req, res, body, url, ctx) {
34195
34866
  }
34196
34867
  if (req.method === "GET" && parts.length === 2 && parts[1] === "openapi") {
34197
34868
  const projectId = url.searchParams.get("projectId") ?? "";
34198
- const html = getWebProjectOpenApi(cfg, sessionId, projectId);
34869
+ const spec = url.searchParams.get("spec") ?? void 0;
34870
+ const html = getWebProjectOpenApi(cfg, sessionId, projectId, spec);
34199
34871
  res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
34200
34872
  res.end(html);
34201
34873
  return;
@@ -34237,6 +34909,18 @@ async function handleWebRequest(cfg, req, res, body, url, ctx) {
34237
34909
  if (req.method === "POST" && parts.length === 4 && parts[2] === "packs" && parts[3] === "remove") {
34238
34910
  return opsRemoveProjectPack(cfg, sessionId, body, res);
34239
34911
  }
34912
+ if (req.method === "GET" && parts.length === 4 && parts[2] === "packs" && parts[3] === "adoptable") {
34913
+ return opsListAdoptableProjectPacks(cfg, sessionId, url, res);
34914
+ }
34915
+ if (req.method === "POST" && parts.length === 4 && parts[2] === "packs" && parts[3] === "adopt") {
34916
+ return opsAdoptProjectPack(cfg, sessionId, body, res);
34917
+ }
34918
+ if (req.method === "GET" && parts.length === 3 && parts[2] === "config") {
34919
+ return opsGetProjectConfig(cfg, sessionId, url, res);
34920
+ }
34921
+ if (req.method === "POST" && parts.length === 3 && parts[2] === "config") {
34922
+ return opsSetProjectConfig(cfg, sessionId, body, res);
34923
+ }
34240
34924
  if (req.method === "GET" && parts.length === 3 && parts[2] === "policy") {
34241
34925
  return opsPolicyEvaluate(cfg, sessionId, url, res);
34242
34926
  }
@@ -34350,6 +35034,9 @@ async function handleWebRequest(cfg, req, res, body, url, ctx) {
34350
35034
  if (req.method === "POST" && parts.length === 4 && parts[2] === "packs" && parts[3] === "remove") {
34351
35035
  return opsRemoveGlobalPack(cfg, sessionId, body, res);
34352
35036
  }
35037
+ if (req.method === "GET" && parts.length === 3 && parts[2] === "profiles") {
35038
+ return opsListAvailableProfiles(cfg, sessionId, res);
35039
+ }
34353
35040
  if (req.method === "GET" && parts.length === 3 && parts[2] === "policy") {
34354
35041
  return opsGetPackPolicy(cfg, sessionId, res);
34355
35042
  }
@@ -34943,7 +35630,7 @@ var CONTENT_TYPE = {
34943
35630
  openapi: "application/json",
34944
35631
  canvas: "application/json"
34945
35632
  };
34946
- function downloadArtifact(cfg, token, kind, meta) {
35633
+ function downloadArtifact(cfg, token, kind, meta, portalId) {
34947
35634
  const link = linkByTokenHash(cfg.dataDir, hashToken(token));
34948
35635
  const check = usable(link);
34949
35636
  if ("outcome" in check) {
@@ -34955,7 +35642,7 @@ function downloadArtifact(cfg, token, kind, meta) {
34955
35642
  record(cfg.dataDir, check.link.id, meta, "denied-download");
34956
35643
  return { found: false, outcome: "denied-download" };
34957
35644
  }
34958
- const content = getSnapshotArtifact(cfg.dataDir, check.link.snapshotId, kind);
35645
+ const content = getSnapshotArtifact(cfg.dataDir, check.link.snapshotId, kind, portalId);
34959
35646
  if (content === null) {
34960
35647
  record(cfg.dataDir, check.link.id, meta, "not-found");
34961
35648
  return { found: false, outcome: "not-found" };
@@ -34965,6 +35652,35 @@ function downloadArtifact(cfg, token, kind, meta) {
34965
35652
  }
34966
35653
 
34967
35654
  // src/server/sharehttp.ts
35655
+ init_filenames();
35656
+ function specParam(req) {
35657
+ const url = req.url ?? "";
35658
+ const q2 = url.indexOf("?");
35659
+ if (q2 < 0) return void 0;
35660
+ return new URLSearchParams(url.slice(q2 + 1)).get("spec") || void 0;
35661
+ }
35662
+ function asOpenApiIndex(payload) {
35663
+ try {
35664
+ const parsed = JSON.parse(payload);
35665
+ return parsed && parsed.openapiIndex === true && Array.isArray(parsed.specs) ? parsed : null;
35666
+ } catch {
35667
+ return null;
35668
+ }
35669
+ }
35670
+ function escapeHtml2(s) {
35671
+ return s.replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
35672
+ }
35673
+ function sharedOpenApiIndexPage(index) {
35674
+ const items = index.specs.map(
35675
+ (s) => `<li><a href="?spec=${encodeURIComponent(s.portalId)}">${escapeHtml2(s.name)}</a><code>${escapeHtml2(s.portalId)}</code></li>`
35676
+ ).join("");
35677
+ return `<!doctype html><html><head><meta charset="utf-8"><title>Shared APIs</title><meta name="viewport" content="width=device-width, initial-scale=1"><style>body{font:15px/1.6 system-ui,sans-serif;max-width:680px;margin:48px auto;padding:0 20px;background:#0b1120;color:#e8e8f0}h1{font-size:20px}a{color:#6ea8fe;text-decoration:none}a:hover{text-decoration:underline}li{margin:10px 0}code{color:#9a9aab;font-size:12px;margin-left:8px}p{color:#9a9aab}</style></head><body><h1>Shared APIs</h1><p>This share exposes ${index.specs.length} separate APIs, each with its own OpenAPI document and auth:</p><ul>${items}</ul></body></html>`;
35678
+ }
35679
+ function downloadFilename(kind, portalId, payload) {
35680
+ if (kind !== "openapi") return `shared-canvas.${safeFilenamePart(kind)}`;
35681
+ if (portalId) return `shared-canvas.${safeFilenamePart(portalId)}.openapi.json`;
35682
+ return asOpenApiIndex(payload) ? "shared-canvas.openapi.index.json" : "shared-canvas.openapi.json";
35683
+ }
34968
35684
  function shareRequestMeta(req) {
34969
35685
  const fwd = req.headers["x-forwarded-for"];
34970
35686
  const ip = (Array.isArray(fwd) ? fwd[0] : fwd)?.split(",")[0].trim() || req.socket?.remoteAddress || "unknown";
@@ -35016,14 +35732,16 @@ function serveSharedModel(cfg, token, req, res) {
35016
35732
  );
35017
35733
  }
35018
35734
  function serveSharedOpenApi(cfg, token, req, res) {
35019
- const result = downloadArtifact(cfg, token, "openapi", shareRequestMeta(req));
35735
+ const result = downloadArtifact(cfg, token, "openapi", shareRequestMeta(req), specParam(req));
35020
35736
  if (!result.found || result.content === void 0) return notFoundPage(res);
35737
+ const index = asOpenApiIndex(result.content);
35021
35738
  harden(res, void 0, "text/html; charset=utf-8");
35022
35739
  res.statusCode = 200;
35023
- res.end(swaggerUiPage(result.content, "Shared API"));
35740
+ res.end(index ? sharedOpenApiIndexPage(index) : swaggerUiPage(result.content, "Shared API"));
35024
35741
  }
35025
35742
  function serveSharedDownload(cfg, token, kind, req, res) {
35026
- const result = downloadArtifact(cfg, token, kind, shareRequestMeta(req));
35743
+ const portalId = specParam(req);
35744
+ const result = downloadArtifact(cfg, token, kind, shareRequestMeta(req), portalId);
35027
35745
  if (!result.found || result.content === void 0) {
35028
35746
  if (result.outcome === "denied-download") {
35029
35747
  harden(res, void 0, "text/plain");
@@ -35033,9 +35751,11 @@ function serveSharedDownload(cfg, token, kind, req, res) {
35033
35751
  }
35034
35752
  return notFoundPage(res);
35035
35753
  }
35036
- const ext = kind === "html" ? "html" : kind === "openapi" ? "openapi.json" : kind;
35037
35754
  harden(res, void 0, result.contentType ?? "application/octet-stream");
35038
- res.setHeader("content-disposition", `attachment; filename="shared-canvas.${ext}"`);
35755
+ res.setHeader(
35756
+ "content-disposition",
35757
+ `attachment; filename="${downloadFilename(kind, portalId, result.content)}"`
35758
+ );
35039
35759
  res.statusCode = 200;
35040
35760
  res.end(result.content);
35041
35761
  }
@@ -35151,6 +35871,7 @@ var WEB_MUTATION_PATHS = /* @__PURE__ */ new Set([
35151
35871
  "/web/projects/packs",
35152
35872
  "/web/projects/packs/upload",
35153
35873
  "/web/projects/packs/remove",
35874
+ "/web/projects/packs/adopt",
35154
35875
  "/web/projects/policy/reconcile",
35155
35876
  "/web/projects/producers",
35156
35877
  "/web/projects/producers/remove",
@@ -36644,15 +37365,29 @@ async function runSurface(action, options = {}) {
36644
37365
  if (format !== "native" && format !== "openapi") {
36645
37366
  throw new WaironError(`Unknown format "${format}" (supported: native, openapi).`);
36646
37367
  }
36647
- const result = exportSurface(audience, format, options.out);
37368
+ if (options.portal && format !== "openapi") {
37369
+ throw new WaironError("`--portal` selects one OpenAPI document and only applies to `--format openapi`.");
37370
+ }
37371
+ const result = exportSurface(audience, format, options.out, options.portal);
36648
37372
  logger.success(
36649
37373
  `Projected surface of "${result.snapshot.projectName}": ${result.snapshot.interfaces.length} interface(s), ${result.snapshot.types.length} type(s) at audience \u2265 ${audience}.`
36650
37374
  );
36651
- if (result.writtenTo) {
36652
- logger.info(`Written to ${result.writtenTo}`);
37375
+ const written = result.writtenPaths ?? (result.writtenTo ? [result.writtenTo] : []);
37376
+ if (written.length === 1) {
37377
+ logger.info(`Written to ${written[0]}`);
37378
+ } else if (written.length > 1) {
37379
+ logger.info(`Written ${written.length} document(s) \u2014 one per portal:`);
37380
+ for (const p of written) logger.info(` ${p}`);
36653
37381
  } else if (result.rendered) {
36654
37382
  process.stdout.write(`${result.rendered}
36655
37383
  `);
37384
+ } else if (result.renderedSet && result.renderedSet.length > 1) {
37385
+ logger.info(
37386
+ `This project publishes ${result.renderedSet.length} portals \u2014 pick one with \`--portal <id>\` (or use --out to write them all):`
37387
+ );
37388
+ for (const spec of result.renderedSet) {
37389
+ logger.info(` ${import_chalk19.default.cyan(spec.portalId)} \u2014 ${spec.name}`);
37390
+ }
36656
37391
  } else {
36657
37392
  for (const entry of result.snapshot.interfaces) {
36658
37393
  logger.info(` ${import_chalk19.default.cyan(entry.id)} (${entry.type}, ${entry.audience}) \u2014 ${entry.methods.length} method(s)`);
@@ -36921,13 +37656,14 @@ mcpCmd.command("status").description("Show whether the wairon MCP server is regi
36921
37656
  program.command("produce <target>").description("project the local project's specs to a producer target (notion | miro)").option("--page <id>", "parent page/board id in the target").option("--token <token>", "integration token (else env, else interactive prompt)").action(async (target, opts) => {
36922
37657
  await runProduce(target, { page: opts.page, token: opts.token });
36923
37658
  });
36924
- program.command("surface <action>").description("public surface exchange: export | import | list | generate-children").option("--audience <level>", "export ceiling: project | department | instance | partner | external (default instance)").option("--format <fmt>", "export format: native | openapi (default native)").option("--out <path>", "export output path (else print)").option("--source <path>", "import: the surface document (native snapshot YAML or OpenAPI)").option("--origin <origin>", "import provenance: exchanged | authored (default authored)").action(async (action, opts) => {
37659
+ program.command("surface <action>").description("public surface exchange: export | import | list | generate-children").option("--audience <level>", "export ceiling: project | department | instance | partner | external (default instance)").option("--format <fmt>", "export format: native | openapi (default native)").option("--out <path>", "export output path (else print)").option("--portal <id>", "export: select one portal's OpenAPI spec (a multi-portal project renders one document per portal)").option("--source <path>", "import: the surface document (native snapshot YAML or OpenAPI)").option("--origin <origin>", "import provenance: exchanged | authored (default authored)").action(async (action, opts) => {
36925
37660
  await runSurface(action, {
36926
37661
  audience: opts.audience,
36927
37662
  format: opts.format,
36928
37663
  out: opts.out,
36929
37664
  source: opts.source,
36930
- origin: opts.origin
37665
+ origin: opts.origin,
37666
+ portal: opts.portal
36931
37667
  });
36932
37668
  });
36933
37669
  program.command("serve").description("Run the wairon hosting server: HTTP MCP for many isolated projects + admin API").option("--host <host>", "data-plane bind host (default 0.0.0.0)").option("--port <port>", "data-plane port (default 8080)").option("--admin-host <host>", "admin-plane bind host (default 127.0.0.1)").option("--admin-port <port>", "admin-plane port (default 8081)").option("--data-dir <path>", "data root holding projects/ and auth/ (default WAIRON_DATA_DIR or ~/.wairon/data)").option("--no-auth", "disable data-plane auth (trusted networks only)").action(async (opts) => {