# `tenant/cross-tenant` _(no submodules)_ ## Purpose Design doc §9.1 (final paragraph) requires that a saga step whose target tenant differs from the saga initiator's tenant be gated by the `saga.cross-tenant` capability on the owner plugin. Without the capability the orchestrator MUST refuse dispatch with `SAGA.CROSS_TENANT_DENIED` — quietly running the step is a tenant isolation violation. This module exports the canonical capability id and two helpers the orchestrator (and any saga aware middleware) funnels every dispatch through. The check is pure and side-effect-free so it can be applied at registry register time, at orchestrator start time, and at every step dispatch — three checkpoints that catch wiring drift at three levels of granularity. ## Features - `CROSS_TENANT_CAPABILITY` — the literal `"saga.cross-tenant"` capability id, pinned in the `@suaveplan/api-types` capabilities table. - `hasCrossTenantCapability(owner)` — predicate for non-throwing branches (e.g. when computing UI affordances). - `assertCrossTenantAllowed(owner, initiator, target)` — throws `SagaError(SAGA.CROSS_TENANT_DENIED)` when the gate refuses; no-op otherwise. ## Usage: Basic ```ts import { assertCrossTenantAllowed, CROSS_TENANT_CAPABILITY, } from "@suaveplan/api-saga/tenant/cross-tenant"; assertCrossTenantAllowed(owner, "acme", "acme"); // same-tenant — ok assertCrossTenantAllowed(owner, "acme", "globex"); // throws unless owner declares capability ``` ## Usage: Advanced Compose the gate with the cross-cutting capability checker used elsewhere in the host — `hasCrossTenantCapability` is non-throwing so it composes inside conditional pipelines: ```ts import { hasCrossTenantCapability } from "@suaveplan/api-saga"; const canDispatch = initiatorTenantId === targetTenantId || hasCrossTenantCapability(owner); if (!canDispatch) { metrics.crossTenantDeniedTotal.add(1, { saga: owner.saga.name, owner: owner.pluginName, }); throw new SagaError(SAGA_CODES.CROSS_TENANT_DENIED, "denied"); } ``` ## API Reference - `CROSS_TENANT_CAPABILITY: "saga.cross-tenant"` — literal constant. - `hasCrossTenantCapability(owner: RegisteredSagaOwner): boolean`. - `assertCrossTenantAllowed(owner, initiatorTenantId, targetTenantId): void` — no-op on accept, throws `SAGA.CROSS_TENANT_DENIED` on refuse. ## Implementation Notes The capability is owner-scoped rather than step-scoped because the owner declares both. Per-step opt-outs would require manifest changes to `sagas.owns[].steps[]` and threading a per-step capability list through the orchestrator — a strictly larger surface than the design doc calls for. If a future change needs finer granularity, the registry's `RegisteredSagaOwner.saga.capabilities` shape is the forward-compatible extension point.