@cosyte/pathways
Classes
DeliveryRejected
A delivery the receiver rejected for good: retrying the same message will not help.
instanceof DeliveryRejected holds for one made by any copy of Pathways in the process: the
pathways command bundles its own copy of this class, and a custom destination imports another
from the package entry, so the check goes by a shared mark rather than by class identity.
Example
if (ack.code === "AE") throw new DeliveryRejected("AE");
Extends
Error
Constructors
Constructor
new DeliveryRejected(
reason):DeliveryRejected
Parameters
reason
string
A short, stable reason, such as an ACK code. Never message content.
Returns
Overrides
Error.constructor
Properties
reason
readonlyreason:string
A short, stable reason, such as an ACK code. Never message content.
Methods
[hasInstance]()
static[hasInstance](value):boolean
Whether value is a DeliveryRejected from any copy of Pathways; a subclass checks as usual.
Parameters
value
unknown
Returns
boolean
EncryptionError
A value that could not be encrypted or decrypted. Its message is fixed text, with at most the master key's id, which is never secret: never the value, a key, or anything a message holds. A value that fails its integrity check always ends here, never as a garbled message.
Example
try {
await decrypt(stored, keys);
} catch (error) {
if (error instanceof EncryptionError) console.error(error.code, error.keyId);
}
Extends
Error
Constructors
Constructor
new EncryptionError(
code,keyId?):EncryptionError
Parameters
code
What went wrong.
keyId?
string
The master key's id, when the failure concerns one.
Returns
Overrides
Error.constructor
Properties
code
readonlycode:EncryptionErrorCode
keyId
readonlykeyId:string|undefined
The master key's id, when the failure concerns one.
LeaseLostError
A fenced write that changed nothing: the epoch it carried is no longer the engine lease's, so
another engine has taken the lease since, or the row it was to change moved under that engine.
The engine that sees it stops sending and taking at once and exits (log code lease-lost).
Example
if (error instanceof LeaseLostError) process.exitCode = 1;
Extends
Error
Constructors
Constructor
new LeaseLostError():
LeaseLostError
Returns
Overrides
Error.constructor
Properties
code
readonlycode:"ERR_PATHWAYS_LEASE_LOST"="ERR_PATHWAYS_LEASE_LOST"
SecretError
A secret could not be resolved. Its text names the secret, never a value. A provider can reject with one, so the code shows where Pathways reports the failure.
Example
throw new SecretError("ERR_PATHWAYS_SECRET_NOT_FOUND", "lab/api");
Extends
Error
Constructors
Constructor
new SecretError(
code,secret,detail?):SecretError
Parameters
code
What went wrong.
secret
string
The secret's name, never its value.
detail?
string
What went wrong, in words: never a value. Defaults to the code's own words.
Returns
Overrides
Error.constructor
Properties
code
readonlycode:SecretErrorCode
secret
readonlysecret:string
The secret's name.
Interfaces
AckRequest
What an AckBuilder is asked to acknowledge.
Example
const controlId = (request: AckRequest): string => request.header?.get("MSH.10") ?? "";
Properties
code
readonlycode:SourceAckCode
The code Pathways decided on.
header
readonlyheader:Hl7Message|undefined
The inbound message's MSH segment, parsed, or undefined when it has none that parses. Only
the header is parsed: never the segments after it.
messageId
readonlymessageId:number|undefined
The stored message's id, or undefined when it could not be stored.
problem
readonlyproblem:string|undefined
For an unreadable message, what was wrong, as a stable code: one of @cosyte/hl7's parse
codes (such as NO_MSH_SEGMENT), NO_CONTROL_ID, MULTIPLE_MESSAGES or
MLLP_TRAILING_BYTES.
reason
readonlyreason:AckReason
AuditEntry
One entry of the audit log, as stored.
Example
const line = (entry: AuditEntry): string => `${entry.at} ${entry.actor} ${entry.action}`;
Properties
action
readonlyaction:"search"|"view"|"content-view"|"replay"|"resend"|"clear"|"retention"|"config"|"lease-release"|"migrate"|"channel-pause"|"channel-resume"|"channel-stop"|"channel-halt"|"channel-reload"|"verify"
actor
readonlyactor:string
The operating system user who acted.
at
readonlyat:string
When, in ISO 8601 UTC.
channel
readonlychannel:string|null
detail
readonlydetail:AuditDetail
id
readonlyid:number
messageId
readonlymessageId:number|null
outcome
readonlyoutcome:AuditOutcome
AuditEvent
An entry of the audit log, once it is stored: who did what, to which message, and how it ended. The operator commands (and the engine's retention runs and configuration checks) write them; engine-wide hooks receive them, and channels' own hooks do not. Like every event it carries ids, counts, statuses, codes and hashes, never content, so a hook can forward the audit log to a file or a SIEM.
Example
if (event.type === "audit" && event.action === "content-view") {
siem.send({ at: event.at, user: event.actor, message: event.messageId });
}
Properties
action
readonlyaction:AuditEventAction
actor
readonlyactor:string
The operating system user who acted.
at
readonlyat:string
When it happened, as an ISO 8601 time in UTC.
channel
readonlychannel:string|undefined
The channel acted on, or undefined for an entry about no one channel.
detail
readonlydetail:Readonly<Record<string,string|number|boolean>>
Counts, statuses, codes and hashes, such as { status: "transformed", queued: 2 }.
entryId
readonlyentryId:number
The entry's id in the audit log, which only grows.
messageId
readonlymessageId:number|undefined
The message acted on, when there is one.
outcome
readonlyoutcome:"ok"|"refused"|"not-found"
ok; refused, with detail.reason saying why; or not-found.
type
readonlytype:"audit"
AuditInput
An audit entry to append. The store adds the time and the actor.
Example
await store.appendAudit("noah", [{ action: "content-view", channel: "adt", messageId: 12 }]);
Properties
action
readonlyaction:"search"|"view"|"content-view"|"replay"|"resend"|"clear"|"retention"|"config"|"lease-release"|"migrate"|"channel-pause"|"channel-resume"|"channel-stop"|"channel-halt"|"channel-reload"|"verify"
channel?
readonlyoptionalchannel?:string
detail?
readonlyoptionaldetail?:Readonly<Record<string,string|number|boolean>>
messageId?
readonlyoptionalmessageId?:number
outcome?
readonlyoptionaloutcome?:AuditOutcome
Defaults to ok.
AuditQuery
Which audit entries MessageStore.auditEntries returns: newest first, filters optional.
Example
const views = await store.auditEntries({ action: "content-view", since: "2026-11-01", limit: 100 });
Properties
action?
readonlyoptionalaction?:"search"|"view"|"content-view"|"replay"|"resend"|"clear"|"retention"|"config"|"lease-release"|"migrate"|"channel-pause"|"channel-resume"|"channel-stop"|"channel-halt"|"channel-reload"|"verify"
before?
readonlyoptionalbefore?:number
Only ids below this one: the next of the page before.
channel?
readonlyoptionalchannel?:string
limit
readonlylimit:number
messageId?
readonlyoptionalmessageId?:number
since?
readonlyoptionalsince?:string
At or after this time (ISO 8601, UTC).
until?
readonlyoptionaluntil?:string
Before this time (ISO 8601, UTC).
ChannelContext
What a connector factory is given beside its options: the context, the channel's name and its format.
Example
context.logger.info({ event: "inbox-ready", channel: context.channel, format: context.format });
Extends
Properties
channel
readonlychannel:string
dir
readonlydir:string
The folder of the YAML file that names the extension: the channel's folder, or the project
root for pathways.yaml. Resolve relative paths in options against it.
Inherited from
env
readonlyenv:Readonly<Record<string,string|undefined>>
Where to read credentials and other settings from: process.env when the engine runs.
Inherited from
format
readonlyformat:"hl7v2"|"json"|"fhir"|"x12"|"ccda"|"text"
logger
readonlylogger:ExtensionLogger
Inherited from
ContentStore
What pathways rekey needs from storage beyond MessageStore: every stored body and error
text as stored, and a way to replace them. The engine never uses it. Store implements it;
a storage adapter that implements it can be rekeyed.
Example
for (const row of await store.storedContent("messages", 0, 100)) {
await store.replaceContent(row, { body: reencrypted(row.body), error: row.error });
}
Methods
replaceContent()
replaceContent(
current,next):Promise<boolean>
Replaces a row's body and error with next, but only while the row still holds what current
read, so a change made meanwhile (a retry's new error) is never overwritten. Returns whether it
replaced them.
Parameters
current
next
body
Uint8Array
error
string | null
Returns
Promise<boolean>
storedContent()
storedContent(
table,after,limit):Promise<StoredContent[]>
Up to limit rows of table with an id above after, oldest first, as stored, leaving out the
rows of a message whose content was dropped: they hold none.
Parameters
table
after
number
limit
number
Returns
Promise<StoredContent[]>
DeliveredEvent
A destination accepted an output.
Example
if (event.type === "delivered" && event.attempt > 1) resolveIncident(event.channel, event.destination);
Extends
Properties
at
readonlyat:string
When it happened, as an ISO 8601 time in UTC, such as 2026-11-07T12:00:00.000Z.
Inherited from
attempt
readonlyattempt:number
Which attempt this was, from 1.
Inherited from
channel
readonlychannel:string
The channel's name.
Inherited from
deliveryId
readonlydeliveryId:number
Inherited from
destination
readonlydestination:number
The destination's place in the channel's destinations: list, from 0.
Inherited from
messageId
readonlymessageId:number
Inherited from
resend?
readonlyoptionalresend?:object
Set when this attempt was a possible duplicate: an earlier one was cut off after the send, so
the receiver may have the message already. previousAttempts counts the attempts before it.
Absent otherwise.
previousAttempts
readonlypreviousAttempts:number
Inherited from
type
readonlytype:"delivered"
Overrides
DeliveryEventBase
What every event about one delivery carries: the message, the destination and the delivery.
Example
const key = (event: DeliveryEventBase): string => `${event.channel}/${event.destination}/${event.deliveryId}`;
Extends
Extended by
DeliveredEventDeliveryExhaustedEventDeliveryRedirectedEventDeliveryRejectedEventDeliveryRetryEvent
Properties
at
readonlyat:string
When it happened, as an ISO 8601 time in UTC, such as 2026-11-07T12:00:00.000Z.
Inherited from
attempt
readonlyattempt:number
Which attempt this was, from 1.
channel
readonlychannel:string
The channel's name.
Inherited from
deliveryId
readonlydeliveryId:number
destination
readonlydestination:number
The destination's place in the channel's destinations: list, from 0.
messageId
readonlymessageId:number
resend?
readonlyoptionalresend?:object
Set when this attempt was a possible duplicate: an earlier one was cut off after the send, so
the receiver may have the message already. previousAttempts counts the attempts before it.
Absent otherwise.
previousAttempts
readonlypreviousAttempts:number
type
readonlytype:"received"|"receive-failed"|"transformed"|"filtered"|"transform-failed"|"delivered"|"delivery-retry"|"delivery-redirected"|"delivery-rejected"|"delivery-exhausted"|"source-down"|"source-up"|"audit"
Inherited from
DeliveryExhaustedEvent
A delivery ran out of retries: it had its retry.limit of attempts, or its next attempt would come
later than retry.maxAge after it was queued. It is marked failed, its body kept for an operator
to resend, and the destination moves on to the next. Only a destination whose retry settings set
a limit or an age sends this.
Example
if (event.type === "delivery-exhausted") page(`${event.channel}: gave up after ${event.attempt}`);
Extends
Properties
at
readonlyat:string
When it happened, as an ISO 8601 time in UTC, such as 2026-11-07T12:00:00.000Z.
Inherited from
attempt
readonlyattempt:number
Which attempt this was, from 1.
Inherited from
channel
readonlychannel:string
The channel's name.
Inherited from
code
readonlycode:string|undefined
The last error's code, such as ECONNREFUSED or HTTP_503.
deliveryId
readonlydeliveryId:number
Inherited from
destination
readonlydestination:number
The destination's place in the channel's destinations: list, from 0.
Inherited from
messageId
readonlymessageId:number
Inherited from
name
readonlyname:string
The last error's name, such as MllpDeliveryError.
reason
readonlyreason:"limit"|"maxAge"
limit when it had its attempts, maxAge when it was too old to wait for another.
resend?
readonlyoptionalresend?:object
Set when this attempt was a possible duplicate: an earlier one was cut off after the send, so
the receiver may have the message already. previousAttempts counts the attempts before it.
Absent otherwise.
previousAttempts
readonlypreviousAttempts:number
Inherited from
type
readonlytype:"delivery-exhausted"
Overrides
DeliveryRedirectedEvent
An HTTP destination answered with a redirect (a 3xx). Redirects are never followed, since following one could send a message to a host nobody configured: the delivery is held and tried again after the longest wait, five minutes, and its later deliveries wait behind it, until the destination's URL is fixed. It names the Location's host only, never its path or query.
Example
if (event.type === "delivery-redirected") page(`${event.channel} moved to ${event.locationHost}`);
Extends
Properties
at
readonlyat:string
When it happened, as an ISO 8601 time in UTC, such as 2026-11-07T12:00:00.000Z.
Inherited from
attempt
readonlyattempt:number
Which attempt this was, from 1.
Inherited from
channel
readonlychannel:string
The channel's name.
Inherited from
deliveryId
readonlydeliveryId:number
Inherited from
destination
readonlydestination:number
The destination's place in the channel's destinations: list, from 0.
Inherited from
locationHost
readonlylocationHost:string|undefined
The host the Location names, with its port when it has one, or undefined when it names none.
messageId
readonlymessageId:number
Inherited from
resend?
readonlyoptionalresend?:object
Set when this attempt was a possible duplicate: an earlier one was cut off after the send, so
the receiver may have the message already. previousAttempts counts the attempts before it.
Absent otherwise.
previousAttempts
readonlypreviousAttempts:number
Inherited from
retryAt
readonlyretryAt:string
When the next attempt is due, as an ISO 8601 time in UTC.
retryInMs
readonlyretryInMs:number
How long until the next attempt, in milliseconds.
status
readonlystatus:number
The 3xx status, such as 301 or 308.
type
readonlytype:"delivery-redirected"
Overrides
DeliveryRejectedEvent
A destination rejected an output for good (an MLLP AE, an HTTP 4xx): it is marked failed and the
destination moves on to the next.
Example
if (event.type === "delivery-rejected") page(`${event.channel}: rejected (${event.reason})`);
Extends
Properties
at
readonlyat:string
When it happened, as an ISO 8601 time in UTC, such as 2026-11-07T12:00:00.000Z.
Inherited from
attempt
readonlyattempt:number
Which attempt this was, from 1.
Inherited from
channel
readonlychannel:string
The channel's name.
Inherited from
deliveryId
readonlydeliveryId:number
Inherited from
destination
readonlydestination:number
The destination's place in the channel's destinations: list, from 0.
Inherited from
messageId
readonlymessageId:number
Inherited from
reason
readonlyreason:string
The destination's short, stable reason, such as AE or HTTP 400.
resend?
readonlyoptionalresend?:object
Set when this attempt was a possible duplicate: an earlier one was cut off after the send, so
the receiver may have the message already. previousAttempts counts the attempts before it.
Absent otherwise.
previousAttempts
readonlypreviousAttempts:number
Inherited from
type
readonlytype:"delivery-rejected"
Overrides
DeliveryRetryEvent
A delivery failed in a way worth trying again (a refused connection, a timeout, an MLLP AR, an
HTTP 408, 425, 429 or 5xx). The destination retries it after retryInMs, and its later deliveries
wait behind it. retryInMs is the backoff, or the wait a 429's or a 503's Retry-After asked for
when that is longer, up to five minutes.
Example
if (event.type === "delivery-retry" && event.attempt === 5) page(`${event.channel} is retrying`);
Extends
Properties
at
readonlyat:string
When it happened, as an ISO 8601 time in UTC, such as 2026-11-07T12:00:00.000Z.
Inherited from
attempt
readonlyattempt:number
Which attempt this was, from 1.
Inherited from
channel
readonlychannel:string
The channel's name.
Inherited from
code
readonlycode:string|undefined
The error's code, such as ECONNREFUSED, ACK_TIMEOUT or HTTP_503.
deliveryId
readonlydeliveryId:number
Inherited from
destination
readonlydestination:number
The destination's place in the channel's destinations: list, from 0.
Inherited from
messageId
readonlymessageId:number
Inherited from
name
readonlyname:string
The error's name, such as MllpDeliveryError.
resend?
readonlyoptionalresend?:object
Set when this attempt was a possible duplicate: an earlier one was cut off after the send, so
the receiver may have the message already. previousAttempts counts the attempts before it.
Absent otherwise.
previousAttempts
readonlypreviousAttempts:number
Inherited from
retryAt
readonlyretryAt:string
When the next attempt is due, as an ISO 8601 time in UTC.
retryInMs
readonlyretryInMs:number
How long until the next attempt, in milliseconds.
type
readonlytype:"delivery-retry"
Overrides
DestinationConnector
Sends messages out. Throw to fail an attempt: by default the engine retries it with growing waits
and holds this destination's later messages behind it. Throw a DeliveryRejected when
sending the same message again cannot succeed (the receiver rejected its content), and the engine
marks it failed and moves on. A custom destination's module builds one with a
DestinationFactory.
Example
const destination: DestinationConnector = {
async deliver(message) {
await upload(`${message.messageId}-${message.seq}`, message.body);
},
};
Methods
deliver()
deliver(
message):Promise<void>
Parameters
message
Returns
Promise<void>
stop()?
optionalstop():Promise<void>
Releases what the destination keeps open between deliveries, such as connections.
Returns
Promise<void>
ExpireResult
What retention deleted, or would delete, from one channel.
Example
const { messages, deliveries }: ExpireResult = await store.expire("adt", cutoff, true);
Properties
deliveries
readonlydeliveries:number
messages
readonlymessages:number
ExtensionContext
What every extension factory is given beside its options.
Example
const token = context.env["INBOX_TOKEN"];
const folder = path.resolve(context.dir, "./in");
Extended by
Properties
dir
readonlydir:string
The folder of the YAML file that names the extension: the channel's folder, or the project
root for pathways.yaml. Resolve relative paths in options against it.
env
readonlyenv:Readonly<Record<string,string|undefined>>
Where to read credentials and other settings from: process.env when the engine runs.
logger
readonlylogger:ExtensionLogger
ExtensionLogger
The engine's logger, handed to an extension. Lines land in the same JSON log as the engine's, tagged with the channel and which extension wrote them.
Example
context.logger.warn({ event: "inbox-read-failed", code: "EACCES" });
Methods
error()
error(
fields):void
Parameters
fields
Returns
void
info()
info(
fields):void
Parameters
fields
Returns
void
warn()
warn(
fields):void
Parameters
fields
Returns
void
FakeSecret
One version of a secret a fakeSecretProvider holds: its value, and optionally its version's
name (by default its place in the list, from "1"), when it expires and the identity that read it.
Example
const secret: FakeSecret = { value: "synthetic-token", version: "v2" };
Properties
expiresAt?
readonlyoptionalexpiresAt?:Date
identity?
readonlyoptionalidentity?:string
The principal it answers as having read it, as ResolvedSecret.identity.
value
readonlyvalue:string
version?
readonlyoptionalversion?:string
FakeSecretProvider
A SecretProvider held in memory, for tests: no network, no cloud account. It answers as the
contract asks of every provider, so a channel that reads { secret: { provider, name } } can be
tested against it in place of a cloud store.
Example
const vault = fakeSecretProvider({ "lab/api": '{"token":"synthetic-token"}' });
await vault.resolve({ name: "lab/api", field: "token" }, AbortSignal.timeout(1_000));
vault.calls; // [{ name: "lab/api", field: "token" }]
Extends
Properties
calls
readonlycalls: readonlySecretRef[]
Every request it was given, in order.
stopped
readonlystopped:number
How many times stop was called.
Methods
delete()
delete(
name):void
Removes name, so a later request for it is not found.
Parameters
name
string
Returns
void
fail()
fail(
name,error):void
Makes every later request for name reject with error, as a store that fails would.
Parameters
name
string
error
Error
Returns
void
resolve()
resolve(
ref,signal):Promise<ResolvedSecret>
Resolves one secret.
Parameters
ref
signal
AbortSignal
Returns
Promise<ResolvedSecret>
Inherited from
set()
set(
name,entry):void
Sets or replaces name; a single value or secret adds it as a new current version.
Parameters
name
string
entry
Returns
void
stop()?
optionalstop():void|Promise<void>
Releases what the provider holds open. Called once start has resolved every secret.
Returns
void | Promise<void>
Inherited from
FileSourceFacts
What a file source knows about a message: the file's name as it was dropped, its size in bytes, and when it was stored.
Example
const name = (source: FileSourceFacts): string => source.name;
Properties
kind
readonlykind:"file"
name
readonlyname:string
The file's name as it was dropped in the folder. It can carry a patient's name: never log it.
receivedAt
readonlyreceivedAt:string
When the message was stored, as an ISO 8601 time.
size
readonlysize:number
The body's size in bytes.
FilteredEvent
A message's transform returned no output, so nothing is delivered.
Example
if (event.type === "filtered") count("filtered", 1);
Extends
Properties
at
readonlyat:string
When it happened, as an ISO 8601 time in UTC, such as 2026-11-07T12:00:00.000Z.
Inherited from
channel
readonlychannel:string
The channel's name.
Inherited from
messageId
readonlymessageId:number
type
readonlytype:"filtered"
Overrides
HookContext
What a hook factory is given beside its options: the context, and the channel for a hook a
channel's YAML names (undefined for an engine-wide one in pathways.yaml).
Example
const scope = context.channel ?? "engine";
Extends
Properties
channel
readonlychannel:string|undefined
dir
readonlydir:string
The folder of the YAML file that names the extension: the channel's folder, or the project
root for pathways.yaml. Resolve relative paths in options against it.
Inherited from
env
readonlyenv:Readonly<Record<string,string|undefined>>
Where to read credentials and other settings from: process.env when the engine runs.
Inherited from
logger
readonlylogger:ExtensionLogger
Inherited from
HttpSourceFacts
What an HTTP source knows about a message: the request's method, its path as the source matched
it, the sender's address, and the headers the channel's contextHeaders names, by lower-case
name. Authorization, Proxy-Authorization and Cookie are never among them.
Example
const requestId = (source: HttpSourceFacts): string | undefined => source.headers["x-request-id"];
Properties
headers
readonlyheaders:Readonly<Record<string,string>>
The listed headers the request carried, by lower-case name; several values joined by , .
kind
readonlykind:"http"
method
readonlymethod:string
POST or PUT.
path
readonlypath:string
The path the request was sent to, as the source matched it, without a query.
receivedAt
readonlyreceivedAt:string
When the message was stored, as an ISO 8601 time.
remoteAddress
readonlyremoteAddress:string|undefined
The sender's IP address, when the connection still had one.
KeyProvider
Where master keys live. Pathways encrypts each stored value with a fresh data key and asks the provider to wrap that key under its current master key; to read the value back, it asks the provider to unwrap it. A master key never has to leave the provider, so a cloud key service (AWS KMS, Azure Key Vault, Google Cloud KMS) can implement this interface by calling its own encrypt and decrypt. The built-in provider holds keys read from files or environment variables.
Rotation is the provider's to offer: it keeps unwrapping data keys under its older master keys while it wraps new ones under the current key.
Pathways overwrites a data key with zeros once it is done with it: wrapKey must not keep the
key it is given, and unwrapKey must return a buffer of its own.
Example
const kms: KeyProvider = {
currentKeyId: "kms-2026-11",
wrapKey: async (dataKey) => ({ keyId: "kms-2026-11", wrapped: await myKms.encrypt(dataKey) }),
unwrapKey: async (_keyId, wrapped) => myKms.decrypt(wrapped),
};
Properties
currentKeyId
readonlycurrentKeyId:string
The id of the master key that wraps new data keys.
keyIds?
readonlyoptionalkeyIds?: readonlystring[]
Every master key id the provider holds, the current one among them: a metadata search derives its index tags under each, so rows tagged under an older key are still found. Optional; without it, a search runs under the current key alone.
Methods
deriveIndexKey()?
optionalderiveIndexKey(keyId,info):Promise<Uint8Array<ArrayBufferLike>>
Derives a 32-byte key for one searchable index from the master key keyId, with HKDF-SHA-256
and info naming the index. Optional: a provider whose master keys never leave a key service
cannot derive one, and a channel with an index: block then refuses to start on it. The
returned buffer is the caller's, which overwrites it with zeros once it is done.
Parameters
keyId
string
info
Uint8Array
Returns
Promise<Uint8Array<ArrayBufferLike>>
unwrapKey()
unwrapKey(
keyId,wrapped):Promise<Uint8Array<ArrayBufferLike>>
Unwraps a data key that the master key keyId wrapped, returning its 32 bytes. Rejects with an
EncryptionError: ERR_PATHWAYS_UNKNOWN_KEY when it holds no key keyId,
ERR_PATHWAYS_INTEGRITY when the wrapped key fails its check.
Parameters
keyId
string
wrapped
Uint8Array
Returns
Promise<Uint8Array<ArrayBufferLike>>
wrapKey()
wrapKey(
dataKey):Promise<WrappedKey>
Wraps a 32-byte data key under the current master key.
Parameters
dataKey
Uint8Array
Returns
Promise<WrappedKey>
LeaseClaimant
Who asks for a lease: the node, an id each engine process draws at start, and its host name.
Example
const claimant: LeaseClaimant = { node: "3f9a1c2b", host: "engine-a" };
Properties
host
readonlyhost:string
node
readonlynode:string
LeaseHolder
A lease's last holder, by the database's clock: who took it, when, under which epoch, and how
long it has left. expiresInMs of 0 or less means it has lapsed or was released, and the next
acquisition takes it.
Example
const held = (holder: LeaseHolder): boolean => holder.expiresInMs > 0;
Properties
epoch
readonlyepoch:number
expiresInMs
readonlyexpiresInMs:number
host
readonlyhost:string
node
readonlynode:string
since
readonlysince:string
When it was taken, as an ISO 8601 time in UTC.
LifecycleEventBase
What every lifecycle event about a channel carries: its type, when it happened and the channel it happened on. An AuditEvent can concern no one channel, so it stands on its own. Events carry ids, counts, error names, codes and times, never message content and never an error's own text, so a hook can send them anywhere.
Example
const line = (event: LifecycleEventBase): string => `${event.at} ${event.channel} ${event.type}`;
Extended by
DeliveryEventBaseFilteredEventReceivedEventReceiveFailedEventSourceDownEventSourceUpEventTransformedEventTransformFailedEvent
Properties
at
readonlyat:string
When it happened, as an ISO 8601 time in UTC, such as 2026-11-07T12:00:00.000Z.
channel
readonlychannel:string
The channel's name.
type
readonlytype:"received"|"receive-failed"|"transformed"|"filtered"|"transform-failed"|"delivered"|"delivery-retry"|"delivery-redirected"|"delivery-rejected"|"delivery-exhausted"|"source-down"|"source-up"|"audit"
LifecycleHook
A lifecycle hook: told of each LifecycleEvent, one at a time and in order. Paging, webhooks and forwarding to a SIEM are hooks.
A hook cannot hold the engine up. Each call has a time limit (the hook entry's timeout, 5
seconds unless set): past it, signal aborts, the call is abandoned and logged, and the hook gets
the next event. A call that throws is logged by error name and code, and the next event goes
ahead. Events queue for a busy hook; past 1,000 waiting, new ones are dropped and logged. On stop,
the engine gives a hook up to its time limit to finish what is queued, then calls stop.
Hooks run in the engine's own thread: an endless synchronous loop stops everything, so a hook awaits its I/O and does little work itself.
Example
// A webhook, much as the built-in `webhook:` hook does it.
import type { HookFactory } from "@cosyte/pathways";
export default ((options, context) => ({
async onEvent(event, signal) {
const response = await fetch(options.url, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${context.env["OPS_WEBHOOK_TOKEN"] ?? ""}`,
},
body: JSON.stringify(event),
signal,
});
if (!response.ok) throw Object.assign(new Error("webhook refused"), { code: `HTTP_${response.status}` });
},
})) satisfies HookFactory<{ url: string }>;
Methods
onEvent()
onEvent(
event,signal):void|Promise<void>
Handles one event. Resolve when done; reject to have the failure logged. signal aborts when
the call runs past its time limit.
Parameters
event
signal
AbortSignal
Returns
void | Promise<void>
stop()?
optionalstop():void|Promise<void>
Releases what the hook keeps open, such as connections. Called once, after the last event.
Returns
void | Promise<void>
LogFields
One structured log line: an event name, then fields that are ids, counts, codes and error
names. Never message content, never an error's own text, never a credential.
Example
const fields: LogFields = { event: "inbox-read-failed", code: "EACCES" };
Indexable
[
field:string]:LogValue
Properties
event
readonlyevent:string
MessageQuery
Which messages MessageStore.listMessages returns: newest first, every filter optional.
Example
const query: MessageQuery = { channel: "adt", state: "failed", since: "2026-11-01T00:00:00.000Z", limit: 50 };
Properties
before?
readonlyoptionalbefore?:number
Only ids below this one: the next of the page before.
channel?
readonlyoptionalchannel?:string
ids?
readonlyoptionalids?: readonlynumber[]
Only these messages, by id.
limit
readonlylimit:number
since?
readonlyoptionalsince?:string
Received at or after this time (ISO 8601, UTC).
state?
readonlyoptionalstate?:"received"|"filtered"|"delivered"|"failed"|"cleared"|"dry-run"|"queued"|"retrying"
until?
readonlyoptionaluntil?:string
Received before this time (ISO 8601, UTC).
MessageSink
What a source hands each incoming message to, and tells when its connection drops and comes back.
Example
const id = await sink.receive(body); // durable once this resolves
await acknowledge(id);
socket.on("close", () => {
const wait = sink.sourceDown?.("ECONNRESET") ?? 1_000;
setTimeout(reconnect, wait);
});
socket.on("ready", () => sink.sourceUp?.());
Methods
receive()
receive(
body,options?):Promise<number>
Stores the message and resolves with its id once it is durable. Acknowledge the sender (move the file, send the ACK) only after this resolves: that ordering is what keeps a crash from losing a message. If it rejects, do not acknowledge; the sender should send again.
options is optional, so a source that passes only body keeps working unchanged.
Parameters
body
Uint8Array
options?
Returns
Promise<number>
sourceDown()?
optionalsourceDown(code):number
Says the source lost what it reads from (a broker, a remote server, a socket it dialled) and
cannot take messages in until it is back. Until sourceUp, the health endpoint shows
the channel's source down and answers 503, readiness fails, and the hooks are sent a
source-down event with name: "SourceDown". code is a short code such as ECONNRESET
(letters, digits and _.:-, at most 64 characters), never an error's text: anything else is
kept as UNSPECIFIED.
Returns how long to wait before trying to connect again, in milliseconds: 1 second, doubling with each report in a row, to 5 minutes. The engine's own sink always has this method; it is optional so a sink a test builds by hand need not.
Parameters
code
string
Returns
number
sourceUp()?
optionalsourceUp():void
Says a source that reported sourceDown is connected again: health shows it up, and
the hooks are sent a source-up event. Nothing happens when it was not down.
Returns
void
MessageStore
What the engine and the operator commands need from storage. Store implements it on
SQLite, PostgreSQL, MySQL or MariaDB, and SQL Server; a storage adapter for anything else
implements the same methods, and its module's StoreFactory returns it. A layer around a store
(encryption at rest) wraps them: every method that takes or returns a body or an error's text is
here, so a wrapping layer sees them all.
Every method is the engine's only path to that data, so an adapter keeps these promises: receive
is durable before it resolves; completeTransform writes all its deliveries and the message's new
status, or none of them; nextDelivery returns the lowest pending id for that destination, so
deliveries stay in order; a new delivery is due at once (nextAttemptAt 0); markSending is
durable before it resolves, nextDelivery returns the marker it wrote, and markDelivered,
scheduleRetry and failDelivery clear it in the same write as their outcome. For the operator
commands: requeue and clear change nothing unless the message still stands where the caller
found it, and write their audit entry in the same transaction as their change; expire deletes
only done messages, with their deliveries, and never an audit entry. A message whose content was
dropped (markDeliveredAndDropContent) is never replayed, resent or expired.
The audit log has two methods, append and read, and no way to change or remove an entry.
The lease methods are optional. A store with acquireLease, renewLease, releaseLease and
leaseHolder lets pathways start refuse a second engine and run a standby; each judges expiry
by one clock, the database's, in the same statement as its change. Given an epoch, the fenced
writes (completeTransform, failTransform, markSending, markDelivered, scheduleRetry and
failDelivery) change their row only while the engine lease still carries that epoch, in the
same statement, and reject with a LeaseLostError when they change nothing. A store
without the lease methods runs as before, with a warning at start, and cannot run a standby.
Example
class PoolStore implements MessageStore {
async receive(channel: string, body: Uint8Array, options?: StoreReceiveOptions) {
const id = await this.pool.insertMessage(channel, body, options?.failure);
return { id, duplicateOf: undefined };
}
// ...and the other methods, each against the same database.
}
Methods
acquireLease()?
optionalacquireLease(name,claimant,ttlMs):Promise<LeaseClaim>
Takes the lease name for ttlMs when it is free or has lapsed by the database's clock,
raising its epoch by one, in one statement; else answers with its holder. Optional.
Parameters
name
string
claimant
ttlMs
number
Returns
Promise<LeaseClaim>
appendAudit()
appendAudit(
actor,entries):Promise<AuditEntry[]>
Appends entries to the audit log, stamped with the time and actor.
Parameters
actor
string
entries
readonly AuditInput[]
Returns
Promise<AuditEntry[]>
auditEntries()
auditEntries(
query):Promise<Page<AuditEntry>>
A page of the audit log, newest first.
Parameters
query
Returns
Promise<Page<AuditEntry>>
channels()
channels():
Promise<string[]>
Every channel with a stored message, by name.
Returns
Promise<string[]>
channelStates()?
optionalchannelStates():Promise<Record<string,ChannelRunState>>
Every channel an operator paused or stopped, by name; a channel missing from it runs. Optional, with setChannelState: a store without them keeps a channel's state in the engine only, so a restart forgets it.
Returns
Promise<Record<string, ChannelRunState>>
clear()
clear(
messageId,actor):Promise<ClearResult>
Marks a failed message, and its deliveries that failed for good, cleared. Refuses when there
is nothing failed. The audit entry is written in the same transaction.
Parameters
messageId
number
actor
string
Returns
Promise<ClearResult>
close()
close():
Promise<void>
Returns
Promise<void>
completeTransform()
completeTransform(
messageId,outputs,destinations,epoch?):Promise<void>
Fenced by epoch when it is given: see the lease methods above.
Parameters
messageId
number
outputs
readonly Uint8Array<ArrayBufferLike>[]
destinations
number
epoch?
number
Returns
Promise<void>
deliveries()
deliveries(
messageId):Promise<StoredDelivery[]>
A message's deliveries, every generation, oldest first, without bodies.
Parameters
messageId
number
Returns
Promise<StoredDelivery[]>
deliveryCounts()
deliveryCounts(
channel):Promise<DeliveryCounts>
How many deliveries of a channel stand at each status; dry-run only when some ran dry.
Parameters
channel
string
Returns
Promise<DeliveryCounts>
expire()
expire(
channel,cutoff,dryRun):Promise<ExpireResult>
Deletes a channel's messages that are done (delivered, dry-run, filtered or cleared) and whose
last activity is before cutoff (ISO 8601), with their deliveries, leaving those whose content
was dropped. With dryRun, counts them only.
Parameters
channel
string
cutoff
string
dryRun
boolean
Returns
Promise<ExpireResult>
failDelivery()
failDelivery(
deliveryId,error,epoch?):Promise<void>
Fenced by epoch when it is given.
Parameters
deliveryId
number
error
string
epoch?
number
Returns
Promise<void>
failTransform()
failTransform(
messageId,error,epoch?):Promise<void>
Fenced by epoch when it is given.
Parameters
messageId
number
error
string
epoch?
number
Returns
Promise<void>
leaseHolder()?
optionalleaseHolder(name):Promise<LeaseHolder|undefined>
The lease's last holder, or undefined when it was never taken. Optional, with acquireLease.
Parameters
name
string
Returns
Promise<LeaseHolder | undefined>
listMessages()
listMessages(
query):Promise<Page<MessageSummary>>
A page of messages, newest first, without bodies or errors.
Parameters
query
Returns
Promise<Page<MessageSummary>>
listRetrying()
listRetrying(
channel,limit):Promise<RetryingDelivery[]>
A channel's retrying deliveries, oldest first, at most limit.
Parameters
channel
string
limit
number
Returns
Promise<RetryingDelivery[]>
markDelivered()
markDelivered(
deliveryId,epoch?):Promise<void>
Fenced by epoch when it is given.
Parameters
deliveryId
number
epoch?
number
Returns
Promise<void>
markDeliveredAndDropContent()?
optionalmarkDeliveredAndDropContent(deliveryId,epoch?):Promise<boolean>
markDelivered, and when that was the last delivery of the message's current generation
still to be delivered, also drops the bodies of the message and of all its deliveries in the
same transaction, keeping its metadata: ids, times, statuses, sizes, the content hash and the
possible-duplicate flag. Its ctx.meta and that metadata's index tags go with the bodies, since
they hold identifiers. Keeps every body while any delivery of the current generation has
failed or waits to be tried again. Resolves to whether it dropped them. Fenced by epoch when
it is given. Optional: a channel with storage.content: dropAfterDelivery needs it, and the
engine refuses to start such a channel on a store without it.
Parameters
deliveryId
number
epoch?
number
Returns
Promise<boolean>
markDryRun()?
optionalmarkDryRun(deliveryId,epoch?):Promise<void>
Marks a delivery dry-run: its output stays recorded, it was never sent, and its destination
moves on to the next. Clears the marker. Fenced by epoch when it is given. Optional: an engine
with a destination that runs dry refuses to start on a store without it.
Parameters
deliveryId
number
epoch?
number
Returns
Promise<void>
markSending()
markSending(
deliveryId,start,epoch?):Promise<void>
Writes the marker before a delivery is handed to its destination, in one write: the attempt and
its start, and the attempt found cut off when start.interruptedAttempt is set. A delivery
nextDelivery returns still marked was cut off, and its next send is flagged. Fenced by
epoch when it is given.
Parameters
deliveryId
number
start
epoch?
number
Returns
Promise<void>
message()
message(
id):Promise<StoredMessage|undefined>
One message, without its body, or undefined if there is none. Its error is raw: see there.
Parameters
id
number
Returns
Promise<StoredMessage | undefined>
messageBody()
messageBody(
id):Promise<Uint8Array<ArrayBufferLike> |undefined>
One message's body as it arrived, or undefined if there is none or it was dropped.
Parameters
id
number
Returns
Promise<Uint8Array<ArrayBufferLike> | undefined>
messageCounts()
messageCounts(
channel):Promise<Record<MessageStatus,number>>
How many messages of a channel stand at each status. The health endpoint reports these.
Parameters
channel
string
Returns
Promise<Record<MessageStatus, number>>
metaCandidates()?
optionalmetaCandidates(query):Promise<MetaCandidate[]>
Up to query.limit messages that carry the tags query names, newest first, each with its
metadata as stored, for a metadata search to confirm. Optional: a store without it cannot be
searched by metadata.
Parameters
query
Returns
Promise<MetaCandidate[]>
nextDelivery()
nextDelivery(
channel,destination,options?):Promise<PendingDelivery|undefined>
Parameters
channel
string
destination
number
options?
Returns
Promise<PendingDelivery | undefined>
nextToTransform()
nextToTransform(
channel):Promise<PendingMessage|undefined>
Parameters
channel
string
Returns
Promise<PendingMessage | undefined>
outputs()
outputs(
messageId):Promise<Uint8Array<ArrayBufferLike>[]>
The outputs of a message's current generation, in order; none when it has none or they were dropped.
Parameters
messageId
number
Returns
Promise<Uint8Array<ArrayBufferLike>[]>
receive()
receive(
channel,body,options?):Promise<ReceivedMessage>
Stores an incoming message, durably, before its source acknowledges it: as received, or as
failed when options.failure is set. Flags it as a possible duplicate when an earlier
message of the channel has the same options.duplicateKey. Given options.resumed, when a
message of the channel was already stored with the same options.idempotencyKey, stores
nothing and returns that message's id with alreadyStored: true. A store that ignores the key
still works, and a source that stops between storing and acknowledging then stores that
message twice.
Parameters
channel
string
body
Uint8Array
options?
Returns
Promise<ReceivedMessage>
releaseLease()?
optionalreleaseLease(name,epoch?):Promise<boolean>
Frees the lease name, so the next acquisition takes it at once: only while it carries epoch
when that is given, whoever holds it when not (pathways lease release --force). Returns
whether it freed it. Optional, with acquireLease.
Parameters
name
string
epoch?
number
Returns
Promise<boolean>
renewLease()?
optionalrenewLease(name,epoch,ttlMs):Promise<boolean>
Extends the lease name to ttlMs from now, by the database's clock, while it still carries
epoch and has not lapsed. Returns whether it did. Optional, with acquireLease.
Parameters
name
string
epoch
number
ttlMs
number
Returns
Promise<boolean>
requeue()
requeue(
messageId,request,actor):Promise<RequeueResult>
Records a replay or a resend: the message's new status, a new generation, and fresh deliveries at the back of each destination's queue. Refuses, changing nothing, when the message has moved since the caller read it or has deliveries still pending. The audit entry is written in the same transaction, whichever way it goes.
Parameters
messageId
number
request
actor
string
Returns
Promise<RequeueResult>
retryingDeliveries()
retryingDeliveries(
channel):Promise<number>
How many of a channel's pending deliveries have failed at least once and wait for a retry.
Parameters
channel
string
Returns
Promise<number>
scheduleRetry()
scheduleRetry(
deliveryId,attempts,nextAttemptAt,error,epoch?):Promise<void>
Fenced by epoch when it is given.
Parameters
deliveryId
number
attempts
number
nextAttemptAt
number
error
string
epoch?
number
Returns
Promise<void>
setChannelState()?
optionalsetChannelState(channel,state):Promise<void>
Sets a channel's state; running removes its row. Optional, with channelStates.
Parameters
channel
string
state
Returns
Promise<void>
MessageSummary
What storage holds about one message, without its body or its error: safe to list.
Example
const { items } = await store.listMessages({ state: "failed", limit: 20 });
const ids = items.map((message: MessageSummary) => message.id);
Extended by
Properties
channel
readonlychannel:string
contentDroppedAt?
readonlyoptionalcontentDroppedAt?:string
When the message's body and its deliveries' bodies were dropped, once its last delivery was
delivered (storage.content: dropAfterDelivery). Undefined while they are kept. Such a
message cannot be replayed or resent, and its content cannot be shown.
contentHash?
readonlyoptionalcontentHash?:string
The hash of the body as received, when one was stored with it: see StoreReceiveOptions.
deliveries
readonlydeliveries:DeliveryTally
The deliveries of the current generation, by status.
duplicateOf
readonlyduplicateOf:number|null
The latest earlier message with the same sender and control ID, when there is one.
generation
readonlygeneration:number
0 until the message is first replayed or resent; one more each time.
id
readonlyid:number
receivedAt
readonlyreceivedAt:string
size
readonlysize:number|null
The body's size in bytes, or null for a message stored before sizes were kept.
state
readonlystate:"received"|"filtered"|"delivered"|"failed"|"cleared"|"dry-run"|"queued"|"retrying"
The status and the current deliveries, as an operator sees them.
status
readonlystatus:MessageStatus
updatedAt
readonlyupdatedAt:string
When the message last changed: transformed, failed, replayed or cleared.
MetaCandidate
A message whose index tags match a search, and its metadata as stored, which the search decrypts to confirm the match: tags are truncated, so a row can carry a tag and hold another value.
Example
const candidate: MetaCandidate = { id: 12, meta: storedJson };
Properties
id
readonlyid:number
meta
readonlymeta:string
MetaCandidateQuery
Which messages MessageStore.metaCandidates returns: those of channel that carry, for
every condition, one of its tags, newest first, with the other filters of a listing.
Example
const query: MetaCandidateQuery = { channel: "adt", conditions: [condition], limit: 100 };
Properties
before?
readonlyoptionalbefore?:number
Only ids below this one.
channel
readonlychannel:string
conditions
readonlyconditions: readonlyMetaCondition[]
limit
readonlylimit:number
since?
readonlyoptionalsince?:string
Received at or after this time (ISO 8601, UTC).
state?
readonlyoptionalstate?:"received"|"filtered"|"delivered"|"failed"|"cleared"|"dry-run"|"queued"|"retrying"
until?
readonlyoptionaluntil?:string
Received before this time (ISO 8601, UTC).
MetaCondition
What a metadata search looks up for one condition: the index, and its tag under each master key id the provider holds. A row matches the condition when it carries any of them.
Example
const condition: MetaCondition = { index: "mrn", tags: [{ keyId: "prod-2026", tag: "a3f0" }] };
Properties
index
readonlyindex:string
tags
readonlytags: readonlyobject[]
MetaStore
What pathways rekey needs from storage to rewrite metadata: every message's metadata and tags,
and a way to replace them. Store implements it.
Example
for (const row of await store.storedMeta(0, 100)) await store.replaceMeta(row, next(row));
Methods
replaceMeta()
replaceMeta(
current,next):Promise<boolean>
Replaces a message's metadata and all its tags with next, in one transaction, only while the
metadata is still what current read. Returns whether it did.
Parameters
current
next
meta
string
tags
readonly MetaTag[]
Returns
Promise<boolean>
storedMeta()
storedMeta(
after,limit):Promise<StoredMeta[]>
Up to limit messages with metadata and an id above after, oldest first, as stored.
Parameters
after
number
limit
number
Returns
Promise<StoredMeta[]>
MetaTag
One index tag, as storage keeps it: the index (the metadata key it covers), the master key id it
was derived under, and the tag, the first bits bits of an HMAC-SHA-256 in lowercase hex.
Example
const tag: MetaTag = { index: "mrn", keyId: "prod-2026", tag: "a3f0" };
Properties
index
readonlyindex:string
keyId
readonlykeyId:string
tag
readonlytag:string
MllpSourceFacts
What an MLLP source knows about a message: the sender's address and, over mutual TLS, the common name in the subject of the certificate the sender presented.
Example
const peer = (source: MllpSourceFacts): string => source.tlsPeerSubject ?? "unknown";
Properties
kind
readonlykind:"mllp"
receivedAt
readonlyreceivedAt:string
When the message was stored, as an ISO 8601 time.
remoteAddress
readonlyremoteAddress:string|undefined
The sender's IP address, when the connection had one.
tlsPeerSubject
readonlytlsPeerSubject:string|undefined
The common name (CN) in the subject of the sender's TLS certificate, when it presented one.
ModuleSourceFacts
What a custom source knows about a message: whatever it passed as source to sink.receive.
Example
const queue = (source: ModuleSourceFacts): JsonValue | undefined => source.facts["queue"];
Properties
facts
readonlyfacts:Readonly<Record<string,JsonValue>>
What the source passed, or {} when it passed nothing.
kind
readonlykind:"module"
receivedAt
readonlyreceivedAt:string
When the message was stored, as an ISO 8601 time.
NextDeliveryOptions
What MessageStore.nextDelivery looks for. Without dueBy it returns the oldest pending
delivery, due or not, which keeps a destination in order.
Example
const due = await store.nextDelivery("adt", 0, { dueBy: Date.now() });
Properties
dueBy?
readonlyoptionaldueBy?:number
Only a delivery due by this time, in epoch milliseconds: the oldest whose nextAttemptAt is at
or before it, passing over any still waiting for a retry. The engine asks this for a destination
with retry.rotate: true. A store that ignores it keeps that destination in order.
OutgoingMessage
One output of one message, on its way to one destination.
Example
const name = (message: OutgoingMessage): string => `${message.messageId}-${message.seq}.hl7`;
Properties
body
readonlybody:Uint8Array
messageId
readonlymessageId:number
The id of the stored message this came from.
resend?
readonlyoptionalresend?:Resend
Set when this is a possible duplicate: an earlier attempt was cut off after the send, so the
receiver may have it already. A destination may tell the receiver so (an HTTP destination's
resendHeader); it sends the message either way. Undefined on every other send.
seq
readonlyseq:number
Which output of that message's transform this is, from 0.
signal?
readonlyoptionalsignal?:AbortSignal
Aborted when an operator halts the channel (pathways channel halt) while this send is in
flight. A destination that honours it gives up the send at once; the engine stops waiting
either way, records no outcome, and sends the message again after the channel resumes, flagged
as a possible duplicate when the send was marked.
Page
One page of results, newest first. next is the before that fetches the page after it.
Example
const first: Page<MessageSummary> = await store.listMessages({ limit: 50 });
const second = await store.listMessages({ limit: 50, before: first.next });
Type Parameters
T
T
Properties
items
readonlyitems: readonlyT[]
next
readonlynext:number|undefined
PendingDelivery
The oldest delivery still pending for one destination of one channel.
Example
const due = (delivery: PendingDelivery): boolean => delivery.nextAttemptAt <= Date.now();
Properties
attempts
readonlyattempts:number
body
readonlybody:Uint8Array
id
readonlyid:number
interruptedAttempt?
readonlyoptionalinterruptedAttempt?:number
The latest attempt found cut off, kept after it: see StoredDelivery.interruptedAttempt.
messageId
readonlymessageId:number
nextAttemptAt
readonlynextAttemptAt:number
Epoch milliseconds before which this delivery must not be tried again.
sending?
readonlyoptionalsending?:SendingMarker
The marker of an attempt whose outcome was never recorded: set means it was cut off after it was handed to the destination, which may have it. Undefined when there is none, and from a store that keeps no marker.
seq
readonlyseq:number
PendingMessage
A stored message waiting for its transform.
Example
const next: PendingMessage | undefined = await store.nextToTransform("adt");
Properties
body
readonlybody:Uint8Array
id
readonlyid:number
ReceivedEvent
A message was stored, before its source acknowledged it.
Example
if (event.type === "received" && event.duplicateOf !== undefined) flagDuplicate(event.messageId);
Extends
Properties
at
readonlyat:string
When it happened, as an ISO 8601 time in UTC, such as 2026-11-07T12:00:00.000Z.
Inherited from
bytes
readonlybytes:number
The message's size in bytes.
channel
readonlychannel:string
The channel's name.
Inherited from
duplicateOf
readonlyduplicateOf:number|undefined
The earlier message this one may repeat (same sender and control ID), when there is one.
failed
readonlyfailed:boolean
True when the source stored it as failed, such as an MLLP message with no usable header.
messageId
readonlymessageId:number
type
readonlytype:"received"
Overrides
ReceivedMessage
An incoming message, stored.
Example
const { id, duplicateOf } = await store.receive("adt", body);
Properties
alreadyStored?
readonlyoptionalalreadyStored?:boolean
True when a message of the channel was already stored with the same idempotency key: id is
that message's, and nothing new was stored.
duplicateOf
readonlyduplicateOf:number|undefined
The latest earlier message of the channel with the same duplicate key, when there is one.
id
readonlyid:number
ReceiveFailedEvent
A message could not be stored, so its source did not acknowledge it and its sender should send it again. Storage being unreachable shows up here first.
Example
if (event.type === "receive-failed") page(`storage refused a message on ${event.channel}`);
Extends
Properties
at
readonlyat:string
When it happened, as an ISO 8601 time in UTC, such as 2026-11-07T12:00:00.000Z.
Inherited from
channel
readonlychannel:string
The channel's name.
Inherited from
code
readonlycode:string|undefined
The error's code, when it has one, such as SQLITE_FULL.
name
readonlyname:string
The error's name, such as SqliteError.
type
readonlytype:"receive-failed"
Overrides
ReceiveOptions
What a source can tell the engine about a message it hands over. Every field is optional.
Example
await sink.receive(body, { duplicateKey: `${sender}|${controlId}` });
await sink.receive(body, { failure: "inbound: NO_MSH_SEGMENT" });
await sink.receive(body, { idempotencyKey: `queue|${deliveryTag}` });
await sink.receive(body, { idempotencyKey: `queue|${deliveryTag}`, resumed: true });
await sink.receive(body, { source: { queue: "inbound", deliveryTag } });
Properties
duplicateKey?
readonlyoptionalduplicateKey?:string
What identifies the message to its sender, such as the sender and its MSH-10 control ID. When an earlier message of the channel carried the same key, this one is stored and processed as usual, flagged as a possible duplicate: never dropped. The engine keeps a hash of the key, never the key itself.
failure?
readonlyoptionalfailure?:string
Stores the message as failed, with this reason, instead of handing it to the transform: for a message the source received but could not accept, such as one with no usable header. A short code, never message content.
idempotencyKey?
readonlyoptionalidempotencyKey?:string
What names this one message to the source, the same every time the source hands it in. It is
stored with the message, so that when the source hands the message in again with resumed,
after a stop between storing it and acknowledging it, it is stored once. The engine keeps a
hash of the key, never the key itself. A custom store may ignore it, and then stores such a
message twice.
resumed?
readonlyoptionalresumed?:boolean
True when the source hands in again, under the same idempotencyKey, a message that an earlier
run may have stored already: one it took in and never acknowledged. When a message of the
channel was stored with that key, nothing new is stored and that message's id comes back. Only
these messages are looked up, so a message handed in for the first time costs no lookup.
source?
readonlyoptionalsource?:Readonly<Record<string,JsonValue>>
What the source knows about the message, which the channel's transform reads as
ctx.source.facts: a map of plain JSON values (strings, finite numbers, booleans, null, lists
and maps). Anything else is refused with the code INVALID_SOURCE_FACTS before the message is
stored. The engine keeps a copy taken at the call, so the source may reuse or change its object
afterwards. It is never logged, sent to a hook or stored: the engine holds it in memory until the
message is transformed, so a message taken in before the engine restarts is transformed
without it.
RequeueRequest
What a replay or a resend asks storage to record. Storage checks the message still stands where the caller found it, and changes nothing otherwise.
Example
const request: RequeueRequest = { mode: "resend", expected: "transformed", outputs, destinations: 2 };
Properties
destinations
readonlydestinations:number
How many destinations the channel has now.
expected
readonlyexpected:MessageStatus
The status the caller read before transforming.
failure?
readonlyoptionalfailure?:string
What the replayed transform failed with, as stored; the message is marked failed.
mode
readonlymode:"replay"|"resend"
replay ran the transform again; resend queues the stored outputs again.
outputs
readonlyoutputs: readonlyUint8Array<ArrayBufferLike>[]
The outputs to queue, in order, for each of destinations. None, without a failure, means the
transform filtered the message out.
Resend
Set on a message that is being sent again after an attempt was cut off: the engine stopped after handing it to the destination and before it recorded the outcome, so the receiver may have it already. The message is the same; only the flag tells it apart.
Example
const header = (resend: Resend | undefined): string | undefined =>
resend === undefined ? undefined : String(resend.previousAttempts);
Properties
previousAttempts
readonlypreviousAttempts:number
How many attempts were made before this one, the one cut off among them.
ResolvedSecret
What a provider answers: the value, the version it is, and when it stops being good, if it does.
Example
const secret: ResolvedSecret = { value: "synthetic-token", version: "3", expiresAt: new Date("2026-12-01T00:00:00Z") };
Properties
expiresAt?
readonlyoptionalexpiresAt?:Date
When the value stops being good, for a lease or a short-lived credential.
identity?
readonlyoptionalidentity?:string
Who read it, when the provider can tell: the cloud principal its call ran as, such as a role's
ARN, a managed identity's id or a service account's email. Never secret. pathways secrets check prints it, when it is printable ASCII with no space, 1 to 256 characters.
value
readonlyvalue:string
The secret itself. Pathways never logs it, and never puts it in an error.
version
readonlyversion:string
The version resolved, as the provider names it; never secret.
RetryingDelivery
A pending delivery that has failed at least once and waits to be tried again.
Example
const late = (d: RetryingDelivery): boolean => d.nextAttemptAt > Date.now() + 60_000;
Properties
attempts
readonlyattempts:number
destination
readonlydestination:number
id
readonlyid:number
messageId
readonlymessageId:number
nextAttemptAt
readonlynextAttemptAt:number
Epoch milliseconds of the next try.
SecretProvider
Where secrets come from: an environment variable, a file, or a secret store such as a cloud
provider's secrets manager. pathways.yaml names each provider under secrets: by a module whose
default export builds it, and a { secret: { provider, name, field, version } } reference anywhere
a secret is read asks it for one.
resolve is called once per secret when the engine starts, with a signal that aborts when the
provider's time limit passes or the start is cancelled. It rejects when the secret cannot be had,
preferably with a SecretError. Pathways reports a rejection by the reference's file and key,
the secret's name and the error's name and code, never the error's text, so an error may say
anything; a value must still never be put in one.
Example
const store: SecretProvider = {
async resolve(ref, signal) {
const found = await myStore.get(ref.name, { version: ref.version, signal });
return { value: found.text, version: found.version };
},
};
Extended by
Methods
resolve()
resolve(
ref,signal):Promise<ResolvedSecret>
Resolves one secret.
Parameters
ref
signal
AbortSignal
Returns
Promise<ResolvedSecret>
stop()?
optionalstop():void|Promise<void>
Releases what the provider holds open. Called once start has resolved every secret.
Returns
void | Promise<void>
SecretRef
One secret a provider is asked for: its name in the provider, and optionally one field of it and a version. A provider that has no fields or versions refuses a request that names one.
Example
const ref: SecretRef = { name: "lab/api", field: "token" };
Properties
field?
readonlyoptionalfield?:string
One field of a structured secret, or of a value that is a JSON object.
name
readonlyname:string
The secret's name in the provider: a variable, a path, a key. Never secret itself.
version?
readonlyoptionalversion?:string
A version the provider keeps. Undefined asks for the current one.
SendingMarker
The marker written on a delivery before it is handed to its destination: which attempt it is and when it started. The outcome write clears it, so a delivery found still marked was cut off between the send and the record of it, and the receiver may have it.
Example
await store.markSending(delivery.id, { attempt: delivery.attempts + 1, at: Date.now() });
Extended by
Properties
at
readonlyat:number
When it started, in epoch milliseconds.
attempt
readonlyattempt:number
The attempt's number, from 1.
SendStart
What MessageStore.markSending writes: the marker, and, when the delivery was found marked, the number of the attempt that was cut off.
Example
const start: SendStart = { attempt: 2, at: Date.now(), interruptedAttempt: 1 };
Extends
Properties
at
readonlyat:number
When it started, in epoch milliseconds.
Inherited from
attempt
readonlyattempt:number
The attempt's number, from 1.
Inherited from
interruptedAttempt?
readonlyoptionalinterruptedAttempt?:number
The attempt found cut off, kept on the delivery so every later send of it is flagged.
SourceConnector
Brings messages in. A source that polls (a folder, an inbox) implements poll, which the engine
calls on each pass its schedule makes due (the channel's poll:, or the engine-wide
pollInterval). A source that listens (MLLP, HTTP) implements start and stop. A custom
source's module builds one with a SourceFactory.
Example
const source: SourceConnector = {
async poll(sink, signal) {
for (const body of await takeWaiting({ signal })) await sink.receive(body);
},
};
Methods
poll()?
optionalpoll(sink,signal?):Promise<void>
One pass: takes in what is waiting, each message through sink. The engine never starts a pass
while the last one's call has not returned. At the pass's deadline (poll: { deadline }, 5
minutes by default) the engine stops waiting: signal aborts, which a source may pass to its
client to give up a hung listing or query, and sink.receive refuses every later message with
the code POLL_DEADLINE, so the source keeps it for the next pass.
Parameters
sink
signal?
AbortSignal
Returns
Promise<void>
start()?
optionalstart(sink):Promise<void>
Parameters
sink
Returns
Promise<void>
stop()?
optionalstop():Promise<void>
Returns
Promise<void>
SourceDownEvent
A source could not start listening (its port is taken, say), or a running source said it lost its
connection (MessageSink.sourceDown; then name is SourceDown). A listener is tried again after
retryInMs; a source that lost its connection is told to wait that long. Each is reported again
on each failure in a row, counting up attempt.
Example
if (event.type === "source-down" && event.attempt === 1) page(`${event.channel} cannot listen`);
Extends
Properties
at
readonlyat:string
When it happened, as an ISO 8601 time in UTC, such as 2026-11-07T12:00:00.000Z.
Inherited from
attempt
readonlyattempt:number
How many tries have failed in a row, from 1.
channel
readonlychannel:string
The channel's name.
Inherited from
code
readonlycode:string|undefined
The error's code, such as EADDRINUSE, or the code the source passed, such as ECONNRESET.
name
readonlyname:string
The error's name, such as Error, or SourceDown when the source said it lost its connection.
retryAt
readonlyretryAt:string
When the next try is due, as an ISO 8601 time in UTC.
retryInMs
readonlyretryInMs:number
type
readonlytype:"source-down"
Overrides
SourceUpEvent
A source that was down is listening, or connected, again. A source that starts on the first try reports nothing.
Example
if (event.type === "source-up") resolveIncident(event.channel, "source");
Extends
Properties
at
readonlyat:string
When it happened, as an ISO 8601 time in UTC, such as 2026-11-07T12:00:00.000Z.
Inherited from
channel
readonlychannel:string
The channel's name.
Inherited from
type
readonlytype:"source-up"
Overrides
StoredContent
One row's content, exactly as storage holds it: a message's body and error, or a delivery's body and last error. With encryption on, these are the encrypted values.
Example
const row: StoredContent = { table: "messages", id: 1, body, error: null };
Properties
body
readonlybody:Uint8Array
error
readonlyerror:string|null
id
readonlyid:number
table
readonlytable:ContentTable
StoredDelivery
One delivery of a message, without its body.
Example
const waiting = (await store.deliveries(12)).filter((d: StoredDelivery) => d.status === "pending");
Properties
attempts
readonlyattempts:number
deliveredAt
readonlydeliveredAt:string|null
destination
readonlydestination:number
error
readonlyerror:string|null
The last failure, as stored: a stage and a code, such as delivery: ECONNREFUSED.
generation
readonlygeneration:number
id
readonlyid:number
interruptedAttempt?
readonlyoptionalinterruptedAttempt?:number
Set when an attempt of this delivery was cut off, its outcome never recorded, so the receiver may have it more than once: the number of the latest such attempt. Every later send of it is flagged as a possible duplicate.
nextAttemptAt
readonlynextAttemptAt:number
Epoch milliseconds before which it is not tried again; 0 when due at once.
seq
readonlyseq:number
Which output of the transform this is, from 0.
status
readonlystatus:DeliveryStatus
StoredMessage
What storage holds about one message, without its body.
Example
const message: StoredMessage | undefined = await store.message(12);
Extends
Properties
channel
readonlychannel:string
Inherited from
contentDroppedAt?
readonlyoptionalcontentDroppedAt?:string
When the message's body and its deliveries' bodies were dropped, once its last delivery was
delivered (storage.content: dropAfterDelivery). Undefined while they are kept. Such a
message cannot be replayed or resent, and its content cannot be shown.
Inherited from
MessageSummary.contentDroppedAt
contentHash?
readonlyoptionalcontentHash?:string
The hash of the body as received, when one was stored with it: see StoreReceiveOptions.
Inherited from
deliveries
readonlydeliveries:DeliveryTally
The deliveries of the current generation, by status.
Inherited from
duplicateOf
readonlyduplicateOf:number|null
The latest earlier message with the same sender and control ID, when there is one.
Inherited from
error
readonlyerror:string|null
Why the message failed, as stored. The part after the second ": " is the error's own text,
which can quote the message: never print it.
generation
readonlygeneration:number
0 until the message is first replayed or resent; one more each time.
Inherited from
id
readonlyid:number
Inherited from
receivedAt
readonlyreceivedAt:string
Inherited from
size
readonlysize:number|null
The body's size in bytes, or null for a message stored before sizes were kept.
Inherited from
state
readonlystate:"received"|"filtered"|"delivered"|"failed"|"cleared"|"dry-run"|"queued"|"retrying"
The status and the current deliveries, as an operator sees them.
Inherited from
status
readonlystatus:MessageStatus
Inherited from
updatedAt
readonlyupdatedAt:string
When the message last changed: transformed, failed, replayed or cleared.
Inherited from
StoredMeta
One message's metadata and its index tags, as stored: what pathways rekey reads to re-encrypt
the metadata and derive its tags again under the current key.
Example
const row: StoredMeta = { id: 12, channel: "adt", meta: storedJson, tags: [] };
Properties
channel
readonlychannel:string
id
readonlyid:number
meta
readonlymeta:string
tags
readonlytags: readonlyMetaTag[]
StoreReceiveOptions
What the engine tells storage about an incoming message. Every field is optional.
Example
await store.receive("adt", body, { duplicateKey: hashOfSenderAndControlId });
Properties
contentHash?
readonlyoptionalcontentHash?:string
A hash of the body as it arrived (the engine passes its SHA-256 in hex, for a channel that drops content after delivery), kept with the message's metadata after its body is dropped.
duplicateKey?
readonlyoptionalduplicateKey?:string
An opaque key for who sent the message and its control ID (the engine passes a hash). When an earlier message of the channel has the same key, this one is stored flagged as a possible duplicate of it.
failure?
readonlyoptionalfailure?:string
Stores the message as failed, with this reason, so it is kept but never transformed.
idempotencyKey?
readonlyoptionalidempotencyKey?:string
An opaque key that names this one message to its source (the engine passes a hash), stored
with the message. With resumed, when a message of the channel was already stored with the
same key, nothing is stored and that message's id comes back with alreadyStored: a source
that stored a message and then stopped before it could acknowledge it hands it in again on
restart, and it is stored once.
meta?
readonlyoptionalmeta?:Readonly<Record<string,string|number|boolean>>
The message's metadata. A store wrapper (encryption at rest) turns it into storedMeta and
metaTags before it reaches the store underneath; a store given it bare keeps it as JSON.
metaTags?
readonlyoptionalmetaTags?: readonlyMetaTag[]
The metadata's index tags, stored with the message in the same transaction. Set by a wrapper.
resumed?
readonlyoptionalresumed?:boolean
True when the message may have been stored already under idempotencyKey, by an earlier run:
only then is the key looked up.
size?
readonlyoptionalsize?:number
The body's size in bytes as it arrived; defaults to body.byteLength. A storage layer that
changes the body on its way in, such as encryption, passes the original size along.
storedMeta?
readonlyoptionalstoredMeta?:string
The metadata as the store keeps it: JSON, encrypted under encryption. Set by a wrapper.
TransformConfig
The configuration a transform reads as ctx.config: the config: block of pathways.yaml, with
the channel's own config: block over it key by key, and each { env: NAME } read from the
engine's environment when it starts. It is empty here: name your keys and their types by
declaration merging, once per project, and ctx.config is typed by them.
Example
declare module "@cosyte/pathways" {
interface TransformConfig {
readonly facility: string;
readonly labCodes: readonly string[];
}
}
Properties
facility?
readonlyoptionalfacility?:string
TransformContext
The second argument a transform receives: a frozen plain object, built for each message.
Nothing in it is logged, sent to a hook or shown on the health endpoint. A transform reads it; to change what is sent, it changes what it returns.
Example
import type { Transform } from "@cosyte/pathways";
export default ((msg, ctx) => {
if (ctx?.source?.kind === "mllp" && ctx.source.tlsPeerSubject !== "lab.example.test") return null;
return msg;
}) satisfies Transform;
Properties
attempt
readonlyattempt:number
Which run of the transform this is for the message: 1 the first time, and one more for each replay or resend before this one.
config
readonlyconfig:Readonly<TransformConfig>
The channel's configuration, resolved when the engine (or the command) started.
messageId
readonlymessageId:number
The stored message's id. 0 under pathways test, which stores nothing.
meta
readonlymeta:Readonly<Record<string,string|number|boolean>>
A per-message map of flat values. Always empty in this release: nothing sets it yet.
source
readonlysource:SourceFacts|undefined
What the source knew about the message when it took it in. Undefined when that is not known:
under pathways test, on a replay, and for a message taken in before the engine last started,
since the facts are kept in memory until the message is transformed and not stored; and for the
oldest of a channel's messages past the 4,096 whose facts the engine holds while they wait.
TransformedEvent
A message's transform returned one or more outputs, each now queued for every destination.
Example
if (event.type === "transformed") count("outputs", event.outputs);
Extends
Properties
at
readonlyat:string
When it happened, as an ISO 8601 time in UTC, such as 2026-11-07T12:00:00.000Z.
Inherited from
channel
readonlychannel:string
The channel's name.
Inherited from
messageId
readonlymessageId:number
outputs
readonlyoutputs:number
How many outputs the transform returned.
type
readonlytype:"transformed"
Overrides
TransformFailedEvent
A message's transform threw, timed out or ran out of memory, or its input or output could not be read. The message is marked failed and the channel moves on.
Example
if (event.type === "transform-failed") page(`${event.channel}: ${event.name} at ${event.where ?? "?"}`);
Extends
Properties
at
readonlyat:string
When it happened, as an ISO 8601 time in UTC, such as 2026-11-07T12:00:00.000Z.
Inherited from
channel
readonlychannel:string
The channel's name.
Inherited from
code
readonlycode:string|undefined
The error's code, when it has one, such as a parser's NO_MSH_SEGMENT.
kind
readonlykind:TransformFailureKind
The stage that failed.
messageId
readonlymessageId:number
name
readonlyname:string
The error's name, such as TypeError.
type
readonlytype:"transform-failed"
Overrides
where
readonlywhere:string|undefined
Where in the transform it was thrown, as file:line:column, when it was.
WrappedKey
A data key, wrapped by a master key.
Example
const { keyId, wrapped }: WrappedKey = await keys.wrapKey(dataKey);
Properties
keyId
readonlykeyId:string
The id of the master key that wrapped it, stored beside the value so the value can still be read after another key becomes current. Printable ASCII with no space, 1 to 255 characters.
wrapped
readonlywrapped:Uint8Array
The data key, encrypted by that master key, in the provider's own layout: 1 to 65,535 bytes.
Type Aliases
AckBuilder
AckBuilder = (
request) =>Uint8Array|Promise<Uint8Array>
Builds the ACK an MLLP source sends, as the bytes of an HL7 v2 message (the source adds the MLLP
framing). The default is defaultAckBuilder; a custom one is the seam for a source whose
sender wants a different ACK, named in the channel's YAML as
source: { mllp: { ..., ackBuilder: ./ack.ts } } and exported as the module's default. If it
throws, or runs past the channel's transform time limit, the source logs the error's name and
sends the default ACK instead. It runs in the engine's own thread, once per message.
Parameters
request
Returns
Uint8Array | Promise<Uint8Array>
Example
// channels/adt/ack.ts: the default ACK, with MSH-3 set to what this sender expects.
import { defaultAckBuilder, type AckBuilder } from "@cosyte/pathways";
import { parseHL7 } from "@cosyte/hl7";
export default ((request) => {
const ack = parseHL7(Buffer.from(defaultAckBuilder(request)));
ack.setField("MSH.3", "PATHWAYS");
return Buffer.from(ack.toString());
}) satisfies AckBuilder;
AckReason
AckReason =
"stored"|"store-failed"|"unreadable"
Why an MLLP source answers as it does: stored (the message is durable), store-failed (it
could not be stored, and onStoreFailure asked for an ACK), or unreadable (it was stored,
marked failed, because it has no usable header or arrived damaged).
Example
if (request.reason === "unreadable") console.warn(request.problem);
AuditAction
AuditAction = typeof
AUDIT_ACTIONS[number]
One of AUDIT_ACTIONS.
Example
const action: AuditAction = "content-view";
AuditDetail
AuditDetail =
Readonly<Record<string,string|number|boolean>>
An audit entry's particulars: counts, statuses, codes and hashes. Never content.
Example
const detail: AuditDetail = { status: "transformed", queued: 2 };
AuditEventAction
AuditEventAction =
"search"|"view"|"content-view"|"replay"|"resend"|"clear"|"retention"|"config"|"lease-release"|"migrate"|"channel-pause"|"channel-resume"|"channel-stop"|"channel-halt"|"channel-reload"|"verify"
Every action the audit log records, as an AuditEvent's action: search (messages
listed), view (one message's details, without content), content-view, replay, resend,
clear, retention (a run, with what it deleted), config (a configuration change seen when
the engine started), lease-release (the engine lease freed by force), migrate (schema steps
applied by pathways migrate), channel-pause, channel-resume, channel-stop,
channel-halt and channel-reload (a pathways channel command), and verify (a stored
message read by pathways verify). Kept equal to the audit log's own list by a test.
Example
const sensitive = (action: AuditEventAction): boolean => action === "content-view";
AuditListener
AuditListener = (
entry) =>void|Promise<void>
Told of each audit entry once it is stored. A lifecycle hook subscribes one to forward the audit log, to a file or a SIEM. It may be async; the action that wrote the entry waits for it to settle, and a listener that throws or rejects never undoes the action or the entry.
Parameters
entry
Returns
void | Promise<void>
Example
const forward: AuditListener = async (entry) => {
await siem.send({ at: entry.at, actor: entry.actor, action: entry.action });
};
AuditOutcome
AuditOutcome =
"ok"|"refused"|"not-found"
How an audited action ended: done, refused (its detail.reason says why), or no such message.
Example
const failed = (outcome: AuditOutcome): boolean => outcome !== "ok";
CcdaDocument
CcdaDocument =
Ccda.CcdaDocument
A C-CDA document as a ccda channel's transform receives it: @cosyte/ccda's CcdaDocument.
Pathways exports the type only; import the class from @cosyte/ccda to build one.
Example
import type { CcdaDocument, Transform } from "@cosyte/pathways";
export default ((document) => (document.getPatient() ? document : null)) satisfies Transform<CcdaDocument>;
ChannelRunState
ChannelRunState =
"running"|"paused"|"stopped"
What an operator set a channel to: running (the default), paused (its source takes nothing
in, while stored messages go on through the transform and delivery) or stopped (nothing of it
runs). Kept in storage, so a restart, and every engine on the same database, honours it.
Example
const state: ChannelRunState = "paused";
ChannelStateStore
ChannelStateStore =
Required<Pick<MessageStore,"channelStates"|"setChannelState">>
The channel state methods of MessageStore, both present.
Example
const states: ChannelStateStore | undefined = channelStateStore(store);
ClearResult
ClearResult = {
deliveries:number;done:true;entry:AuditEntry;messages:number; } | {done:false;entry:AuditEntry;reason:Extract<RefusalReason,"not-found"|"not-failed">; }
What a clear did, with the audit entry written in the same transaction.
Union Members
Type Literal
{ deliveries: number; done: true; entry: AuditEntry; messages: number; }
deliveries
readonlydeliveries:number
Its deliveries that had failed for good.
done
readonlydone:true
entry
readonlyentry:AuditEntry
messages
readonlymessages:number
1 when the message itself had failed, else 0.
Type Literal
{ done: false; entry: AuditEntry; reason: Extract<RefusalReason, "not-found" | "not-failed">; }
Example
const result: ClearResult = await store.clear(12, "noah");
if (result.done) console.log(result.messages, result.deliveries);
ContentTable
ContentTable =
"messages"|"deliveries"
Which table a StoredContent row is from.
Example
const table: ContentTable = "deliveries";
DeliveryCounts
DeliveryCounts =
Readonly<Record<Exclude<DeliveryStatus,"dry-run">,number>> &object
How many deliveries stand at each status. dry-run is there only when some delivery ran dry, so
the counts of a project that never runs a destination dry read as they did before dry runs.
Type Declaration
dry-run?
readonlyoptionaldry-run?:number
Example
const recorded = (counts: DeliveryCounts): number => counts.delivered + (counts["dry-run"] ?? 0);
DeliveryStatus
DeliveryStatus =
"pending"|"delivered"|"failed"|"cleared"|"dry-run"
Where one delivery of a message to one destination stands. cleared: it failed for good, and an
operator cleared it, or replayed its message. dry-run: its destination runs dry (dryRun: true,
or pathways start --dry-run), so its output was recorded and never sent.
Example
const open = (status: DeliveryStatus): boolean => status === "pending";
DeliveryTally
DeliveryTally =
DeliveryCounts&object
How many of one message's deliveries stand at each status. retrying counts the pending ones
that have failed at least once, so it is part of pending. dry-run is there only when one ran
dry.
Type Declaration
retrying
readonlyretrying:number
Example
const open = (tally: DeliveryTally): number => tally.pending + tally.failed;
DestinationFactory
DestinationFactory<
Options> = (options,context) =>DestinationConnector|Promise<DestinationConnector>
Builds a custom destination: a module's default export, named in a channel's YAML as
destinations: [{ module: ./my-destination.ts, options: {...} }]. Its deliver throws to have the
engine retry, or throws a DeliveryRejected when sending again cannot help.
Type Parameters
Options
Options = ExtensionOptions
Parameters
options
Options
context
Returns
DestinationConnector | Promise<DestinationConnector>
Example
// channels/inbox/my-destination.ts
import { DeliveryRejected, type DestinationFactory } from "@cosyte/pathways";
export default ((options) => ({
async deliver(message) {
const status = await putObject(options.bucket, `${message.messageId}-${message.seq}`, message.body);
if (status === 400) throw new DeliveryRejected("HTTP 400");
if (status !== 200) throw Object.assign(new Error("not stored"), { code: `HTTP_${status}` });
},
})) satisfies DestinationFactory<{ bucket: string }>;
EncryptionErrorCode
EncryptionErrorCode =
"ERR_PATHWAYS_NOT_ENCRYPTED"|"ERR_PATHWAYS_ENCRYPTED"|"ERR_PATHWAYS_MALFORMED"|"ERR_PATHWAYS_FORMAT_VERSION"|"ERR_PATHWAYS_UNKNOWN_KEY"|"ERR_PATHWAYS_INTEGRITY"|"ERR_PATHWAYS_KEY_PROVIDER"
Why an encrypted value could not be written or read:
ERR_PATHWAYS_NOT_ENCRYPTED: the value is not in the encrypted format: it was stored while encryption wasnone, or its first bytes were changed;ERR_PATHWAYS_ENCRYPTED: the value is encrypted, and encryption isnone;ERR_PATHWAYS_MALFORMED: the value starts like an encrypted one, but its layout is broken;ERR_PATHWAYS_FORMAT_VERSION: the value is in a format version this Pathways does not read;ERR_PATHWAYS_UNKNOWN_KEY: the master key the value names is not configured;ERR_PATHWAYS_INTEGRITY: the value, or its wrapped data key, failed its integrity check: it was changed or damaged, or the key configured under its id is not the key that wrote it;ERR_PATHWAYS_KEY_PROVIDER: a key provider returned a key id or a key that cannot be used.
Example
const code: EncryptionErrorCode = "ERR_PATHWAYS_INTEGRITY";
ExtensionOptions
ExtensionOptions =
Readonly<Record<string,unknown>>
The options: an extension's YAML block gives it, as parsed: plain YAML values, not checked by
Pathways. A factory names its own shape as the type parameter and should check what it reads.
Credentials never go in options; read them from context.env.
Example
const folder = typeof options["folder"] === "string" ? options["folder"] : "./in";
FakeSecretEntry
FakeSecretEntry =
string|FakeSecret| readonlyFakeSecret[]
What a fake holds for one name: a value, a version, or every version with the current one last.
FhirResource
FhirResource =
Fhir.FhirComplex
A FHIR R4 resource as a fhir channel's transform receives it: @cosyte/fhir's FhirComplex, an
immutable model that keeps every decimal exactly as written. Read it with @cosyte/fhir's
helpers (getProperty, resourceType, readSafety), and return it, a new model, or a string.
Example
import type { FhirResource, Transform } from "@cosyte/pathways";
export default ((resource) => resource) satisfies Transform<FhirResource>;
Format
Format = typeof
FORMATS[number]
A channel's message format, and so what its transform receives:
hl7v2: parsed with@cosyte/hl7(the default);fhir: FHIR R4 JSON, parsed with@cosyte/fhir;x12: an X12 interchange, parsed with@cosyte/x12;ccda: a C-CDA document, parsed with@cosyte/ccda;json: any JSON, asJSON.parsereads it;text: the body as a string, unparsed.
Example
const format: Format = "x12";
Hl7Message
Hl7Message =
Hl7.Hl7Message
An HL7 v2 message as an hl7v2 channel's transform receives it: @cosyte/hl7's Hl7Message.
Pathways exports the type only; import the class from @cosyte/hl7 to build one.
Example
import type { Hl7Message, Transform } from "@cosyte/pathways";
export default ((msg) => (msg.is("ORU^R01") ? msg : null)) satisfies Transform<Hl7Message>;
HookFactory
HookFactory<
Options> = (options,context) =>LifecycleHook|Promise<LifecycleHook>
Builds a lifecycle hook: a module's default export, named in pathways.yaml (engine-wide) or a
channel's YAML (that channel only) as hooks: [./hooks/pager.ts] or
hooks: [{ module: ./hooks/pager.ts, options: {...}, events: [...], timeout: 2s }].
Type Parameters
Options
Options = ExtensionOptions
Parameters
options
Options
context
Returns
LifecycleHook | Promise<LifecycleHook>
Example
// hooks/pager.ts
import type { HookFactory } from "@cosyte/pathways";
export default ((_options, context) => ({
async onEvent(event, signal) {
if (event.type !== "delivery-rejected" && event.type !== "source-down") return;
await page(context.env["PAGER_KEY"], `${event.channel}: ${event.type}`, signal);
},
})) satisfies HookFactory;
JsonValue
JsonValue =
string|number|boolean|null|JsonValue[] | {[key:string]:JsonValue; }
Any JSON value, as a json channel's transform receives it from JSON.parse.
Example
const value: JsonValue = { status: "ok", tries: [1, 2] };
KeyProviderFactory
KeyProviderFactory<
Options> = (options,context) =>KeyProvider|Promise<KeyProvider>
Builds a custom key provider for encryption at rest: a module's default export, named in
pathways.yaml as encryption: { provider: { module: ./kms.ts, options: {...} } }. It returns a
KeyProvider (or a promise of one) that wraps each data key under its current master key and
unwraps keys under its older ones, such as one over a cloud key service. It is built once, when
the engine (or pathways rekey) starts.
Type Parameters
Options
Options = ExtensionOptions
Parameters
options
Options
context
Returns
KeyProvider | Promise<KeyProvider>
Example
// kms.ts
import type { KeyProviderFactory } from "@cosyte/pathways";
export default (async (options, context) => {
const kms = await connectKms(options.region, context.env["KMS_CREDENTIALS"]);
return {
currentKeyId: options.keyId,
wrapKey: async (dataKey) => ({ keyId: options.keyId, wrapped: await kms.encrypt(options.keyId, dataKey) }),
unwrapKey: (keyId, wrapped) => kms.decrypt(keyId, wrapped),
};
}) satisfies KeyProviderFactory<{ region: string; keyId: string }>;
LeaseClaim
LeaseClaim = {
acquired:true;epoch:number; } | {acquired:false;holder:LeaseHolder; }
What MessageStore.acquireLease answers: the lease and its new epoch, or the holder that keeps it.
Example
const claim = await store.acquireLease(ENGINE_LEASE, claimant, 15_000);
if (!claim.acquired) console.error(claim.holder.node);
LeaseStore
LeaseStore =
Required<Pick<MessageStore,"acquireLease"|"renewLease"|"releaseLease"|"leaseHolder">>
The lease methods of MessageStore, all four present.
Example
const leases: LeaseStore | undefined = leaseStore(store);
LifecycleEvent
LifecycleEvent =
ReceivedEvent|ReceiveFailedEvent|TransformedEvent|FilteredEvent|TransformFailedEvent|DeliveredEvent|DeliveryRetryEvent|DeliveryRedirectedEvent|DeliveryRejectedEvent|DeliveryExhaustedEvent|SourceDownEvent|SourceUpEvent|AuditEvent
Any lifecycle event: switch on type to narrow it. Engine-wide hooks receive every channel's
events and the audit log's; a channel's own hooks receive that channel's events.
Example
function describe(event: LifecycleEvent): string {
switch (event.type) {
case "delivery-rejected":
return `delivery ${event.deliveryId} rejected (${event.reason})`;
case "source-down":
return `source down, try ${event.attempt}`;
default:
return event.type;
}
}
LifecycleEventType
LifecycleEventType = typeof
LIFECYCLE_EVENTS[number]
The name of one lifecycle event: one of LIFECYCLE_EVENTS.
Example
const paged: readonly LifecycleEventType[] = ["delivery-rejected", "transform-failed"];
LogValue
LogValue =
string|number|boolean|null|undefined
A value a log line may carry. Objects are left out on purpose: an error or a message object can carry content.
Example
const value: LogValue = 42;
MessageFor
MessageFor<
F> =MessageTypes[F]
The message a channel of format F hands its transform.
Type Parameters
F
F extends Format
Example
type Document = MessageFor<"ccda">; // CcdaDocument
MessageMeta
MessageMeta =
Readonly<Record<string,string|number|boolean>>
A message's metadata: flat values by key, as ctx.meta holds them. Stored encrypted with the
message; the keys a channel's index: block names are also stored as truncated index tags, so a
search can find them without decrypting every row.
Example
const meta: MessageMeta = { mrn: "SYN-000123", visit: 2 };
MessageState
MessageState = typeof
MESSAGE_STATES[number]
A message's state as an operator sees it: its status, and for a transformed one, its deliveries.
Example
const waiting: readonly MessageState[] = ["received", "queued", "retrying"];
MessageStatus
MessageStatus =
"received"|"transformed"|"filtered"|"failed"|"cleared"
Where a stored message stands. cleared: it failed, and an operator cleared it with
pathways clear.
Example
const done = (status: MessageStatus): boolean => status === "transformed" || status === "filtered";
MessageTypes
MessageTypes =
{ [F in Format]: FormatModel<F> }
The message each format hands a transform, by the format a channel declares, read from the
format registry: hl7v2 an Hl7Message, fhir a FhirResource, x12 an
X12Interchange, ccda a CcdaDocument, json a JsonValue (a string a
json transform returns is sent as it is, not JSON-quoted), and text the body as a string.
Example
type Claim = MessageTypes["x12"]; // X12Interchange
RefusalReason
RefusalReason =
"not-found"|"changed"|"pending"|"not-failed"|"content-dropped"
Why storage did not record a replay or a clear. content-dropped: the message's content was
dropped after delivery, so there is nothing to replay or resend.
Example
const retryable = (reason: RefusalReason): boolean => reason === "changed" || reason === "pending";
RequeueResult
RequeueResult = {
done:true;entry:AuditEntry;generation:number;queued:number;status:MessageStatus;superseded:number; } | {done:false;entry:AuditEntry;reason:Extract<RefusalReason,"not-found"|"changed"|"pending"|"content-dropped">; }
What a replay or a resend did, with the audit entry written in the same transaction.
Union Members
Type Literal
{ done: true; entry: AuditEntry; generation: number; queued: number; status: MessageStatus; superseded: number; }
done
readonlydone:true
entry
readonlyentry:AuditEntry
generation
readonlygeneration:number
queued
readonlyqueued:number
Deliveries queued, at the back of each destination's queue.
status
readonlystatus:MessageStatus
superseded
readonlysuperseded:number
Earlier deliveries that had failed for good, now cleared.
Type Literal
{ done: false; entry: AuditEntry; reason: Extract<RefusalReason, "not-found" | "changed" | "pending" | "content-dropped">; }
Example
const result: RequeueResult = await store.requeue(12, request, "noah");
if (!result.done) console.log(result.reason);
SecretErrorCode
SecretErrorCode =
"ERR_PATHWAYS_SECRET_NOT_FOUND"|"ERR_PATHWAYS_SECRET_FIELD"|"ERR_PATHWAYS_SECRET_VERSION"|"ERR_PATHWAYS_SECRET_UNREADABLE"
The codes a SecretError carries:
ERR_PATHWAYS_SECRET_NOT_FOUND: the provider has no secret by that name;ERR_PATHWAYS_SECRET_FIELD: the secret has no such field, or holds no fields;ERR_PATHWAYS_SECRET_VERSION: the provider has no such version, or keeps none;ERR_PATHWAYS_SECRET_UNREADABLE: the provider could not read it.
Example
const code: SecretErrorCode = "ERR_PATHWAYS_SECRET_NOT_FOUND";
SecretProviderFactory
SecretProviderFactory<
Options> = (options,context) =>SecretProvider|Promise<SecretProvider>
Builds a secret provider: a module's default export, named in pathways.yaml as
secrets: { vault: { module: ./vault.ts, options: {...} } }. It returns a SecretProvider (or a
promise of one) that { secret: { provider: vault, name } } references are resolved through. It
is built when the engine (or a command that opens the store) starts, asked for each secret, and
stopped. Building it gets the provider's timeout, as each request does: a factory still waiting
then fails the start. Its own credentials come from context.env, or from a file its options name.
Type Parameters
Options
Options = ExtensionOptions
Parameters
options
Options
context
Returns
SecretProvider | Promise<SecretProvider>
Example
// vault.ts
import type { SecretProviderFactory } from "@cosyte/pathways";
export default (async (options, context) => {
const client = await connectStore(options.address, context.env["STORE_ROLE_TOKEN"]);
return {
resolve: async (ref, signal) => {
const found = await client.read(ref.name, { field: ref.field, version: ref.version, signal });
return { value: found.value, version: found.version };
},
stop: () => client.close(),
};
}) satisfies SecretProviderFactory<{ address: string }>;
SourceAckCode
SourceAckCode =
"AA"|"AE"|"AR"
The ACK codes an MLLP source answers with.
Example
const positive = (code: SourceAckCode): boolean => code === "AA";
SourceFactory
SourceFactory<
Options> = (options,context) =>SourceConnector|Promise<SourceConnector>
Builds a custom source: a module's default export, named in a channel's YAML as
source: { module: ./my-source.ts, options: {...} }. It may return the connector or a promise of
it. The engine calls poll on every pass, or start once and stop at shutdown.
Type Parameters
Options
Options = ExtensionOptions
Parameters
options
Options
context
Returns
SourceConnector | Promise<SourceConnector>
Example
// channels/inbox/my-source.ts
import type { SourceFactory } from "@cosyte/pathways";
export default ((options, context) => ({
async poll(sink) {
for (const body of await fetchWaiting(options.queue, context.env["QUEUE_TOKEN"])) {
await sink.receive(body); // stored before it resolves: acknowledge only after
await acknowledge(body);
}
},
})) satisfies SourceFactory<{ queue: string }>;
SourceFacts
SourceFacts =
FileSourceFacts|HttpSourceFacts|MllpSourceFacts|ModuleSourceFacts
What the source knew about a message when it took it in, by the kind of source.
Example
const from = (source: SourceFacts): string | undefined =>
source.kind === "http" || source.kind === "mllp" ? source.remoteAddress : undefined;
StoreFactory
StoreFactory<
Options> = (options,context) =>MessageStore|Promise<MessageStore>
Builds a custom storage adapter: a module's default export, named in pathways.yaml as
storage: { module: ./my-store.ts, options: {...} }. It returns a MessageStore, ready to
use (its schema created or migrated), or a promise of one. The engine calls close at shutdown.
Type Parameters
Options
Options = ExtensionOptions
Parameters
options
Options
context
Returns
MessageStore | Promise<MessageStore>
Example
// stores/my-store.ts
import type { MessageStore, StoreFactory } from "@cosyte/pathways";
export default (async (options, context) => {
const pool = await connect(options.url, context.env["STORE_PASSWORD"]);
await migrate(pool);
return new PoolStore(pool) satisfies MessageStore;
}) satisfies StoreFactory<{ url: string }>;
Transform
Transform<
In,Out> = (message,ctx?) =>TransformOutput<Out> |Promise<TransformOutput<Out>>
A channel's transform: the default export of its transform.ts. It receives the parsed message
(an Hl7Message from @cosyte/hl7 unless you name another type) and returns what to send. It
may be async.
Its second argument, ctx, is a TransformContext: the message's id, which run this is,
what its source knew, and the channel's configuration. The engine always passes it; it is optional
in the type, so a transform that takes one argument is a Transform too, and a test can call one
with the message alone.
Out is what it returns, when that differs from what it receives: a transform from HL7 v2 to FHIR
is a Transform<Hl7Message, FhirResource>. A model from any of the four libraries is written by
its own library, and a string is sent as it is.
Type Parameters
In
In = Hl7Message
Out
Out = In
Parameters
message
In
ctx?
Returns
TransformOutput<Out> | Promise<TransformOutput<Out>>
Example
import type { Transform } from "@cosyte/pathways";
export default ((msg) => {
if (!msg.is("ADT^A01")) return null;
msg.setField("MSH.5", "LAB");
return msg;
}) satisfies Transform;
TransformFailureKind
TransformFailureKind =
"load"|"parse"|"transform"|"output"|"timeout"|"memory"|"crash"
The stage a message's transform failed in: load (the transform would not load), parse (the
message could not be read in the channel's format), transform (the transform threw), output
(what it returned could not be written), timeout, memory or crash (its worker died). Kept
equal to the worker's own list by a test.
Example
const yourFix = (kind: TransformFailureKind): boolean => kind === "transform" || kind === "output";
TransformFor
TransformFor<
F,To> =Transform<MessageFor<F>,MessageFor<To>>
A transform for a channel of format F, typed from the format name in channel.yaml. Name a
second format, To, when the transform returns another format's model.
Type Parameters
F
F extends Format
To
To extends Format = F
Example
import type { TransformFor } from "@cosyte/pathways";
// channel.yaml: format: x12
export default ((interchange) =>
interchange.groups.some((group) => group.gs.elements[1] === "HC") ? interchange : null
) satisfies TransformFor<"x12">;
TransformOutput
TransformOutput<
M> =M|string|Uint8Array| readonly (M|string|Uint8Array)[] |null|undefined
What a transform may return: a message, a string or bytes to send on, several of them, or null
(or nothing) to filter the message out. A string is sent as UTF-8; bytes (a Uint8Array, which a
Buffer is) are sent as they are, so a body in another charset keeps its bytes.
Type Parameters
M
M
Example
const filtered: TransformOutput<string> = null;
const bytes: TransformOutput<string> = Buffer.from("MSH|^~\\&|LAB", "latin1");
X12Interchange
X12Interchange =
X12.X12Interchange
An X12 interchange as an x12 channel's transform receives it: @cosyte/x12's
X12Interchange, whose typed readers (get835, get837Claims and the rest) take it from there.
Example
import type { Transform, X12Interchange } from "@cosyte/pathways";
export default ((interchange) => interchange) satisfies Transform<X12Interchange>;
Variables
ENGINE_LEASE
constENGINE_LEASE:"engine"="engine"
The name of the lease an engine takes on its database before it opens a source, so a second engine on the same database is refused. The fenced writes check this lease's epoch.
Example
const holder = await store.leaseHolder?.(ENGINE_LEASE);
LIFECYCLE_EVENTS
constLIFECYCLE_EVENTS: readonly ["received","receive-failed","transformed","filtered","transform-failed","delivered","delivery-retry","delivery-redirected","delivery-rejected","delivery-exhausted","source-down","source-up","audit"]
Every lifecycle event's type, in the order a message meets them. A hook entry's events: list in
the YAML names some of these to receive only those.
Example
LIFECYCLE_EVENTS.includes("delivered"); // true
VERSION
constVERSION:"0.0.2"="0.0.2"
The version of @cosyte/pathways, kept equal to package.json#version (a test fails when they
differ).
Example
import { VERSION } from "@cosyte/pathways";
console.log(VERSION);
Functions
defaultAckBuilder()
defaultAckBuilder(
request):Uint8Array
Builds an MLLP source's ACK with @cosyte/hl7's buildAck: sender and receiver swapped, MSA-2
echoing the inbound MSH-10, and, for a negative ACK, one ERR segment with an HL7 table 0357
condition code (207 when storing failed, 101 for a missing MSH-10, 100 otherwise). It never puts
message content in the ERR segment. An inbound header that declares an ISO 8859 character set in
MSH-18 is answered in Latin-1, so MSA-2 echoes the control ID byte for byte; anything else is
answered in UTF-8.
Parameters
request
Returns
Uint8Array
Example
const ack = defaultAckBuilder({
code: "AA",
reason: "stored",
problem: undefined,
header: parseHL7(mshSegment),
messageId: 1,
});
fakeSecretProvider()
fakeSecretProvider(
secrets?):FakeSecretProvider
A provider for tests, holding secrets by name. It keeps versions, takes field from a value that
is a JSON object as the built-in providers do, honours the abort signal, and rejects a missing
name, field or version with a SecretError.
Parameters
secrets?
Readonly<Record<string, FakeSecretEntry>> = {}
Returns
Example
// test/fake-vault.ts, named in a test project's pathways.yaml as secrets: { vault: { module: ./fake-vault.ts } }
export default () => fakeSecretProvider({ "lab/api": "synthetic-token-0123456789" });