@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/index.js CHANGED
@@ -393,7 +393,7 @@ var init_template = __esm({
393
393
  });
394
394
 
395
395
  // src/models/specs.ts
396
- 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;
396
+ 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;
397
397
  var init_specs = __esm({
398
398
  "src/models/specs.ts"() {
399
399
  "use strict";
@@ -604,6 +604,21 @@ var init_specs = __esm({
604
604
  type: ExternalLinkTypeSchema.default("informative"),
605
605
  label: import_zod6.z.string().optional()
606
606
  });
607
+ PortalAuthSchemeSchema = import_zod6.z.enum(["none", "apiKey", "bearer", "basic", "oauth2", "openIdConnect", "custom"]);
608
+ PortalAuthSchema = import_zod6.z.object({
609
+ scheme: PortalAuthSchemeSchema,
610
+ in: import_zod6.z.enum(["header", "query", "cookie"]).optional(),
611
+ name: import_zod6.z.string().optional(),
612
+ bearerFormat: import_zod6.z.string().optional(),
613
+ authorizationUrl: import_zod6.z.string().optional(),
614
+ tokenUrl: import_zod6.z.string().optional(),
615
+ refreshUrl: import_zod6.z.string().optional(),
616
+ scopes: import_zod6.z.array(import_zod6.z.object({ name: import_zod6.z.string(), description: import_zod6.z.string() })).optional(),
617
+ flow: import_zod6.z.enum(["authorizationCode", "clientCredentials", "implicit", "password"]).optional(),
618
+ openIdConnectUrl: import_zod6.z.string().optional(),
619
+ description: import_zod6.z.string().optional(),
620
+ example: import_zod6.z.string().optional()
621
+ });
607
622
  ComponentSpecSchema = import_zod6.z.object({
608
623
  id: SpecIdSchema,
609
624
  name: import_zod6.z.string(),
@@ -617,6 +632,10 @@ var init_specs = __esm({
617
632
  dependsOn: import_zod6.z.array(import_zod6.z.string()).default([]),
618
633
  portalType: PortalTypeSchema.optional(),
619
634
  basePath: import_zod6.z.string().optional(),
635
+ /** Portal-only: the API's authentication scheme (see PortalAuthSchema) — projected
636
+ * into the generated OpenAPI's securitySchemes/security. Portals with different auth
637
+ * must be separate components (one auth per portal ⇒ one OpenAPI spec per portal). */
638
+ auth: PortalAuthSchema.optional(),
620
639
  /** Portal-only: capability → component.method dispatch table (see DispatchBindingSchema). */
621
640
  dispatch: import_zod6.z.array(DispatchBindingSchema).optional(),
622
641
  /** Store-only: whether held state survives restart (see DurabilitySchema). */
@@ -766,6 +785,14 @@ var init_specs = __esm({
766
785
  // Required if type is 'call', references Method name on target interface
767
786
  capability: import_zod6.z.string().optional(),
768
787
  // Required if type is 'dispatch': the capability routed through the target Portal's dispatch table
788
+ /**
789
+ * call/dispatch only: the credential this step presents to an authed callee
790
+ * Portal, and WHERE it is loaded from (`from` — a secret-store component id,
791
+ * `env:API_KEY`, a config key, a vault ref, …). A declared DESIGN NOTE — wairon
792
+ * never fetches it — but its absence on a call into a Portal whose `auth ≠ none`
793
+ * warns (PORTAL_AUTH_UNMET), so credential loading is never overlooked.
794
+ */
795
+ auth: import_zod6.z.object({ from: import_zod6.z.string(), note: import_zod6.z.string().optional() }).optional(),
769
796
  assertsGuarantees: import_zod6.z.array(GuaranteeSchema).optional(),
770
797
  /**
771
798
  * Declared entity invariants this step upholds, as "<type-id>.<invariant-id>"
@@ -982,7 +1009,12 @@ var init_specs = __esm({
982
1009
  dispatch: import_zod6.z.array(DispatchBindingSchema).optional(),
983
1010
  details: import_zod6.z.string().default(""),
984
1011
  version: import_zod6.z.string().optional(),
985
- stability: import_zod6.z.string().optional()
1012
+ stability: import_zod6.z.string().optional(),
1013
+ /** Projected copy of the backing Portal's auth (see PortalAuthSchema) — the codec
1014
+ * emits it as OpenAPI securitySchemes/security. */
1015
+ auth: PortalAuthSchema.optional(),
1016
+ /** The backing Portal's basePath — becomes the per-portal OpenAPI `servers` url. */
1017
+ basePath: import_zod6.z.string().optional()
986
1018
  });
987
1019
  SurfaceSnapshotSchema = import_zod6.z.object({
988
1020
  /** Producing project/system name — the snapshot's resolution identity. */
@@ -997,6 +1029,11 @@ var init_specs = __esm({
997
1029
  /** Transitive type closure of every exported signature — self-contained. */
998
1030
  types: import_zod6.z.array(SurfaceTypeDefSchema).default([])
999
1031
  });
1032
+ NamedOpenApiSpecSchema = import_zod6.z.object({
1033
+ portalId: import_zod6.z.string(),
1034
+ name: import_zod6.z.string(),
1035
+ document: import_zod6.z.string()
1036
+ });
1000
1037
  GroupSpecSchema = import_zod6.z.object({
1001
1038
  kind: import_zod6.z.literal("group"),
1002
1039
  id: SpecIdSchema,
@@ -5461,7 +5498,7 @@ var MODEL = __MODEL_JSON__;
5461
5498
  + (scopeFocus ? staticChip('current view') : '')
5462
5499
  + chip(c.subsystem, 'subsystem', c.subsystem)
5463
5500
  + (scopeFocus ? '' : openViewButton('component', c.id, c.owns.length > 0))
5464
- + openApiButton(componentExposesApi(c), c.apiTag)
5501
+ + openApiButton(componentExposesApi(c), c.id)
5465
5502
  + openSpecButton('component', c.id);
5466
5503
 
5467
5504
  var linkedTypes = MODEL.types.filter(function (t) { return t.componentClass === c.id; });
@@ -6269,11 +6306,63 @@ function operationFor(method, closureIds) {
6269
6306
  }
6270
6307
  return op;
6271
6308
  }
6272
- function toOpenApi(snapshot) {
6273
- const closureIds = new Set(snapshot.types.map((t) => t.id));
6274
- const httpEntries = snapshot.interfaces.filter((e) => e.type === "REST" || e.methods.some((m) => m.endpoint?.transport === "HTTP"));
6309
+ function securitySchemeObject(auth) {
6310
+ if (auth.scheme === "none") return null;
6311
+ const desc = auth.description ? { description: auth.description } : {};
6312
+ switch (auth.scheme) {
6313
+ case "apiKey":
6314
+ return { type: "apiKey", in: auth.in ?? "header", name: auth.name ?? "X-API-Key", ...desc };
6315
+ case "bearer":
6316
+ return { type: "http", scheme: "bearer", ...auth.bearerFormat ? { bearerFormat: auth.bearerFormat } : {}, ...desc };
6317
+ case "basic":
6318
+ return { type: "http", scheme: "basic", ...desc };
6319
+ case "oauth2": {
6320
+ const flow = { scopes: Object.fromEntries((auth.scopes ?? []).map((s) => [s.name, s.description])) };
6321
+ if (auth.authorizationUrl) flow.authorizationUrl = auth.authorizationUrl;
6322
+ if (auth.tokenUrl) flow.tokenUrl = auth.tokenUrl;
6323
+ if (auth.refreshUrl) flow.refreshUrl = auth.refreshUrl;
6324
+ return { type: "oauth2", flows: { [auth.flow ?? "authorizationCode"]: flow }, ...desc };
6325
+ }
6326
+ case "openIdConnect":
6327
+ return { type: "openIdConnect", openIdConnectUrl: auth.openIdConnectUrl ?? "", ...desc };
6328
+ case "custom":
6329
+ return { type: "apiKey", in: auth.in ?? "header", name: auth.name ?? "Authorization", description: auth.description ?? auth.example ?? "Custom authentication scheme." };
6330
+ default:
6331
+ return null;
6332
+ }
6333
+ }
6334
+ function schemeBaseName(scheme) {
6335
+ return { apiKey: "ApiKeyAuth", bearer: "BearerAuth", basic: "BasicAuth", oauth2: "OAuth2", openIdConnect: "OpenIdConnect", custom: "CustomAuth" }[scheme] ?? "Auth";
6336
+ }
6337
+ function buildSecurity(entries) {
6338
+ const schemes = {};
6339
+ const nameByContent = /* @__PURE__ */ new Map();
6340
+ const securityByEntry = /* @__PURE__ */ new Map();
6341
+ for (const entry of entries) {
6342
+ if (!entry.auth) continue;
6343
+ const obj = securitySchemeObject(entry.auth);
6344
+ if (!obj) continue;
6345
+ const content = JSON.stringify(obj);
6346
+ let name = nameByContent.get(content);
6347
+ if (!name) {
6348
+ name = schemeBaseName(entry.auth.scheme);
6349
+ for (let n = 2; schemes[name]; n++) name = schemeBaseName(entry.auth.scheme) + n;
6350
+ schemes[name] = obj;
6351
+ nameByContent.set(content, name);
6352
+ }
6353
+ const scopeNames = entry.auth.scheme === "oauth2" ? (entry.auth.scopes ?? []).map((s) => s.name) : [];
6354
+ securityByEntry.set(entry.id, { [name]: scopeNames });
6355
+ }
6356
+ return { schemes, securityByEntry };
6357
+ }
6358
+ function httpEntriesOf(snapshot) {
6359
+ return snapshot.interfaces.filter((e) => e.type === "REST" || e.methods.some((m) => m.endpoint?.transport === "HTTP"));
6360
+ }
6361
+ function renderDoc(snapshot, entries, closureIds, opts = {}) {
6362
+ const { schemes, securityByEntry } = buildSecurity(entries);
6275
6363
  const paths = {};
6276
- for (const entry of httpEntries) {
6364
+ for (const entry of entries) {
6365
+ const security = securityByEntry.get(entry.id);
6277
6366
  for (const method of entry.methods) {
6278
6367
  const endpoint = method.endpoint;
6279
6368
  if (!endpoint || endpoint.transport !== "HTTP") continue;
@@ -6281,7 +6370,8 @@ function toOpenApi(snapshot) {
6281
6370
  paths[p] = paths[p] ?? {};
6282
6371
  paths[p][endpoint.method.toLowerCase()] = {
6283
6372
  tags: [entry.id],
6284
- ...operationFor(method, closureIds)
6373
+ ...operationFor(method, closureIds),
6374
+ ...security ? { security: [security] } : {}
6285
6375
  };
6286
6376
  }
6287
6377
  }
@@ -6294,19 +6384,45 @@ function toOpenApi(snapshot) {
6294
6384
  required: t.fields.filter((f) => !f.optional).map((f) => f.name)
6295
6385
  };
6296
6386
  }
6297
- const doc = {
6387
+ const components = {};
6388
+ if (Object.keys(schemas).length) components.schemas = schemas;
6389
+ if (Object.keys(schemes).length) components.securitySchemes = schemes;
6390
+ const basePaths = [...new Set(entries.map((e) => e.basePath).filter((b) => !!b))];
6391
+ const servers = opts.servers && basePaths.length === 1 ? [{ url: basePaths[0] }] : void 0;
6392
+ return {
6298
6393
  openapi: "3.1.0",
6299
6394
  info: {
6300
- title: snapshot.projectName,
6395
+ title: opts.title ?? snapshot.projectName,
6301
6396
  version: snapshot.version ?? "0.0.0",
6302
6397
  ...snapshot.stateId ? { "x-wairon-state-id": snapshot.stateId } : {},
6303
6398
  "x-wairon-origin": snapshot.origin,
6304
6399
  "x-wairon-generated-at": snapshot.generatedAt
6305
6400
  },
6401
+ ...servers ? { servers } : {},
6306
6402
  paths,
6307
- ...Object.keys(schemas).length ? { components: { schemas } } : {}
6403
+ ...Object.keys(components).length ? { components } : {}
6308
6404
  };
6309
- return JSON.stringify(doc, null, 2);
6405
+ }
6406
+ function toOpenApi(snapshot) {
6407
+ const closureIds = new Set(snapshot.types.map((t) => t.id));
6408
+ return JSON.stringify(renderDoc(snapshot, httpEntriesOf(snapshot), closureIds), null, 2);
6409
+ }
6410
+ function toOpenApiSet(snapshot) {
6411
+ const closureIds = new Set(snapshot.types.map((t) => t.id));
6412
+ const byPortal = /* @__PURE__ */ new Map();
6413
+ const order = [];
6414
+ for (const entry of httpEntriesOf(snapshot)) {
6415
+ if (!byPortal.has(entry.component)) {
6416
+ byPortal.set(entry.component, []);
6417
+ order.push(entry.component);
6418
+ }
6419
+ byPortal.get(entry.component).push(entry);
6420
+ }
6421
+ return order.map((portalId) => {
6422
+ const entries = byPortal.get(portalId);
6423
+ const name = entries[0]?.name ?? portalId;
6424
+ return { portalId, name, document: JSON.stringify(renderDoc(snapshot, entries, closureIds, { title: name, servers: true }), null, 2) };
6425
+ });
6310
6426
  }
6311
6427
  function isOpenApiDocument(body) {
6312
6428
  try {
@@ -6328,6 +6444,39 @@ function typeRefFromSchema(schema) {
6328
6444
  if (typeof t === "string" && t !== "object") return t;
6329
6445
  return "json";
6330
6446
  }
6447
+ function authFromSecurityScheme(scheme) {
6448
+ const desc = typeof scheme.description === "string" ? { description: scheme.description } : {};
6449
+ if (scheme.type === "apiKey") {
6450
+ return {
6451
+ scheme: "apiKey",
6452
+ ...scheme.in === "header" || scheme.in === "query" || scheme.in === "cookie" ? { in: scheme.in } : {},
6453
+ ...typeof scheme.name === "string" ? { name: scheme.name } : {},
6454
+ ...desc
6455
+ };
6456
+ }
6457
+ if (scheme.type === "http") {
6458
+ if (scheme.scheme === "bearer") return { scheme: "bearer", ...typeof scheme.bearerFormat === "string" ? { bearerFormat: scheme.bearerFormat } : {}, ...desc };
6459
+ if (scheme.scheme === "basic") return { scheme: "basic", ...desc };
6460
+ }
6461
+ if (scheme.type === "oauth2") {
6462
+ const flows = scheme.flows ?? {};
6463
+ const flowKey = Object.keys(flows)[0];
6464
+ const f = flows[flowKey] ?? {};
6465
+ return {
6466
+ scheme: "oauth2",
6467
+ ...flowKey === "authorizationCode" || flowKey === "clientCredentials" || flowKey === "implicit" || flowKey === "password" ? { flow: flowKey } : {},
6468
+ ...typeof f.authorizationUrl === "string" ? { authorizationUrl: f.authorizationUrl } : {},
6469
+ ...typeof f.tokenUrl === "string" ? { tokenUrl: f.tokenUrl } : {},
6470
+ ...typeof f.refreshUrl === "string" ? { refreshUrl: f.refreshUrl } : {},
6471
+ scopes: Object.entries(f.scopes ?? {}).map(([name, description]) => ({ name, description })),
6472
+ ...desc
6473
+ };
6474
+ }
6475
+ if (scheme.type === "openIdConnect") {
6476
+ return { scheme: "openIdConnect", openIdConnectUrl: typeof scheme.openIdConnectUrl === "string" ? scheme.openIdConnectUrl : "", ...desc };
6477
+ }
6478
+ return void 0;
6479
+ }
6331
6480
  function fromOpenApi(document, projectName) {
6332
6481
  let parsed;
6333
6482
  try {
@@ -6405,6 +6554,9 @@ function fromOpenApi(document, projectName) {
6405
6554
  }))
6406
6555
  });
6407
6556
  }
6557
+ const securitySchemes = parsed.components?.securitySchemes ?? {};
6558
+ const firstScheme = Object.values(securitySchemes)[0];
6559
+ const importedAuth = firstScheme ? authFromSecurityScheme(firstScheme) : void 0;
6408
6560
  const entry = {
6409
6561
  id: `${projectName}-api`,
6410
6562
  name: typeof info.title === "string" ? info.title : projectName,
@@ -6413,7 +6565,8 @@ function fromOpenApi(document, projectName) {
6413
6565
  component: `${projectName}-api`,
6414
6566
  methods,
6415
6567
  details: typeof info.description === "string" ? info.description : `Imported OpenAPI surface of ${projectName}.`,
6416
- ...typeof info.version === "string" ? { version: info.version } : {}
6568
+ ...typeof info.version === "string" ? { version: info.version } : {},
6569
+ ...importedAuth ? { auth: importedAuth } : {}
6417
6570
  };
6418
6571
  return SurfaceSnapshotSchema.parse({
6419
6572
  projectName,
@@ -6528,6 +6681,10 @@ function projectOwnSurface(maxAudience) {
6528
6681
  component: comp.id,
6529
6682
  methods,
6530
6683
  ...comp.dispatch && comp.dispatch.length ? { dispatch: comp.dispatch } : {},
6684
+ // Project the backing Portal's auth + basePath so the codec can emit
6685
+ // OpenAPI security + per-portal servers self-contained from the snapshot.
6686
+ ...comp.auth && comp.auth.scheme !== "none" ? { auth: comp.auth } : {},
6687
+ ...comp.basePath ? { basePath: comp.basePath } : {},
6531
6688
  details: raw.details ?? "",
6532
6689
  ...raw.version ? { version: raw.version } : {},
6533
6690
  ...raw.stability ? { stability: raw.stability } : {}
@@ -6577,20 +6734,28 @@ function removeSnapshot(projectName, rootDir = getProjectRoot()) {
6577
6734
  function loadSurfaceSnapshots() {
6578
6735
  return listSnapshots();
6579
6736
  }
6737
+ function renderOpenApiForms(snapshot) {
6738
+ const renderedSet = toOpenApiSet(snapshot);
6739
+ return { renderedSet, ...renderedSet.length === 1 ? { rendered: renderedSet[0].document } : {} };
6740
+ }
6741
+ function writeSurfaceFile(outPath, body, snapshot) {
6742
+ const resolved = path5.resolve(outPath);
6743
+ fs4.mkdirSync(path5.dirname(resolved), { recursive: true });
6744
+ if (body !== void 0) fs4.writeFileSync(resolved, body);
6745
+ else writeYamlFile(resolved, snapshot);
6746
+ return resolved;
6747
+ }
6580
6748
  function exportSurface(maxAudience, format, outPath) {
6581
6749
  const snapshot = projectOwnSurface(maxAudience);
6582
- const rendered = format === "openapi" ? toOpenApi(snapshot) : void 0;
6583
- let writtenTo;
6584
- if (outPath) {
6585
- fs4.mkdirSync(path5.dirname(path5.resolve(outPath)), { recursive: true });
6586
- if (rendered !== void 0) {
6587
- fs4.writeFileSync(path5.resolve(outPath), rendered);
6588
- } else {
6589
- writeYamlFile(path5.resolve(outPath), snapshot);
6590
- }
6591
- writtenTo = path5.resolve(outPath);
6592
- }
6593
- return { snapshot, ...rendered !== void 0 ? { rendered } : {}, ...writtenTo ? { writtenTo } : {} };
6750
+ const openapi = format === "openapi" ? renderOpenApiForms(snapshot) : void 0;
6751
+ const body = openapi?.rendered ?? openapi?.renderedSet[0]?.document;
6752
+ const writtenTo = outPath ? writeSurfaceFile(outPath, body, snapshot) : void 0;
6753
+ return {
6754
+ snapshot,
6755
+ ...openapi?.rendered !== void 0 ? { rendered: openapi.rendered } : {},
6756
+ ...openapi ? { renderedSet: openapi.renderedSet } : {},
6757
+ ...writtenTo ? { writtenTo } : {}
6758
+ };
6594
6759
  }
6595
6760
  function importSurface(sourcePath, origin) {
6596
6761
  const resolved = path5.resolve(sourcePath);
@@ -11389,6 +11554,44 @@ var init_lint_allows = __esm({
11389
11554
  }
11390
11555
  });
11391
11556
 
11557
+ // src/core/rules/portal-call-auth.ts
11558
+ var portalCallAuthRule;
11559
+ var init_portal_call_auth = __esm({
11560
+ "src/core/rules/portal-call-auth.ts"() {
11561
+ "use strict";
11562
+ portalCallAuthRule = {
11563
+ name: "portal-call-auth",
11564
+ 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.",
11565
+ codes: [
11566
+ { code: "PORTAL_AUTH_UNMET", defaultSeverity: "warning", summary: "A narrative call into another component's authed Portal does not declare where its credential loads from" }
11567
+ ],
11568
+ check(ctx) {
11569
+ for (const impl of ctx.implementations) {
11570
+ const ownComponent = ctx.interfaceMap.get(impl.contract)?.component;
11571
+ const draft = ctx.isImplementationDraft(impl);
11572
+ for (const method of impl.methods ?? []) {
11573
+ for (const step of method.narrative ?? []) {
11574
+ if (step.type !== "call") continue;
11575
+ if (!step.targetComponent || step.targetComponent === ownComponent) continue;
11576
+ const target = ctx.componentMap.get(step.targetComponent);
11577
+ if (!target || target.componentType !== "Portal") continue;
11578
+ if (!target.auth || target.auth.scheme === "none") continue;
11579
+ if (step.auth?.from) continue;
11580
+ ctx.addIssue(
11581
+ "warning",
11582
+ "PORTAL_AUTH_UNMET",
11583
+ `Narrative step ${step.stepNumber} of "${method.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.`,
11584
+ impl.id,
11585
+ draft
11586
+ );
11587
+ }
11588
+ }
11589
+ }
11590
+ }
11591
+ };
11592
+ }
11593
+ });
11594
+
11392
11595
  // src/core/rules/index.ts
11393
11596
  function composeRuleSequence(extraRules = []) {
11394
11597
  const base = SDD_RULES.filter((r) => r !== lintAllowsRule);
@@ -11638,6 +11841,7 @@ var init_rules = __esm({
11638
11841
  init_hidden_state();
11639
11842
  init_dependency_conformance();
11640
11843
  init_lint_allows();
11844
+ init_portal_call_auth();
11641
11845
  init_source_analysis();
11642
11846
  init_types();
11643
11847
  init_type_analysis();
@@ -11659,6 +11863,9 @@ var init_rules = __esm({
11659
11863
  narrativeAntipatternsRule,
11660
11864
  narrativeDetailRule,
11661
11865
  portalsRule,
11866
+ // Cross-call auth: a narrative call into an authed Portal must name its
11867
+ // credential source (rides with the portal family).
11868
+ portalCallAuthRule,
11662
11869
  stereotypeDepsRule,
11663
11870
  patternsRule,
11664
11871
  // Facade shape rides with pattern ownership: same §7 doctrine, narrative side.
@@ -11721,6 +11928,7 @@ var init_rules = __esm({
11721
11928
  // L4 expectations: implementations and their code linkage.
11722
11929
  MISSING_IMPLEMENTATION_METHOD: "implementations",
11723
11930
  MISSING_SOURCE_PATH: "implementations",
11931
+ PORTAL_AUTH_UNMET: "implementations",
11724
11932
  MISSING_SOURCE_FILE: "implementations",
11725
11933
  SOURCE_PATH_ESCAPES_ROOT: "implementations",
11726
11934
  UNREALIZED_METHOD: "implementations",
@@ -15146,6 +15354,7 @@ __export(src_exports, {
15146
15354
  MethodImplementationSchema: () => MethodImplementationSchema,
15147
15355
  MethodParamSchema: () => MethodParamSchema,
15148
15356
  MethodSignatureSchema: () => MethodSignatureSchema,
15357
+ NamedOpenApiSpecSchema: () => NamedOpenApiSpecSchema,
15149
15358
  NamingRuleConfigSchema: () => NamingRuleConfigSchema,
15150
15359
  NarrativeDetailSchema: () => NarrativeDetailSchema,
15151
15360
  NarrativeStepSchema: () => NarrativeStepSchema,
@@ -15159,6 +15368,8 @@ __export(src_exports, {
15159
15368
  PathsConfigSchema: () => PathsConfigSchema,
15160
15369
  PatternDefSchema: () => PatternDefSchema,
15161
15370
  PatternRefSchema: () => PatternRefSchema,
15371
+ PortalAuthSchema: () => PortalAuthSchema,
15372
+ PortalAuthSchemeSchema: () => PortalAuthSchemeSchema,
15162
15373
  PortalTypeSchema: () => PortalTypeSchema,
15163
15374
  ProfileDefSchema: () => ProfileDefSchema,
15164
15375
  ProjectConfigSchema: () => ProjectConfigSchema,
@@ -15378,6 +15589,7 @@ __export(src_exports, {
15378
15589
  syncContextFiles: () => syncContextFiles,
15379
15590
  toMarkdown: () => toMarkdown,
15380
15591
  toOpenApi: () => toOpenApi,
15592
+ toOpenApiSet: () => toOpenApiSet,
15381
15593
  updateSpec: () => updateSpec,
15382
15594
  validateAsComplete: () => validateAsComplete,
15383
15595
  validateProjectConfig: () => validateProjectConfig,
@@ -15413,7 +15625,7 @@ function defaultTargetConfig(type) {
15413
15625
  enabled: true
15414
15626
  };
15415
15627
  }
15416
- var WAIRON_VERSION = "5.0.2-dev.7";
15628
+ var WAIRON_VERSION = "5.0.2-dev.9";
15417
15629
  var GITHUB_REPO = "SYW-Apps/Waffle-AIron";
15418
15630
  var ARCHITECT_AGENT_ID = "agent-architect";
15419
15631
  var ARCHITECT_TEMPLATE_ID = "architect";
@@ -16729,6 +16941,7 @@ init_yaml();
16729
16941
  MethodImplementationSchema,
16730
16942
  MethodParamSchema,
16731
16943
  MethodSignatureSchema,
16944
+ NamedOpenApiSpecSchema,
16732
16945
  NamingRuleConfigSchema,
16733
16946
  NarrativeDetailSchema,
16734
16947
  NarrativeStepSchema,
@@ -16742,6 +16955,8 @@ init_yaml();
16742
16955
  PathsConfigSchema,
16743
16956
  PatternDefSchema,
16744
16957
  PatternRefSchema,
16958
+ PortalAuthSchema,
16959
+ PortalAuthSchemeSchema,
16745
16960
  PortalTypeSchema,
16746
16961
  ProfileDefSchema,
16747
16962
  ProjectConfigSchema,
@@ -16961,6 +17176,7 @@ init_yaml();
16961
17176
  syncContextFiles,
16962
17177
  toMarkdown,
16963
17178
  toOpenApi,
17179
+ toOpenApiSet,
16964
17180
  updateSpec,
16965
17181
  validateAsComplete,
16966
17182
  validateProjectConfig,