## Modules

<dl>
<dt><a href="#module_@onlineapps/service-wrapper/ConfigLoader">@onlineapps/service-wrapper/ConfigLoader</a></dt>
<dd><p>ConfigLoader - Unified configuration file loading for business services</p>
<p>Single responsibility: Load all service configuration files
from standardized locations with consistent error handling.</p>
<p>CENTRAL REGISTRY of the paths this package owns. Both configs below are
mandatory: neither has a default and neither is skippable, so a service
missing one does not boot — <code>loadServiceConfig()</code> and <code>loadOperations()</code>
throw, naming the absolute path they tried. That is where &quot;required&quot; is
stated; the registry entries carry no such field, because nothing read one
(d.772, <code>change-discipline.md</code> § Removing something removes its declaration).</p>
<ul>
<li>config/service/config.json     - Service configuration</li>
<li>config/service/operations.json - Operations specification</li>
</ul>
<p>One more path stands in that registry, and it is NOT a config this loader
loads:</p>
<ul>
<li>conn-runtime/validation-proof.json (NOT loaded) - the validation OUTPUT;
only its path is owned here, for the one thing the wrapper does with the
file: delete it on revalidation.</li>
</ul>
<p>Usage:
  const { ConfigLoader } = require(&#39;@onlineapps/service-wrapper&#39;);
  const config = ConfigLoader.loadAll({ env: process.env });</p>
<p>Design principle: ONE place to define where every config lives, and to load
the two that are configs. Validation of their CONTENT is not here — the
shape of <code>operations.json</code> is <code>@onlineapps/conn-orch-validator</code>&#39;s subject;
this loader checks only that the file parses and carries its top-level key.</p>
</dd>
<dt><a href="#module_@onlineapps/service-wrapper">@onlineapps/service-wrapper</a></dt>
<dd><p>Infrastructure orchestration for microservices.
Thin layer that delegates all work to specialized connectors.</p>
</dd>
<dt><a href="#module_@onlineapps/service-wrapper/logger">@onlineapps/service-wrapper/logger</a></dt>
<dd><p>Shared logger proxy for business services.</p>
<p>Handlers, services and controllers are <code>require</code>d at module load time, long
before <code>wrapper.initialize()</code> has produced a real logger. This module is the
late-binding indirection between the two: bootstrap calls <code>setLogger()</code> once,
every consumer calls <code>getLogger()</code> whenever it actually logs.</p>
<p>Fail-fast, no fallback. If <code>getLogger()</code> is reached before <code>setLogger()</code>, that
is a wiring defect and it throws — it does NOT hand back a console-shaped
substitute. A silent console fallback makes a service that lost its real
logger look healthy: the log lines still appear on stdout, while nothing
reaches the monitoring connector, the log files or the correlation context.
See <code>architecture-principles.md</code> §3 (No Fallbacks) and §4 (Fail-Fast).</p>
<p>Consumers call <code>getLogger()</code> at call time, never at module scope — binding
the result into a module-level constant would capture the state before
bootstrap and defeat the indirection.</p>
</dd>
<dt><a href="#module_redactedInputGuard">redactedInputGuard</a></dt>
<dd><p>redactedInputGuard.js — a redacted copy of the input never reaches a handler.</p>
<p><code>@onlineapps/logger-contract</code> <code>redactSensitiveDeep</code> replaces the value of
every declared sensitive field with <code>REDACTED_PLACEHOLDER</code> in the copies made
for logs, traces and events (SecretBox §8). Those are observability copies.
The execution message — the one that carries the value to its owning handler
— must never be one of them: a handler handed the placeholder authenticates
with the string, stores it as a password or sends it as a token, and the run
reports success.</p>
<p>This wrapper redacts nothing. It GUARDS, in <code>invokeOperation()</code>: every
declared sensitive field whose value IS the placeholder — matched by key name
at any depth, the way <code>redactSensitiveDeep</code> matches — refuses the message
with <code>InvalidEnvelopeError</code> (400 <code>INVALID_ENVELOPE</code>) before the handler runs.
The refusal names the FIELD, never a value.</p>
<p>Where the rule comes from (lead&#39;s decision N1, 0.2b-1053):</p>
<ul>
<li>the orchestrator&#39;s message carries <code>sensitive_input_fields</code>: the recipe&#39;s
rule, the sorted unique union of every operation&#39;s declaration
(<code>@onlineapps/conn-orch-orchestrator</code> <code>_executeTaskStep</code>), and the guard
applies exactly that;</li>
<li>a message without the key is a direct dispatch
(<code>_dispatchViaInvokeOperation</code>), which has no recipe, so its rule is the
operation&#39;s own <code>operations.json</code> declaration — read through the ONE
reader of that declaration, <code>sensitiveFieldsForOperation</code>;</li>
<li>a key present in any other shape than an array of non-empty strings is a
defective envelope and is refused by name.</li>
</ul>
<p>The walk below is deliberately minimal and local: <code>logger-contract</code> exports
the placeholder and the declaration reader, but not a walk that FINDS a value
without copying the tree (<code>redactSensitiveDeep</code> clones and replaces). Lifting
this walk into <code>logger-contract</code> beside <code>redactSensitiveDeep</code> is a candidate
for that package, recorded as a finding of d.1280 — until then this is the
one implementation in this package.</p>
</dd>
<dt><a href="#module_shutdownDeadline">shutdownDeadline</a></dt>
<dd><p>The deadline of <code>ServiceWrapper.shutdown()</code> — one watchdog, armed before the first
teardown step, that ends the process when the teardown does not.</p>
<p>Owner&#39;s decision: api/docs/governance/confirmations/biz-shutdown-deadline.md 001 (d.1102
measured, d.1105 built). Without it a teardown against a Redis or a broker that holds
the connection and never answers never ended: <code>bootstrap()</code>&#39;s <code>process.exit(0)</code> never
came, and on the fatal path — where nothing sends a SIGKILL — the process stayed up,
dead, for good.</p>
<p>The process edge (<code>writeStderr</code>, <code>exit</code>) is injected; nothing here touches <code>process</code>.</p>
</dd>
<dt><a href="#module_signalShutdown">signalShutdown</a></dt>
<dd><p>The SIGTERM/SIGINT handler <code>bootstrap()</code> installs — the one place a signal becomes a
teardown and an exit code.</p>
<p>Owner&#39;s decision: api/docs/governance/confirmations/biz-shutdown-deadline.md 001 — exit 0
only after a clean teardown, exit 1 when <code>shutdown()</code> rejects, and a second signal
during a running teardown is ignored. Until d.1105 the handler was
<code>await wrapper.shutdown(); process.exit(0)</code> on both signals: a rejected teardown ended
the process on an unhandled rejection instead, and a second signal started a second
teardown over the first (d.1102).</p>
<p>A teardown that never settles is not this handler&#39;s business: the deadline armed
inside <code>shutdown()</code> ends it (<code>shutdownDeadline.js</code>).</p>
</dd>
<dt><a href="#module_stepDeadline">stepDeadline</a></dt>
<dd><p>The deadline of ONE ATTEMPT of a step — how long this wrapper lets an
operation&#39;s handler run before it abandons the attempt.</p>
<p>Owner&#39;s word (conf <code>step-error-classification</code> 005 — variant A, a deadline
per ATTEMPT per operation): the deadline
is per operation, declared as <code>timeout_ms</code> in <code>config/service/operations.json</code>
beside <code>retry</code>; an operation that declares none gets the platform default;
a declaration outside the platform range is refused by the operation&#39;s name,
never replaced by the default — the rule <code>retry</code> follows in
<code>@onlineapps/conn-orch-orchestrator</code> (<code>retryPolicy.js</code>).</p>
<p>The numbers — the platform default and the range — are not this module&#39;s:
<code>@onlineapps/handler-contract</code> owns them (<code>STEP_TIMEOUT_MIN_MS</code>,
<code>STEP_TIMEOUT_MAX_MS</code>, <code>DEFAULT_STEP_TIMEOUT_MS</code>), and this module reads
them and keeps no copy (conf <code>handler-contract</code> 002, d.1382). It exports
<code>DEFAULT_STEP_TIMEOUT_MS</code>, <code>MIN_STEP_TIMEOUT_MS</code> and <code>MAX_STEP_TIMEOUT_MS</code>,
which are those values.</p>
<p>The wrapper ENFORCES it (<code>ServiceWrapper.invokeOperation</code> races the handler
against it and aborts <code>ctx.abortSignal</code>); whether a timed-out step is tried
again is the orchestrator&#39;s decision (005: a reading operation by the retry
policy, a writing one straight to <code>workflow.failed</code>) — the attempt ends with a TIMEOUT-class
error (<code>StepTimeoutError</code>, 504 <code>STEP_TIMEOUT</code>), and status 504 is
<code>ErrorTypes.TIMEOUT</code> in <code>@onlineapps/error-handler-core</code> <code>ErrorClassifier.js</code>.</p>
</dd>
</dl>

## Classes

<dl>
<dt><a href="#ContextBuilder">ContextBuilder</a></dt>
<dd><p>ContextBuilder — assembles an OperationContext for a single invocation.</p>
<p>Contract: api/docs/biz/10-invocation/operation-context.md and api/docs/biz/10-invocation/handler-dispatch.md
Layer: L3 orchestration (see ARCHITECTURE_PRINCIPLES.md §7).</p>
<ul>
<li>Depends on connectors (L1/L2) via constructor injection only.</li>
<li>Imports <code>./OperationContext</code> and the error CLASSES from <code>./ErrorMapper</code>
(L2 types, no behaviour — ErrorMapper itself imports nothing, so there is
no cycle; <code>./OperationContext</code> already depends on the same module).
MUST NOT import HandlerRegistry or SchemaValidator, and MUST NOT call
ErrorMapper.map() — mapping stays the wrapper&#39;s job.</li>
<li>Does NOT validate handler input/output schemas (SchemaValidator owns that).</li>
<li>Does NOT invoke handlers (ServiceWrapper owns that).</li>
</ul>
<p>SRP: single job — translate (mqMessage, operationSpec) into a ready-to-use
OperationContext plus a release() hook the wrapper must call after the
handler returns (success or failure).</p>
</dd>
<dt><a href="#ErrorMapper">ErrorMapper</a></dt>
<dd></dd>
<dt><a href="#HandlerLoader">HandlerLoader</a></dt>
<dd></dd>
<dt><a href="#HandlerRegistry">HandlerRegistry</a></dt>
<dd></dd>
<dt><a href="#SchemaValidator">SchemaValidator</a></dt>
<dd></dd>
<dt><a href="#HandlerContractError">HandlerContractError</a></dt>
<dd><p>A handler broke the error contract — it raised a code the wrapper produces.</p>
<p>Deliberately NOT branded and carrying no <code>code</code> of its own: it is not a
business outcome, it is our defect, so <code>ErrorMapper</code> takes it down the
unhandled-fault branch — <code>error</code> level WITH the stack in the log, and a masked
<code>500 INTERNAL_ERROR</code> to the caller
(<code>api/docs/biz/70-contracts/error-handling.md</code> §5). A code of its own would be
a platform-wide identity, and that dictionary is added to by changing the
contract node, not by a library minting a name.</p>
</dd>
<dt><a href="#ServiceWrapper">ServiceWrapper</a></dt>
<dd><p>ServiceWrapper class
Collection of connectors that enhance business service with infrastructure</p>
</dd>
</dl>

## Constants

<dl>
<dt><a href="#INTERNAL_ERROR_CODE">INTERNAL_ERROR_CODE</a></dt>
<dd><p>The code an error gets when nothing about it was declared.</p>
</dd>
<dt><a href="#MALFORMED_BUSINESS_ERROR_CODE">MALFORMED_BUSINESS_ERROR_CODE</a></dt>
<dd><p>The code a BRANDED error gets when its shape is broken — our own defect.</p>
</dd>
<dt><a href="#UNDECLARED_FAILURE_CODES">UNDECLARED_FAILURE_CODES</a> : <code>ReadonlySet.&lt;string&gt;</code></dt>
<dd><p>The codes this file puts on an envelope when the failure declared NOTHING.</p>
<p>They are the opposite of every other code the table produces: the rest are a
handler&#39;s (or the wrapper&#39;s) statement about an outcome, these two are the
absence of one. An <code>INTERNAL_ERROR</code> envelope means an unbranded error nobody
classified; <code>MALFORMED_BUSINESS_ERROR</code> means an error that claimed the brand
and broke the shape.</p>
<p>Exported because an envelope loses the distinction otherwise. Once
<code>ServiceWrapper._dispatchViaInvokeOperation()</code> rebuilds a failure as a plain
<code>Error</code> with <code>code</code> and <code>status</code>, a masked 500 <code>INTERNAL_ERROR</code> is shaped
exactly like a declared 500 <code>DATA_INTEGRITY_VIOLATION</code>, and only this list
separates them (<code>ServiceWrapper._classifyDeliveryError()</code> — an undeclared
failure keeps its delivery attempts, a declared one does not).</p>
</dd>
<dt><a href="#CONTRACT_RETRYABLE_CODES">CONTRACT_RETRYABLE_CODES</a> : <code>ReadonlySet.&lt;string&gt;</code></dt>
<dd><p>The codes whose dictionary row declares that a retry is meaningful.</p>
<p>A code&#39;s declared identity is not only its name and its status: the accepted
dictionary carries a <code>Retry</code> column, and that column is part of what the code
means platform-wide. <code>PROVIDER_UNAVAILABLE</code> is the row that says yes.</p>
<p>Owner: <code>api/docs/biz/70-contracts/error-handling.md</code> § Accepted domain codes —
a row there is the fact, this list is only the machine-readable side of it, so
a code joins or leaves this set when that table changes and never otherwise.
❌ Not a flag on an error instance: which codes may be retried is a property
of the contract dictionary, not of one throw
(<code>.claude/rules/single-source-of-truth.md</code>).</p>
<p>Read by <code>ServiceWrapper._classifyDeliveryError()</code>: such a code keeps its
delivery attempts, exactly as an undeclared failure does, while every other
declared business code is settled on the first delivery (d.547).</p>
</dd>
<dt><a href="#path">path</a></dt>
<dd><p>HandlerLoader</p>
<p>L3 Orchestration helper used by HandlerRegistry. Resolves v3 handler
references of the form <code>&quot;relative/path#exportName&quot;</code> to actual exported
functions, relative to the host service&#39;s source directory.</p>
<p>Layer: L3 (Orchestration). Pure filesystem + require. No cross-imports
from other wrapper modules.</p>
</dd>
<dt><a href="#MQConnector">MQConnector</a></dt>
<dd><p>Service Wrapper - Collection of connectors for business services</p>
<p>Provides infrastructure components in single-process architecture:</p>
<ul>
<li>MQ integration (RabbitMQ)</li>
<li>Service discovery (Registry)</li>
<li>Monitoring and metrics</li>
<li>Health checks</li>
</ul>
<p>Uses HTTP pattern exclusively - no direct handler calls</p>
</dd>
<dt><a href="#WRAPPER_RESERVED_ERROR_CODES">WRAPPER_RESERVED_ERROR_CODES</a></dt>
<dd><p>The error codes this wrapper produces about its own dispatch, with what a
handler that means the same thing throws instead.</p>
<p>The dictionary of codes, and which of them belong to the wrapper rather than
to a handler, has one owner — the nodes cited below, not this comment.</p>
<p><code>CLIENT_CANCELLED</code> is deliberately NOT here. <code>ErrorMapper</code> matches it on the
error&#39;s <code>name</code> outside the brand contract, because the abort it must
recognise is Node&#39;s own <code>AbortController</code> <code>DOMException</code> — which a handler
awaiting an aborted call legitimately propagates.</p>
<p>Until this Map existed nothing enforced the split: a handler lives in a
service repository, which review here never sees, and the cost of missing one
is not cosmetic — see <code>_invokeHandlerUnderContract()</code>.</p>
</dd>
<dt><a href="#REQUIRED_CONN_ORCH_ORCHESTRATOR_MAJOR">REQUIRED_CONN_ORCH_ORCHESTRATOR_MAJOR</a></dt>
<dd><p>The orchestrator major the wrapper&#39;s v3 invocation model requires.</p>
<p>ONE constant: the guard below branches on it and the refusal names it, so the
sentence an operator reads cannot drift from the comparison that raised it
(<code>change-discipline.md</code> § One rail per concern). WHICH released version
carries this major is not written here — that is the platform SSOT&#39;s fact
(<code>api/config/libraries.json</code>), and a number copied into a message is a
descriptive fact maintained by hand (<code>doc-code-binding.md</code> §1).</p>
</dd>
<dt><a href="#REGISTRY_DISCOVERY_TIMEOUT_KEY">REGISTRY_DISCOVERY_TIMEOUT_KEY</a></dt>
<dd><p>The PLATFORM env key that owns the deadline of one discovery lookup (d.1093). Named
once here, so renaming it is one edit in this package; its declaration (why, consumers,
value) is <code>api/config/shared-env.json</code>.</p>
</dd>
<dt><a href="#BOOT_DISPOSITION">BOOT_DISPOSITION</a></dt>
<dd><p>What an operator should do next about a failed boot phase, carried ON the
error rather than decided at the <code>catch</code> that reports it.</p>
<p>FÁZE 0.3 already worked this way — it reads <code>error.code !== &#39;MQ_CONNECTION_FATAL&#39;</code>
off the error <code>@onlineapps/mq-client-core</code> threw, because only the layer that
produced the failure knows whether asking again can change the answer. This is
the same rail, named once and readable by every phase that has more than one
disposition.</p>
<p>A phase whose failure is always of one kind (0.12, 0.15, 0.10) passes its
constant and reads nothing — the marker is for the phases where the disposition
is a FACT about the failure, not about the phase.</p>
</dd>
<dt><a href="#STATIC_VALIDATION_STEPS">STATIC_VALIDATION_STEPS</a></dt>
<dd><p>The Tier-1 steps that read NOTHING but the files already on disk.</p>
<p><code>@onlineapps/conn-orch-validator</code> 10.0.0 runs seven steps and reports each one
under its own key in <code>result.steps</code> (<code>ValidationOrchestrator.runFullValidation()</code>):</p>
<p>  1 <code>structure</code>   the service tree — <code>ServiceStructureValidator</code>
  2 <code>config</code>      <code>config/service/*.json</code>
  3 <code>env</code>         the env contract the service declares
  4 <code>operations</code>  operations.json against the v3 shape
  5 <code>cookbooks</code>   the service&#39;s own recipes, RUN against handlers and DB
  6 <code>connectors</code>  warnings only — it can never fail a validation
  7 <code>manifest</code>    the uniform, measured on the checkout</p>
<p>Five of them decide from files alone. A verdict of theirs is the same on the
next attempt and on the attempt after that, because nothing between two
attempts changes what they read — so retrying is not caution, it is 18,5 minutes
of a boot that cannot succeed (measured twice: biz-converter <code>exit 1</code> after
18 min 53 s, W591 on <code>MISSING_COOKBOOKS</code>). Step 5 is the one that depends on the
world around the service, and it is the one that keeps the budget.</p>
<p>The classification reads the STEP, never the message: <code>result.errors</code> is one
flat array into which step 1 pushes objects and steps 2/4/5/7 push sentences
(that mixture is why <code>describeValidationFinding()</code> exists), so a message tells
an operator what happened and tells this code nothing it can decide on.</p>
</dd>
<dt><a href="#DEPENDENCY_HEALTH_FIELD">DEPENDENCY_HEALTH_FIELD</a></dt>
<dd><p>The cached health signal each dependency component keeps, by component name.</p>
<p>ONE place says which components there are and where each one&#39;s last verdict
lives, so <code>refreshDependencyHealth()</code> writes the field and
<code>_recordDependencyHealth()</code> compares against it without either of them
spelling the field name again.</p>
</dd>
<dt><a href="#ERROR_HANDLING_KEYS">ERROR_HANDLING_KEYS</a></dt>
<dd><p>The <code>wrapper.errorHandling</code> keys this package forwards to
<code>@onlineapps/conn-infra-error-handler</code>. Named, not spread: a key outside this
list configures nothing in the core, and forwarding it blindly would let a
misspelling pass in silence.</p>
<p><code>dlqEnabled</code> left the list in d.493, for the reason
<code>@onlineapps/error-handler-core</code> retired it (that package&#39;s CHANGELOG,
<code>### Removed — DLQRouter</code>): the core stopped reading the key, so forwarding it
configured nothing. It is not merely dropped — a config still writing it is
REFUSED by name (<code>_assertRetiredDlqEnabledKey</code>).</p>
<p><code>maxRetries</code>, <code>retryDelay</code> and <code>retryMultiplier</code> left it in d.1264 and are
refused the same way (<code>RETIRED_RETRY_KEYS</code>).</p>
</dd>
<dt><a href="#RETIRED_RETRY_KEYS">RETIRED_RETRY_KEYS</a></dt>
<dd><p>The three <code>wrapper.errorHandling</code> keys that configured <code>RetryHandler</code> of
<code>@onlineapps/error-handler-core</code>, retired in d.1264 and REFUSED by name
(<code>_assertRetiredRetryKeys</code>).</p>
<p>Nothing on this package&#39;s path read what they configured: the orchestrator
calls the connector&#39;s <code>classifyError()</code> only, and this wrapper its
<code>logError()</code> only — neither reaches <code>retryHandler</code>. A step is retried by
<code>@onlineapps/conn-orch-orchestrator</code> under the retry policy of its operation,
declared as <code>retry</code> in <code>operations.json</code> and handed over by the
<code>operationMutatesLookup</code> hook (conf <code>step-error-classification</code> 002 variant B;
004: &quot;<code>wrapper.errorHandling.*</code> is removed&quot;). The four § Removing questions
are answered in this package&#39;s CHANGELOG, <code>[Unreleased]</code> — <code>Removed</code>.</p>
</dd>
<dt><a href="#REVALIDATION_SLOW_MS">REVALIDATION_SLOW_MS</a></dt>
<dd><p>The restart cooldown window, and ONLY that: how long a service that failed
validation for the SAME cause waits before it tries again after a restart
(<code>_applyRestartCooldown()</code>).</p>
<p>It is not a schedule. Until d.755b a third ladder stage <code>slow_backoff</code> claimed
it as one — a 6-hour retry forever — and nothing ever entered that state, so
the claim was never true. The ladder ends at the hourly ceiling with a fatal
teardown, which is the documented end of the road.</p>
</dd>
<dt><a href="#REVALIDATION_STATE">REVALIDATION_STATE</a></dt>
<dd><p>Every state the re-validation cycle can be in. TWO backoff stages, because
two is what the concept declares: fast ladder, then hourly up to the ceiling,
then <code>_shutdownFatally()</code> — &quot;there is no third retry phase&quot;
(<code>api/docs/architecture/validator.md</code> - Re-Validation Backoff Schedule).</p>
</dd>
<dt><a href="#WRAPPER_RAISED_ERROR">WRAPPER_RAISED_ERROR</a></dt>
<dd><p>The provenance mark: this error was raised by the wrapper, not by a handler.</p>
</dd>
</dl>

## Functions

<dl>
<dt><a href="#isPositiveIntegerId">isPositiveIntegerId(value)</a> ⇒ <code>boolean</code></dt>
<dd><p>Is this envelope value a usable tenancy id?</p>
<p>The rule comes from the wire contract, not from this file:
api/docs/biz/10-invocation/mq-envelope.md declares <code>envelope.tenant_id</code> an
integer and says &quot;Positive integer.&quot;. Zero is explicitly not a value
(api/docs/biz/20-tenancy/multitenancy.md §1.6; the tenant-wide scope was
retired by the owner on 2026-09-03).</p>
<p>A decimal STRING is accepted alongside the number because that is what
already crosses this boundary and is forwarded unchanged — the builder
validates, it never converts. The accepted string form is exact: no sign, no
leading zero, no whitespace, no decimal point, so <code>&quot;5&quot;</code> passes while <code>&quot;05&quot;</code>,
<code>&quot; 5&quot;</code>, <code>&quot;5.0&quot;</code> and <code>&quot;-5&quot;</code> do not.</p>
</dd>
<dt><a href="#isMissingId">isMissingId(value)</a> ⇒ <code>boolean</code></dt>
<dd><p>Is this envelope id missing?</p>
<p>ONE predicate for <code>workflow_id</code>, <code>correlation_id</code> and <code>step_id</code> — three
fields of the same kind, so a value that is a value for one of them is a
value for all three. Until d.363 the first two were checked with
<code>.length === 0</code> and the third with <code>.trim() === &#39;&#39;</code>, so an id of nothing but
whitespace passed two guards and was refused by the third, inside one method.
The value that got through is not harmless: <code>workflow_id</code> and <code>step_id</code> name
the storage prefix <code>content/&lt;workflow_id&gt;/&lt;step_id&gt;</code>, and <code>correlation_id</code> is
what a trace is joined on.</p>
<p>It refuses blankness and NOTHING else. An id with real content and spaces
around it is a value and is forwarded exactly as published — trimming it here
would be the builder inventing a second spelling of somebody&#39;s id
(<code>architecture-principles.md</code> §3), and this file validates, it never converts.</p>
</dd>
<dt><a href="#describeRejectedId">describeRejectedId(value)</a> ⇒ <code>string</code></dt>
<dd><p>How a rejected value may be named in an error message.</p>
<p>A number is safe to print — it carries no foreign data. Anything else is
named by its type ONLY: the envelope is caller-supplied, so a string or an
object could carry a secret or personal data into a log line.</p>
</dd>
<dt><a href="#requireSchedulerPublisherFactory">requireSchedulerPublisherFactory()</a> ⇒ <code>function</code></dt>
<dd><p>Továrna plánovače z jádra — načtená LÍNĚ a s hláškou §5.</p>
<p>Port potřebuje z pinovaného <code>@onlineapps/mq-client-core</code> jmenný prostor
<code>scheduler</code> s továrnou <code>createSchedulerPublisher</code>. Verze, která port
plánovače ještě nevydává, se načte BEZ chyby a prostor <code>scheduler</code> prostě
nemá — to je druhá větev níž („export chybí&quot;), a běžný případ. Samotné
<code>require</code> selže jen tehdy, když jádro nejde načíst vůbec (v <code>node_modules</code>
chybí nebo je rozbité) — první větev („modul se nenačte&quot;). Chybějící sekce
<code>scheduler</code> v <code>queueConfig</code> <code>require</code> neshodí: odmítne až složení jména
fronty, tedy volání, ne načtení.</p>
<p>Kdyby se jádro načítalo v hlavičce souboru, spadl by při nenačitatelném
jádru CELÝ wrapper při načtení modulu — tedy i službě, která plánovač vůbec
nepoužívá. Načítá se proto až při stavbě portu: vada pinu se pozná při
stavbě kontextu, jménem slotu a s opravou (<code>architecture-principles.md</code> §5),
a ne pádem, který o plánovači nic neříká.</p>
</dd>
<dt><a href="#errorLogFields">errorLogFields(err)</a> ⇒ <code>Object</code></dt>
<dd><p>The fields every log line ABOUT an error must carry: the error itself, its
text as <code>exception.message</code> and its class as <code>exception.type</code> — the
OpenTelemetry semantic-convention attributes, as own, enumerable properties.</p>
<p><code>Error.message</code> is non-enumerable. A meta of <code>{ err }</code> therefore survives
<code>JSON.stringify</code> as <code>{ name, code, status, details }</code> — the sentence saying
what went wrong is gone, except where a <code>stack</code> key happens to be passed too,
and then only as the first line of it. Measured (BIZ-converter, dev, wrapper
8.1.1): four of this file&#39;s log lines pass no <code>stack</code>, so their message did
not reach the operator at all.</p>
<p>Why not a <code>message</code> key: the telemetry log envelope carries the record&#39;s own
text in its body, and the monitoring consumer merges the body over the data,
so a <code>message</code> in the data never reached Loki (measured d.868, DEV Loki).</p>
<p><code>exception.type</code> is the constructor&#39;s name, not <code>err.name</code>: <code>BusinessError</code>
sets <code>name</code> itself, so a service subclass that sets none of its own reports
<code>BusinessError</code> by name while its constructor still names the class.</p>
<p>One rail for the whole package: <code>ServiceWrapper</code> uses it for the two lines it
writes about an error itself (<code>change-discipline.md</code> § One rail per concern).</p>
</dd>
<dt><a href="#buildEnvelope">buildEnvelope(status, code, message, [details], [correlation_id])</a> ⇒ <code><a href="#ErrorEnvelope">ErrorEnvelope</a></code></dt>
<dd><p>Assemble the caller-facing envelope. <code>details</code> and <code>correlation_id</code> appear
only when supplied — the shape the contract documents, nothing padded.</p>
</dd>
<dt><a href="#describeFailure">describeFailure(err, [context])</a> ⇒ <code>Object</code></dt>
<dd><p>THE mapping. One place decides both halves of what a throw becomes:</p>
<ul>
<li><code>envelope</code> — what the caller receives;</li>
<li><code>log</code>      — the line whoever HANDLES the throw writes for it.</li>
</ul>
<p>Two halves, one branching table, because a second copy of the table is how a
caller-facing status and its log line drift apart
(<code>.claude/rules/change-discipline.md</code> § One rail per concern).</p>
<p>Pure: it decides, it does not log. <a href="#ErrorMapper+map">map</a> is the wrapper&#39;s
own path and writes the line; <a href="#toCallerEnvelope">toCallerEnvelope</a> is the path for a
service that already logs the failure itself and needs the envelope only.</p>
</dd>
<dt><a href="#toCallerEnvelope">toCallerEnvelope(err, [context])</a> ⇒ <code><a href="#ErrorEnvelope">ErrorEnvelope</a></code></dt>
<dd><p>The envelope a caller receives for <code>err</code> — and NOTHING else. No logger, no
log line, no other side effect: a service that already logged the failure
itself calls this instead of instantiating an <a href="#ErrorMapper">ErrorMapper</a>, so one
failure stays one log line (measured: the converter logged every 5xx twice,
BIZ-converter 2026-09-14).</p>
<p>Same mapping table as <a href="#ErrorMapper+map">map</a>, because it IS the same
function underneath — the two differ only in whether the log half is used.</p>
<p>Contract: api/docs/biz/70-contracts/error-handling.md;
the table: api/docs/biz/10-invocation/handler-dispatch.md § <code>ErrorMapper</code> — throw-to-envelope.</p>
</dd>
<dt><a href="#_buildDefaultAjv">_buildDefaultAjv()</a> ⇒ <code>object</code></dt>
<dd><p>Build the default Ajv instance used when the caller does not inject one.
Change the option set only together with the documents named under See.</p>
</dd>
<dt><a href="#_summariseAjvError">_summariseAjvError(phase, errors)</a> ⇒ <code>string</code></dt>
<dd><p>Summarise the first ajv error into a human-readable string.
Shape: <code>&lt;phase&gt;/&lt;instancePath&gt; &lt;ajv message&gt;</code></p>
</dd>
<dt><a href="#_describeNonAjv">_describeNonAjv(value)</a> ⇒ <code>string</code></dt>
<dd><p>Name what was handed in where an Ajv instance was expected, for the refusal
message. It describes the value, never prints it: the caller knows what it
passed, and a schema compiler&#39;s innards are not error-message material.</p>
</dd>
<dt><a href="#linkAbortSignal">linkAbortSignal(callerSignal, controller)</a> ⇒ <code>function</code></dt>
<dd><p>Tie the caller&#39;s own abort signal, when one came with the message, to the
controller this wrapper owns for the attempt: the caller aborting aborts the
attempt too, with the caller&#39;s reason (d.1264b).</p>
</dd>
<dt><a href="#readConnOrchOrchestratorContractVersion">readConnOrchOrchestratorContractVersion()</a> ⇒ <code>Object</code></dt>
<dd><p>The wrapper&#39;s v3 invocation model wires OrchestratorConnector with invoker RPC,
which the orchestrator has carried since the major above. If npm resolves an
older one (stale tree / lockfile), WorkflowOrchestrator throws
&quot;apiMapper is required&quot; — fail here with an explicit contract error instead.</p>
</dd>
<dt><a href="#describeValidationFinding">describeValidationFinding(finding)</a> ⇒ <code>string</code></dt>
<dd><p>One finding of <code>ValidationOrchestrator</code> as a sentence an operator can act on.</p>
<p><code>results.errors</code> is a MIXED array and always has been: steps 2, 4, 5 and 7 push
sentences (the manifest&#39;s own <code>describeFinding</code>, a cookbook failure), while step
1 — <code>ServiceStructureValidator</code> — pushes objects <code>{ type, path, message, fix }</code>.
Interpolating that array into a template literal turned every object into
<code>[object Object]</code>, so the code (<code>MISSING_COOKBOOKS</code>), the path and the way out
were dropped at the last step before the log (<code>architecture-principles.md</code> §5;
measured by W591 on a boot whose <code>tests/cookbooks</code> was empty).</p>
<p>A sentence travels verbatim — the step that produced it already wrote it in the
<code>[Context] Problem - Expected/Fix</code> shape. An object is rendered into that shape
here. Anything else is serialized rather than described: a formatter that threw
would replace the failure it was called to report.</p>
</dd>
<dt><a href="#describeValidationFailure">describeValidationFailure(errors)</a> ⇒ <code>string</code></dt>
<dd><p>The message the two Tier-1 throw sites raise — the startup path (FÁZE 0.2,
<code>_ensureValidationProof</code>) and the hourly revalidation (<code>_executeRevalidation</code>).
ONE helper, because one wording of one fact is one rail
(<code>.claude/rules/change-discipline.md</code> § One rail per concern).</p>
</dd>
<dt><a href="#markBootDisposition">markBootDisposition(error, disposition)</a> ⇒ <code>Error</code></dt>
<dd><p>Stamp the disposition on an error and hand it back, so a <code>throw</code> stays one
expression.</p>
</dd>
<dt><a href="#failedStaticValidationSteps">failedStaticValidationSteps(result)</a> ⇒ <code>Array.&lt;string&gt;</code></dt>
<dd><p>Which static steps of this result failed.</p>
<p>All five report <code>{ valid, errors }</code>; only step 5 answers with <code>success</code>, and it
is deliberately not in the list. A step that did not run at all is absent from
<code>result.steps</code> — the run fails fast after steps 1 and 3 — and an absent step has
no verdict to read.</p>
</dd>
<dt><a href="#markWrapperRaised">markWrapperRaised(err)</a> ⇒ <code>Error</code></dt>
<dd><p>Stamp an error the wrapper is about to raise from inside a handler&#39;s call.</p>
</dd>
<dt><a href="#isWrapperRaised">isWrapperRaised(err)</a> ⇒ <code>boolean</code></dt>
<dd><p>Did this wrapper raise the error itself?</p>
</dd>
</dl>

## Typedefs

<dl>
<dt><a href="#ContextBuildResult">ContextBuildResult</a> : <code>Object</code></dt>
<dd><p>What build() resolves to.</p>
</dd>
<dt><a href="#ErrorEnvelope">ErrorEnvelope</a> : <code>Object</code></dt>
<dd><p>MQ response envelope produced by map(). Never null — map() never throws.</p>
</dd>
<dt><a href="#ErrorEnvelopeBody">ErrorEnvelopeBody</a> : <code>Object</code></dt>
<dd><p>The <code>error</code> member of an <a href="#ErrorEnvelope">ErrorEnvelope</a>.</p>
</dd>
<dt><a href="#WorkflowRpcReplyBody">WorkflowRpcReplyBody</a> : <code>Object</code></dt>
<dd><p>Body published to a caller&#39;s reply queue by _publishWorkflowRpcReply().</p>
</dd>
<dt><a href="#OperationEnvelope">OperationEnvelope</a> : <code>Object</code></dt>
<dd><p>What invokeOperation() resolves to. Never rejects — failures arrive as <code>error</code>.</p>
</dd>
</dl>

<a name="module_@onlineapps/service-wrapper/ConfigLoader"></a>

## @onlineapps/service-wrapper/ConfigLoader
ConfigLoader - Unified configuration file loading for business services

Single responsibility: Load all service configuration files
from standardized locations with consistent error handling.

CENTRAL REGISTRY of the paths this package owns. Both configs below are
mandatory: neither has a default and neither is skippable, so a service
missing one does not boot — `loadServiceConfig()` and `loadOperations()`
throw, naming the absolute path they tried. That is where "required" is
stated; the registry entries carry no such field, because nothing read one
(d.772, `change-discipline.md` § Removing something removes its declaration).
- config/service/config.json     - Service configuration
- config/service/operations.json - Operations specification

One more path stands in that registry, and it is NOT a config this loader
loads:
- conn-runtime/validation-proof.json (NOT loaded) - the validation OUTPUT;
  only its path is owned here, for the one thing the wrapper does with the
  file: delete it on revalidation.

Usage:
  const { ConfigLoader } = require('@onlineapps/service-wrapper');
  const config = ConfigLoader.loadAll({ env: process.env });

Design principle: ONE place to define where every config lives, and to load
the two that are configs. Validation of their CONTENT is not here — the
shape of `operations.json` is `@onlineapps/conn-orch-validator`'s subject;
this loader checks only that the file parses and carries its top-level key.

**Since**: 2.1.0  
<a name="module_@onlineapps/service-wrapper/ConfigLoader..CONFIG_REGISTRY"></a>

### @onlineapps/service-wrapper/ConfigLoader~CONFIG\_REGISTRY
CENTRAL REGISTRY: All configuration files used by business services
If you add a new config file, add it here!

`config/service/` is the ONLY configuration path. The legacy `conn-config/`
candidate was removed on 2026-09-04 (owner confirmation
validator-legacy-config-path 001, extended to the wrapper on the same
evidence): no repository carries such a directory, so the fallback could
never find anything — it could only turn a misplaced file into a config that
loads, and keep a shape alive that nobody may build.

**Kind**: inner constant of [<code>@onlineapps/service-wrapper/ConfigLoader</code>](#module_@onlineapps/service-wrapper/ConfigLoader)  
<a name="module_@onlineapps/service-wrapper"></a>

## @onlineapps/service-wrapper
Infrastructure orchestration for microservices.
Thin layer that delegates all work to specialized connectors.

**Since**: 2.0.0  
**Author**: OA Drive Team  
**License**: MIT  
<a name="module_@onlineapps/service-wrapper..bootstrap"></a>

### @onlineapps/service-wrapper~bootstrap(serviceRoot, [options]) ⇒ <code>Promise.&lt;{wrapper: ServiceWrapper}&gt;</code>
Bootstrap a business service with standard configuration.
Eliminates boilerplate code in service index.js.

Zero-HTTP shape (ADR 0005): no Express app is created, no port is bound.
Business operations flow via MQ + in-process HandlerRegistry dispatch.
Health flows via MQ heartbeat (Phase 0.10 inside wrapper.initialize()).

There is no logger option. `wrapper.initialize()` hands `wrapper.logger` to the
shared proxy in FÁZE 0.12 — before FÁZE 0.2 runs the service's Tier-1
cookbooks — so by the time this function returns every consumer already reads
the real instance through `getLogger()`. A caller that still passes
`options.setLogger` is refused by name (`assertNoRetiredLoggerOption`).

A service that owns a database hands its ONE Sequelize instance over as
`options.db` — the instance `createSequelize()` from `@onlineapps/conn-base-db`
built, the same object its handlers import
(api/docs/biz/70-contracts/database-contract.md §10). The wrapper calls
`authenticate()` on it for `components.db` and nothing else. It is required
exactly when `config/service/integration-contract.json` declares
`requiredConnectors.db === true`; a mismatch either way stops the boot in
FÁZE 0.15.

**Kind**: inner method of [<code>@onlineapps/service-wrapper</code>](#module_@onlineapps/service-wrapper)  

| Param | Type | Description |
| --- | --- | --- |
| serviceRoot | <code>string</code> | Service root directory (__dirname from index.js) |
| [options] | <code>Object</code> | Optional overrides |
| [options.db] | <code>Object</code> | The service's Sequelize instance (see above) |
| [options.scheduler] | <code>Object</code> | Boot hook `{ recurrences }` for FÁZE 0.7b (conf `scheduler` 010) |

**Example**  
```js
// index.js (minimal, no database)
require('@onlineapps/service-wrapper').bootstrap(__dirname);
```
**Example**  
```js
// index.js of a service that owns a database
const sequelize = require('./src/db');   // built by createSequelize()
require('@onlineapps/service-wrapper').bootstrap(__dirname, { db: sequelize });
```
<a name="module_@onlineapps/service-wrapper/logger"></a>

## @onlineapps/service-wrapper/logger
Shared logger proxy for business services.

Handlers, services and controllers are `require`d at module load time, long
before `wrapper.initialize()` has produced a real logger. This module is the
late-binding indirection between the two: bootstrap calls `setLogger()` once,
every consumer calls `getLogger()` whenever it actually logs.

Fail-fast, no fallback. If `getLogger()` is reached before `setLogger()`, that
is a wiring defect and it throws — it does NOT hand back a console-shaped
substitute. A silent console fallback makes a service that lost its real
logger look healthy: the log lines still appear on stdout, while nothing
reaches the monitoring connector, the log files or the correlation context.
See `architecture-principles.md` §3 (No Fallbacks) and §4 (Fail-Fast).

Consumers call `getLogger()` at call time, never at module scope — binding
the result into a module-level constant would capture the state before
bootstrap and defeat the indirection.

**Example**  
```js
// bootstrap, once, after wrapper.initialize()
const { setLogger } = require('@onlineapps/service-wrapper');
setLogger(wrapper.logger);
```
**Example**  
```js
// any consumer
const { getLogger } = require('@onlineapps/service-wrapper');
function handle() {
  getLogger().info('Message', { data: 'value' });
}
```

* [@onlineapps/service-wrapper/logger](#module_@onlineapps/service-wrapper/logger)
    * [~_logger](#module_@onlineapps/service-wrapper/logger.._logger) : <code>Object</code> \| <code>null</code>
    * [~setLogger(logger)](#module_@onlineapps/service-wrapper/logger..setLogger) ⇒ <code>void</code>
    * [~getLogger()](#module_@onlineapps/service-wrapper/logger..getLogger) ⇒ <code>Object</code>

<a name="module_@onlineapps/service-wrapper/logger.._logger"></a>

### @onlineapps/service-wrapper/logger~\_logger : <code>Object</code> \| <code>null</code>
**Kind**: inner property of [<code>@onlineapps/service-wrapper/logger</code>](#module_@onlineapps/service-wrapper/logger)  
<a name="module_@onlineapps/service-wrapper/logger..setLogger"></a>

### @onlineapps/service-wrapper/logger~setLogger(logger) ⇒ <code>void</code>
Inject the logger instance. Called once during bootstrap; a later call
replaces the instance (tests wire an in-memory logger this way).

**Kind**: inner method of [<code>@onlineapps/service-wrapper/logger</code>](#module_@onlineapps/service-wrapper/logger)  
**Throws**:

- <code>Error</code> If `logger` is not an object exposing all four methods. The
  previously stored instance is left untouched when validation fails.


| Param | Type | Description |
| --- | --- | --- |
| logger | <code>Object</code> | Logger exposing info/warn/error/debug as functions. |

<a name="module_@onlineapps/service-wrapper/logger..getLogger"></a>

### @onlineapps/service-wrapper/logger~getLogger() ⇒ <code>Object</code>
Return the injected logger.

**Kind**: inner method of [<code>@onlineapps/service-wrapper/logger</code>](#module_@onlineapps/service-wrapper/logger)  
**Returns**: <code>Object</code> - The logger passed to `setLogger()`.  
**Throws**:

- <code>Error</code> If `setLogger()` has not run yet. There is no console
  fallback — an uninitialised logger is a bootstrap defect, not a degraded
  mode to be papered over.

<a name="module_redactedInputGuard"></a>

## redactedInputGuard
redactedInputGuard.js — a redacted copy of the input never reaches a handler.

`@onlineapps/logger-contract` `redactSensitiveDeep` replaces the value of
every declared sensitive field with `REDACTED_PLACEHOLDER` in the copies made
for logs, traces and events (SecretBox §8). Those are observability copies.
The execution message — the one that carries the value to its owning handler
— must never be one of them: a handler handed the placeholder authenticates
with the string, stores it as a password or sends it as a token, and the run
reports success.

This wrapper redacts nothing. It GUARDS, in `invokeOperation()`: every
declared sensitive field whose value IS the placeholder — matched by key name
at any depth, the way `redactSensitiveDeep` matches — refuses the message
with `InvalidEnvelopeError` (400 `INVALID_ENVELOPE`) before the handler runs.
The refusal names the FIELD, never a value.

Where the rule comes from (lead's decision N1, 0.2b-1053):
  - the orchestrator's message carries `sensitive_input_fields`: the recipe's
    rule, the sorted unique union of every operation's declaration
    (`@onlineapps/conn-orch-orchestrator` `_executeTaskStep`), and the guard
    applies exactly that;
  - a message without the key is a direct dispatch
    (`_dispatchViaInvokeOperation`), which has no recipe, so its rule is the
    operation's own `operations.json` declaration — read through the ONE
    reader of that declaration, `sensitiveFieldsForOperation`;
  - a key present in any other shape than an array of non-empty strings is a
    defective envelope and is refused by name.

The walk below is deliberately minimal and local: `logger-contract` exports
the placeholder and the declaration reader, but not a walk that FINDS a value
without copying the tree (`redactSensitiveDeep` clones and replaces). Lifting
this walk into `logger-contract` beside `redactSensitiveDeep` is a candidate
for that package, recorded as a finding of d.1280 — until then this is the
one implementation in this package.

**See**

- api/docs/architecture/secretbox.md §8
- api/docs/biz/10-invocation/handler-dispatch.md


* [redactedInputGuard](#module_redactedInputGuard)
    * [~SENSITIVE_RULE_KEY](#module_redactedInputGuard..SENSITIVE_RULE_KEY)
    * [~isWellFormedRule(value)](#module_redactedInputGuard..isWellFormedRule) ⇒ <code>boolean</code>
    * [~describeShape(value)](#module_redactedInputGuard..describeShape) ⇒ <code>string</code>
    * [~resolveSensitiveRule(mqMessage, operations, operation)](#module_redactedInputGuard..resolveSensitiveRule) ⇒ <code>Array.&lt;string&gt;</code>
    * [~findRedactedFields(input, fieldNames)](#module_redactedInputGuard..findRedactedFields) ⇒ <code>Array.&lt;string&gt;</code>
    * [~assertNoRedactedInput(params)](#module_redactedInputGuard..assertNoRedactedInput)
    * [~assertDeclaredSensitiveInputFields(operationsMap)](#module_redactedInputGuard..assertDeclaredSensitiveInputFields)

<a name="module_redactedInputGuard..SENSITIVE_RULE_KEY"></a>

### redactedInputGuard~SENSITIVE\_RULE\_KEY
The wire key the orchestrator sends the recipe rule under.

**Kind**: inner constant of [<code>redactedInputGuard</code>](#module_redactedInputGuard)  
<a name="module_redactedInputGuard..isWellFormedRule"></a>

### redactedInputGuard~isWellFormedRule(value) ⇒ <code>boolean</code>
Is `value` the shape a sensitive-field rule must have: an array of non-empty
strings (`[]` included)?

**Kind**: inner method of [<code>redactedInputGuard</code>](#module_redactedInputGuard)  

| Param | Type |
| --- | --- |
| value | <code>\*</code> | 

<a name="module_redactedInputGuard..describeShape"></a>

### redactedInputGuard~describeShape(value) ⇒ <code>string</code>
One word for what arrived instead of a rule — the TYPE, never the content:
a malformed rule may carry anything, and this message reaches a log.

**Kind**: inner method of [<code>redactedInputGuard</code>](#module_redactedInputGuard)  

| Param | Type |
| --- | --- |
| value | <code>\*</code> | 

<a name="module_redactedInputGuard..resolveSensitiveRule"></a>

### redactedInputGuard~resolveSensitiveRule(mqMessage, operations, operation) ⇒ <code>Array.&lt;string&gt;</code>
The sensitive-field rule that applies to one invocation.

**Kind**: inner method of [<code>redactedInputGuard</code>](#module_redactedInputGuard)  
**Returns**: <code>Array.&lt;string&gt;</code> - the field names to guard; `[]` when nothing is declared  
**Throws**:

- <code>InvalidEnvelopeError</code> when the message carries the key in a shape
  other than an array of non-empty strings
- <code>Error</code> when the message carries no rule and the operation's own
  declaration cannot be read (the spec names the key in a non-array shape)


| Param | Type | Description |
| --- | --- | --- |
| mqMessage | <code>Object</code> | the operation message; `sensitive_input_fields`   is read when present |
| operations | <code>Object</code> | the service's operations.json object   (`{ operations: { <name>: spec } }`) |
| operation | <code>string</code> | the operation being invoked |

<a name="module_redactedInputGuard..findRedactedFields"></a>

### redactedInputGuard~findRedactedFields(input, fieldNames) ⇒ <code>Array.&lt;string&gt;</code>
The names among `fieldNames` whose value, anywhere in `input`, IS the
redaction placeholder — unique, sorted. Matching follows
`redactSensitiveDeep`: by key name, at any depth, through arrays and plain
objects; a Date or a Buffer is a leaf, a shared or circular object is
visited once.

**Kind**: inner method of [<code>redactedInputGuard</code>](#module_redactedInputGuard)  

| Param | Type | Description |
| --- | --- | --- |
| input | <code>Object</code> | the operation input (an object; `invokeOperation`   has already refused anything else) |
| fieldNames | <code>Array.&lt;string&gt;</code> | the rule |

<a name="module_redactedInputGuard..assertNoRedactedInput"></a>

### redactedInputGuard~assertNoRedactedInput(params)
Refuse an input that is a redacted copy: any declared sensitive field whose
value is the placeholder.

**Kind**: inner method of [<code>redactedInputGuard</code>](#module_redactedInputGuard)  
**Throws**:

- <code>InvalidEnvelopeError</code> naming the field(s) — and never a value


| Param | Type | Description |
| --- | --- | --- |
| params | <code>Object</code> |  |
| params.input | <code>Object</code> | the operation input |
| params.fieldNames | <code>Array.&lt;string&gt;</code> | the rule (`resolveSensitiveRule`) |
| params.operation | <code>string</code> | the operation, for the message |

<a name="module_redactedInputGuard..assertDeclaredSensitiveInputFields"></a>

### redactedInputGuard~assertDeclaredSensitiveInputFields(operationsMap)
Refuse, at boot (FÁZE 0.1b), a `sensitive_input_fields` declaration no guard
can apply — by the operation's name, before the service registers.

Absent = no declaration. Present = an array of unique non-empty strings
(`[]` included). Anything else used to surface as an `Error` on the first
direct dispatch of that operation (finding d.1280/2), after registration.

This is a MINIMAL check of this wrapper's own: the registration contract
(`@onlineapps/service-validator-core` `operationsSchemaValidator.js`, the
owner of what `operations.json` must satisfy) checks `retry` and
`timeout_ms` and says nothing about this key — recorded as a finding of
d.1285 for that contract (`api/docs/biz/30-operations/registration-wire.md`,
`schema-v3.md` § `sensitive_input_fields`). When the contract takes the rule,
this check cites it and defines nothing of its own.

**Kind**: inner method of [<code>redactedInputGuard</code>](#module_redactedInputGuard)  
**Throws**:

- <code>Error</code> naming the first operation whose declaration is malformed


| Param | Type | Description |
| --- | --- | --- |
| operationsMap | <code>Object.&lt;string, Object&gt;</code> | `operations.json` `operations` |

<a name="module_shutdownDeadline"></a>

## shutdownDeadline
The deadline of `ServiceWrapper.shutdown()` — one watchdog, armed before the first
teardown step, that ends the process when the teardown does not.

Owner's decision: api/docs/governance/confirmations/biz-shutdown-deadline.md 001 (d.1102
measured, d.1105 built). Without it a teardown against a Redis or a broker that holds
the connection and never answers never ended: `bootstrap()`'s `process.exit(0)` never
came, and on the fatal path — where nothing sends a SIGKILL — the process stayed up,
dead, for good.

The process edge (`writeStderr`, `exit`) is injected; nothing here touches `process`.


* [shutdownDeadline](#module_shutdownDeadline)
    * [~SHUTDOWN_TIMEOUT_KEY](#module_shutdownDeadline..SHUTDOWN_TIMEOUT_KEY)
    * [~SHUTDOWN_STEPS](#module_shutdownDeadline..SHUTDOWN_STEPS)
    * [~list()](#module_shutdownDeadline..list)
    * [~armShutdownDeadline(options)](#module_shutdownDeadline..armShutdownDeadline) ⇒ <code>Object</code>

<a name="module_shutdownDeadline..SHUTDOWN_TIMEOUT_KEY"></a>

### shutdownDeadline~SHUTDOWN\_TIMEOUT\_KEY
The platform env key that owns the deadline. Named once; its declaration (value, why,
consumers) is `api/config/shared-env.json`.

**Kind**: inner constant of [<code>shutdownDeadline</code>](#module_shutdownDeadline)  
<a name="module_shutdownDeadline..SHUTDOWN_STEPS"></a>

### shutdownDeadline~SHUTDOWN\_STEPS
The teardown steps of `shutdown()` that can wait on something outside the process, in
the order they run (d.998b — this list describes the order, it does not set it). The
heartbeat stop and the revalidation timer are synchronous and cannot hang, so they are
not steps a deadline can find pending.

**Kind**: inner constant of [<code>shutdownDeadline</code>](#module_shutdownDeadline)  
<a name="module_shutdownDeadline..list"></a>

### shutdownDeadline~list()
A list an operator can read, or the word for an empty one.

**Kind**: inner method of [<code>shutdownDeadline</code>](#module_shutdownDeadline)  
<a name="module_shutdownDeadline..armShutdownDeadline"></a>

### shutdownDeadline~armShutdownDeadline(options) ⇒ <code>Object</code>
Arm the watchdog. On expiry it writes ONE line through `writeStderr` — synchronously,
and not through the telemetry transport, which may be the very step that hangs and
closes last — and then calls `exit(1)`: an incomplete teardown must not look clean.

The timer is deliberately NOT `unref()`ed: a teardown blocked on a socket keeps the
loop alive anyway, and the watchdog must outlive it.

**Kind**: inner method of [<code>shutdownDeadline</code>](#module_shutdownDeadline)  
**Returns**: <code>Object</code> - call `disarm()` when the teardown ends, however it ends  
**Throws**:

- <code>Error</code> when an option is missing or malformed


| Param | Type | Description |
| --- | --- | --- |
| options | <code>Object</code> |  |
| options.timeoutMs | <code>number</code> | positive whole number of milliseconds |
| options.steps | <code>Array.&lt;string&gt;</code> | every step label, in teardown order |
| options.progress | <code>function</code> | `() => ({ pending, released, failed })`, read at expiry |
| options.writeStderr | <code>function</code> | synchronous writer of one text chunk to stderr |
| options.exit | <code>function</code> | ends the process with the given code |

<a name="module_signalShutdown"></a>

## signalShutdown
The SIGTERM/SIGINT handler `bootstrap()` installs — the one place a signal becomes a
teardown and an exit code.

Owner's decision: api/docs/governance/confirmations/biz-shutdown-deadline.md 001 — exit 0
only after a clean teardown, exit 1 when `shutdown()` rejects, and a second signal
during a running teardown is ignored. Until d.1105 the handler was
`await wrapper.shutdown(); process.exit(0)` on both signals: a rejected teardown ended
the process on an unhandled rejection instead, and a second signal started a second
teardown over the first (d.1102).

A teardown that never settles is not this handler's business: the deadline armed
inside `shutdown()` ends it (`shutdownDeadline.js`).

<a name="module_signalShutdown..createSignalShutdown"></a>

### signalShutdown~createSignalShutdown(options) ⇒ <code>function</code>
**Kind**: inner method of [<code>signalShutdown</code>](#module_signalShutdown)  
**Returns**: <code>function</code> - `async (signalName) => void`, to be called on every signal  
**Throws**:

- <code>Error</code> when an option is missing


| Param | Type | Description |
| --- | --- | --- |
| options | <code>Object</code> |  |
| options.wrapper | <code>Object</code> | the ServiceWrapper to tear down |
| options.exit | <code>function</code> | ends the process with the given code |
| options.log | <code>function</code> | writes one informational line |
| options.error | <code>function</code> | writes one error line |

<a name="module_stepDeadline"></a>

## stepDeadline
The deadline of ONE ATTEMPT of a step — how long this wrapper lets an
operation's handler run before it abandons the attempt.

Owner's word (conf `step-error-classification` 005 — variant A, a deadline
per ATTEMPT per operation): the deadline
is per operation, declared as `timeout_ms` in `config/service/operations.json`
beside `retry`; an operation that declares none gets the platform default;
a declaration outside the platform range is refused by the operation's name,
never replaced by the default — the rule `retry` follows in
`@onlineapps/conn-orch-orchestrator` (`retryPolicy.js`).

The numbers — the platform default and the range — are not this module's:
`@onlineapps/handler-contract` owns them (`STEP_TIMEOUT_MIN_MS`,
`STEP_TIMEOUT_MAX_MS`, `DEFAULT_STEP_TIMEOUT_MS`), and this module reads
them and keeps no copy (conf `handler-contract` 002, d.1382). It exports
`DEFAULT_STEP_TIMEOUT_MS`, `MIN_STEP_TIMEOUT_MS` and `MAX_STEP_TIMEOUT_MS`,
which are those values.

The wrapper ENFORCES it (`ServiceWrapper.invokeOperation` races the handler
against it and aborts `ctx.abortSignal`); whether a timed-out step is tried
again is the orchestrator's decision (005: a reading operation by the retry
policy, a writing one straight to `workflow.failed`) — the attempt ends with a TIMEOUT-class
error (`StepTimeoutError`, 504 `STEP_TIMEOUT`), and status 504 is
`ErrorTypes.TIMEOUT` in `@onlineapps/error-handler-core` `ErrorClassifier.js`.

**See**: api/docs/governance/confirmations/step-error-classification.md  

* [stepDeadline](#module_stepDeadline)
    * [~StepTimeoutError](#module_stepDeadline..StepTimeoutError)
        * [new StepTimeoutError(params)](#new_module_stepDeadline..StepTimeoutError_new)
    * [~MIN_STEP_TIMEOUT_MS](#module_stepDeadline..MIN_STEP_TIMEOUT_MS)
    * [~MAX_STEP_TIMEOUT_MS](#module_stepDeadline..MAX_STEP_TIMEOUT_MS)
    * [~STEP_TIMEOUT_CODE](#module_stepDeadline..STEP_TIMEOUT_CODE)
    * [~show()](#module_stepDeadline..show)
    * [~resolveStepTimeout(declared, operation)](#module_stepDeadline..resolveStepTimeout) ⇒ <code>number</code>
    * [~assertDeclaredStepTimeouts(operationsMap)](#module_stepDeadline..assertDeclaredStepTimeouts) ⇒ <code>void</code>

<a name="module_stepDeadline..StepTimeoutError"></a>

### stepDeadline~StepTimeoutError
One attempt ran past its deadline.

Not a business error and not branded: the wrapper raises it around the
handler, never a handler, and `ErrorMapper` recognises it by `name` and
`code` (`describeFailure`, the branch beside `AbortError`) — the light entry
`./errors` does not load this module. It carries `type`
`TIMEOUT` explicitly — the class the orchestrator retries — and `details`
naming the deadline, which the envelope keeps (it names nothing of the
caller's data).

**Kind**: inner class of [<code>stepDeadline</code>](#module_stepDeadline)  
<a name="new_module_stepDeadline..StepTimeoutError_new"></a>

#### new StepTimeoutError(params)

| Param | Type |
| --- | --- |
| params | <code>Object</code> | 

<a name="module_stepDeadline..MIN_STEP_TIMEOUT_MS"></a>

### stepDeadline~MIN\_STEP\_TIMEOUT\_MS
The shortest deadline an operation may declare, in ms - `STEP_TIMEOUT_MIN_MS` of handler-contract under the name this module exports.

**Kind**: inner constant of [<code>stepDeadline</code>](#module_stepDeadline)  
<a name="module_stepDeadline..MAX_STEP_TIMEOUT_MS"></a>

### stepDeadline~MAX\_STEP\_TIMEOUT\_MS
The longest deadline an operation may declare, in ms - `STEP_TIMEOUT_MAX_MS` of handler-contract under the name this module exports.

**Kind**: inner constant of [<code>stepDeadline</code>](#module_stepDeadline)  
<a name="module_stepDeadline..STEP_TIMEOUT_CODE"></a>

### stepDeadline~STEP\_TIMEOUT\_CODE
The code an attempt ended by its deadline carries. Produced by the wrapper only.

**Kind**: inner constant of [<code>stepDeadline</code>](#module_stepDeadline)  
<a name="module_stepDeadline..show"></a>

### stepDeadline~show()
A declared value as the refusal shows it — `NaN` and `-0` stay what they are.

**Kind**: inner method of [<code>stepDeadline</code>](#module_stepDeadline)  
<a name="module_stepDeadline..resolveStepTimeout"></a>

### stepDeadline~resolveStepTimeout(declared, operation) ⇒ <code>number</code>
The deadline of one attempt of `operation`.

**Kind**: inner method of [<code>stepDeadline</code>](#module_stepDeadline)  
**Returns**: <code>number</code> - the deadline in ms  
**Throws**:

- <code>Error</code> when a declaration is not an integer within the platform range


| Param | Type | Description |
| --- | --- | --- |
| declared | <code>\*</code> | `timeout_ms` from the operation's record (`undefined` = none declared) |
| operation | <code>string</code> | the operation's name, for the refusal |

<a name="module_stepDeadline..assertDeclaredStepTimeouts"></a>

### stepDeadline~assertDeclaredStepTimeouts(operationsMap) ⇒ <code>void</code>
Refuse the first operation whose `timeout_ms` declaration is invalid — run
at boot (FÁZE 0.1b), so a service never starts with a deadline nothing
applies.

**Kind**: inner method of [<code>stepDeadline</code>](#module_stepDeadline)  
**Throws**:

- <code>Error</code> the refusal of `resolveStepTimeout` for the first bad declaration


| Param | Type | Description |
| --- | --- | --- |
| operationsMap | <code>Object.&lt;string, Object&gt;</code> | the `operations` map of operations.json |

<a name="ContextBuilder"></a>

## ContextBuilder
ContextBuilder — assembles an OperationContext for a single invocation.

Contract: api/docs/biz/10-invocation/operation-context.md and api/docs/biz/10-invocation/handler-dispatch.md
Layer: L3 orchestration (see ARCHITECTURE_PRINCIPLES.md §7).
  - Depends on connectors (L1/L2) via constructor injection only.
  - Imports `./OperationContext` and the error CLASSES from `./ErrorMapper`
    (L2 types, no behaviour — ErrorMapper itself imports nothing, so there is
    no cycle; `./OperationContext` already depends on the same module).
    MUST NOT import HandlerRegistry or SchemaValidator, and MUST NOT call
    ErrorMapper.map() — mapping stays the wrapper's job.
  - Does NOT validate handler input/output schemas (SchemaValidator owns that).
  - Does NOT invoke handlers (ServiceWrapper owns that).

SRP: single job — translate (mqMessage, operationSpec) into a ready-to-use
OperationContext plus a release() hook the wrapper must call after the
handler returns (success or failure).

**Kind**: global class  

* [ContextBuilder](#ContextBuilder)
    * [new ContextBuilder(deps)](#new_ContextBuilder_new)
    * [.build(mqMessage, operationSpec)](#ContextBuilder+build) ⇒ [<code>Promise.&lt;ContextBuildResult&gt;</code>](#ContextBuildResult)

<a name="new_ContextBuilder_new"></a>

### new ContextBuilder(deps)

| Param | Type | Description |
| --- | --- | --- |
| deps | <code>Object</code> |  |
| deps.connectors | <code>Object</code> | { cache, http, secrets, state, monitoring, mq } |
| deps.config | <code>Object</code> | Read-only service config snapshot. |
| deps.logger | <code>Object</code> | Base logger (must expose `.child()` to be useful). |

<a name="ContextBuilder+build"></a>

### contextBuilder.build(mqMessage, operationSpec) ⇒ [<code>Promise.&lt;ContextBuildResult&gt;</code>](#ContextBuildResult)
Build an OperationContext for one MQ invocation.

Fail-fast order:
  1. MQ envelope shape (operation, envelope, workflow_id, correlation_id,
     step_id), plus the shape of a PRESENT envelope.person_id — `null` and
     an absent key stay valid (`integer | null` per the wire contract), a
     present value must be a positive integer (isPositiveIntegerId).
  2. Tenancy from operationSpec.bundle_scope — presence AND shape:
       'tenant'    -> tenant_id required
       'workspace' -> tenant_id AND workspace_id required
       'platform'  -> both optional, and neither is shape-checked
     A required id must also BE a positive integer (isPositiveIntegerId).
     An operation that declares NO bundle_scope is refused by name — the key
     is required (api/docs/biz/30-operations/schema-v3.md), so its absence is
     a packaging bug of ours and never a silent 'workspace'.
  3. Scoped logger (via base.child() when available; fall back to base logger
     only when the base does not implement .child — bunyan/pino both do).
  4. Connector acquisition (cache, httpClient, state, secrets facade).
  5. stream — a slot, always `null`; chunk streaming has no protocol yet
     and a handler that calls it fails loudly (d.802b).
  6. abortSignal — mqMessage.abortSignal. `ServiceWrapper.invokeOperation()`
     always passes the signal of the controller it owns for the attempt
     (aborted by the step deadline and by the caller's own signal, d.1264b);
     a direct caller without one gets a controller nothing aborts.

**Kind**: instance method of [<code>ContextBuilder</code>](#ContextBuilder)  
**Returns**: [<code>Promise.&lt;ContextBuildResult&gt;</code>](#ContextBuildResult) - Callers MUST await release() in a finally block.
         release() is idempotent.  
**See**: api/docs/biz/10-invocation/operation-context.md § Release  

| Param | Type | Description |
| --- | --- | --- |
| mqMessage | <code>Object</code> | Parsed MQ message (envelope, operation, ids). |
| operationSpec | <code>Object</code> | operations.json entry for this op (bundle_scope, ...). |

<a name="ErrorMapper"></a>

## ErrorMapper
**Kind**: global class  

* [ErrorMapper](#ErrorMapper)
    * [new ErrorMapper(options)](#new_ErrorMapper_new)
    * [.map(err, meta)](#ErrorMapper+map) ⇒ [<code>ErrorEnvelope</code>](#ErrorEnvelope)

<a name="new_ErrorMapper_new"></a>

### new ErrorMapper(options)

| Param | Type | Description |
| --- | --- | --- |
| options | <code>Object</code> |  |
| options.logger | <code>Object</code> | Base logger (must expose .info/.warn/.error). |

<a name="ErrorMapper+map"></a>

### errorMapper.map(err, meta) ⇒ [<code>ErrorEnvelope</code>](#ErrorEnvelope)
Map a handler exception to an MQ response envelope AND log it — the
wrapper's own path, where the wrapper is the one handling the throw.
Never throws. Always returns `{ status, error: { code, message, [details], [correlation_id], [type] } }`.

Exactly one line is written per call, at the level the mapping decided.
Need the envelope without that line — because you log the failure yourself
— use [toCallerEnvelope](#toCallerEnvelope); it is the same mapping, minus the log.

Recognition is by the brand + shape contract (see the file header), never by
`instanceof`. Branded + malformed → 500 `MALFORMED_BUSINESS_ERROR` with the
offending field named in the log; unbranded → 500 `INTERNAL_ERROR`, masked.

**Kind**: instance method of [<code>ErrorMapper</code>](#ErrorMapper)  

| Param | Type |
| --- | --- |
| err | <code>Error</code> | 
| meta | <code>Object</code> | 
| [meta.operation] | <code>string</code> | 
| [meta.correlation_id] | <code>string</code> | 
| [meta.workflow_id] | <code>string</code> | 

<a name="HandlerLoader"></a>

## HandlerLoader
**Kind**: global class  

* [HandlerLoader](#HandlerLoader)
    * [new HandlerLoader(deps)](#new_HandlerLoader_new)
    * [.resolve(handlerRef)](#HandlerLoader+resolve) ⇒ <code>function</code>

<a name="new_HandlerLoader_new"></a>

### new HandlerLoader(deps)

| Param | Type | Description |
| --- | --- | --- |
| deps | <code>Object</code> |  |
| deps.baseDir | <code>string</code> | Absolute path to the service source root   (typically `path.join(serviceRoot, 'src')`, e.g. `/app/src`). |
| deps.logger | <code>Object</code> | Logger per `@onlineapps/logger-contract`. |

<a name="HandlerLoader+resolve"></a>

### handlerLoader.resolve(handlerRef) ⇒ <code>function</code>
Resolve `"relative/path#exportName"` to the exported function.
Cached per handlerRef string. Throws on any malformed / missing input.

**Kind**: instance method of [<code>HandlerLoader</code>](#HandlerLoader)  

| Param | Type | Description |
| --- | --- | --- |
| handlerRef | <code>string</code> | v3 handler descriptor from operations.json. |

<a name="HandlerRegistry"></a>

## HandlerRegistry
**Kind**: global class  

* [HandlerRegistry](#HandlerRegistry)
    * [new HandlerRegistry(deps)](#new_HandlerRegistry_new)
    * [.validate()](#HandlerRegistry+validate) ⇒ <code>void</code>
    * [.invoke(operation, input, ctx)](#HandlerRegistry+invoke) ⇒ <code>Promise.&lt;\*&gt;</code>

<a name="new_HandlerRegistry_new"></a>

### new HandlerRegistry(deps)

| Param | Type | Description |
| --- | --- | --- |
| deps | <code>Object</code> |  |
| deps.operations | <code>Object</code> | Parsed operations.json (v3): map opName -> opSpec. |
| deps.handlerLoader | <code>Object</code> | Abstraction with .resolve(handlerRef) -> Function.                                       `handlerRef` is the v3 "<relative/path>#<exportName>" form.                                       Implemented by ServiceWrapper (it owns the service base dir). |
| deps.logger | <code>Object</code> | Logger with at least .debug/.info; .trace is optional. |

<a name="HandlerRegistry+validate"></a>

### handlerRegistry.validate() ⇒ <code>void</code>
Eagerly resolve every declared handler. Called once by
ServiceWrapper.initialize() BEFORE MQ subscription.

Fail-fast: throws on the first bad entry. No catch-and-continue.

**Kind**: instance method of [<code>HandlerRegistry</code>](#HandlerRegistry)  
**Throws**:

- <code>Error</code> On missing/invalid handler field or non-function resolution.

**See**: api/docs/biz/10-invocation/handler-dispatch.md § Where the pipeline is built  
<a name="HandlerRegistry+invoke"></a>

### handlerRegistry.invoke(operation, input, ctx) ⇒ <code>Promise.&lt;\*&gt;</code>
Hot-path invocation. O(1) Map lookup, no filesystem I/O, no reflection.

Handler errors propagate untouched — ErrorMapper is the component
that translates them into response envelopes.

**Kind**: instance method of [<code>HandlerRegistry</code>](#HandlerRegistry)  
**Returns**: <code>Promise.&lt;\*&gt;</code> - Whatever the handler returns.  
**Throws**:

- <code>UnknownOperationError</code> If the operation is not registered.

**See**: api/docs/biz/10-invocation/handler-dispatch.md § `ErrorMapper` — throw-to-envelope  

| Param | Type | Description |
| --- | --- | --- |
| operation | <code>string</code> | Operation name as declared in operations.json. |
| input | <code>Object</code> | Already-validated input payload (SchemaValidator ran earlier). |
| ctx | <code>Object</code> | Immutable OperationContext from ContextBuilder. |

<a name="SchemaValidator"></a>

## SchemaValidator
**Kind**: global class  

* [SchemaValidator](#SchemaValidator)
    * [new SchemaValidator([options])](#new_SchemaValidator_new)
    * [.precompile(operations)](#SchemaValidator+precompile) ⇒ <code>void</code>
    * [.validateInput(operation, data)](#SchemaValidator+validateInput) ⇒ <code>null</code>
    * [.validateOutput(operation, data)](#SchemaValidator+validateOutput) ⇒ <code>null</code>

<a name="new_SchemaValidator_new"></a>

### new SchemaValidator([options])
**Throws**:

- <code>Error</code> When `options.ajv` is present but exposes no `compile()`.


| Param | Type | Description |
| --- | --- | --- |
| [options] | <code>object</code> |  |
| [options.ajv] | <code>object</code> | Pre-configured Ajv instance (DI). Omit it and a   strict Ajv is created internally; PASS it and it is checked. Injection is for   tests and for callers that need custom formats/keywords. |

<a name="SchemaValidator+precompile"></a>

### schemaValidator.precompile(operations) ⇒ <code>void</code>
Compile input and output schemas for every declared operation.
Fail-fast: any compile error throws immediately and aborts startup.

**Kind**: instance method of [<code>SchemaValidator</code>](#SchemaValidator)  
**Throws**:

- <code>Error</code> When a schema carries a `default` the gates would not apply (any
  in an output schema; in an input schema, any not on a property under
  `properties`), when two slots declare the same root `$id` (see
  `_claimSchemaId`), or when a schema does not compile.


| Param | Type | Description |
| --- | --- | --- |
| operations | <code>object</code> | operations.json v3 object (opName → spec).   Each spec MAY have `.input` (JSON Schema) and `.output` (JSON Schema).   Missing input or output is permitted (operation declares none). |

<a name="SchemaValidator+validateInput"></a>

### schemaValidator.validateInput(operation, data) ⇒ <code>null</code>
Validate input payload for a known operation.

**Kind**: instance method of [<code>SchemaValidator</code>](#SchemaValidator)  
**Returns**: <code>null</code> - Returns null on success (or when the op has no input schema).  
**Throws**:

- <code>Error</code> When the operation was never precompiled (operator bug).
- <code>ValidationError</code> When `data` does not conform to the compiled schema.


| Param | Type | Description |
| --- | --- | --- |
| operation | <code>string</code> | Operation name from operations.json. |
| data | <code>\*</code> | Payload to validate. |

<a name="SchemaValidator+validateOutput"></a>

### schemaValidator.validateOutput(operation, data) ⇒ <code>null</code>
Validate output payload produced by the handler.

**Kind**: instance method of [<code>SchemaValidator</code>](#SchemaValidator)  
**Returns**: <code>null</code> - Returns null on success (or when the op has no output schema).  
**Throws**:

- <code>Error</code> When the operation was never precompiled (operator bug).
- <code>ValidationError</code> Tagged `phase: 'output'` — ErrorMapper treats this as a server bug.


| Param | Type | Description |
| --- | --- | --- |
| operation | <code>string</code> | Operation name from operations.json. |
| data | <code>\*</code> | Handler result. |

<a name="HandlerContractError"></a>

## HandlerContractError
A handler broke the error contract — it raised a code the wrapper produces.

Deliberately NOT branded and carrying no `code` of its own: it is not a
business outcome, it is our defect, so `ErrorMapper` takes it down the
unhandled-fault branch — `error` level WITH the stack in the log, and a masked
`500 INTERNAL_ERROR` to the caller
(`api/docs/biz/70-contracts/error-handling.md` §5). A code of its own would be
a platform-wide identity, and that dictionary is added to by changing the
contract node, not by a library minting a name.

**Kind**: global class  
<a name="new_HandlerContractError_new"></a>

### new HandlerContractError(message, [options])

| Param | Type | Description |
| --- | --- | --- |
| message | <code>string</code> |  |
| [options] | <code>Object</code> |  |
| [options.cause] | <code>Error</code> | The error the handler actually threw. |

<a name="ServiceWrapper"></a>

## ServiceWrapper
ServiceWrapper class
Collection of connectors that enhance business service with infrastructure

**Kind**: global class  

* [ServiceWrapper](#ServiceWrapper)
    * [new ServiceWrapper(options)](#new_ServiceWrapper_new)
    * [.refreshDependencyHealth()](#ServiceWrapper+refreshDependencyHealth) ⇒ <code>Promise.&lt;void&gt;</code>
    * [.startHealthHeartbeat()](#ServiceWrapper+startHealthHeartbeat) ⇒ <code>Promise.&lt;void&gt;</code>
    * [.stopHealthHeartbeat()](#ServiceWrapper+stopHealthHeartbeat) ⇒ <code>void</code>
    * [.invokeOperation(mqMessage)](#ServiceWrapper+invokeOperation) ⇒ [<code>Promise.&lt;OperationEnvelope&gt;</code>](#OperationEnvelope)
    * [.shutdown()](#ServiceWrapper+shutdown)

<a name="new_ServiceWrapper_new"></a>

### new ServiceWrapper(options)
Create a new ServiceWrapper instance

**Throws**:

- <code>Error</code> when a platform env key the constructor reads is missing, not a whole
  number, or not positive: `BIZ_SHUTDOWN_TIMEOUT_MS` (d.1105), and
  `REGISTRY_DISCOVERY_TIMEOUT_MS` with `BIZ_HEARTBEAT_INTERVAL_MS` (d.1130) — FÁZE 0.7
  uses those two, but reads the fields, so a bad key refuses the wrapper here instead
  of surfacing in that phase as a transient error.


| Param | Type | Default | Description |
| --- | --- | --- | --- |
| options | <code>Object</code> |  | Configuration options |
| options.config | <code>Object</code> |  | Service and wrapper configuration |
| options.operations | <code>Object</code> |  | Operations schema |
| options.serviceBaseDir | <code>string</code> |  | Absolute path to the service's handler   directory, like `/app/src`: pass `__dirname` from the service index.js, or   `path.join(serviceRoot, 'src')`. Required — `HandlerLoader` requires the handler   modules `operations.json` names relative to it, so a wrapper without it could   dispatch nothing. |
| [options.serviceRoot] | <code>string</code> |  | Service root directory (for validation) |
| [options.db] | <code>Object</code> |  | The service's ONE Sequelize instance, as   `createSequelize()` from `@onlineapps/conn-base-db` returns it   (api/docs/biz/70-contracts/database-contract.md §10). The wrapper never   builds or locates it; it only calls `authenticate()` on it to state   `components.db`. Required exactly when the integration contract declares   `requiredConnectors.db === true`, refused when it declares `false`. |
| [options._validationOrchestrator] | <code>Object</code> | <code></code> | Optional ValidationOrchestrator for testing |

<a name="ServiceWrapper+refreshDependencyHealth"></a>

### serviceWrapper.refreshDependencyHealth() ⇒ <code>Promise.&lt;void&gt;</code>
Refresh cached component health signals for the heartbeat.

The heartbeat itself is fire-and-forget on a short interval; we do not
probe DB/Redis/cache on every heartbeat. Instead, this helper is invoked
periodically (or on demand) to keep `_lastDbHealth` / `_lastRedisHealth` /
`_lastCacheHealth` fresh so the next heartbeat carries meaningful status.

Ties into `_checkDatabaseDependency` / `_checkRedisDependency` /
`_probeCacheHealth` which remain the single source of truth for what
"healthy" means.

**Kind**: instance method of [<code>ServiceWrapper</code>](#ServiceWrapper)  
<a name="ServiceWrapper+startHealthHeartbeat"></a>

### serviceWrapper.startHealthHeartbeat() ⇒ <code>Promise.&lt;void&gt;</code>
Start the health heartbeat publisher (ADR 0005, Phase 0.10).

Wires this biz service into the existing infrastructure heartbeat
mechanism at `@onlineapps/infrastructure-tools` — the same mechanism
already used in production by api_gateway, api_services_validator,
api_delivery_endpoint. Registry's `infrastructureHealth.listener`
consumes the messages and `infrastructureHealthTracker` maintains
the state.

Payload shape follows the validated infra format:
  { serviceName, version, status, timestamp,
    components: { mq, redis, db, cache }, correlationId }

First heartbeat is emitted synchronously by publisher.start().
On publish failure, the shared publisher logs and continues — the
service is not crashed by a heartbeat glitch, and the next tick carries
the same verdict.

Failing to START is the other thing entirely, and it THROWS: every cause
is a permanent configuration fault and nothing here retries. Phase 0.10
ends the boot on it (`initialize()`; owner decision
api/docs/governance/confirmations/boot-phase-0-10.md 001).

**Kind**: instance method of [<code>ServiceWrapper</code>](#ServiceWrapper)  
**Throws**:

- <code>Error</code> the publisher could not be built or could not start

<a name="ServiceWrapper+stopHealthHeartbeat"></a>

### serviceWrapper.stopHealthHeartbeat() ⇒ <code>void</code>
Stop the heartbeat publisher (used in cleanup paths).

**Kind**: instance method of [<code>ServiceWrapper</code>](#ServiceWrapper)  
<a name="ServiceWrapper+invokeOperation"></a>

### serviceWrapper.invokeOperation(mqMessage) ⇒ [<code>Promise.&lt;OperationEnvelope&gt;</code>](#OperationEnvelope)
v3 single-operation invocation.

Dispatch path for MQ-delivered operation messages. Never throws — always
returns an envelope `{ status, result }` or `{ status, error }` produced
by ErrorMapper.

**Kind**: instance method of [<code>ServiceWrapper</code>](#ServiceWrapper)  
**See**: api/docs/biz/10-invocation/handler-dispatch.md § End-to-end call flow  

| Param | Type | Description |
| --- | --- | --- |
| mqMessage | <code>Object</code> | Parsed MQ message with { operation, input,   envelope, workflow_id, correlation_id, ... }. |

<a name="ServiceWrapper+shutdown"></a>

### serviceWrapper.shutdown()
Shutdown wrapper and cleanup resources.

Runs under a deadline (d.1105, confirmation biz-shutdown-deadline 001): a watchdog
armed before the first step ends the process with exit 1 and one stderr line naming
the pending step when the teardown outlives `BIZ_SHUTDOWN_TIMEOUT_MS`. Every caller
is covered — `bootstrap()`'s signal handler, `_handleInitializationError()` and
`_shutdownFatally()`, the last of which nothing else would ever kill.

**Kind**: instance method of [<code>ServiceWrapper</code>](#ServiceWrapper)  
**Throws**:

- <code>AggregateError</code> when a step failed; the teardown still ran every step

<a name="INTERNAL_ERROR_CODE"></a>

## INTERNAL\_ERROR\_CODE
The code an error gets when nothing about it was declared.

**Kind**: global constant  
<a name="MALFORMED_BUSINESS_ERROR_CODE"></a>

## MALFORMED\_BUSINESS\_ERROR\_CODE
The code a BRANDED error gets when its shape is broken — our own defect.

**Kind**: global constant  
<a name="UNDECLARED_FAILURE_CODES"></a>

## UNDECLARED\_FAILURE\_CODES : <code>ReadonlySet.&lt;string&gt;</code>
The codes this file puts on an envelope when the failure declared NOTHING.

They are the opposite of every other code the table produces: the rest are a
handler's (or the wrapper's) statement about an outcome, these two are the
absence of one. An `INTERNAL_ERROR` envelope means an unbranded error nobody
classified; `MALFORMED_BUSINESS_ERROR` means an error that claimed the brand
and broke the shape.

Exported because an envelope loses the distinction otherwise. Once
`ServiceWrapper._dispatchViaInvokeOperation()` rebuilds a failure as a plain
`Error` with `code` and `status`, a masked 500 `INTERNAL_ERROR` is shaped
exactly like a declared 500 `DATA_INTEGRITY_VIOLATION`, and only this list
separates them (`ServiceWrapper._classifyDeliveryError()` — an undeclared
failure keeps its delivery attempts, a declared one does not).

**Kind**: global constant  
<a name="CONTRACT_RETRYABLE_CODES"></a>

## CONTRACT\_RETRYABLE\_CODES : <code>ReadonlySet.&lt;string&gt;</code>
The codes whose dictionary row declares that a retry is meaningful.

A code's declared identity is not only its name and its status: the accepted
dictionary carries a `Retry` column, and that column is part of what the code
means platform-wide. `PROVIDER_UNAVAILABLE` is the row that says yes.

Owner: `api/docs/biz/70-contracts/error-handling.md` § Accepted domain codes —
a row there is the fact, this list is only the machine-readable side of it, so
a code joins or leaves this set when that table changes and never otherwise.
❌ Not a flag on an error instance: which codes may be retried is a property
of the contract dictionary, not of one throw
(`.claude/rules/single-source-of-truth.md`).

Read by `ServiceWrapper._classifyDeliveryError()`: such a code keeps its
delivery attempts, exactly as an undeclared failure does, while every other
declared business code is settled on the first delivery (d.547).

**Kind**: global constant  
<a name="path"></a>

## path
HandlerLoader

L3 Orchestration helper used by HandlerRegistry. Resolves v3 handler
references of the form `"relative/path#exportName"` to actual exported
functions, relative to the host service's source directory.

Layer: L3 (Orchestration). Pure filesystem + require. No cross-imports
from other wrapper modules.

**Kind**: global constant  
**See**: api/docs/biz/10-invocation/handler-dispatch.md  
<a name="MQConnector"></a>

## MQConnector
Service Wrapper - Collection of connectors for business services

Provides infrastructure components in single-process architecture:
- MQ integration (RabbitMQ)
- Service discovery (Registry)
- Monitoring and metrics
- Health checks

Uses HTTP pattern exclusively - no direct handler calls

**Kind**: global constant  
<a name="WRAPPER_RESERVED_ERROR_CODES"></a>

## WRAPPER\_RESERVED\_ERROR\_CODES
The error codes this wrapper produces about its own dispatch, with what a
handler that means the same thing throws instead.

The dictionary of codes, and which of them belong to the wrapper rather than
to a handler, has one owner — the nodes cited below, not this comment.

`CLIENT_CANCELLED` is deliberately NOT here. `ErrorMapper` matches it on the
error's `name` outside the brand contract, because the abort it must
recognise is Node's own `AbortController` `DOMException` — which a handler
awaiting an aborted call legitimately propagates.

Until this Map existed nothing enforced the split: a handler lives in a
service repository, which review here never sees, and the cost of missing one
is not cosmetic — see `_invokeHandlerUnderContract()`.

**Kind**: global constant  
**See**

- api/docs/biz/70-contracts/error-handling.md
- api/docs/biz/10-invocation/handler-dispatch.md

<a name="REQUIRED_CONN_ORCH_ORCHESTRATOR_MAJOR"></a>

## REQUIRED\_CONN\_ORCH\_ORCHESTRATOR\_MAJOR
The orchestrator major the wrapper's v3 invocation model requires.

ONE constant: the guard below branches on it and the refusal names it, so the
sentence an operator reads cannot drift from the comparison that raised it
(`change-discipline.md` § One rail per concern). WHICH released version
carries this major is not written here — that is the platform SSOT's fact
(`api/config/libraries.json`), and a number copied into a message is a
descriptive fact maintained by hand (`doc-code-binding.md` §1).

**Kind**: global constant  
<a name="REGISTRY_DISCOVERY_TIMEOUT_KEY"></a>

## REGISTRY\_DISCOVERY\_TIMEOUT\_KEY
The PLATFORM env key that owns the deadline of one discovery lookup (d.1093). Named
once here, so renaming it is one edit in this package; its declaration (why, consumers,
value) is `api/config/shared-env.json`.

**Kind**: global constant  
<a name="BOOT_DISPOSITION"></a>

## BOOT\_DISPOSITION
What an operator should do next about a failed boot phase, carried ON the
error rather than decided at the `catch` that reports it.

FÁZE 0.3 already worked this way — it reads `error.code !== 'MQ_CONNECTION_FATAL'`
off the error `@onlineapps/mq-client-core` threw, because only the layer that
produced the failure knows whether asking again can change the answer. This is
the same rail, named once and readable by every phase that has more than one
disposition.

A phase whose failure is always of one kind (0.12, 0.15, 0.10) passes its
constant and reads nothing — the marker is for the phases where the disposition
is a FACT about the failure, not about the phase.

**Kind**: global constant  
<a name="STATIC_VALIDATION_STEPS"></a>

## STATIC\_VALIDATION\_STEPS
The Tier-1 steps that read NOTHING but the files already on disk.

`@onlineapps/conn-orch-validator` 10.0.0 runs seven steps and reports each one
under its own key in `result.steps` (`ValidationOrchestrator.runFullValidation()`):

  1 `structure`   the service tree — `ServiceStructureValidator`
  2 `config`      `config/service/*.json`
  3 `env`         the env contract the service declares
  4 `operations`  operations.json against the v3 shape
  5 `cookbooks`   the service's own recipes, RUN against handlers and DB
  6 `connectors`  warnings only — it can never fail a validation
  7 `manifest`    the uniform, measured on the checkout

Five of them decide from files alone. A verdict of theirs is the same on the
next attempt and on the attempt after that, because nothing between two
attempts changes what they read — so retrying is not caution, it is 18,5 minutes
of a boot that cannot succeed (measured twice: biz-converter `exit 1` after
18 min 53 s, W591 on `MISSING_COOKBOOKS`). Step 5 is the one that depends on the
world around the service, and it is the one that keeps the budget.

The classification reads the STEP, never the message: `result.errors` is one
flat array into which step 1 pushes objects and steps 2/4/5/7 push sentences
(that mixture is why `describeValidationFinding()` exists), so a message tells
an operator what happened and tells this code nothing it can decide on.

**Kind**: global constant  
<a name="DEPENDENCY_HEALTH_FIELD"></a>

## DEPENDENCY\_HEALTH\_FIELD
The cached health signal each dependency component keeps, by component name.

ONE place says which components there are and where each one's last verdict
lives, so `refreshDependencyHealth()` writes the field and
`_recordDependencyHealth()` compares against it without either of them
spelling the field name again.

**Kind**: global constant  
<a name="ERROR_HANDLING_KEYS"></a>

## ERROR\_HANDLING\_KEYS
The `wrapper.errorHandling` keys this package forwards to
`@onlineapps/conn-infra-error-handler`. Named, not spread: a key outside this
list configures nothing in the core, and forwarding it blindly would let a
misspelling pass in silence.

`dlqEnabled` left the list in d.493, for the reason
`@onlineapps/error-handler-core` retired it (that package's CHANGELOG,
`### Removed — DLQRouter`): the core stopped reading the key, so forwarding it
configured nothing. It is not merely dropped — a config still writing it is
REFUSED by name (`_assertRetiredDlqEnabledKey`).

`maxRetries`, `retryDelay` and `retryMultiplier` left it in d.1264 and are
refused the same way (`RETIRED_RETRY_KEYS`).

**Kind**: global constant  
<a name="RETIRED_RETRY_KEYS"></a>

## RETIRED\_RETRY\_KEYS
The three `wrapper.errorHandling` keys that configured `RetryHandler` of
`@onlineapps/error-handler-core`, retired in d.1264 and REFUSED by name
(`_assertRetiredRetryKeys`).

Nothing on this package's path read what they configured: the orchestrator
calls the connector's `classifyError()` only, and this wrapper its
`logError()` only — neither reaches `retryHandler`. A step is retried by
`@onlineapps/conn-orch-orchestrator` under the retry policy of its operation,
declared as `retry` in `operations.json` and handed over by the
`operationMutatesLookup` hook (conf `step-error-classification` 002 variant B;
004: "`wrapper.errorHandling.*` is removed"). The four § Removing questions
are answered in this package's CHANGELOG, `[Unreleased]` — `Removed`.

**Kind**: global constant  
<a name="REVALIDATION_SLOW_MS"></a>

## REVALIDATION\_SLOW\_MS
The restart cooldown window, and ONLY that: how long a service that failed
validation for the SAME cause waits before it tries again after a restart
(`_applyRestartCooldown()`).

It is not a schedule. Until d.755b a third ladder stage `slow_backoff` claimed
it as one — a 6-hour retry forever — and nothing ever entered that state, so
the claim was never true. The ladder ends at the hourly ceiling with a fatal
teardown, which is the documented end of the road.

**Kind**: global constant  
**See**: api/docs/architecture/validator.md - Re-Validation Backoff Schedule  
<a name="REVALIDATION_STATE"></a>

## REVALIDATION\_STATE
Every state the re-validation cycle can be in. TWO backoff stages, because
two is what the concept declares: fast ladder, then hourly up to the ceiling,
then `_shutdownFatally()` — "there is no third retry phase"
(`api/docs/architecture/validator.md` - Re-Validation Backoff Schedule).

**Kind**: global constant  
<a name="WRAPPER_RAISED_ERROR"></a>

## WRAPPER\_RAISED\_ERROR
The provenance mark: this error was raised by the wrapper, not by a handler.

**Kind**: global constant  
<a name="isPositiveIntegerId"></a>

## isPositiveIntegerId(value) ⇒ <code>boolean</code>
Is this envelope value a usable tenancy id?

The rule comes from the wire contract, not from this file:
api/docs/biz/10-invocation/mq-envelope.md declares `envelope.tenant_id` an
integer and says "Positive integer.". Zero is explicitly not a value
(api/docs/biz/20-tenancy/multitenancy.md §1.6; the tenant-wide scope was
retired by the owner on 2026-09-03).

A decimal STRING is accepted alongside the number because that is what
already crosses this boundary and is forwarded unchanged — the builder
validates, it never converts. The accepted string form is exact: no sign, no
leading zero, no whitespace, no decimal point, so `"5"` passes while `"05"`,
`" 5"`, `"5.0"` and `"-5"` do not.

**Kind**: global function  

| Param | Type |
| --- | --- |
| value | <code>\*</code> | 

<a name="isMissingId"></a>

## isMissingId(value) ⇒ <code>boolean</code>
Is this envelope id missing?

ONE predicate for `workflow_id`, `correlation_id` and `step_id` — three
fields of the same kind, so a value that is a value for one of them is a
value for all three. Until d.363 the first two were checked with
`.length === 0` and the third with `.trim() === ''`, so an id of nothing but
whitespace passed two guards and was refused by the third, inside one method.
The value that got through is not harmless: `workflow_id` and `step_id` name
the storage prefix `content/<workflow_id>/<step_id>`, and `correlation_id` is
what a trace is joined on.

It refuses blankness and NOTHING else. An id with real content and spaces
around it is a value and is forwarded exactly as published — trimming it here
would be the builder inventing a second spelling of somebody's id
(`architecture-principles.md` §3), and this file validates, it never converts.

**Kind**: global function  

| Param | Type |
| --- | --- |
| value | <code>\*</code> | 

<a name="describeRejectedId"></a>

## describeRejectedId(value) ⇒ <code>string</code>
How a rejected value may be named in an error message.

A number is safe to print — it carries no foreign data. Anything else is
named by its type ONLY: the envelope is caller-supplied, so a string or an
object could carry a secret or personal data into a log line.

**Kind**: global function  

| Param | Type |
| --- | --- |
| value | <code>\*</code> | 

<a name="requireSchedulerPublisherFactory"></a>

## requireSchedulerPublisherFactory() ⇒ <code>function</code>
Továrna plánovače z jádra — načtená LÍNĚ a s hláškou §5.

Port potřebuje z pinovaného `@onlineapps/mq-client-core` jmenný prostor
`scheduler` s továrnou `createSchedulerPublisher`. Verze, která port
plánovače ještě nevydává, se načte BEZ chyby a prostor `scheduler` prostě
nemá — to je druhá větev níž („export chybí"), a běžný případ. Samotné
`require` selže jen tehdy, když jádro nejde načíst vůbec (v `node_modules`
chybí nebo je rozbité) — první větev („modul se nenačte"). Chybějící sekce
`scheduler` v `queueConfig` `require` neshodí: odmítne až složení jména
fronty, tedy volání, ne načtení.

Kdyby se jádro načítalo v hlavičce souboru, spadl by při nenačitatelném
jádru CELÝ wrapper při načtení modulu — tedy i službě, která plánovač vůbec
nepoužívá. Načítá se proto až při stavbě portu: vada pinu se pozná při
stavbě kontextu, jménem slotu a s opravou (`architecture-principles.md` §5),
a ne pádem, který o plánovači nic neříká.

**Kind**: global function  
**Returns**: <code>function</code> - `createSchedulerPublisher` jádra  
**Throws**:

- <code>Error</code> když jádro továrnu nemá nebo se nenačte

<a name="errorLogFields"></a>

## errorLogFields(err) ⇒ <code>Object</code>
The fields every log line ABOUT an error must carry: the error itself, its
text as `exception.message` and its class as `exception.type` — the
OpenTelemetry semantic-convention attributes, as own, enumerable properties.

`Error.message` is non-enumerable. A meta of `{ err }` therefore survives
`JSON.stringify` as `{ name, code, status, details }` — the sentence saying
what went wrong is gone, except where a `stack` key happens to be passed too,
and then only as the first line of it. Measured (BIZ-converter, dev, wrapper
8.1.1): four of this file's log lines pass no `stack`, so their message did
not reach the operator at all.

Why not a `message` key: the telemetry log envelope carries the record's own
text in its body, and the monitoring consumer merges the body over the data,
so a `message` in the data never reached Loki (measured d.868, DEV Loki).

`exception.type` is the constructor's name, not `err.name`: `BusinessError`
sets `name` itself, so a service subclass that sets none of its own reports
`BusinessError` by name while its constructor still names the class.

One rail for the whole package: `ServiceWrapper` uses it for the two lines it
writes about an error itself (`change-discipline.md` § One rail per concern).

**Kind**: global function  
**See**: api/shared/monitoring/monitoring-core/README.md § Wire shape — the telemetry log envelope  

| Param | Type | Description |
| --- | --- | --- |
| err | <code>\*</code> | Anything that was thrown. |

<a name="buildEnvelope"></a>

## buildEnvelope(status, code, message, [details], [correlation_id]) ⇒ [<code>ErrorEnvelope</code>](#ErrorEnvelope)
Assemble the caller-facing envelope. `details` and `correlation_id` appear
only when supplied — the shape the contract documents, nothing padded.

**Kind**: global function  

| Param | Type |
| --- | --- |
| status | <code>number</code> | 
| code | <code>string</code> | 
| message | <code>string</code> | 
| [details] | <code>\*</code> | 
| [correlation_id] | <code>string</code> | 

<a name="describeFailure"></a>

## describeFailure(err, [context]) ⇒ <code>Object</code>
THE mapping. One place decides both halves of what a throw becomes:
  - `envelope` — what the caller receives;
  - `log`      — the line whoever HANDLES the throw writes for it.

Two halves, one branching table, because a second copy of the table is how a
caller-facing status and its log line drift apart
(`.claude/rules/change-discipline.md` § One rail per concern).

Pure: it decides, it does not log. [map](#ErrorMapper+map) is the wrapper's
own path and writes the line; [toCallerEnvelope](#toCallerEnvelope) is the path for a
service that already logs the failure itself and needs the envelope only.

**Kind**: global function  

| Param | Type | Description |
| --- | --- | --- |
| err | <code>\*</code> | Every `meta` it builds carries the same trace fields: `err`, `exception.message` and `exception.type` (see [errorLogFields](#errorLogFields)), plus BOTH ids of the request, so a search by either one finds the whole trace. |
| [context] | <code>Object</code> |  |
| [context.operation] | <code>string</code> |  |
| [context.correlation_id] | <code>string</code> |  |
| [context.workflow_id] | <code>string</code> |  |

<a name="toCallerEnvelope"></a>

## toCallerEnvelope(err, [context]) ⇒ [<code>ErrorEnvelope</code>](#ErrorEnvelope)
The envelope a caller receives for `err` — and NOTHING else. No logger, no
log line, no other side effect: a service that already logged the failure
itself calls this instead of instantiating an [ErrorMapper](#ErrorMapper), so one
failure stays one log line (measured: the converter logged every 5xx twice,
BIZ-converter 2026-09-14).

Same mapping table as [map](#ErrorMapper+map), because it IS the same
function underneath — the two differ only in whether the log half is used.

Contract: api/docs/biz/70-contracts/error-handling.md;
the table: api/docs/biz/10-invocation/handler-dispatch.md § `ErrorMapper` — throw-to-envelope.

**Kind**: global function  

| Param | Type | Description |
| --- | --- | --- |
| err | <code>\*</code> | Anything a handler threw. Never rethrown. |
| [context] | <code>Object</code> |  |
| [context.operation] | <code>string</code> |  |
| [context.correlation_id] | <code>string</code> |  |
| [context.workflow_id] | <code>string</code> |  |

<a name="_buildDefaultAjv"></a>

## \_buildDefaultAjv() ⇒ <code>object</code>
Build the default Ajv instance used when the caller does not inject one.
Change the option set only together with the documents named under See.

**Kind**: global function  
**Returns**: <code>object</code> - Configured Ajv instance with ajv-formats applied.  
**See**

- api/docs/biz/10-invocation/handler-dispatch.md
- api/docs/governance/confirmations/input-validation-single-rail.md (003)

<a name="_summariseAjvError"></a>

## \_summariseAjvError(phase, errors) ⇒ <code>string</code>
Summarise the first ajv error into a human-readable string.
Shape: `<phase>/<instancePath> <ajv message>`

**Kind**: global function  

| Param | Type | Description |
| --- | --- | --- |
| phase | <code>string</code> | 'input' or 'output'. |
| errors | <code>Array.&lt;object&gt;</code> | ajv error array (fn.errors). |

<a name="_describeNonAjv"></a>

## \_describeNonAjv(value) ⇒ <code>string</code>
Name what was handed in where an Ajv instance was expected, for the refusal
message. It describes the value, never prints it: the caller knows what it
passed, and a schema compiler's innards are not error-message material.

**Kind**: global function  
**Returns**: <code>string</code> - A short noun phrase, e.g. `null`, `number`, `object without compile()`.  

| Param | Type | Description |
| --- | --- | --- |
| value | <code>\*</code> | The rejected `options.ajv`. |

<a name="linkAbortSignal"></a>

## linkAbortSignal(callerSignal, controller) ⇒ <code>function</code>
Tie the caller's own abort signal, when one came with the message, to the
controller this wrapper owns for the attempt: the caller aborting aborts the
attempt too, with the caller's reason (d.1264b).

**Kind**: global function  
**Returns**: <code>function</code> - removes the link, or null when there is none  

| Param | Type |
| --- | --- |
| callerSignal | <code>AbortSignal</code> \| <code>undefined</code> | 
| controller | <code>AbortController</code> | 

<a name="readConnOrchOrchestratorContractVersion"></a>

## readConnOrchOrchestratorContractVersion() ⇒ <code>Object</code>
The wrapper's v3 invocation model wires OrchestratorConnector with invoker RPC,
which the orchestrator has carried since the major above. If npm resolves an
older one (stale tree / lockfile), WorkflowOrchestrator throws
"apiMapper is required" — fail here with an explicit contract error instead.

**Kind**: global function  
<a name="describeValidationFinding"></a>

## describeValidationFinding(finding) ⇒ <code>string</code>
One finding of `ValidationOrchestrator` as a sentence an operator can act on.

`results.errors` is a MIXED array and always has been: steps 2, 4, 5 and 7 push
sentences (the manifest's own `describeFinding`, a cookbook failure), while step
1 — `ServiceStructureValidator` — pushes objects `{ type, path, message, fix }`.
Interpolating that array into a template literal turned every object into
`[object Object]`, so the code (`MISSING_COOKBOOKS`), the path and the way out
were dropped at the last step before the log (`architecture-principles.md` §5;
measured by W591 on a boot whose `tests/cookbooks` was empty).

A sentence travels verbatim — the step that produced it already wrote it in the
`[Context] Problem - Expected/Fix` shape. An object is rendered into that shape
here. Anything else is serialized rather than described: a formatter that threw
would replace the failure it was called to report.

**Kind**: global function  

| Param | Type | Description |
| --- | --- | --- |
| finding | <code>string</code> \| <code>object</code> | one entry of `result.errors` |

<a name="describeValidationFailure"></a>

## describeValidationFailure(errors) ⇒ <code>string</code>
The message the two Tier-1 throw sites raise — the startup path (FÁZE 0.2,
`_ensureValidationProof`) and the hourly revalidation (`_executeRevalidation`).
ONE helper, because one wording of one fact is one rail
(`.claude/rules/change-discipline.md` § One rail per concern).

**Kind**: global function  

| Param | Type | Description |
| --- | --- | --- |
| errors | <code>Array.&lt;(string\|object)&gt;</code> | `result.errors` of a failed validation |

<a name="markBootDisposition"></a>

## markBootDisposition(error, disposition) ⇒ <code>Error</code>
Stamp the disposition on an error and hand it back, so a `throw` stays one
expression.

**Kind**: global function  
**Returns**: <code>Error</code> - the same error  

| Param | Type | Description |
| --- | --- | --- |
| error | <code>Error</code> |  |
| disposition | <code>string</code> | one of `BOOT_DISPOSITION` |

<a name="failedStaticValidationSteps"></a>

## failedStaticValidationSteps(result) ⇒ <code>Array.&lt;string&gt;</code>
Which static steps of this result failed.

All five report `{ valid, errors }`; only step 5 answers with `success`, and it
is deliberately not in the list. A step that did not run at all is absent from
`result.steps` — the run fails fast after steps 1 and 3 — and an absent step has
no verdict to read.

**Kind**: global function  
**Returns**: <code>Array.&lt;string&gt;</code> - names of the failed static steps, empty when there are none  

| Param | Type | Description |
| --- | --- | --- |
| result | <code>object</code> | the value `ValidationOrchestrator.validate()` resolved to |

<a name="markWrapperRaised"></a>

## markWrapperRaised(err) ⇒ <code>Error</code>
Stamp an error the wrapper is about to raise from inside a handler's call.

**Kind**: global function  
**Returns**: <code>Error</code> - The same error, marked.  

| Param | Type | Description |
| --- | --- | --- |
| err | <code>Error</code> | The error being thrown. |

<a name="isWrapperRaised"></a>

## isWrapperRaised(err) ⇒ <code>boolean</code>
Did this wrapper raise the error itself?

**Kind**: global function  

| Param | Type |
| --- | --- |
| err | <code>\*</code> | 

<a name="ContextBuildResult"></a>

## ContextBuildResult : <code>Object</code>
What build() resolves to.

**Kind**: global typedef  
**See**: api/docs/biz/10-invocation/operation-context.md § Release  
**Properties**

| Name | Type | Description |
| --- | --- | --- |
| ctx | <code>OperationContext</code> | Context for this one invocation. |
| release | <code>function</code> | Idempotent teardown hook.   Callers MUST await it in a finally block. |

<a name="ErrorEnvelope"></a>

## ErrorEnvelope : <code>Object</code>
MQ response envelope produced by map(). Never null — map() never throws.

**Kind**: global typedef  
**Properties**

| Name | Type | Description |
| --- | --- | --- |
| status | <code>number</code> | HTTP-shaped status carried on the envelope. |
| error | [<code>ErrorEnvelopeBody</code>](#ErrorEnvelopeBody) | The error body. |

<a name="ErrorEnvelopeBody"></a>

## ErrorEnvelopeBody : <code>Object</code>
The `error` member of an [ErrorEnvelope](#ErrorEnvelope).

**Kind**: global typedef  
**See**: api/docs/biz/70-contracts/error-handling.md  
**Properties**

| Name | Type | Description |
| --- | --- | --- |
| code | <code>string</code> | Stable error code (e.g. INTERNAL_ERROR). |
| message | <code>string</code> | Human-readable message; masked for unbranded errors. |
| [details] | <code>\*</code> | Present only for branded business errors. |
| [correlation_id] | <code>string</code> | Echoed from meta when supplied. |
| [type] | <code>string</code> | Error class the thrower declared (`DECLINED`, ...).   Present only where `details` is: a masked path carries neither. Absent, never   `null`, when the thrower declared none. |

<a name="WorkflowRpcReplyBody"></a>

## WorkflowRpcReplyBody : <code>Object</code>
Body published to a caller's reply queue by _publishWorkflowRpcReply().

**Kind**: global typedef  
**Properties**

| Name | Type | Description |
| --- | --- | --- |
| status | <code>string</code> | Outcome marker for the RPC caller. |
| [output] | <code>object</code> | Handler output on success. |
| [error] | <code>object</code> | Error envelope on failure. |
| [correlation_id] | <code>string</code> |  |
| [workflow_id] | <code>string</code> |  |

<a name="OperationEnvelope"></a>

## OperationEnvelope : <code>Object</code>
What invokeOperation() resolves to. Never rejects — failures arrive as `error`.

**Kind**: global typedef  
**Properties**

| Name | Type | Description |
| --- | --- | --- |
| status | <code>number</code> | HTTP-shaped status for this invocation. |
| [result] | <code>\*</code> | Handler result on success. |
| [error] | <code>Object</code> | Error body produced by ErrorMapper on failure. |

