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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -65,7 +65,7 @@ var init_defaults = __esm({
65
65
  copilot: ".github/prompts",
66
66
  codex: ".codex/agents"
67
67
  };
68
- WAIRON_VERSION = "5.0.2-dev.7";
68
+ WAIRON_VERSION = "5.0.2-dev.9";
69
69
  GITHUB_REPO = "SYW-Apps/Waffle-AIron";
70
70
  ARCHITECT_AGENT_ID = "agent-architect";
71
71
  ARCHITECT_TEMPLATE_ID = "architect";
@@ -703,7 +703,7 @@ var init_template = __esm({
703
703
  });
704
704
 
705
705
  // src/models/specs.ts
706
- var import_zod6, SpecIdSchema, SpecStatusSchema, BoundaryItemSchema, RequirementItemSchema, DatabaseSpecSchema, DiagramConfigSchema, SURFACE_AUDIENCES, SurfaceAudienceSchema, SystemPublicInterfaceSchema, SystemSpecSchema, PublicInterfaceTypeSchema, PublicInterfaceSchema, TrustedLinkSchema, LintAllowSchema, LintConfigSchema, ExtDataSchema, LifecycleEntrypointSchema, SubsystemSpecSchema, ComponentTypeSchema, PATTERN_TYPES, PortalTypeSchema, DispatchBindingSchema, DurabilitySchema, PatternRefSchema, EventBindingSchema, ExternalLinkTypeSchema, ExternalLinkSchema, ComponentSpecSchema, HttpMethodSchema, TransportSchema, EndpointSchema, SEMANTIC_GUARANTEES, GuaranteeSchema, MethodParamSchema, MethodSignatureSchema, InterfaceSpecSchema, NarrativeStepTypeSchema, LoopKindSchema, SwitchCaseSchema, CatchClauseSchema, ParallelBranchSchema, NarrativeStepSchema, NarrativeDetailSchema, ConformanceTierSchema, MethodImplementationSchema, ImplementationSpecSchema, TypeKindSchema, TypeFieldSchema, TypeMethodSchema, InvariantSchema, TypeSpecSchema, SurfaceOriginSchema, SurfaceTypeDefSchema, SurfaceContractEntrySchema, SurfaceSnapshotSchema, GroupSpecSchema;
706
+ var import_zod6, SpecIdSchema, SpecStatusSchema, BoundaryItemSchema, RequirementItemSchema, DatabaseSpecSchema, DiagramConfigSchema, SURFACE_AUDIENCES, SurfaceAudienceSchema, SystemPublicInterfaceSchema, SystemSpecSchema, PublicInterfaceTypeSchema, PublicInterfaceSchema, TrustedLinkSchema, LintAllowSchema, LintConfigSchema, ExtDataSchema, LifecycleEntrypointSchema, SubsystemSpecSchema, ComponentTypeSchema, PATTERN_TYPES, PortalTypeSchema, DispatchBindingSchema, DurabilitySchema, PatternRefSchema, EventBindingSchema, ExternalLinkTypeSchema, ExternalLinkSchema, PortalAuthSchemeSchema, PortalAuthSchema, ComponentSpecSchema, HttpMethodSchema, TransportSchema, EndpointSchema, SEMANTIC_GUARANTEES, GuaranteeSchema, MethodParamSchema, MethodSignatureSchema, InterfaceSpecSchema, NarrativeStepTypeSchema, LoopKindSchema, SwitchCaseSchema, CatchClauseSchema, ParallelBranchSchema, NarrativeStepSchema, NarrativeDetailSchema, ConformanceTierSchema, MethodImplementationSchema, ImplementationSpecSchema, TypeKindSchema, TypeFieldSchema, TypeMethodSchema, InvariantSchema, TypeSpecSchema, SurfaceOriginSchema, SurfaceTypeDefSchema, SurfaceContractEntrySchema, SurfaceSnapshotSchema, NamedOpenApiSpecSchema, GroupSpecSchema;
707
707
  var init_specs = __esm({
708
708
  "src/models/specs.ts"() {
709
709
  "use strict";
@@ -914,6 +914,21 @@ var init_specs = __esm({
914
914
  type: ExternalLinkTypeSchema.default("informative"),
915
915
  label: import_zod6.z.string().optional()
916
916
  });
917
+ PortalAuthSchemeSchema = import_zod6.z.enum(["none", "apiKey", "bearer", "basic", "oauth2", "openIdConnect", "custom"]);
918
+ PortalAuthSchema = import_zod6.z.object({
919
+ scheme: PortalAuthSchemeSchema,
920
+ in: import_zod6.z.enum(["header", "query", "cookie"]).optional(),
921
+ name: import_zod6.z.string().optional(),
922
+ bearerFormat: import_zod6.z.string().optional(),
923
+ authorizationUrl: import_zod6.z.string().optional(),
924
+ tokenUrl: import_zod6.z.string().optional(),
925
+ refreshUrl: import_zod6.z.string().optional(),
926
+ scopes: import_zod6.z.array(import_zod6.z.object({ name: import_zod6.z.string(), description: import_zod6.z.string() })).optional(),
927
+ flow: import_zod6.z.enum(["authorizationCode", "clientCredentials", "implicit", "password"]).optional(),
928
+ openIdConnectUrl: import_zod6.z.string().optional(),
929
+ description: import_zod6.z.string().optional(),
930
+ example: import_zod6.z.string().optional()
931
+ });
917
932
  ComponentSpecSchema = import_zod6.z.object({
918
933
  id: SpecIdSchema,
919
934
  name: import_zod6.z.string(),
@@ -927,6 +942,10 @@ var init_specs = __esm({
927
942
  dependsOn: import_zod6.z.array(import_zod6.z.string()).default([]),
928
943
  portalType: PortalTypeSchema.optional(),
929
944
  basePath: import_zod6.z.string().optional(),
945
+ /** Portal-only: the API's authentication scheme (see PortalAuthSchema) — projected
946
+ * into the generated OpenAPI's securitySchemes/security. Portals with different auth
947
+ * must be separate components (one auth per portal ⇒ one OpenAPI spec per portal). */
948
+ auth: PortalAuthSchema.optional(),
930
949
  /** Portal-only: capability → component.method dispatch table (see DispatchBindingSchema). */
931
950
  dispatch: import_zod6.z.array(DispatchBindingSchema).optional(),
932
951
  /** Store-only: whether held state survives restart (see DurabilitySchema). */
@@ -1076,6 +1095,14 @@ var init_specs = __esm({
1076
1095
  // Required if type is 'call', references Method name on target interface
1077
1096
  capability: import_zod6.z.string().optional(),
1078
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` — a secret-store component id,
1101
+ * `env:API_KEY`, a config key, a vault ref, …). A declared DESIGN NOTE — wairon
1102
+ * never fetches it — but its absence on a call into a Portal whose `auth ≠ none`
1103
+ * warns (PORTAL_AUTH_UNMET), so credential loading is never overlooked.
1104
+ */
1105
+ auth: import_zod6.z.object({ from: import_zod6.z.string(), note: import_zod6.z.string().optional() }).optional(),
1079
1106
  assertsGuarantees: import_zod6.z.array(GuaranteeSchema).optional(),
1080
1107
  /**
1081
1108
  * Declared entity invariants this step upholds, as "<type-id>.<invariant-id>"
@@ -1292,7 +1319,12 @@ var init_specs = __esm({
1292
1319
  dispatch: import_zod6.z.array(DispatchBindingSchema).optional(),
1293
1320
  details: import_zod6.z.string().default(""),
1294
1321
  version: import_zod6.z.string().optional(),
1295
- stability: import_zod6.z.string().optional()
1322
+ stability: import_zod6.z.string().optional(),
1323
+ /** Projected copy of the backing Portal's auth (see PortalAuthSchema) — the codec
1324
+ * emits it as OpenAPI securitySchemes/security. */
1325
+ auth: PortalAuthSchema.optional(),
1326
+ /** The backing Portal's basePath — becomes the per-portal OpenAPI `servers` url. */
1327
+ basePath: import_zod6.z.string().optional()
1296
1328
  });
1297
1329
  SurfaceSnapshotSchema = import_zod6.z.object({
1298
1330
  /** Producing project/system name — the snapshot's resolution identity. */
@@ -1307,6 +1339,11 @@ var init_specs = __esm({
1307
1339
  /** Transitive type closure of every exported signature — self-contained. */
1308
1340
  types: import_zod6.z.array(SurfaceTypeDefSchema).default([])
1309
1341
  });
1342
+ NamedOpenApiSpecSchema = import_zod6.z.object({
1343
+ portalId: import_zod6.z.string(),
1344
+ name: import_zod6.z.string(),
1345
+ document: import_zod6.z.string()
1346
+ });
1310
1347
  GroupSpecSchema = import_zod6.z.object({
1311
1348
  kind: import_zod6.z.literal("group"),
1312
1349
  id: SpecIdSchema,
@@ -6115,7 +6152,7 @@ var MODEL = __MODEL_JSON__;
6115
6152
  + (scopeFocus ? staticChip('current view') : '')
6116
6153
  + chip(c.subsystem, 'subsystem', c.subsystem)
6117
6154
  + (scopeFocus ? '' : openViewButton('component', c.id, c.owns.length > 0))
6118
- + openApiButton(componentExposesApi(c), c.apiTag)
6155
+ + openApiButton(componentExposesApi(c), c.id)
6119
6156
  + openSpecButton('component', c.id);
6120
6157
 
6121
6158
  var linkedTypes = MODEL.types.filter(function (t) { return t.componentClass === c.id; });
@@ -6920,11 +6957,63 @@ function operationFor(method2, closureIds) {
6920
6957
  }
6921
6958
  return op;
6922
6959
  }
6923
- function toOpenApi(snapshot) {
6924
- const closureIds = new Set(snapshot.types.map((t) => t.id));
6925
- const httpEntries = snapshot.interfaces.filter((e) => e.type === "REST" || e.methods.some((m) => m.endpoint?.transport === "HTTP"));
6960
+ function securitySchemeObject(auth) {
6961
+ if (auth.scheme === "none") return null;
6962
+ const desc = auth.description ? { description: auth.description } : {};
6963
+ switch (auth.scheme) {
6964
+ case "apiKey":
6965
+ return { type: "apiKey", in: auth.in ?? "header", name: auth.name ?? "X-API-Key", ...desc };
6966
+ case "bearer":
6967
+ return { type: "http", scheme: "bearer", ...auth.bearerFormat ? { bearerFormat: auth.bearerFormat } : {}, ...desc };
6968
+ case "basic":
6969
+ return { type: "http", scheme: "basic", ...desc };
6970
+ case "oauth2": {
6971
+ const flow = { scopes: Object.fromEntries((auth.scopes ?? []).map((s) => [s.name, s.description])) };
6972
+ if (auth.authorizationUrl) flow.authorizationUrl = auth.authorizationUrl;
6973
+ if (auth.tokenUrl) flow.tokenUrl = auth.tokenUrl;
6974
+ if (auth.refreshUrl) flow.refreshUrl = auth.refreshUrl;
6975
+ return { type: "oauth2", flows: { [auth.flow ?? "authorizationCode"]: flow }, ...desc };
6976
+ }
6977
+ case "openIdConnect":
6978
+ return { type: "openIdConnect", openIdConnectUrl: auth.openIdConnectUrl ?? "", ...desc };
6979
+ case "custom":
6980
+ return { type: "apiKey", in: auth.in ?? "header", name: auth.name ?? "Authorization", description: auth.description ?? auth.example ?? "Custom authentication scheme." };
6981
+ default:
6982
+ return null;
6983
+ }
6984
+ }
6985
+ function schemeBaseName(scheme) {
6986
+ return { apiKey: "ApiKeyAuth", bearer: "BearerAuth", basic: "BasicAuth", oauth2: "OAuth2", openIdConnect: "OpenIdConnect", custom: "CustomAuth" }[scheme] ?? "Auth";
6987
+ }
6988
+ function buildSecurity(entries) {
6989
+ const schemes = {};
6990
+ const nameByContent = /* @__PURE__ */ new Map();
6991
+ const securityByEntry = /* @__PURE__ */ new Map();
6992
+ for (const entry of entries) {
6993
+ if (!entry.auth) continue;
6994
+ const obj = securitySchemeObject(entry.auth);
6995
+ if (!obj) continue;
6996
+ const content = JSON.stringify(obj);
6997
+ let name = nameByContent.get(content);
6998
+ if (!name) {
6999
+ name = schemeBaseName(entry.auth.scheme);
7000
+ for (let n = 2; schemes[name]; n++) name = schemeBaseName(entry.auth.scheme) + n;
7001
+ schemes[name] = obj;
7002
+ nameByContent.set(content, name);
7003
+ }
7004
+ const scopeNames = entry.auth.scheme === "oauth2" ? (entry.auth.scopes ?? []).map((s) => s.name) : [];
7005
+ securityByEntry.set(entry.id, { [name]: scopeNames });
7006
+ }
7007
+ return { schemes, securityByEntry };
7008
+ }
7009
+ function httpEntriesOf(snapshot) {
7010
+ return snapshot.interfaces.filter((e) => e.type === "REST" || e.methods.some((m) => m.endpoint?.transport === "HTTP"));
7011
+ }
7012
+ function renderDoc(snapshot, entries, closureIds, opts = {}) {
7013
+ const { schemes, securityByEntry } = buildSecurity(entries);
6926
7014
  const paths = {};
6927
- for (const entry of httpEntries) {
7015
+ for (const entry of entries) {
7016
+ const security = securityByEntry.get(entry.id);
6928
7017
  for (const method2 of entry.methods) {
6929
7018
  const endpoint = method2.endpoint;
6930
7019
  if (!endpoint || endpoint.transport !== "HTTP") continue;
@@ -6932,7 +7021,8 @@ function toOpenApi(snapshot) {
6932
7021
  paths[p] = paths[p] ?? {};
6933
7022
  paths[p][endpoint.method.toLowerCase()] = {
6934
7023
  tags: [entry.id],
6935
- ...operationFor(method2, closureIds)
7024
+ ...operationFor(method2, closureIds),
7025
+ ...security ? { security: [security] } : {}
6936
7026
  };
6937
7027
  }
6938
7028
  }
@@ -6945,19 +7035,41 @@ function toOpenApi(snapshot) {
6945
7035
  required: t.fields.filter((f) => !f.optional).map((f) => f.name)
6946
7036
  };
6947
7037
  }
6948
- const doc = {
7038
+ const components = {};
7039
+ if (Object.keys(schemas).length) components.schemas = schemas;
7040
+ if (Object.keys(schemes).length) components.securitySchemes = schemes;
7041
+ const basePaths = [...new Set(entries.map((e) => e.basePath).filter((b) => !!b))];
7042
+ const servers = opts.servers && basePaths.length === 1 ? [{ url: basePaths[0] }] : void 0;
7043
+ return {
6949
7044
  openapi: "3.1.0",
6950
7045
  info: {
6951
- title: snapshot.projectName,
7046
+ title: opts.title ?? snapshot.projectName,
6952
7047
  version: snapshot.version ?? "0.0.0",
6953
7048
  ...snapshot.stateId ? { "x-wairon-state-id": snapshot.stateId } : {},
6954
7049
  "x-wairon-origin": snapshot.origin,
6955
7050
  "x-wairon-generated-at": snapshot.generatedAt
6956
7051
  },
7052
+ ...servers ? { servers } : {},
6957
7053
  paths,
6958
- ...Object.keys(schemas).length ? { components: { schemas } } : {}
7054
+ ...Object.keys(components).length ? { components } : {}
6959
7055
  };
6960
- return JSON.stringify(doc, null, 2);
7056
+ }
7057
+ function toOpenApiSet(snapshot) {
7058
+ const closureIds = new Set(snapshot.types.map((t) => t.id));
7059
+ const byPortal = /* @__PURE__ */ new Map();
7060
+ const order = [];
7061
+ for (const entry of httpEntriesOf(snapshot)) {
7062
+ if (!byPortal.has(entry.component)) {
7063
+ byPortal.set(entry.component, []);
7064
+ order.push(entry.component);
7065
+ }
7066
+ byPortal.get(entry.component).push(entry);
7067
+ }
7068
+ return order.map((portalId) => {
7069
+ const entries = byPortal.get(portalId);
7070
+ const name = entries[0]?.name ?? portalId;
7071
+ return { portalId, name, document: JSON.stringify(renderDoc(snapshot, entries, closureIds, { title: name, servers: true }), null, 2) };
7072
+ });
6961
7073
  }
6962
7074
  function isOpenApiDocument(body) {
6963
7075
  try {
@@ -6979,6 +7091,39 @@ function typeRefFromSchema(schema) {
6979
7091
  if (typeof t === "string" && t !== "object") return t;
6980
7092
  return "json";
6981
7093
  }
7094
+ function authFromSecurityScheme(scheme) {
7095
+ const desc = typeof scheme.description === "string" ? { description: scheme.description } : {};
7096
+ if (scheme.type === "apiKey") {
7097
+ return {
7098
+ scheme: "apiKey",
7099
+ ...scheme.in === "header" || scheme.in === "query" || scheme.in === "cookie" ? { in: scheme.in } : {},
7100
+ ...typeof scheme.name === "string" ? { name: scheme.name } : {},
7101
+ ...desc
7102
+ };
7103
+ }
7104
+ if (scheme.type === "http") {
7105
+ if (scheme.scheme === "bearer") return { scheme: "bearer", ...typeof scheme.bearerFormat === "string" ? { bearerFormat: scheme.bearerFormat } : {}, ...desc };
7106
+ if (scheme.scheme === "basic") return { scheme: "basic", ...desc };
7107
+ }
7108
+ if (scheme.type === "oauth2") {
7109
+ const flows = scheme.flows ?? {};
7110
+ const flowKey = Object.keys(flows)[0];
7111
+ const f = flows[flowKey] ?? {};
7112
+ return {
7113
+ scheme: "oauth2",
7114
+ ...flowKey === "authorizationCode" || flowKey === "clientCredentials" || flowKey === "implicit" || flowKey === "password" ? { flow: flowKey } : {},
7115
+ ...typeof f.authorizationUrl === "string" ? { authorizationUrl: f.authorizationUrl } : {},
7116
+ ...typeof f.tokenUrl === "string" ? { tokenUrl: f.tokenUrl } : {},
7117
+ ...typeof f.refreshUrl === "string" ? { refreshUrl: f.refreshUrl } : {},
7118
+ scopes: Object.entries(f.scopes ?? {}).map(([name, description]) => ({ name, description })),
7119
+ ...desc
7120
+ };
7121
+ }
7122
+ if (scheme.type === "openIdConnect") {
7123
+ return { scheme: "openIdConnect", openIdConnectUrl: typeof scheme.openIdConnectUrl === "string" ? scheme.openIdConnectUrl : "", ...desc };
7124
+ }
7125
+ return void 0;
7126
+ }
6982
7127
  function fromOpenApi(document, projectName) {
6983
7128
  let parsed;
6984
7129
  try {
@@ -7056,6 +7201,9 @@ function fromOpenApi(document, projectName) {
7056
7201
  }))
7057
7202
  });
7058
7203
  }
7204
+ const securitySchemes = parsed.components?.securitySchemes ?? {};
7205
+ const firstScheme = Object.values(securitySchemes)[0];
7206
+ const importedAuth = firstScheme ? authFromSecurityScheme(firstScheme) : void 0;
7059
7207
  const entry = {
7060
7208
  id: `${projectName}-api`,
7061
7209
  name: typeof info.title === "string" ? info.title : projectName,
@@ -7064,7 +7212,8 @@ function fromOpenApi(document, projectName) {
7064
7212
  component: `${projectName}-api`,
7065
7213
  methods,
7066
7214
  details: typeof info.description === "string" ? info.description : `Imported OpenAPI surface of ${projectName}.`,
7067
- ...typeof info.version === "string" ? { version: info.version } : {}
7215
+ ...typeof info.version === "string" ? { version: info.version } : {},
7216
+ ...importedAuth ? { auth: importedAuth } : {}
7068
7217
  };
7069
7218
  return SurfaceSnapshotSchema.parse({
7070
7219
  projectName,
@@ -7179,6 +7328,10 @@ function projectOwnSurface(maxAudience) {
7179
7328
  component: comp.id,
7180
7329
  methods,
7181
7330
  ...comp.dispatch && comp.dispatch.length ? { dispatch: comp.dispatch } : {},
7331
+ // Project the backing Portal's auth + basePath so the codec can emit
7332
+ // OpenAPI security + per-portal servers self-contained from the snapshot.
7333
+ ...comp.auth && comp.auth.scheme !== "none" ? { auth: comp.auth } : {},
7334
+ ...comp.basePath ? { basePath: comp.basePath } : {},
7182
7335
  details: raw.details ?? "",
7183
7336
  ...raw.version ? { version: raw.version } : {},
7184
7337
  ...raw.stability ? { stability: raw.stability } : {}
@@ -7222,20 +7375,28 @@ function saveSnapshot(snapshot, rootDir = getProjectRoot()) {
7222
7375
  function loadSurfaceSnapshots() {
7223
7376
  return listSnapshots();
7224
7377
  }
7378
+ function renderOpenApiForms(snapshot) {
7379
+ const renderedSet = toOpenApiSet(snapshot);
7380
+ return { renderedSet, ...renderedSet.length === 1 ? { rendered: renderedSet[0].document } : {} };
7381
+ }
7382
+ function writeSurfaceFile(outPath, body, snapshot) {
7383
+ const resolved = path10.resolve(outPath);
7384
+ fs9.mkdirSync(path10.dirname(resolved), { recursive: true });
7385
+ if (body !== void 0) fs9.writeFileSync(resolved, body);
7386
+ else writeYamlFile(resolved, snapshot);
7387
+ return resolved;
7388
+ }
7225
7389
  function exportSurface(maxAudience, format, outPath) {
7226
7390
  const snapshot = projectOwnSurface(maxAudience);
7227
- const rendered = format === "openapi" ? toOpenApi(snapshot) : void 0;
7228
- let writtenTo;
7229
- if (outPath) {
7230
- fs9.mkdirSync(path10.dirname(path10.resolve(outPath)), { recursive: true });
7231
- if (rendered !== void 0) {
7232
- fs9.writeFileSync(path10.resolve(outPath), rendered);
7233
- } else {
7234
- writeYamlFile(path10.resolve(outPath), snapshot);
7235
- }
7236
- writtenTo = path10.resolve(outPath);
7237
- }
7238
- return { snapshot, ...rendered !== void 0 ? { rendered } : {}, ...writtenTo ? { writtenTo } : {} };
7391
+ const openapi = format === "openapi" ? renderOpenApiForms(snapshot) : void 0;
7392
+ const body = openapi?.rendered ?? openapi?.renderedSet[0]?.document;
7393
+ const writtenTo = outPath ? writeSurfaceFile(outPath, body, snapshot) : void 0;
7394
+ return {
7395
+ snapshot,
7396
+ ...openapi?.rendered !== void 0 ? { rendered: openapi.rendered } : {},
7397
+ ...openapi ? { renderedSet: openapi.renderedSet } : {},
7398
+ ...writtenTo ? { writtenTo } : {}
7399
+ };
7239
7400
  }
7240
7401
  function importSurface(sourcePath, origin) {
7241
7402
  const resolved = path10.resolve(sourcePath);
@@ -12034,6 +12195,44 @@ var init_lint_allows = __esm({
12034
12195
  }
12035
12196
  });
12036
12197
 
12198
+ // src/core/rules/portal-call-auth.ts
12199
+ var portalCallAuthRule;
12200
+ var init_portal_call_auth = __esm({
12201
+ "src/core/rules/portal-call-auth.ts"() {
12202
+ "use strict";
12203
+ portalCallAuthRule = {
12204
+ name: "portal-call-auth",
12205
+ description: "An OUTBOUND narrative `call` into ANOTHER component's Portal whose auth is not `none` must declare the credential source it presents \u2014 the step's `auth.from`, a design note naming where the secret loads from (a secret-store component, env var, config key, vault ref). Its absence warns (PORTAL_AUTH_UNMET) so credential loading is never overlooked; 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.",
12206
+ codes: [
12207
+ { code: "PORTAL_AUTH_UNMET", defaultSeverity: "warning", summary: "A narrative call into another component's authed Portal does not declare where its credential loads from" }
12208
+ ],
12209
+ check(ctx) {
12210
+ for (const impl of ctx.implementations) {
12211
+ const ownComponent = ctx.interfaceMap.get(impl.contract)?.component;
12212
+ const draft = ctx.isImplementationDraft(impl);
12213
+ for (const method2 of impl.methods ?? []) {
12214
+ for (const step of method2.narrative ?? []) {
12215
+ if (step.type !== "call") continue;
12216
+ if (!step.targetComponent || step.targetComponent === ownComponent) continue;
12217
+ const target = ctx.componentMap.get(step.targetComponent);
12218
+ if (!target || target.componentType !== "Portal") continue;
12219
+ if (!target.auth || target.auth.scheme === "none") continue;
12220
+ if (step.auth?.from) continue;
12221
+ ctx.addIssue(
12222
+ "warning",
12223
+ "PORTAL_AUTH_UNMET",
12224
+ `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.`,
12225
+ impl.id,
12226
+ draft
12227
+ );
12228
+ }
12229
+ }
12230
+ }
12231
+ }
12232
+ };
12233
+ }
12234
+ });
12235
+
12037
12236
  // src/core/rules/index.ts
12038
12237
  function makeScopeFilter(opts) {
12039
12238
  const { components, interfaces, implementations, types, scopeSubsystem } = opts;
@@ -12279,6 +12478,7 @@ var init_rules = __esm({
12279
12478
  init_hidden_state();
12280
12479
  init_dependency_conformance();
12281
12480
  init_lint_allows();
12481
+ init_portal_call_auth();
12282
12482
  init_source_analysis();
12283
12483
  init_types();
12284
12484
  init_type_analysis();
@@ -12300,6 +12500,9 @@ var init_rules = __esm({
12300
12500
  narrativeAntipatternsRule,
12301
12501
  narrativeDetailRule,
12302
12502
  portalsRule,
12503
+ // Cross-call auth: a narrative call into an authed Portal must name its
12504
+ // credential source (rides with the portal family).
12505
+ portalCallAuthRule,
12303
12506
  stereotypeDepsRule,
12304
12507
  patternsRule,
12305
12508
  // Facade shape rides with pattern ownership: same §7 doctrine, narrative side.
@@ -12362,6 +12565,7 @@ var init_rules = __esm({
12362
12565
  // L4 expectations: implementations and their code linkage.
12363
12566
  MISSING_IMPLEMENTATION_METHOD: "implementations",
12364
12567
  MISSING_SOURCE_PATH: "implementations",
12568
+ PORTAL_AUTH_UNMET: "implementations",
12365
12569
  MISSING_SOURCE_FILE: "implementations",
12366
12570
  SOURCE_PATH_ESCAPES_ROOT: "implementations",
12367
12571
  UNREALIZED_METHOD: "implementations",
@@ -16925,6 +17129,7 @@ NOTICE:
16925
17129
  type: import_zod9.z.enum(["local", "call", "dispatch", "branch", "switch", "loop", "try", "parallel", "jump", "return", "throw"]),
16926
17130
  targetComponent: import_zod9.z.string().optional().describe("call/dispatch: L2 component id (for dispatch, the Portal routed through)"),
16927
17131
  targetMethod: import_zod9.z.string().optional().describe("call: method name on the target"),
17132
+ 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` = a secret-store component id, env:API_KEY, a config key, a vault ref). A design note \u2014 wairon never fetches it. Its absence on a call into a Portal whose auth \u2260 none warns (PORTAL_AUTH_UNMET), so credential loading is never overlooked."),
16928
17133
  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)"),
16929
17134
  capability: import_zod9.z.string().optional().describe("dispatch: the capability routed through the target Portal's dispatch table (validated against it \u2014 UNSERVED_CAPABILITY)"),
16930
17135
  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)"),
@@ -32127,7 +32332,7 @@ function getWebProjectCanvasModel(cfg, sessionId, projectId) {
32127
32332
  }
32128
32333
  return runWithProjectRoot(root, () => hostCore.buildCanvasDataModel());
32129
32334
  }
32130
- function getWebProjectOpenApi(cfg, sessionId, projectId) {
32335
+ function getWebProjectOpenApi(cfg, sessionId, projectId, portalId) {
32131
32336
  const principal = authenticateSession(cfg.dataDir, sessionId);
32132
32337
  if (!principal.authenticated) throw new UnauthenticatedError();
32133
32338
  if (!listProjects2(cfg, sessionId).some((r) => r.id === projectId)) {
@@ -32136,7 +32341,19 @@ function getWebProjectOpenApi(cfg, sessionId, projectId) {
32136
32341
  const root = resolveProjectRoot(cfg.dataDir, principal, projectId);
32137
32342
  if (!root) throw new ForbiddenError("project not authorized or unknown");
32138
32343
  const result = runWithProjectRoot(root, () => hostSurfaces.exportBoundSurface("project", "openapi"));
32139
- return swaggerUiPage(result.rendered ?? "{}", projectId);
32344
+ const specs = result.renderedSet ?? (result.rendered ? [{ portalId: "", name: projectId, document: result.rendered }] : []);
32345
+ if (specs.length === 0) return swaggerUiPage("{}", projectId);
32346
+ if (portalId) {
32347
+ const sel = specs.find((s) => s.portalId === portalId) ?? specs[0];
32348
+ return swaggerUiPage(sel.document, sel.name);
32349
+ }
32350
+ if (specs.length === 1) return swaggerUiPage(specs[0].document, specs[0].name);
32351
+ return openApiIndexPage(projectId, specs);
32352
+ }
32353
+ function openApiIndexPage(projectId, specs) {
32354
+ const esc2 = (s) => s.replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
32355
+ 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("");
32356
+ return `<!doctype html><html><head><meta charset="utf-8"><title>${esc2(projectId)} \u2014 API specs</title><style>body{font-family:system-ui,sans-serif;max-width:680px;margin:48px auto;padding:0 20px;color:#e6e6e6;background:#161616}h1{font-size:20px}a{color:#6ea8fe;text-decoration:none}a:hover{text-decoration:underline}li{margin:10px 0}code{color:#8a94a6;font-size:12px;margin-left:8px}</style></head><body><h1>${esc2(projectId)} \u2014 API specs</h1><p>This project exposes ${specs.length} separate public APIs, each with its own OpenAPI document and auth:</p><ul>${items}</ul></body></html>`;
32140
32357
  }
32141
32358
  function reshapeLandscapeGraph(model, level) {
32142
32359
  const unitNodeIds = new Set(model.nodes.filter((n) => n.nodeKind === "orgUnit").map((n) => n.id));
@@ -34498,7 +34715,8 @@ async function handleWebRequest(cfg, req, res, body, url, ctx) {
34498
34715
  }
34499
34716
  if (req.method === "GET" && parts.length === 2 && parts[1] === "openapi") {
34500
34717
  const projectId = url.searchParams.get("projectId") ?? "";
34501
- const html = getWebProjectOpenApi(cfg, sessionId, projectId);
34718
+ const spec = url.searchParams.get("spec") ?? void 0;
34719
+ const html = getWebProjectOpenApi(cfg, sessionId, projectId, spec);
34502
34720
  res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
34503
34721
  res.end(html);
34504
34722
  return;