diff --git a/src/keri/app/httping.py b/src/keri/app/httping.py index 76acfafe0..696864162 100644 --- a/src/keri/app/httping.py +++ b/src/keri/app/httping.py @@ -1,7 +1,8 @@ # -*- encoding: utf-8 -*- """ -keri.peer.httping module +keri.app.httping module +Provides utilities for sending and receiving KERI events over HTTP """ import datetime import json @@ -28,19 +29,39 @@ class SignatureValidationComponent(object): - """ Validate SKWA signatures """ + """Validates SKWA signatures on incoming requests. + + Verifies that each request carries a ``Signature`` header whose value is a + valid signature over the JSON-encoded request body, produced by the + controller identified by ``pre``. + + Attributes: + hby: Habery instance providing access to the local key state. + pre (str): qb64 identifier prefix of the expected signer. + """ def __init__(self, hby, pre): + """Initializes SignatureValidationComponent. + + Args: + hby: Habery instance used to look up key state for ``pre``. + pre (str): qb64 identifier prefix of the controller whose + signature must be present on every request. + """ self.hby = hby self.pre = pre def process_request(self, req, resp): - """ Process request to ensure has a valid signature from controller + """Validates the ``Signature`` header against the request body. - Parameters: - req: Http request object - resp: Http response object + Reads the ``Signature`` header and the JSON-encoded media body, + then delegates to :meth:`validate`. Sets the response status to + ``401 Unauthorized`` and marks the response complete if validation + fails, preventing further processing. + Args: + req (falcon.Request): Incoming HTTP request object. + resp (falcon.Response): Outgoing HTTP response object. """ sig = req.headers.get("SIGNATURE") ked = req.media @@ -51,6 +72,23 @@ def process_request(self, req, resp): return def validate(self, sig, ser): + """Verifies a raw signature string against serialized data. + + Parses the ``sig`` string into signage markers and checks each + indexed verfer in the current key state of ``self.pre`` against + the corresponding siger. + + Args: + sig (str): Raw signature header value, parseable by + :func:`~keri.end.designature`. + ser (bytes): Serialized data that was signed. + + Returns: + bool: ``True`` if all verfers successfully verify their + corresponding sigers; ``False`` if ``self.pre`` is absent + from the key state, a required index is missing, or any + signature fails verification. + """ signages = designature(sig) markers = signages[0].markers @@ -73,18 +111,37 @@ def validate(self, sig, ser): @dataclass class CesrRequest: + """Container for a parsed CESR HTTP request. + + Attributes: + payload (dict): Decoded JSON body of the request. + attachments (str): Value of the ``CESR-ATTACHMENT`` header. + """ payload: dict attachments: str def parseCesrHttpRequest(req): - """ - Parse Falcon HTTP request and create a CESR message from the body of the request and the two - CESR HTTP headers (Date, Attachment). - - Parameters - req (falcon.Request) http request object in CESR format: - + """Parses a Falcon HTTP request in CESR format into a :class:`CesrRequest`. + + Validates the ``Content-Type`` header, decodes the JSON body, and + extracts the required ``CESR-ATTACHMENT`` header. + + Args: + req (falcon.Request): Incoming HTTP request object. Must have + ``Content-Type: application/cesr`` and a valid JSON body. + + Returns: + CesrRequest: Dataclass holding the decoded payload and the raw + attachment header value. + + Raises: + falcon.HTTPError: With status ``406 Not Acceptable`` if the + content type is not ``application/cesr``. + falcon.HTTPError: With status ``400 Bad Request`` if the body + cannot be decoded as JSON. + falcon.HTTPError: With status ``412 Precondition Failed`` if the + ``CESR-ATTACHMENT`` header is absent. """ if req.content_type != CESR_CONTENT_TYPE: raise falcon.HTTPError(falcon.HTTP_NOT_ACCEPTABLE, @@ -113,15 +170,24 @@ def parseCesrHttpRequest(req): def createCESRRequest(msg, client, dest, path=None): - """ - Turns a KERI message into a CESR http request against the provided hio http Client - - Parameters - msg: KERI message parsable as Serder.raw - dest (str): qb64 identifier prefix of destination controller - client: hio http Client that will send the message as a CESR request - path (str): path to post to - + """Converts a single KERI message into a CESR HTTP POST request. + + Deserializes the leading event from ``msg`` using + :class:`~keri.core.SerderKERI`, strips it from the bytearray, treats + the remainder as the attachment, and issues a ``POST`` via ``client``. + + Args: + msg (bytearray): Raw KERI message stream. The leading event is + consumed; remaining bytes become the attachment. + client: hio HTTP ``Client`` instance used to send the request. + dest (str): qb64 identifier prefix of the destination controller, + written to the ``CESR-DESTINATION`` header. + path (str, optional): URL path to post to. Defaults to ``"/"``. + + Raises: + ExtractionError: If fewer bytes are available than the declared + event size (:class:`~keri.kering.ShortageError` is caught + internally and re-raised as :class:`~keri.kering.ExtractionError`). """ path = path if path is not None else "/" @@ -152,18 +218,35 @@ def createCESRRequest(msg, client, dest, path=None): def streamCESRRequests(client, ims, dest, path=None, headers=None): - """ - Turns a stream of KERI messages into CESR http requests against the provided hio http Client - - Parameters - client (Client): hio http Client that will send the message as a CESR request - ims (bytearray): stream of KERI messages parsable as Serder.raw - dest (str): qb64 identifier prefix of destination controller - path (str): path to post to - - Returns - int: Number of individual requests posted - + """Decomposes a KERI message stream into individual CESR HTTP POST requests. + + Iterates over ``ims``, extracting one :class:`~keri.core.Sadder` event + at a time followed by its attachment bytes (everything up to the next + ``0x7b`` / ``{`` byte). Each event-plus-attachment pair is dispatched + as a separate ``POST`` via ``client``. + + Args: + client: hio HTTP ``Client`` instance that will send each request. + ims (bytearray): Stream of concatenated KERI messages. Consumed + in place as events and attachments are extracted. + dest (str): qb64 identifier prefix of the destination controller, + written to the ``CESR-DESTINATION`` header of every request. + path (str, optional): URL path to post to. Defaults to ``"/"``. + Joined with ``client.requester.path`` using + :func:`urllib.parse.urljoin`. + headers (Hict, optional): Additional headers merged into each + request after the standard CESR headers. Defaults to an + empty :class:`~hio.help.Hict`. + + Returns: + int: Number of individual HTTP requests posted. + + Raises: + ColdStartError: If the stream begins with a counter triplet + (``txt`` or ``bny`` cold-start indicator) rather than a + message. + ExtractionError: If a message cannot be fully extracted due to + insufficient bytes. """ path = path if path is not None else "/" path = parse.urljoin(client.requester.path, path) @@ -212,14 +295,23 @@ def streamCESRRequests(client, ims, dest, path=None, headers=None): class Clienter(doing.DoDoer): - """ - Clienter is a DoDoer that manages hio HTTP clients using a ClientDoer for each HTTP request. - It executes HTTP requests using a HIO HTTP Client run by a ClientDoer. Once a request has - received a response then the corresponding Doer is removed from this Clienter. + """DoDoer that manages a pool of hio HTTP clients, one per outbound request. + + Each call to :meth:`request` creates a new :class:`~hio.core.http.clienting.Client` + and a corresponding :class:`~hio.core.http.clienting.ClientDoer`, both + tracked internally. A background coroutine (:meth:`clientDo`) periodically + removes clients whose responses have been pending longer than + :attr:`TimeoutClient` seconds. + + Attributes: + TimeoutClient (int): Class-level timeout in seconds before a client + with no response is pruned. Default is ``300`` (5 minutes). + clients (list[tuple]): Active client records as + ``(client, clientDoer, datetime)`` triples. Doers: - - clientDo: Periodically checks for stale clients and removes them if they have not received a response - within the specified timeout period. + clientDo: Background generator that periodically scans for and + removes timed-out clients. """ TimeoutClient = 300 # seconds to wait for response before removing client, default is 5 minutes @@ -238,17 +330,24 @@ def __init__(self): super(Clienter, self).__init__(doers=doers) def request(self, method, url, body=None, headers=None): - """ - Perform an HTTP request using a hio http Client and ClientDoer and returns the Client. + """Issues an HTTP request and registers the client in the managed pool. - Parameters: - method (str): HTTP method to use (e.g., "GET", "POST") - url (str): URL to send the request to - body (str or bytes, optional): Body of the request, defaults to None - headers (dict, optional): Headers to include in the request, defaults to None + Parses ``url``, constructs a :class:`~hio.core.http.clienting.Client`, + sends the request, wraps the client in a + :class:`~hio.core.http.clienting.ClientDoer`, and appends both to the + internal ``clients`` list alongside the current UTC timestamp. + + Args: + method (str): HTTP method (e.g., ``"GET"``, ``"POST"``). + url (str): Fully qualified URL including scheme, host, port, path, + and optional query string. + body (str or bytes, optional): Request body. ``str`` values are + encoded to UTF-8 before sending. Defaults to ``None``. + headers (dict, optional): Request headers. Defaults to ``None``. Returns: - http.clienting.Client: The hio HTTP Client used for the request, or None if an error occurs. + hio.core.http.clienting.Client: The client used to send the request, + or ``None`` if the connection could not be established. """ purl = parse.urlparse(url) @@ -279,11 +378,16 @@ def request(self, method, url, body=None, headers=None): return client def remove(self, client): - """ - Find a client tuple by hio HTTP Client and remove it and its Doer from the Clienter. + """Removes a client and its associated doer from the managed pool. - Parameters: - client (http.clienting.Client): The hio HTTP Client to remove from the Clienter. + Looks up the first entry in ``self.clients`` whose client object + matches ``client``, removes it from the list, and delegates doer + removal to the parent :class:`~hio.base.doing.DoDoer`. No-ops if + ``client`` is not found. + + Args: + client (hio.core.http.clienting.Client): The client instance to + remove. """ doers = [(c, d, dt) for (c, d, dt) in self.clients if c == client] if len(doers) == 0: @@ -295,15 +399,24 @@ def remove(self, client): super(Clienter, self).remove([doer]) def clientDo(self, tymth, tock=0.0, **kwa): - """ Periodically prune stale clients - - Process existing clients and prune any that have receieved a response longer than timeout - - Parameters: - tymth (function): injected function wrapper closure returned by .tymen() of - Tymist instance. Calling tymth() returns associated Tymist .tyme. - tock (float): injected initial tock value - + """Background coroutine that prunes timed-out clients. + + Runs continuously, yielding between iterations. On each pass, + collects clients that have received a response and whose elapsed + time since creation exceeds :attr:`TimeoutClient`, then removes + them via :meth:`remove`. + + Args: + tymth (callable): Injected closure returned by ``.tymen()`` on + the governing :class:`~hio.base.tyming.Tymist`. Calling + ``tymth()`` returns the current tyme. + tock (float): Initial tock value injected by the DoDoer + framework. Controls the yield cadence. + **kwa: Additional keyword arguments (unused). + + Yields: + float: ``self.tock`` on each iteration to cede control back to + the hio scheduler. """ self.wind(tymth) self.tock = tock diff --git a/src/keri/app/indirecting.py b/src/keri/app/indirecting.py index aacf32497..665edc773 100644 --- a/src/keri/app/indirecting.py +++ b/src/keri/app/indirecting.py @@ -41,9 +41,37 @@ def setupWitness(hby, alias="witness", mbx=None, aids=None, tcpPort=5631, httpPort=5632, keypath=None, certpath=None, cafilepath=None): - """ - Setup witness controller and doers + """Set up a witness controller and return its list of doers. + + Creates or retrieves the witness Hab, wires up a Verifier, Mailboxer, + ForwardHandler, Exchanger, HTTP and (optionally) TCP servers, and all + associated Doers required to run a witness node. + + Args: + hby (Habery): Habery instance that manages Hab creation and lookup. + alias (str): Name of the witness Hab. Created if it does not exist. + Defaults to ``"witness"``. + mbx (Mailboxer, optional): Mailbox storage instance. A new + ``Mailboxer`` is created when ``None``. Defaults to ``None``. + aids (list, optional): Allowlist of AIDs this witness will receipt. + ``None`` means no restriction. Defaults to ``None``. + tcpPort (int): Port for the TCP server. Pass ``None`` to disable TCP. + Defaults to ``5631``. + httpPort (int): Port for the HTTP server. Defaults to ``5632``. + keypath (str, optional): File path to the TLS private key. + Defaults to ``None``. + certpath (str, optional): File path to the TLS signed certificate. + Defaults to ``None``. + cafilepath (str, optional): File path to the TLS CA certificate chain. + Defaults to ``None``. + + Returns: + list: Doers that must be scheduled to operate the witness. + Raises: + RuntimeError: If the HTTP server cannot bind to ``httpPort``. + RuntimeError: If the TCP server cannot bind to ``tcpPort`` (when + ``tcpPort`` is not ``None``). """ host = "0.0.0.0" if platform.system() == "Windows": @@ -128,17 +156,22 @@ def setupWitness(hby, alias="witness", mbx=None, aids=None, tcpPort=5631, httpPo def createHttpServer(host, port, app, keypath=None, certpath=None, cafilepath=None): - """ - Create an HTTP or HTTPS server depending on whether TLS key material is present - Parameters: - host(str) : host to bind to for this server, or None for default of '0.0.0.0', all ifaces - port (int) : port to listen on for all HTTP(s) server instances - app (Any) : WSGI application instance to pass to the http.Server instance - keypath (string) : the file path to the TLS private key - certpath (string) : the file path to the TLS signed certificate (public key) - cafilepath (string): the file path to the TLS CA certificate chain file + """Create an HTTP or HTTPS server depending on whether TLS key material is present. + + Args: + host (str): Hostname or IP address to bind. Use ``"0.0.0.0"`` for all + interfaces. + port (int): Port to listen on. + app: WSGI application instance passed to the ``http.Server``. + keypath (str, optional): File path to the TLS private key. + Defaults to ``None``. + certpath (str, optional): File path to the TLS signed certificate + (public key). Defaults to ``None``. + cafilepath (str, optional): File path to the TLS CA certificate chain. + Defaults to ``None``. + Returns: - hio.core.http.Server + hio.core.http.Server: Configured HTTP or HTTPS server instance. """ if keypath is not None and certpath is not None and cafilepath is not None: servant = tcp.ServerTls(certify=False, @@ -153,11 +186,49 @@ def createHttpServer(host, port, app, keypath=None, certpath=None, cafilepath=No class WitnessStart(doing.DoDoer): - """ Doer to print witness prefix after initialization + """DoDoer that prints the witness prefix after Hab initialization and then + continuously processes incoming messages, escrows, and receipt cues. + This is an internal orchestration doer used by :func:`setupWitness`. It + composes four sub-doers: ``start``, ``msgDo``, ``escrowDo``, and + ``cueDo``. + + Attributes: + hab (Hab): Local witness Hab. + parser (Parser): CESR stream parser whose ``ims`` buffer receives + inbound bytes. + kvy (Kevery): KEL event processor. + tvy (Tevery): TEL event processor. + rvy (Revery): Reply-event router/processor. + exc (Exchanger): Exchange (``exn``) message handler. + cues (Deck): Inbound receipt cues from ``ReceiptEnd``. + replies (Deck): Inbound reply messages from the Respondant. + responses (Deck): Outbound response cues that are not ``stream``-kind. + queries (Deck): Outbound queue for ``stream``-kind cues routed to + HTTP query handlers. """ def __init__(self, hab, parser, kvy, tvy, rvy, exc, cues=None, replies=None, responses=None, queries=None, **opts): + """Initialize WitnessStart. + + Args: + hab (Hab): Local witness Hab. + parser (Parser): CESR stream parser. + kvy (Kevery): KEL event processor. + tvy (Tevery): TEL event processor. + rvy (Revery): Reply-event router/processor. + exc (Exchanger): Exchange (``exn``) message handler. + cues (Deck, optional): Inbound receipt cues from ``ReceiptEnd``. + A new ``Deck`` is created when ``None``. Defaults to ``None``. + replies (Deck, optional): Reply messages from the Respondant. + A new ``Deck`` is created when ``None``. Defaults to ``None``. + responses (Deck, optional): Non-stream outbound response cues. + A new ``Deck`` is created when ``None``. Defaults to ``None``. + queries (Deck, optional): Stream-kind cues forwarded to HTTP query + handlers. A new ``Deck`` is created when ``None``. + Defaults to ``None``. + **opts: Keyword arguments forwarded to ``doing.DoDoer.__init__``. + """ self.hab = hab self.parser = parser self.kvy = kvy @@ -173,13 +244,17 @@ def __init__(self, hab, parser, kvy, tvy, rvy, exc, cues=None, replies=None, res super().__init__(doers=doers, **opts) def start(self, tymth=None, tock=0.0, **kwa): - """ Prints witness name and prefix + """Doer generator that waits for Hab initialization and prints the + witness name and AID prefix to stdout. - Parameters: - tymth (function): injected function wrapper closure returned by .tymen() of - Tymist instance. Calling tymth() returns associated Tymist .tyme. - tock (float): injected initial tock value + Args: + tymth (callable, optional): Tymist tyme accessor closure injected + by the parent Doist or DoDoer. + tock (float): Initial tock value in seconds. Defaults to ``0.0``. + **kwa: Keyword arguments. + Yields: + float: Tock value to the scheduler. """ self.wind(tymth) self.tock = tock @@ -191,17 +266,21 @@ def start(self, tymth=None, tock=0.0, **kwa): print("Witness", self.hab.name, ":", self.hab.pre) def msgDo(self, tymth=None, tock=0.0, **kwa): - """ - Returns doifiable Doist compatibile generator method (doer dog) to process - incoming message stream of .kevery + """Doer generator that continuously processes the inbound CESR message + stream via ``self.parser``. + + Args: + tymth (callable, optional): Tymist tyme accessor closure injected + by the parent Doist or DoDoer. + tock (float): Initial tock value in seconds. Defaults to ``0.0``. + **kwa: Keyword arguments. - Parameters: - tymth (function): injected function wrapper closure returned by .tymen() of - Tymist instance. Calling tymth() returns associated Tymist .tyme. - tock (float): injected initial tock value + Yields: + float: Tock value to the scheduler. - Usage: - add result of doify on this method to doers list + Returns: + bool: Completion flag from ``parser.parsator``; only returned on + forced close. """ self.wind(tymth) self.tock = tock @@ -213,17 +292,17 @@ def msgDo(self, tymth=None, tock=0.0, **kwa): return done # should nover get here except forced close def escrowDo(self, tymth=None, tock=0.0, **kwa): - """ - Returns doifiable Doist compatibile generator method (doer dog) to process - .kevery and .tevery escrows. + """Doer generator that continuously drains the escrow queues of + ``self.kvy``, ``self.rvy``, ``self.tvy``, and ``self.exc``. - Parameters: - tymth (function): injected function wrapper closure returned by .tymen() of - Tymist instance. Calling tymth() returns associated Tymist .tyme. - tock (float): injected initial tock value + Args: + tymth (callable, optional): Tymist tyme accessor closure injected + by the parent Doist or DoDoer. + tock (float): Initial tock value in seconds. Defaults to ``0.0``. + **kwa: Keyword arguments. - Usage: - add result of doify on this method to doers list + Yields: + float: Tock value to the scheduler (``0.0`` to run each cycle). """ self.wind(tymth) self.tock = tock @@ -239,22 +318,18 @@ def escrowDo(self, tymth=None, tock=0.0, **kwa): yield def cueDo(self, tymth=None, tock=0.0, **kwa): - """ - Returns doifiable Doist compatibile generator method (doer dog) to process - .kevery.cues deque - - Doist Injected Attributes: - g.tock = tock # default tock attributes - g.done = None # default done state - g.opts - - Parameters: - tymth is injected function wrapper closure returned by .tymen() of - Tymist instance. Calling tymth() returns associated Tymist .tyme. - tock is injected initial tock value - - Usage: - add result of doify on this method to doers list + """Doer generator that routes cues from ``self.cues`` to either + ``self.queries`` (for ``stream``-kind cues) or ``self.responses`` + (for all other kinds). + + Args: + tymth (callable, optional): Tymist tyme accessor closure injected + by the parent Doist or DoDoer. + tock (float): Initial tock value in seconds. Defaults to ``0.0``. + **kwa: Keyword arguments. + + Yields: + float: Tock value to the scheduler. """ self.wind(tymth) self.tock = tock @@ -272,79 +347,37 @@ def cueDo(self, tymth=None, tock=0.0, **kwa): yield self.tock class Indirector(doing.DoDoer): - """ - Base class for Indirect Mode KERI Controller Doer with habitat and - TCP Clients for talking to witnesses - - Subclass of DoDoer with doers list from do generator methods: .msgDo, .cueDo, and .escrowDo. - - Enables continuous scheduling of doers (do generator instances or functions) - - Implements Doist like functionality to allow nested scheduling of doers. - Each DoDoer runs a list of doers like a Doist but using the tyme from its - injected tymist as injected by its parent DoDoer or Doist. + """DoDoer for an Indirect Mode KERI controller that communicates with + witnesses over a single TCP client connection. - Scheduling hierarchy: Doist->DoDoer...->DoDoer->Doers + Composes ``msgDo`` and ``escrowDo`` sub-doers, and optionally ``cueDo`` + when operating in direct mode. Part of the scheduling hierarchy: + Doist->DoDoer...->DoDoer->Doers Attributes: - .done is Boolean completion state: - True means completed - Otherwise incomplete. Incompletion maybe due to close or abort. - - .opts is dict of injected options for its generator .do - .doers is list of Doers or Doer like generator functions - - Attributes: - hab (Habitat: local controller's context - client (serving.Client): hio TCP client instance. - Assumes operated by another doer. - - Properties: - tyme (float): relative cycle time of associated Tymist, obtained - via injected .tymth function wrapper closure. - tymth (function): function wrapper closure returned by Tymist .tymeth() - method. When .tymth is called it returns associated Tymist .tyme. - .tymth provides injected dependency on Tymist tyme base. - tock (float): desired time in seconds between runs or until next run, - non negative, zero means run asap - - - Methods: - .wind injects ._tymth dependency from associated Tymist to get its .tyme - .__call__ makes instance callable - Appears as generator function that returns generator - .do is generator method that returns generator - .enter is enter context action method - .recur is recur context action method or generator method - .clean is clean context action method - .exit is exit context method - .close is close context method - .abort is abort context method - - - Hidden: - ._tymth is injected function wrapper closure returned by .tymen() of - associated Tymist instance that returns Tymist .tyme. when called. - - ._tock is hidden attribute for .tock property - + hab (Hab): Local controller Hab. + client (hio.core.tcp.Client): TCP client used for both sending and + receiving. + direct (bool): ``True`` when running in direct mode (receipt cues are + processed and sent back); ``False`` for indirect mode (receipt cues + are ignored). + kevery (Kevery): KEL event processor bound to ``client.rxbs``. + parser (Parser): CESR stream parser reading from ``client.rxbs``. """ def __init__(self, hab, client, direct=True, doers=None, **kwa): - """ - Initialize instance. - - Inherited Parameters: - tymist is Tymist instance - tock is float seconds initial value of .tock - doers is list of doers (do generator instances, functions or methods) - - Parameters: - hab is Habitat instance of local controller's context - client is TCP Client instance - direct is Boolean, True means direwct mode process cured receipts - False means indirect mode don't process cue'ed receipts - + """Initialize Indirector. + + Args: + hab (Hab): Local controller Hab. + client (hio.core.tcp.Client): TCP client for sending and receiving. + direct (bool): ``True`` enables direct mode, which processes + receipt cues and sends chits back to the remote. ``False`` + disables cue processing (indirect/cloned mode). + Defaults to ``True``. + doers (list, optional): Additional doers to include. Defaults to + ``None``. + **kwa: Keyword arguments forwarded to ``doing.DoDoer.__init__``. """ self.hab = hab self.client = client # use client for both rx and tx @@ -369,30 +402,30 @@ def __init__(self, hab, client, direct=True, doers=None, **kwa): self.client.wind(self.tymth) def wind(self, tymth): - """ - Inject new tymist.tymth as new ._tymth. Changes tymist.tyme base. - Updates winds .tymer .tymth + """Inject a new Tymist tyme accessor and propagate it to the TCP client. + + Args: + tymth (callable): Tymist tyme accessor closure. """ super(Indirector, self).wind(tymth) self.client.wind(tymth) def msgDo(self, tymth=None, tock=0.0, **kwa): - """ - Returns doifiable Doist compatibile generator method (doer dog) to process - incoming message stream of .kevery + """Doer generator that continuously processes the inbound CESR message + stream read from ``self.client.rxbs`` via ``self.parser``. - Doist Injected Attributes: - g.tock = tock # default tock attributes - g.done = None # default done state - g.opts + Args: + tymth (callable, optional): Tymist tyme accessor closure injected + by the parent Doist or DoDoer. + tock (float): Initial tock value in seconds. Defaults to ``0.0``. + **kwa: Keyword arguments - Parameters: - tymth is injected function wrapper closure returned by .tymen() of - Tymist instance. Calling tymth() returns associated Tymist .tyme. - tock is injected initial tock value + Yields: + float: Tock value to the scheduler. - Usage: - add result of doify on this method to doers list + Returns: + bool: Completion flag from ``parser.parsator``; only returned on + forced close. """ self.wind(tymth) self.tock = tock @@ -401,25 +434,23 @@ def msgDo(self, tymth=None, tock=0.0, **kwa): if self.parser.ims: logger.debug("Client %s received:\n%s\n...\n", self.hab.pre, self.parser.ims[:1024]) done = yield from self.parser.parsator(local=True) # process messages continuously - return done # should nover get here except forced close + return done # should never get here except forced close def cueDo(self, tymth=None, tock=0.0, **kwa): - """ - Returns doifiable Doist compatibile generator method (doer dog) to process - .kevery.cues deque + """Doer generator that processes ``self.kevery.cues`` one at a time, + sending the resulting chit or receipt message to the remote via + ``self.client``. - Doist Injected Attributes: - g.tock = tock # default tock attributes - g.done = None # default done state - g.opts + Only scheduled when ``self.direct`` is ``True``. - Parameters: - tymth is injected function wrapper closure returned by .tymen() of - Tymist instance. Calling tymth() returns associated Tymist .tyme. - tock is injected initial tock value + Args: + tymth (callable, optional): Tymist tyme accessor closure injected + by the parent Doist or DoDoer. + tock (float): Initial tock value in seconds. Defaults to ``0.0``. + **kwa: Keyword arguments - Usage: - add result of doify on this method to doers list + Yields: + float: Tock value to the scheduler. """ self.wind(tymth) self.tock = tock @@ -432,22 +463,17 @@ def cueDo(self, tymth=None, tock=0.0, **kwa): yield def escrowDo(self, tymth=None, tock=0.0, **kwa): - """ - Returns doifiable Doist compatibile generator method (doer dog) to process - .kevery escrows. + """Doer generator that continuously drains ``self.kevery``'s escrow + queue. - Doist Injected Attributes: - g.tock = tock # default tock attributes - g.done = None # default done state - g.opts + Args: + tymth (callable, optional): Tymist tyme accessor closure injected + by the parent Doist or DoDoer. + tock (float): Initial tock value in seconds. Defaults to ``0.0``. + **kwa: Keyword arguments - Parameters: - tymth is injected function wrapper closure returned by .tymen() of - Tymist instance. Calling tymth() returns associated Tymist .tyme. - tock is injected initial tock value - - Usage: - add result of doify on this method to doers list + Yields: + float: Tock value to the scheduler (``0.0`` to run each cycle). """ self.wind(tymth) self.tock = tock @@ -458,84 +484,84 @@ def escrowDo(self, tymth=None, tock=0.0, **kwa): yield def sendMessage(self, msg, label=""): - """ - Sends message msg and loggers label if any + """Transmit a message to the remote and log it. + + Args: + msg (bytes | bytearray): Serialized CESR message to send. + label (str): Human-readable label for log output. + Defaults to ``""``. """ self.client.tx(msg) # send to remote logger.debug("%s sent %s:\n%s\n\n", self.hab.pre, label, bytes(msg)) class MailboxDirector(doing.DoDoer): - """ - Class for Indirect Mode KERI Controller Doer with habitat and - TCP Clients for talking to witnesses - - Subclass of DoDoer with doers list from do generator methods: .msgDo, .cueDo, and .escrowDo. + """DoDoer that polls witness mailboxes for indirect-mode KERI controllers + and feeds the retrieved messages through a shared parser pipeline. - Enables continuous scheduling of doers (do generator instances or functions) - - Implements Doist like functionality to allow nested scheduling of doers. - Each DoDoer runs a list of doers like a Doist but using the tyme from its - injected tymist as injected by its parent DoDoer or Doist. - - Scheduling hierarchy: Doist->DoDoer...->DoDoer->Doers - - Attributes: - .done is Boolean completion state: - True means completed - Otherwise incomplete. Incompletion maybe due to close or abort. - - .opts is dict of injected options for its generator .do - .doers is list of Doers or Doer like generator functions + Manages a dynamic set of :class:`Poller` sub-doers, one per witness (or + mailbox endpoint) per controlled prefix. New Habs discovered in ``hby`` + are picked up automatically on each ``pollDo`` cycle. Part of the + scheduling hierarchy: Doist->DoDoer...->DoDoer->Doers Attributes: - hby (Habitat: local controller's context - - Properties: - hby (Habery): the Habery in which mailbox messages are routed - verifier (Verifier): TEL event acceptor and validator - exchanger (Exchanger): Exchange (exn) message delivery component - rep (Respondant): Respondant for reply messages - cues (Deck): Queue for new actions to schedule shared between the Revery, Kevery (and Kever), and Tevery (and Tever) - - - Methods: - .wind injects ._tymth dependency from associated Tymist to get its .tyme - .__call__ makes instance callable - Appears as generator function that returns generator - .do is generator method that returns generator - .enter is enter context action method - .recur is recur context action method or generator method - .clean is clean context action method - .exit is exit context method - .close is close context method - .abort is abort context method - - - Hidden: - ._tymth is injected function wrapper closure returned by .tymen() of - associated Tymist instance that returns Tymist .tyme. when called. - - ._tock is hidden attribute for .tock property - + hby (Habery): Habery whose Habs are monitored for new prefixes. + verifier (Verifier): TEL event acceptor and validator. May be ``None``. + exchanger (Exchanger): Exchange (``exn``) message handler. May be + ``None``. + rep (Respondant): Respondant for reply messages. May be ``None``. + topics (list[str]): Mailbox topic names to poll (e.g. + ``["/receipt", "/replay"]``). + pollers (list[Poller]): Active Poller sub-doers. + prefixes (OrderedSet): AID prefixes for which pollers have already + been created. + cues (Deck): Shared cue queue populated by Kevery, Kever, and Tevery. + witnesses (bool): When ``True``, also add pollers for each Hab's + declared witnesses in addition to explicit mailbox role endpoints. + ims (bytearray): Inbound message stream buffer fed by pollers and + consumed by ``self.parser``. + rtr (Router): Reply-event router shared across Kevery, Tevery, and + Revery. + rvy (Revery): Reply-event processor. + kvy (Kevery): KEL event processor. + tvy (Tevery | None): TEL event processor; ``None`` when no verifier is + provided. + parser (Parser): CESR stream parser consuming ``self.ims``. """ def __init__(self, hby, topics, ims=None, verifier=None, kvy=None, exc=None, rep=None, cues=None, rvy=None, tvy=None, witnesses=True, **kwa): - """ - Initialize instance. - - Inherited Parameters: - tymist is Tymist instance - tock is float seconds initial value of .tock - doers is list of doers (do generator instances, functions or methods) - - Parameters: - hab is Habitat instance of local controller's context - client is TCP Client instance - direct is Boolean, True means direwct mode process cured receipts - False means indirect mode don't process cue'ed receipts - + """Initialize MailboxDirector. + + Args: + hby (Habery): Habery instance whose Habs are polled for mailbox + messages. + topics (list[str]): Mailbox topic paths to subscribe to. + ims (bytearray, optional): Shared inbound message stream buffer. + A new ``bytearray`` is created when ``None``. + Defaults to ``None``. + verifier (Verifier, optional): TEL event verifier. When provided, + a ``Tevery`` is also created. Defaults to ``None``. + kvy (Kevery, optional): Pre-constructed KEL event processor. + A new ``Kevery`` is created when ``None``. + Defaults to ``None``. + exc (Exchanger, optional): Exchange (``exn``) message handler. + Defaults to ``None``. + rep (Respondant, optional): Respondant for reply messages. + Defaults to ``None``. + cues (Deck, optional): Shared cue queue. A new ``Deck`` is created + when ``None``. Defaults to ``None``. + rvy (Revery, optional): Pre-constructed reply-event processor. + A new ``Revery`` is created when ``None``. + Defaults to ``None``. + tvy (Tevery, optional): Pre-constructed TEL event processor. Only + used when ``verifier`` is also provided. A new ``Tevery`` is + created when ``None`` and ``verifier`` is set. + Defaults to ``None``. + witnesses (bool): When ``True``, add pollers for each Hab's + declared witnesses in addition to explicit mailbox role + endpoints. Defaults to ``True``. + **kwa: Keyword arguments forwarded to ``doing.DoDoer.__init__``. """ self.hby = hby self.verifier = verifier @@ -589,19 +615,28 @@ def __init__(self, hby, topics, ims=None, verifier=None, kvy=None, exc=None, rep super(MailboxDirector, self).__init__(doers=doers, **kwa) def wind(self, tymth): - """ - Inject new tymist.tymth as new ._tymth. Changes tymist.tyme base. - Updates winds .tymer .tymth + """Inject a new Tymist tyme accessor. + + Args: + tymth (callable): Tymist tyme accessor closure. """ super(MailboxDirector, self).wind(tymth) def pollDo(self, tymth=None, tock=0.0, **kwa): - """ - Returns: - doifiable Doist compatible generator method + """Doer generator that creates :class:`Poller` sub-doers for accepted + Habs and continuously appends new poller messages to ``self.ims``. - Usage: - add result of doify on this method to doers list + On startup, pollers are created for all already-accepted Habs. On each + subsequent cycle, newly discovered prefixes are checked and pollers are + added as needed. + + Args: + tymth (callable, optional): Tymist tyme accessor closure injected + by the parent Doist or DoDoer. + tock (float): Initial tock value in seconds. Defaults to ``0.0``. + + Yields: + float: Tock value to the scheduler. """ # enter context self.wind(tymth) @@ -629,11 +664,14 @@ def pollDo(self, tymth=None, tock=0.0, **kwa): _ = (yield self.tock) def addPollers(self, hab): - """ add mailbox pollers for every witness for this prefix identifier + """Create and register :class:`Poller` sub-doers for every mailbox + role endpoint and, when ``self.witnesses`` is ``True``, for every + declared witness of ``hab``. - Parameters: - hab (Hab): the Hab of the prefix + Marks ``hab.pre`` in ``self.prefixes`` so it is not processed again. + Args: + hab (Hab): The Hab whose mailbox endpoints and witnesses are polled. """ for (_, erole, eid), end in hab.db.ends.getTopItemIter(keys=(hab.pre, Roles.mailbox)): if end.allowed: @@ -651,17 +689,25 @@ def addPollers(self, hab): self.prefixes.add(hab.pre) def addPoller(self, hab, witness): + """Create and register a single :class:`Poller` sub-doer for a + specific witness. + + Args: + hab (Hab): The Hab whose mailbox is being polled. + witness (str): QB64 AID of the witness to poll. + """ poller = Poller(hab=hab, topics=self.topics, witness=witness) self.pollers.append(poller) self.extend([poller]) def processPollIter(self): - """ - Iterate through cues and yields one or more responses for each cue. + """Collect and yield all pending messages from every active poller. - Parameters: - cues is deque of cues + Drains each poller's ``msgs`` deque in order, then yields the + collected messages one at a time. + Yields: + bytes | bytearray: The next raw CESR message from a poller. """ mail = [] for poller in self.pollers: # get responses from all behaviors @@ -674,22 +720,20 @@ def processPollIter(self): yield msg def msgDo(self, tymth=None, tock=0.0, **kwa): - """ - Returns doifiable Doist compatibile generator method (doer dog) to process - incoming message stream of .kevery + """Doer generator that continuously processes the inbound CESR message + stream in ``self.ims`` via ``self.parser``. - Doist Injected Attributes: - g.tock = tock # default tock attributes - g.done = None # default done state - g.opts + Args: + tymth (callable, optional): Tymist tyme accessor closure injected + by the parent Doist or DoDoer. + tock (float): Initial tock value in seconds. Defaults to ``0.0``. - Parameters: - tymth is injected function wrapper closure returned by .tymen() of - Tymist instance. Calling tymth() returns associated Tymist .tyme. - tock is injected initial tock value + Yields: + float: Tock value to the scheduler. - Usage: - add result of doify on this method to doers list + Returns: + bool: Completion flag from ``parser.parsator``; only returned on + forced close. """ self.wind(tymth) self.tock = tock @@ -699,22 +743,17 @@ def msgDo(self, tymth=None, tock=0.0, **kwa): return done # should nover get here except forced close def escrowDo(self, tymth=None, tock=0.0, **kwa): - """ - Returns doifiable Doist compatibile generator method (doer dog) to process - .kevery escrows. + """Doer generator that continuously drains the escrow queues of + ``self.kvy``, ``self.rvy``, ``self.exchanger``, ``self.tvy``, and + ``self.verifier``. - Doist Injected Attributes: - g.tock = tock # default tock attributes - g.done = None # default done state - g.opts + Args: + tymth (callable, optional): Tymist tyme accessor closure injected + by the parent Doist or DoDoer. + tock (float): Initial tock value in seconds. Defaults to ``0.0``. - Parameters: - tymth is injected function wrapper closure returned by .tymen() of - Tymist instance. Calling tymth() returns associated Tymist .tyme. - tock is injected initial tock value - - Usage: - add result of doify on this method to doers list + Yields: + float: Tock value to the scheduler (``0.0`` to run each cycle). """ self.wind(tymth) self.tock = tock @@ -734,6 +773,14 @@ def escrowDo(self, tymth=None, tock=0.0, **kwa): @property def times(self): + """Aggregate the latest poll timestamps across all active pollers. + + Returns: + dict[str, datetime]: Mapping of topic name to the UTC datetime of + the most recent message received on that topic, merged from + all pollers. Later entries overwrite earlier ones for the same + topic. + """ times = dict() for poller in self.pollers: # get responses from all pollers times |= poller.times @@ -742,22 +789,38 @@ def times(self): class Poller(doing.DoDoer): - """ - Polls remote SSE endpoint for event that are KERI messages to be processed + """DoDoer that polls a single witness SSE mailbox endpoint for a given + prefix and appends received CESR messages to ``self.msgs``. + Uses :func:`~keri.app.agenting.httpClient` to open an HTTP connection, + sends a CESR ``qry`` request for ``mbx`` topics, and streams SSE events + in a 30-second window before reconnecting. + + Attributes: + hab (Hab): Local controller Hab. + pre (str): QB64 AID prefix being polled. + witness (str): QB64 AID of the witness being polled. + topics (list[str]): Mailbox topic paths to subscribe to. + retry (int): SSE retry interval in milliseconds. Updated from server + ``retry`` events. Defaults to ``1000``. + msgs (Deck): Output queue of raw encoded CESR messages received from + the witness. + times (dict[str, datetime]): Mapping of topic name to the UTC datetime + of the most recent message received on that topic. """ def __init__(self, hab, witness, topics, msgs=None, retry=1000, **kwa): - """ - Returns doist compatible doing.Doer that polls a witness for mailbox messages - as SSE events - - Parameters: - hab: - witness: - topics: - msgs: - + """Initialize Poller. + + Args: + hab (Hab): Local controller Hab used to build query messages. + witness (str): QB64 AID of the witness mailbox to poll. + topics (list[str]): Mailbox topic paths to subscribe to. + msgs (Deck, optional): Output message queue. A new ``Deck`` is + created when ``None``. Defaults to ``None``. + retry (int): Initial SSE retry interval in milliseconds. + Defaults to ``1000``. + **kwa: Keyword arguments forwarded to ``doing.DoDoer.__init__``. """ self.hab = hab self.pre = hab.pre @@ -772,12 +835,22 @@ def __init__(self, hab, witness, topics, msgs=None, retry=1000, **kwa): super(Poller, self).__init__(doers=doers, **kwa) def eventDo(self, tymth=None, tock=0.0, **kwa): - """ - Returns: - doifiable Doist compatible generator method - - Usage: - add result of doify on this method to doers list + """Doer generator that repeatedly opens an HTTP connection to the + witness, sends a CESR ``mbx`` query, and consumes SSE events for up + to 30 seconds before reconnecting. + + Received event data is appended to ``self.msgs`` as UTF-8 encoded + bytes. Topic offsets are persisted to ``hab.db.tops`` after each + event so polling resumes from the correct position on reconnect. + + Args: + tymth (callable, optional): Tymist tyme accessor closure injected + by the parent Doist or DoDoer. + tock (float): Initial tock value in seconds. Defaults to ``0.0``. + **kwa: Keyword arguments + + Yields: + float: Tock value to the scheduler. """ self.wind(tymth) self.tock = tock @@ -850,27 +923,36 @@ def eventDo(self, tymth=None, tock=0.0, **kwa): class HttpEnd: - """ - HTTP handler that accepts and KERI events POSTed as the body of a request with all attachments to - the message as a CESR attachment HTTP header. KEL Messages are processed and added to the database - of the provided Habitat. + """Falcon HTTP endpoint for receiving KERI CESR events and serving mailbox streams. + + This endpoint supports ingestion of KERI events via HTTP POST and raw byte + streams via PUT. It also handles mailbox query (``qry``) messages with route + ``mbx`` by returning a Server-Sent Events (SSE) stream. - This also handles `req`, `exn` and `tel` messages that respond with a KEL replay. + POST requests expect a JSON-encoded KERI event with CESR attachments + provided in the ``CESR-Attachment`` header. Parsed messages are appended + to a shared byte buffer for downstream processing. + + Attributes: + TimeoutQNF (int): Timeout in seconds for query-not-found conditions. + TimeoutMBX (int): Timeout in seconds for mailbox SSE streams. + rxbs (bytearray): Shared inbound byte buffer for serialized messages. + mbx (Mailboxer): Mailbox storage used for SSE replay streams. + qrycues (Deck): Queue of query cues used for mailbox streaming. """ TimeoutQNF = 30 TimeoutMBX = 5 def __init__(self, rxbs=None, mbx=None, qrycues=None): - """ - Create the KEL HTTP server from the Habitat with an optional Falcon App to - register the routes with. - - Parameters - rxbs (bytearray): output queue of bytes for message processing - mbx (Mailboxer): Mailbox storage - qrycues (Deck): inbound qry response queues - + """Initialize the KEL HTTP server. + + Args: + rxbs (bytearray, optional): Shared inbound byte buffer. If not + provided, a new ``bytearray`` is created. + mbx (Mailboxer, optional): Mailbox storage for serving SSE streams. + qrycues (Deck, optional): Queue for inbound query cues. If not + provided, a new ``Deck`` instance is created. """ self.rxbs = rxbs if rxbs is not None else bytearray() @@ -878,32 +960,19 @@ def __init__(self, rxbs=None, mbx=None, qrycues=None): self.qrycues = qrycues if qrycues is not None else decking.Deck() def on_post(self, req, rep): - """ - Handles POST for KERI event messages. + """Handle HTTP POST requests containing KERI CESR events. - Parameters: - req (Request) Falcon HTTP request - rep (Response) Falcon HTTP response + The request is parsed into a KERI event and associated CESR attachments. + The serialized result is appended to ``self.rxbs`` for processing. - .. code-block:: none + Response behavior depends on the message type: + - Mailbox query (``qry`` with route ``mbx``): returns an SSE stream + with HTTP 200 status. + - All other valid KERI message types: returns HTTP 204 (No Content). - --- - summary: Accept KERI events with attachment headers and parse - description: Accept KERI events with attachment headers and parse. - tags: - - Events - requestBody: - required: true - content: - application/json: - schema: - type: object - description: KERI event message - responses: - 200: - description: Mailbox query response for server sent events - 204: - description: KEL or EXN event accepted. + Args: + req (falcon.Request): Incoming HTTP request containing a KERI event. + rep (falcon.Response): HTTP response object to populate. """ if req.method == "OPTIONS": rep.status = falcon.HTTP_200 @@ -940,12 +1009,16 @@ def on_post(self, req, rep): rep.status = falcon.HTTP_204 def on_put(self, req, rep): - """ - Handles PUT for KERI mbx event messages. + """Handle HTTP PUT requests containing raw CESR byte streams. + + The entire request body is read as a byte stream and appended directly + to ``self.rxbs`` without parsing. + + Always responds with HTTP 204 (No Content). - Parameters: - req (Request) Falcon HTTP request - rep (Response) Falcon HTTP response + Args: + req (falcon.Request): Incoming HTTP request containing raw bytes. + rep (falcon.Response): HTTP response object to populate. .. code-block:: none @@ -981,8 +1054,38 @@ def on_put(self, req, rep): class QryRpyMailboxIterable: + """Synchronous iterator that waits for a matching ``stream`` cue from + ``self.cues`` and then delegates to a :class:`MailboxIterable`. + + Used as ``rep.stream`` in Falcon to serve SSE responses for ``qry`` + messages with route ``mbx``. On each call to ``__next__``, it inspects + ``self.cues`` for a cue whose ``serder.said`` matches ``self.said``. Once + matched, it creates a :class:`MailboxIterable` and yields from it for the + remainder of the request lifetime. Non-matching cues are put back into + the deck. + + Attributes: + mbx (Mailboxer): Mailbox storage passed to :class:`MailboxIterable`. + retry (int): SSE retry interval in milliseconds passed to + :class:`MailboxIterable`. Defaults to ``5000``. + cues (Deck): Queue of query cues produced by ``WitnessStart.cueDo``. + said (str): SAID of the ``qry`` event whose response stream is being + served. + iter (iterator | None): Active :class:`MailboxIterable` iterator once + a matching cue has been found; ``None`` beforehand. + """ def __init__(self, cues, mbx, said, retry=5000): + """Initialize QryRpyMailboxIterable. + + Args: + cues (Deck): Queue of ``stream``-kind query cues. + mbx (Mailboxer): Mailbox storage for event replay. + said (str): SAID of the originating ``qry`` event; used to match + the correct cue. + retry (int): SSE retry interval in milliseconds forwarded to + :class:`MailboxIterable`. Defaults to ``5000``. + """ self.mbx = mbx self.retry = retry self.cues = cues @@ -1011,19 +1114,78 @@ def __next__(self): class MailboxIterable: + """Iterator that streams Server-Sent Events (SSE) from a mailbox source. + + This iterator produces byte-encoded SSE frames for messages stored in a + :class:`~keri.app.storing.Mailboxer`. On the first iteration, a ``retry:`` + directive is emitted. Subsequent iterations poll the mailbox for new + messages across configured topics and yield formatted SSE event frames. + + Iteration continues until no new messages are emitted within a configured + timeout window, after which ``StopIteration`` is raised. + + Class Attributes: + TimeoutMBX (int): Maximum idle duration in microseconds (based on + ``time.perf_counter``) before iteration stops. Defaults to + ``30000000`` (~30 seconds). + + Attributes: + mbx (Mailboxer): Mailbox storage instance used to retrieve messages. + pre (str): QB64 AID prefix whose mailbox events are streamed. + topics (dict[str, int]): Mapping of topic names to the next sequence + number to retrieve. Updated in place as messages are consumed. + retry (int): SSE retry interval in milliseconds included in emitted + frames. + """ TimeoutMBX = 30000000 def __init__(self, mbx, pre, topics, retry=5000): + """Initialize the MailboxIterable. + + Args: + mbx (Mailboxer): Mailbox storage instance used for retrieving + messages. + pre (str): QB64 AID prefix identifying the mailbox. + topics (dict[str, int]): Mapping of topic names to starting + sequence numbers. This mapping is updated in place. + retry (int, optional): SSE retry interval in milliseconds. + Defaults to ``5000``. + """ self.mbx = mbx self.pre = pre self.topics = topics self.retry = retry def __iter__(self): + """Return the iterator instance and initialize timing state. + + Resets the internal start and end timestamps used to track inactivity + for timeout purposes. + + Returns: + MailboxIterable: The iterator instance. + """ self.start = self.end = time.perf_counter() return self def __next__(self): + """Return the next SSE-formatted message batch. + + On the first call after iteration begins, emits a ``retry:`` directive. + On subsequent calls, polls the mailbox for new messages across all + configured topics and returns them formatted as SSE frames. + + The iterator stops when the elapsed time since the last emitted message + exceeds :attr:`TimeoutMBX`. + + Returns: + bytearray: Byte-encoded SSE data containing zero or more event + frames. May be empty if no new messages are available. + + Raises: + StopIteration: If no messages are received within the timeout + window. + """ if self.end - self.start < self.TimeoutMBX: if self.start == self.end: self.end = time.perf_counter() @@ -1046,18 +1208,49 @@ def __next__(self): raise StopIteration - class ReceiptEnd(doing.DoDoer): - """ Endpoint class for Witnessing receipting functionality + """Falcon HTTP endpoint and DoDoer for witness receipt operations. + + Provides two HTTP handlers: + + ``POST /receipts`` — Accepts a KERI event, verifies that this witness is + listed in the event's witness set, issues a receipt, and returns it + inline (``200``) or defers with ``202`` when the event is still in escrow. - Most times a witness will be able to return its receipt for an event inband. This API - will provide that functionality. When an event needs to be escrowed, this POST API - will return a 202 and also provides a generic GET API for retrieving a receipt for any - event. + ``GET /receipts`` — Retrieves a previously issued receipt for an event + identified by ``pre`` plus either ``sn`` or ``said``. - """ + The ``interceptDo`` generator monitors ``self.inbound`` for ``receipt`` + cues and forwards them to ``self.outbound``, suppressing duplicates that + were already returned inline by the POST handler. + + Attributes: + hab (Hab): Local witness Hab used to issue receipts. + inbound (Deck): Cue queue populated by Kevery (shared with the + witness cue pipeline). + outbound (Deck): Cue queue consumed by ``WitnessStart.cueDo`` + (fed into the Respondant / reply pipeline). + aids (list | None): Allowlist of AIDs this endpoint will receipt. + ``None`` means no restriction. + receipts (set): SAIDs of events receipted inline by POST; used to + suppress duplicate outbound cues. + psr (Parser): Parser used to process the inbound event and the + issued receipt locally. + """ def __init__(self, hab, inbound=None, outbound=None, aids=None): + """Initialize ReceiptEnd. + + Args: + hab (Hab): Local witness Hab. + inbound (Deck, optional): Inbound cue queue from Kevery. A new + ``Deck`` is created when ``None``. Defaults to ``None``. + outbound (Deck, optional): Outbound cue queue consumed by the + witness cue pipeline. A new ``Deck`` is created when ``None``. + Defaults to ``None``. + aids (list, optional): Allowlist of AIDs to receipt. ``None`` + means no restriction. Defaults to ``None``. + """ self.hab = hab self.inbound = inbound if inbound is not None else decking.Deck() self.outbound = outbound if outbound is not None else decking.Deck() @@ -1070,12 +1263,20 @@ def __init__(self, hab, inbound=None, outbound=None, aids=None): super(ReceiptEnd, self).__init__(doers=[doing.doify(self.interceptDo)]) def on_post(self, req, rep): - """ Receipt POST endpoint handler + """Handle POST requests to issue a witness receipt for a KERI event. + + Parses the event, verifies this witness is in the event's witness set, + and returns the receipt inline. When the event is not yet in + ``self.hab.kevers`` (still in escrow), responds with ``202 Accepted``. - Parameters: - req (Request): Falcon HTTP request object - rep (Response): Falcon HTTP response object + Args: + req (falcon.Request): Incoming Falcon HTTP request. + rep (falcon.Response): Outgoing Falcon HTTP response. + Raises: + falcon.HTTPBadRequest: If ``pre`` is not in ``self.aids`` (when + the allowlist is set), the event type is not receipable, or + this witness is not listed in the event's witness set. """ if req.method == "OPTIONS": @@ -1120,12 +1321,28 @@ def on_post(self, req, rep): rep.status = falcon.HTTP_202 def on_get(self, req, rep): - """ Receipt GET endpoint handler + """Handle GET requests to retrieve a previously issued witness receipt. - Parameters: - req (Request): Falcon HTTP request object - rep (Response): Falcon HTTP response object + Looks up the event by ``pre`` and either ``sn`` or ``said``, assembles + the receipt with indexed witness signatures from ``hab.db.wigs``, and + returns it as CESR bytes. + Args: + req (falcon.Request): Incoming Falcon HTTP request. Expected query + parameters: + + - ``pre`` (str, required): AID prefix of the event. + - ``sn`` (int, optional): Sequence number of the event. + - ``said`` (str, optional): SAID of the event. Required when + ``sn`` is omitted. + + rep (falcon.Response): Outgoing Falcon HTTP response. + + Raises: + falcon.HTTPBadRequest: If ``pre`` is missing, both ``sn`` and + ``said`` are missing, or this witness is not in the event's + witness set. + falcon.HTTPNotFound: If the event cannot be found in the database. """ pre = req.get_param("pre") sn = req.get_param_as_int("sn") @@ -1169,12 +1386,19 @@ def on_get(self, req, rep): rep.data = rct def interceptDo(self, tymth=None, tock=0.0, **kwa): - """ - Returns doifiable Doist compatibile generator method (doer dog) to process - Kevery and Tevery cues deque + """Doer generator that monitors ``self.inbound`` for ``receipt`` cues + and forwards them to ``self.outbound``, suppressing duplicates for + events already receipted inline by :meth:`on_post`. + + Non-``receipt`` cues are forwarded to ``self.outbound`` unconditionally. - Usage: - add result of doify on this method to doers list + Args: + tymth (callable, optional): Tymist tyme accessor closure injected + by the parent Doist or DoDoer. + tock (float): Initial tock value in seconds. Defaults to ``0.0``. + + Yields: + float: Tock value to the scheduler. """ # enter context self.wind(tymth) @@ -1202,41 +1426,80 @@ def interceptDo(self, tymth=None, tock=0.0, **kwa): class QueryEnd: - """ Endpoint class for quering witness for KELs and TELs using HTTP GET - - """ - - def __init__(self, hab, reger): - self.hab = hab - self.reger = reger - - def on_get(self, req, rep): - """ Handles GET requests to query KEL or TEL events of a pre from a witness. + """Falcon HTTP endpoint for querying KEL and TEL events. - Parameters: - req (Request) Falcon HTTP request - rep (Response) Falcon HTTP response + This endpoint handles ``GET /query`` requests and streams events from the + local witness databases. Responses are returned as raw CESR-encoded bytes + with ``Content-Type: application/cesr``. - Query Parameters: - typ (string): The type of event data to query for. Accepted values are: - - 'kel': Retrieve KEL events for a specified 'pre'. - - 'tel': Retrieve TEL events based on 'reg' or 'vcid'. + Supported query types: - pre (string, optional): For 'kel' queries, the specific 'pre' to query. - sn (int, optional): For "kel" queries. If provided, returns events with seq-num >= sn. + - ``typ=kel``: Streams all KEL events for a given identifier prefix + (``pre``). Optionally filters events starting from a given sequence + number (``sn``). + - ``typ=tel``: Streams TEL events for a registry (``reg``), a credential + (``vcid``), or both. - reg (string, optional): For 'tel' queries, registry pre. Required if vcid not provided. - vcid (string, optional): For 'tel' queries, credential said. Required if reg not provided. + Attributes: + hab (Hab): Local witness habitat used to access the KEL database. + reger (Reger): Registry database interface used for TEL queries. + """ - Response: - - 200 OK: Returns event data in "application/cesr" format. - - 400 Bad Request: Returned if required query parameters are missing or if an invalid `typ` is specified. + def __init__(self, hab, reger): + """Initialize the QueryEnd endpoint. - Example: - - /query?typ=kel&pre=ELZ1KBCFOmdj1RPu6kMUnzgMBTl4YsHfpw7wIGvLgW5W - - /query?typ=kel&pre=ELZ1KBCFOmdj1RPu6kMUnzgMBTl4YsHfpw7wIGvLgW5W&sn=5 - - /query?typ=tel®=EHrbPfpRLU9wpFXTzGY-LIo2FjMiljjEnt238eWHb7yZ&vcid=EO5y0jMXS5XKTYBKjCUPmNKPr1FWcWhtKwB2Go2ozvr0 + Args: + hab (Hab): Local witness habitat. Its associated database is used + to retrieve KEL events, and is also passed to the registry + handler for TEL queries. + reger (Reger): Registry database interface used for TEL queries. + """ + self.hab = hab + self.reger = reger + def on_get(self, req, rep): + """Handle HTTP GET requests for querying events. + + Depending on the ``typ`` query parameter, this method retrieves and + streams either KEL or TEL events. + + Args: + req (falcon.Request): Incoming HTTP request. Expected query + parameters: + + - ``typ`` (str): Required. Specifies query type. Must be either + ``"kel"`` or ``"tel"``. + - ``pre`` (str): Required when ``typ="kel"``. Identifier prefix + whose KEL events are requested. + - ``sn`` (int): Optional when ``typ="kel"``. If provided, + returns events with sequence number greater than or equal + to this value. + - ``reg`` (str): Optional when ``typ="tel"``. Registry prefix + whose TEL events are requested. + - ``vcid`` (str): Optional when ``typ="tel"``. Credential SAID + whose TEL events are requested. + + rep (falcon.Response): Outgoing HTTP response. On success, the + response contains CESR-encoded event bytes with + ``Content-Type: application/cesr``. + + Raises: + falcon.HTTPBadRequest: If required query parameters are missing or + invalid for the selected query type. + + Notes: + - For ``typ="kel"``, the ``pre`` parameter is required. + - For ``typ="tel"``, at least one of ``reg`` or ``vcid`` must be + provided. + - If an invalid ``typ`` is provided, a 400 response is returned + with a JSON content type. + + Examples: + GET /query?typ=kel&pre= + + GET /query?typ=kel&pre=&sn=5 + + GET /query?typ=tel®=&vcid= """ typ = req.get_param("typ") diff --git a/src/keri/app/keeping.py b/src/keri/app/keeping.py index 01e30dc52..20b365efe 100644 --- a/src/keri/app/keeping.py +++ b/src/keri/app/keeping.py @@ -3,26 +3,50 @@ KERI keri.app.keeping module +Provides functionality for storing and retrieving cryptographic keys, events, +and certifiable data in a transactional store. + Terminology: - salt is 128 bit 16 char random bytes used as root entropy to derive seed or secret - private key same as seed or secret for key pair - seed or secret or private key is crypto suite length dependent random bytes - public key + salt: + 128-bit (16 character) random bytes used as root entropy to derive a + seed or secret. + + private key: + Same as seed or secret for a key pair. + + seed / secret: + Crypto suite length-dependent random bytes used to derive key material. -Example usage:: + public key: + Corresponding public key derived from the private key. - txn.put( +Example: + Store certifiable data:: + + .. code-block:: python + + txn.put( did.encode(), json.dumps(certifiable_data).encode("utf-8") ) - raw_data = txn.get(did.encode()) - if raw_data is None: - return None - return json.loads(raw_data) - ked = json.loads(raw[:size].decode("utf-8")) - raw = json.dumps(ked, separators=(",", ":"), ensure_ascii=False).encode("utf-8") + Retrieve certifiable data:: + + .. code-block:: python + raw_data = txn.get(did.encode()) + if raw_data is None: + return None + return json.loads(raw_data) + + Encode/decode key event data:: + + .. code-block:: python + + ked = json.loads(raw[:size].decode("utf-8")) + raw = json.dumps( + ked, separators=(",", ":"), ensure_ascii=False + ).encode("utf-8") """ import math from collections import namedtuple, deque @@ -46,18 +70,23 @@ @dataclass() class PubLot: - """ - Public key list with indexes and datetime created - Attributes: - pubs (list): list of fully qualified Base64 public keys. Defaults to empty. - ridx (int): rotation index of set of public keys at establishment event. - Includes of key set at inception event is 0. - kidx (int): key index of starting key in key set in sequence wrt to all - public keys. Example if each set has 3 keys then ridx 2 has - kidx of 2*3 = 6. - dt (str): datetime in ISO8601 format of when key set was first created + """A set of public keys with their position metadata and creation timestamp. + Represents one ordered list of public keys associated with a single + establishment event (inception or rotation), along with the indexes that + describe where that set sits in the overall key sequence. + Attributes: + pubs (list[str]): Fully qualified Base64 public keys. Defaults to + empty list. + ridx (int): Rotation index of the establishment event that uses this + public key set. The inception event has ``ridx == 0``. + kidx (int): Key index of the first key in this set within the overall + sequence of all public keys across all establishment events. For + example, if each establishment event has 3 keys, the set at + ``ridx == 2`` has ``kidx == 6``. + dt (str): ISO 8601 datetime string recording when this key set was + first created. """ pubs: list = field(default_factory=list) # list qb64 public keys. ridx: int = 0 # index of rotation (est event) that uses public key set @@ -70,8 +99,20 @@ def __iter__(self): @dataclass() class PreSit: - """ - Prefix's public key situation (sets of public kets) + """The current public key situation for an identifier prefix. + + Tracks the three consecutive public key lots (old, new, nxt) that + correspond to the previous, current, and next-rotation key sets for a + prefix. At any point in the rotation lifecycle: + + * ``old`` — the key set used before the most recent rotation. + * ``new`` — the key set currently active for signing. + * ``nxt`` — the pre-committed key set for the next rotation. + + Attributes: + old (PubLot): The previous (now-rotated-away) public key lot. + new (PubLot): The currently active public key lot. + nxt (PubLot): The pre-committed next public key lot. """ old: PubLot = field(default_factory=PubLot) # previous publot new: PubLot = field(default_factory=PubLot) # newly current publot @@ -83,8 +124,25 @@ def __iter__(self): @dataclass() class PrePrm: - """ - Prefix's parameters for creating new key pairs + """Key-creation parameters bound to an identifier prefix. + + Stores the algorithm and seed material needed to recreate or extend the + key-pair sequence for a given prefix. + + Attributes: + pidx (int): Prefix index that uniquely identifies this key-pair + sequence within the keeper. Defaults to ``0``. + algo (str): Key-creation algorithm code (e.g. ``Algos.salty`` or + ``Algos.randy``). Defaults to ``Algos.salty``. + salt (str): Fully qualified qb64 salt (or encrypted ciphertext of + the salt) used by the salty algorithm. Empty string when not + applicable. + stem (str): Unique path stem combined with the salt to derive + individual private keys. Empty string causes the salty creator + to fall back to using the hex-encoded ``pidx`` as the stem. + tier (str): Security tier that controls the hashing work factor + during key stretching. Empty string defers to the keeper's root + tier. """ pidx: int = 0 # prefix index for this keypair sequence algo: str = Algos.salty # salty default uses indices and salt to create new key pairs @@ -98,8 +156,15 @@ def __iter__(self): @dataclass() class PubSet: - """ - Prefix's public key set (list) at rotation index ridx + """An ordered list of public keys for a given prefix and rotation index. + + Used as the value type stored in the ``Keeper.pubs`` sub-database, keyed + by ``riKey(pre, ridx)``. Enables lookup of the full public key list for a + specific establishment event during replay. + + Attributes: + pubs (list[str]): Fully qualified qb64 public keys for a single + establishment event. Defaults to empty list. """ pubs: list = field(default_factory=list) # list qb64 public keys. @@ -108,10 +173,21 @@ def __iter__(self): def riKey(pre, ri): - """ - Returns bytes DB key from concatenation with '.' of qualified Base64 prefix - bytes pre and int ri (rotation index) of key rotation. - Inception has ri == 0 + """Return a byte-string database key composed of a prefix and a rotation index. + + Concatenates the identifier prefix and the integer rotation index ``ri`` + with a ``'.'`` separator. The rotation index is zero-padded to 32 hex + characters so that lexicographic ordering of keys matches numeric ordering + of rotation indexes. + + Args: + pre (str | bytes): Fully qualified Base64 identifier prefix. A + ``str`` is UTF-8 encoded to ``bytes`` automatically. + ri (int): Rotation index of the establishment event. Inception has + ``ri == 0``. + + Returns: + bytes: Byte-string key of the form ``b'
.'``.
     """
     if hasattr(pre, "encode"):
         pre = pre.encode("utf-8")  # convert str to bytes
@@ -119,111 +195,101 @@ def riKey(pre, ri):
 
 
 def openKS(name="test", **kwa):
-    """
-    Returns contextmanager generated by openLMDB but with Keeper instance as
-    KeyStore
-    default name="test"
-    default temp=True,
-
-    openLMDB Parameters:
-        cls is Class instance of subclass instance
-        name is str name of LMDBer dirPath so can have multiple databasers
-        at different directory path names thar each use different name
-
-        temp is Boolean, True means open in temporary directory, clear on close
-        Otherwise open in persistent directory, do not clear on close
+    """Return a context manager that opens a :class:`Keeper` key-store database.
+
+    Thin wrapper around :func:`openLMDB` that passes :class:`Keeper` as the
+    database class, so callers receive a temporary or persistent LMDB-backed
+    key store without having to reference :class:`Keeper` directly.
+
+    Args:
+        name (str): Directory path name component used to differentiate
+            multiple database instances. Defaults to ``"test"``.
+        **kwa: Additional keyword arguments forwarded to :func:`openLMDB`
+            (e.g. ``temp=True``).
+
+    Returns:
+        contextmanager: A context manager that yields an opened
+        :class:`Keeper` instance and closes it on exit.
     """
     return openLMDB(cls=Keeper, name=name, **kwa)
 
 
 class Keeper(LMDBer):
-    """
-    Keeper sets up named sub databases for key pair storage (KS).
-    Methods provide key pair creation, storage, and data signing.
-
-    Attributes:  (inherited)
-        name (str): unique path component used in directory or file path name
-        base (str): another unique path component inserted before name
-        temp (bool): True means use /tmp directory
-        headDirPath is head directory path
-        path is full directory path
-        perm is numeric os permissions for directory and/or file(s)
-        filed (bool): True means .path ends in file. Otherwise .path ends in directory.
-
-        mode (str): file open mode if filed
-        fext (str): file extension if filed
-        file (File)
-        opened is Boolean, True means directory created and if file then file is opened. False otherwise.
-
-        env (lmdb.env): LMDB main (super) database environment
-        readonly (bool): True means open LMDB env as readonly
+    """LMDB-backed key store for KERI key-pair management.
+
+    Extends :class:`LMDBer` with named sub-databases tailored for storing
+    cryptographic key pairs, encrypted secrets, prefix parameters, and public
+    key situation state.  All private key material may optionally be encrypted
+    at rest using an asymmetric encryption key derived from an authentication
+    and encryption identifier (``aeid``).
 
     Attributes:
-        gbls (Suber): named sub DB whose values are global parameters
-            for all prefixes
-            Key is parameter labels
-            Value is parameter::
-
-                parameters:
-                    aeid (bytes): fully qualified qb64 non-transferable identifier
-                        prefix for authentication via signing and asymmetric encryption
-                        of secrets using the associated (public, private) key pair.
-                        Secrets include both salts and private keys for all key sets
-                        in keeper. Defaults to empty which means no authentication
-                        or encryption of key sets.
-
-                    pidx (bytes): hex index of next prefix key-pair sequence to be incepted
-                    algo (str): default root algorithm for generating key pair
-                    salt (bytes): root salt for generating key pairs
-                    tier (bytes): default root security tier for root salt
-
-        pris (CryptSignerSuber): named sub DB whose keys are public key
-            from key pair and values are private keys from key pair
-            Key is public key (fully qualified qb64)
-            Value is private key (fully qualified qb64)
-
-        pres (CesrSuber): named sub DB whose values are prefixes or first
-            public keys
-            Key is first public key in key sequence for a prefix (fully qualified qb64)
-            Value is prefix or first public key (temporary) (fully qualified qb64
-
-        prms (Komer): named sub DB whose values are serialized dicts of
-            PrePrm instance
-            Key is identifier prefix (fully qualified qb64)
-            Value is  serialized parameter dict of public key parameters::
+        gbls (Suber): Named sub-database of global parameters shared across
+            all prefixes.  Keys are parameter labels (plain strings); values
+            are parameter values.  Recognized labels:
+
+            * ``"aeid"`` — fully qualified qb64 non-transferable identifier
+              prefix whose associated key pair is used to authenticate the
+              keeper and to asymmetrically encrypt secrets stored at rest.
+              An empty value means no authentication or encryption is applied.
+            * ``"pidx"`` — hex-encoded integer index of the next prefix
+              key-pair sequence to be incepted.
+            * ``"algo"`` — default root algorithm code for generating key
+              pairs.
+            * ``"salt"`` — root salt (plain or encrypted qb64) for generating
+              key pairs.
+            * ``"tier"`` — default root security tier for the root salt.
+
+        pris (CryptSignerSuber): Named sub-database mapping each public key
+            to its corresponding private key (signer).  Keys are fully
+            qualified qb64 public keys; values are :class:`Signer` instances,
+            stored encrypted when an ``aeid`` is configured.
+
+        prxs (CesrSuber): Named sub-database of encrypted proxy ciphers,
+            keyed by public key.  Values are :class:`Cipher` instances.
+
+        nxts (CesrSuber): Named sub-database of encrypted next-key ciphers,
+            keyed by public key.  Values are :class:`Cipher` instances.
+
+        smids (CatCesrIoSetSuber): Named sub-database of signing member
+            identifier sets, storing ``(Prefixer, Number)`` pairs as ordered
+            duplicate sets.
+
+        rmids (CatCesrIoSetSuber): Named sub-database of rotation member
+            identifier sets, storing ``(Prefixer, Number)`` pairs as ordered
+            duplicate sets.
+
+        pres (CesrSuber): Named sub-database mapping the first public key of
+            a key sequence (used as a temporary prefix) to the canonical
+            identifier prefix once it is known.  Values are
+            :class:`Prefixer` instances.
+
+        prms (Komer): Named sub-database of key-creation parameters per
+            prefix.  Keys are identifier prefixes (qb64); values are
+            :class:`PrePrm` dataclass instances serialized as dicts::
 
                 {
-                    pidx: ,
-                    algo: ,
-                    salt: ,
-                    stem: ,
-                    tier: ,
+                    "pidx": ,
+                    "algo": ,
+                    "salt": ,
+                    "stem": ,
+                    "tier": ,
                 }
 
-        sits (Komer): named sub DB whose values are serialized dicts of
-            PreSit instance
-            Key is identifier prefix (fully qualified qb64)
-            Value is  serialized parameter dict of public key situation::
+        sits (Komer): Named sub-database of public key situation state per
+            prefix.  Keys are identifier prefixes (qb64); values are
+            :class:`PreSit` dataclass instances serialized as dicts::
 
                 {
-                  old: { pubs: ridx, kidx,  dt },
-                  new: { pubs: ridx, kidx, dt },
-                  nxt: { pubs: ridx, kidx, dt }
+                    "old": {"pubs": [...], "ridx": , "kidx": , "dt": },
+                    "new": {"pubs": [...], "ridx": , "kidx": , "dt": },
+                    "nxt": {"pubs": [...], "ridx": , "kidx": , "dt": },
                 }
 
-        .pubs (Komer): named sub DB whose values are serialized lists of
-            public keys
-            Enables lookup of public keys from prefix and ridx for replay of
-            public keys by prefix in establishment event order.
-            Key is prefix.ridx (rotation index as 32 char hex string)
-            use riKey(pre, ri)
-
-            Value is serialized list of fully qualified public keys that are the
-            current signing keys after the rotation given by rotation index
-
-    Properties:
-
-
+        pubs (Komer): Named sub-database of public key sets indexed by prefix
+            and rotation index.  Keys are byte-string keys produced by
+            :func:`riKey`; values are :class:`PubSet` dataclass instances.
+            Enables ordered replay of all establishment events for a prefix.
     """
     TailDirPath = "keri/ks"
     AltTailDirPath = ".keri/ks"
@@ -231,34 +297,23 @@ class Keeper(LMDBer):
     MaxNamedDBs = 10
 
     def __init__(self, headDirPath=None, perm=None, reopen=False, **kwa):
-        """
-        Setup named sub databases.
-
-        Inherited Parameters:
-            name is str directory path name differentiator for main database.
-            When system employs more than one keri database, name allows
-            differentiating each instance by name. Default name='main'.
-            temp is boolean, assign to .temp.
-            True then open in temporary directory, clear on close.
-            Otherwise then open persistent directory, do not clear on close.
-            Default temp=False.
-            headDirPath is optional str head directory pathname for main database.
-            If not provided use default .HeadDirpath. Default headDirPath=None.
-            perm is numeric optional os dir permissions mode. Default perm=None.
-            reopen is boolean, IF True then database will be reopened by this init.
-            Default reopen=True.
-
-        Notes:
-
-        dupsort=True for sub DB means allow unique (key,pair) duplicates at a key.
-        Duplicate means that is more than one value at a key but not a redundant
-        copies a (key,value) pair per key. In other words the pair (key,value)
-        must be unique both key and value in combination.
-        Attempting to put the same (key,value) pair a second time does
-        not add another copy.
-
-        Duplicates are inserted in lexocographic order by value, insertion order.
-
+        """Initialize the Keeper key store.
+
+        Sets restrictive filesystem permissions by default (more restrictive
+        than the :class:`LMDBer` base default) to protect private key
+        material, then delegates to :meth:`LMDBer.__init__`.
+
+        Args:
+            headDirPath (str | None): Override for the head directory path of
+                the LMDB environment.  ``None`` uses the class-level default.
+            perm (int | None): Numeric OS permissions mode applied to the
+                database directory and files.  ``None`` defaults to
+                ``self.Perm``, which is more restrictive than the base class
+                default in order to protect secret material.
+            reopen (bool): When ``True`` the database environment is opened
+                immediately inside ``__init__``.  Defaults to ``False``.
+            **kwa: Additional keyword arguments forwarded to
+                :class:`LMDBer.__init__` (e.g. ``name``, ``temp``).
         """
         if perm is None:
             perm = self.Perm  # defaults to restricted permissions for non temp
@@ -267,8 +322,20 @@ def __init__(self, headDirPath=None, perm=None, reopen=False, **kwa):
                                      reopen=reopen, **kwa)
 
     def reopen(self, **kwa):
-        """
-        Open sub databases
+        """Open or re-open the LMDB environment and all named sub-databases.
+
+        Called by :meth:`__init__` when ``reopen=True`` and may be called
+        again later to reattach after a close.  Creates each named
+        sub-database the first time it is opened.  Sub-database names end
+        with ``'.'`` to avoid namespace collisions with Base64 identifier
+        prefixes.
+
+        Args:
+            **kwa: Keyword arguments forwarded to :meth:`LMDBer.reopen`.
+
+        Returns:
+            bool: ``True`` if the environment is open after this call,
+            ``False`` otherwise (mirrors :attr:`LMDBer.opened`).
         """
         opened = super(Keeper, self).reopen(**kwa)
 
@@ -306,156 +373,147 @@ def reopen(self, **kwa):
 
 
 class KeeperDoer(doing.Doer):
-    """
-    Basic Keeper Doer ( LMDB Database )
+    """Doer that manages the lifecycle of a :class:`Keeper` key-store database.
 
-    Inherited Attributes:
-        .done is Boolean completion state:
-            True means completed
-            Otherwise incomplete. Incompletion maybe due to close or abort.
+    Opens the :class:`Keeper` database on enter (if not already open) and
+    closes it on exit, clearing the environment when the keeper was opened in
+    temporary mode.
 
     Attributes:
-        .keeper is Keeper or LMDBer subclass
-
-    Inherited Properties:
-        .tyme is float relative cycle time of associated Tymist .tyme obtained
-            via injected .tymth function wrapper closure.
-
-        .tymth is function wrapper closure returned by Tymist .tymeth() method.
-            When .tymth is called it returns associated Tymist .tyme.
-            .tymth provides injected dependency on Tymist tyme base.
-
-        .tock is float, desired time in seconds between runs or until next run,
-            non negative, zero means run asap
-
-    Properties:
-
-    Methods:
-        .wind  injects ._tymth dependency from associated Tymist to get its .tyme
+        keeper (Keeper): The managed key-store database instance.
 
-        .__call__ makes instance callable
-            Appears as generator function that returns generator
+    Inherited Attributes:
+        done (bool): Completion state. ``True`` means the doer finished
+            normally; ``False`` means it is still running, was closed, or was
+            aborted.
 
-        .do is generator method that returns generator
-        .enter is enter context action method
-        .recur is recur context action method or generator method
-        .exit is exit context method
-        .close is close context method
-        .abort is abort context method
+        tyme (float): Relative cycle time supplied by the injected
+            :class:`Tymist`.
 
-    Hidden:
-        ._tymth is injected function wrapper closure returned by .tymen() of
-            associated Tymist instance that returns Tymist .tyme. when called.
+        tymth (callable): Closure that returns the associated
+            :class:`Tymist`'s ``.tyme`` when called.
 
-        ._tock is hidden attribute for .tock property
+        tock (float): Desired seconds between recur calls. ``0`` means run
+            as soon as possible.
     """
 
     def __init__(self, keeper, **kwa):
-        """
+        """Initialize the KeeperDoer.
 
-        Parameters:
-           keeper is Keeper instance
+        Args:
+            keeper (Keeper): The :class:`Keeper` instance whose lifecycle
+                this doer manages.
+            **kwa: Additional keyword arguments forwarded to
+                :class:`doing.Doer.__init__`.
         """
         super(KeeperDoer, self).__init__(**kwa)
         self.keeper = keeper
 
 
     def enter(self, *, temp=None):
-        """"""
+        """Open the keeper database if it is not already open.
+
+        Called automatically when the doer enters its execution context.
+
+        Args:
+            temp (bool | None): Unused; present for interface compatibility.
+        """
         if not self.keeper.opened:
             self.keeper.reopen()  # reopen(temp=temp)
 
 
     def exit(self):
-        """"""
+        """Close the keeper database, clearing it if it was opened in temp mode.
+
+        Called automatically when the doer exits its execution context,
+        whether normally or due to an exception.
+        """
         self.keeper.close(clear=self.keeper.temp)
 
 
 class Creator:
-    """
-    Class for creating a key pair based on algorithm.
-
-    Attributes:
-
-    Properties:
-
-    Methods:
-        .create is method to create key pair
-
-    Hidden:
+    """Base class for key-pair creators.
 
+    Defines the interface shared by all key-pair creation strategies.
+    Subclasses override :meth:`create` to implement a specific algorithm
+    (random re-keying, salt-path derivation, etc.).
     """
 
     def __init__(self, **kwa):
-        """
-        Setup Creator.
-
-        Parameters:
+        """Initialize the Creator.
 
+        Args:
+            **kwa: Accepted for subclass compatibility; not used by the base
+                class.
         """
 
     def create(self, **kwa):
-        """
-        Returns tuple of signers one per key pair
+        """Create and return key-pair signers.
+
+        Args:
+            **kwa: Algorithm-specific parameters defined by subclasses.
+
+        Returns:
+            list: Empty list in the base class; subclasses return a list of
+            :class:`Signer` instances.
         """
         return []
 
     @property
     def salt(self):
-        """
-        salt property getter
-        """
+        """str: The qb64 salt used by this creator, or empty string if none."""
         return ''
 
     @property
     def stem(self):
-        """
-        stem property getter
-        """
+        """str: The path stem used by this creator, or empty string if none."""
         return ''
 
     @property
     def tier(self):
-        """
-        tier property getter
-        """
+        """str: The security tier used by this creator, or empty string if none."""
         return ''
 
 
 class RandyCreator(Creator):
-    """
-    Class for creating a key pair based on re-randomizing each seed algorithm.
-
-    Attributes:
-
-    Properties:
-
-    Methods:
-        .create is method to create key pair
-
-    Hidden:
+    """Key-pair creator that generates a fresh random seed for every key pair.
 
+    Each call to :meth:`create` independently randomizes the seed for each
+    requested key pair, so key pairs produced by successive calls share no
+    derivation relationship.
     """
 
     def __init__(self, **kwa):
-        """
-        Setup Creator.
-
-        Parameters:
+        """Initialize the RandyCreator.
 
+        Args:
+            **kwa: Forwarded to :class:`Creator.__init__`.
         """
         super(RandyCreator, self).__init__(**kwa)
 
     def create(self, codes=None, count=1, code=MtrDex.Ed25519_Seed,
                transferable=True, **kwa):
-        """
-        Returns list of signers one per kidx in kidxs
+        """Create and return a list of randomly keyed :class:`Signer` instances.
+
+        When ``codes`` is not provided, ``count`` signers are created, each
+        using ``code`` as their derivation code.
+
+        Args:
+            codes (list[str] | None): Derivation codes, one per key pair.
+                When provided, its length determines the number of signers
+                created and ``count`` / ``code`` are ignored.
+            count (int): Number of key pairs to create when ``codes`` is not
+                provided. Defaults to ``1``.
+            code (str): Derivation code applied to all ``count`` key pairs
+                when ``codes`` is not provided. Defaults to
+                ``MtrDex.Ed25519_Seed``.
+            transferable (bool): When ``True`` the signer uses a transferable
+                derivation code; otherwise a non-transferable code is used.
+                Defaults to ``True``.
+            **kwa: Accepted for interface compatibility; not used.
 
-        Parameters:
-            codes is list of derivation codes one per key pair to create
-            count is count of key pairs to create is codes not provided
-            code is derivation code to use for count key pairs if codes not provided
-            transferable is Boolean, True means use trans deriv code. Otherwise nontrans
+        Returns:
+            list[Signer]: One :class:`Signer` per requested key pair.
         """
         signers = []
         if not codes:  # if not codes make list len count of same code
@@ -467,32 +525,32 @@ def create(self, codes=None, count=1, code=MtrDex.Ed25519_Seed,
 
 
 class SaltyCreator(Creator):
-    """
-    Class for creating a key pair based on random salt plus path stretch algorithm.
-
-    Attributes:
-        .salter is salter instance
-
-    Properties:
+    """Key-pair creator that derives private keys from a salt using path stretching.
 
+    Combines a root salt with a structured path string (built from the prefix
+    index, rotation index, and key index) to deterministically derive each
+    private key via the :class:`Salter` stretching algorithm.  The same salt
+    and parameters always reproduce the same key pairs, enabling recovery.
 
-    Methods:
-        .create is method to create key pair
-
-    Hidden:
-        ._salter holds instance for .salter property
+    Attributes:
+        salter (Salter): The :class:`Salter` instance that owns the root salt
+            and performs the key-stretching derivation.
     """
 
     def __init__(self, salt=None, stem=None, tier=None, **kwa):
-        """
-        Setup Creator.
-
-        Parameters:
-            salt is unique salt from which to derive private key
-            stem is path modifier used with salt to derive private keys.
-                    if stem is None then uses pidx
-            tier is derivation criticality that determines how much hashing to use.
-
+        """Initialize the SaltyCreator.
+
+        Args:
+            salt (str | None): Fully qualified qb64 root salt.  ``None``
+                causes :class:`Salter` to generate a fresh random salt.
+            stem (str | None): Unique path stem prepended to the per-key
+                path when deriving private keys.  ``None`` or empty string
+                causes the creator to use the hex-encoded ``pidx`` as the
+                stem instead.
+            tier (str | None): Security tier controlling the hashing work
+                factor used during key stretching.  ``None`` defers to the
+                :class:`Salter` default.
+            **kwa: Forwarded to :class:`Creator.__init__`.
         """
         super(SaltyCreator, self).__init__(**kwa)
         self.salter = Salter(qb64=salt, tier=tier)
@@ -500,40 +558,55 @@ def __init__(self, salt=None, stem=None, tier=None, **kwa):
 
     @property
     def salt(self):
-        """
-        salt property getter
-        """
+        """str: Fully qualified qb64 root salt owned by this creator."""
         return self.salter.qb64
 
     @property
     def stem(self):
-        """
-        stem property getter
-        """
+        """str: The path stem component used during key derivation."""
         return self._stem
 
     @property
     def tier(self):
-        """
-        tier property getter
-        """
+        """str: The security tier of the underlying :class:`Salter`."""
         return self.salter.tier
 
     def create(self, codes=None, count=1, code=MtrDex.Ed25519_Seed,
                pidx=0, ridx=0, kidx=0, transferable=True, temp=False, **kwa):
-        """
-        Returns list of signers one per kidx in kidxs
-
-        Parameters:
-            codes is list of derivation codes one per key pair to create
-            count is count of key pairs to create is codes not provided
-            code is derivation code to use for count key pairs if codes not provided
-            pidx is int prefix index for key pair sequence
-            ridx is int rotation index for key pair set
-            kidx is int starting key index for key pair set
-            transferable is Boolean, True means use trans deriv code. Otherwise nontrans
-            temp is Boolean True means use temp stretch otherwise use time set
-                 by tier for streching
+        """Create and return a list of deterministically derived :class:`Signer` instances.
+
+        Constructs a unique derivation path for each key by concatenating the
+        stem (or hex-encoded ``pidx`` when stem is empty), the hex rotation
+        index ``ridx``, and the hex key index ``kidx + i``.  Each path is
+        passed to :meth:`Salter.signer` to stretch the salt into a private
+        key.
+
+        Args:
+            codes (list[str] | None): Derivation codes, one per key pair.
+                When provided, its length determines the number of signers
+                and ``count`` / ``code`` are ignored.
+            count (int): Number of key pairs to create when ``codes`` is not
+                provided. Defaults to ``1``.
+            code (str): Derivation code applied to all ``count`` key pairs
+                when ``codes`` is not provided. Defaults to
+                ``MtrDex.Ed25519_Seed``.
+            pidx (int): Prefix index identifying the key-pair sequence. Used
+                as the stem when :attr:`stem` is empty. Defaults to ``0``.
+            ridx (int): Rotation index of the establishment event for which
+                these keys are created. Incorporated into the derivation
+                path. Defaults to ``0``.
+            kidx (int): Key index of the first key in the requested set
+                within the overall key sequence. Defaults to ``0``.
+            transferable (bool): When ``True`` the signer uses a transferable
+                derivation code. Defaults to ``True``.
+            temp (bool): When ``True`` bypasses the time-based work factor
+                applied by the security tier, for use in tests.
+                Defaults to ``False``.
+            **kwa: Accepted for interface compatibility; not used.
+
+        Returns:
+            list[Signer]: One :class:`Signer` per requested key pair, in
+            ascending key-index order.
         """
         signers = []
         if not codes:  # if not codes make list len count of same code
@@ -551,32 +624,24 @@ def create(self, codes=None, count=1, code=MtrDex.Ed25519_Seed,
 
 
 class Creatory:
-    """
-    Factory class for creating Creator subclasses to create key pairs based on
-    the provided algorithm.
-
-    Usage: creator = Creatory(algo='salty').make(salt=b'0123456789abcdef')
-
-    Attributes:
-
-    Properties:
+    """Factory that constructs the appropriate :class:`Creator` subclass for a given algorithm.
 
-    Methods:
-        .create is method to create key pair
+    Example:
+    
+        .. code-block:: python
 
-    Hidden:
-        ._create is method reference set to one of algorithm methods
-        ._novelCreate
-        ._indexCreate
+            creator = Creatory(algo=Algos.salty).make(salt='...')
     """
 
     def __init__(self, algo=Algos.salty):
-        """
-        Setup Creator.
+        """Initialize the Creatory factory.
 
-        Parameters:
-            algo is str code for algorithm
+        Args:
+            algo (str): Key-creation algorithm code.  Supported values are
+                ``Algos.randy`` and ``Algos.salty``.
 
+        Raises:
+            ValueError: If ``algo`` is not a supported algorithm code.
         """
         if algo == Algos.randy:
             self._make = self._makeRandy
@@ -586,20 +651,40 @@ def __init__(self, algo=Algos.salty):
             raise ValueError("Unsupported creation algorithm ={}.".format(algo))
 
     def make(self, **kwa):
-        """
-        Returns Creator subclass based on inited algo
+        """Construct and return a :class:`Creator` subclass for the configured algorithm.
+
+        Args:
+            **kwa: Keyword arguments forwarded to the selected creator
+                constructor (e.g. ``salt``, ``stem``, ``tier`` for
+                :class:`SaltyCreator`).
+
+        Returns:
+            Creator: An instance of the creator subclass appropriate for the
+            algorithm passed to :meth:`__init__`.
         """
         return (self._make(**kwa))
 
 
     def _makeRandy(self, **kwa):
-        """
+        """Construct and return a :class:`RandyCreator`.
+
+        Args:
+            **kwa: Forwarded to :class:`RandyCreator.__init__`.
+
+        Returns:
+            RandyCreator: A new random-seed key-pair creator.
         """
         return RandyCreator(**kwa)
 
 
     def _makeSalty(self, **kwa):
-        """
+        """Construct and return a :class:`SaltyCreator`.
+
+        Args:
+            **kwa: Forwarded to :class:`SaltyCreator.__init__`.
+
+        Returns:
+            SaltyCreator: A new salt-derived key-pair creator.
         """
         return SaltyCreator(**kwa)
 
@@ -609,87 +694,52 @@ def _makeSalty(self, **kwa):
 
 
 class Manager:
-    """Manages key pairs creation, storage, and signing
-    Class for managing key pair creation, storage, retrieval, and message signing.
+    """Manages key pair creation, storage, retrieval, and message signing.
 
-    Attributes:
-        ks (Keeper): key store LMDB database instance for storing public and private keys
-        encrypter (Encrypter): instance for encrypting secrets. Public
-            encryption key is derived from aeid (public signing key)
-        decrypter (Decrypter): instance for decrypting secrets. Private
-            decryption key is derived seed (private signing key seed)
-        inited (bool): True means fully initialized wrt database.
-                          False means not yet fully initialized
-
-    Attributes (Hidden):
-
-        _seed (str): qb64 private-signing key (seed) for the aeid from which
-                the private decryption key is derived. If aeid stored in
-                database is not empty then seed may required to do any key
-                management operations. The seed value is memory only and MUST NOT
-                be persisted to the database for the manager with which it is used.
-                It MUST only be loaded once when the process that runs the Manager
-                is initialized. Its presence acts as an authentication, authorization,
-                and decryption secret for the Manager and must be stored on
-                another device from the device that runs the Manager.
-
-
-    Properties:
-        aeid (str): authentication and encryption fully qualified qb64
-            non-transferable identifier prefix for authentication via signing
-            and asymmetric encryption of secrets using the associated
-            (public, private) key pair. Secrets include both salts and private
-            keys for all key sets in keeper. Defaults to empty which means no
-            authentication or encryption of key sets. Use initial attribute because
-            keeper may not be open on init.
-
-        pidx (int): pidx prefix index.
-            Use initial attribute because keeper
-            may not be open on init. Each sequence gets own pidx. Enables
-            unique recreatable salting of key sequence based on pidx.
-
-        salt (str): qb64 of root salt. Makes random root salt if not provided
-            initial salt. Use inital attribute because keeper may not be
-            open on init.
-
-        tier (str): initial security tier as value of Tierage. Use initial attribute
-            because keeper may not be open on init
-
-    Methods:
+    Wraps a :class:`Keeper` key store and provides high-level operations—
+    inception, rotation, signing, decryption, and replay—for one or more
+    identifier prefixes.  All private key material and salts stored in the
+    database may be encrypted at rest using an asymmetric key pair derived
+    from an authentication and encryption identifier (``aeid``).
+
+    The ``aeid`` public key is used to encrypt secrets; the corresponding
+    private key (the ``seed``) is held only in memory and must never be
+    written to the database.  Its presence authenticates the caller and
+    enables decryption of stored secrets.
 
+    Attributes:
+        ks (Keeper): The key-store LMDB database used for persistence.
+        encrypter (Encrypter | None): Encrypter derived from ``aeid``.
+            ``None`` when no ``aeid`` is configured (i.e., no encryption at
+            rest).
+        decrypter (Decrypter | None): Decrypter derived from ``seed``.
+            ``None`` when no ``aeid`` / ``seed`` is configured.
+        inited (bool): ``True`` once :meth:`setup` has completed
+            successfully.
     """
 
     def __init__(self, *, ks=None, seed=None, **kwa):
-        """
-        Setup Manager.
-
-        Parameters:
-            ks (Keeper): key store instance (LMDB)
-            seed (str): qb64 private-signing key (seed) for the aeid from which
-                the private decryption key may be derived. If aeid stored in
-                database is not empty then seed may required to do any key
-                management operations. The seed value is memory only and MUST NOT
-                be persisted to the database for the manager with which it is used.
-                It MUST only be loaded once when the process that runs the Manager
-                is initialized. Its presence acts as an authentication, authorization,
-                and decryption secret for the Manager and must be stored on
-                another device from the device that runs the Manager.
-                Currently only code MtrDex.Ed25519_Seed is supported.
-
-        Parameters: Passthrough to .setup for later initialization
-            aeid (str): qb64 of non-transferable identifier prefix for
-                authentication and encryption of secrets in keeper. If provided
-                aeid (not None) and different from aeid stored in database then
-                all secrets are re-encrypted using new aeid. In this case the
-                provided seed must not be empty. A change in aeid should require
-                a second authentication mechanism besides the seed.
-
-            pidx (int): index of next new created key pair sequence bound to a
-                given identifier prefix. Each sequence gets own pidx. Enables
-                unique recreatable salting of key sequence based on pidx.
-
-            salt (str): qb64 of root salt. Makes random root salt if not provided
-            tier (str): default security tier (Tierage) for root salt
+        """Initialize the Manager.
+
+        When the :class:`Keeper` database is already open at construction
+        time, :meth:`setup` is called immediately.  Otherwise,
+        initialization is deferred until :meth:`setup` is called explicitly
+        (or via :class:`ManagerDoer`).
+
+        Args:
+            ks (Keeper | None): Key store instance. A new default
+                :class:`Keeper` opened with ``reopen=True`` is created when
+                ``None``.
+            seed (str | None): Fully qualified qb64 private signing key seed
+                for ``aeid``. Held in memory only—never written to the
+                database.  Required whenever an ``aeid`` is stored in the
+                database in order to decrypt secrets and authenticate
+                operations.  Only ``MtrDex.Ed25519_Seed`` is currently
+                supported.
+            **kwa: Keyword arguments forwarded to :meth:`setup` (``aeid``,
+                ``pidx``, ``algo``, ``salt``, ``tier``). Captured in
+                ``_inits`` for deferred initialization when the database is
+                not yet open.
         """
         self.ks = ks if ks is not None else Keeper(reopen=True)
         self.encrypter = None
@@ -705,33 +755,49 @@ def __init__(self, *, ks=None, seed=None, **kwa):
 
 
     def setup(self, aeid=None, pidx=None, algo=None, salt=None, tier=None):
-        """
-        Setups manager root or global attributes and properties
-        Assumes that .keeper db is open.
-        If .keeper.gbls sub database has not been initialized for the first time
-        then initializes from ._inits. This allows dependency injection of
-        keepr db into manager instance prior to keeper db being opened to
-        accomodate asynchronous process setup of db resources. Putting the db
-        initialization here enables asynchronous opening of keeper db after
-        keeper instance is instantiated. First call to .setup will initialize
-        keeper db defaults if never before initialized (vacuous initialization).
-
-        Parameters:
-            aeid (str): qb64 of non-transferable identifier prefix for
-                authentication and encryption of secrets in keeper. If provided
-                aeid (not None) and different from aeid stored in database then
-                all secrets are re-encrypted using new aeid. In this case the
-                provided seed must not be empty. A change in aeid should require
-                a second authentication mechanism besides the secret.
-                aeid same as current aeid no change innocuous
-                aeid different but empty which unencrypts and removes aeid
-                aeid different not empty which reencrypts and updates aeid
-            pidx (int): index of next new created key pair sequence for given
-                identifier prefix
-            algo (str): root algorithm (randy or salty) for creating key pairs
-            salt (str): qb64 of root salt. Makes random root salt if not provided
-            tier (str): default security tier (Tierage) for root salt
-
+        """Initialize or validate the manager's root attributes in the database.
+
+        Must be called with the keeper database open.  On the first ever
+        call (vacuous initialization), missing global parameters are written
+        to the database from the supplied arguments or their defaults.  On
+        subsequent calls, the existing database values are used and the
+        provided ``aeid`` and ``seed`` are verified for consistency.
+
+        Supports deferred initialization: if the keeper database was not
+        open when :meth:`__init__` was called, this method can be invoked
+        later once the database becomes available (e.g., via
+        :class:`ManagerDoer`).
+
+        Args:
+            aeid (str | None): Fully qualified qb64 non-transferable
+                identifier prefix for authentication and encryption.
+                Behavior depends on the relationship to the value already
+                stored:
+
+                * Same as stored value — no-op.
+                * Different non-empty value — re-encrypts all secrets and
+                  updates the stored aeid.  Requires ``seed`` to match the
+                  new ``aeid``.
+                * Empty string or ``None`` — treated as empty string;
+                  decrypts all secrets and removes the aeid, disabling
+                  encryption at rest.
+
+            pidx (int | None): Initial value for the prefix index.  Only
+                used during vacuous initialization; ignored if already set.
+                Defaults to ``0``.
+            algo (str | None): Default root algorithm code.  Only used
+                during vacuous initialization. Defaults to ``Algos.salty``.
+            salt (str | None): Fully qualified qb64 root salt.  Only used
+                during vacuous initialization; a fresh random salt is
+                generated when ``None``.  Must be valid qb64 if provided.
+            tier (str | None): Default security tier.  Only used during
+                vacuous initialization. Defaults to ``Tiers.low``.
+
+        Raises:
+            ClosedError: If the keeper database is not open.
+            ValueError: If ``salt`` is provided but is not valid qb64.
+            AuthError: If an ``aeid`` is already stored and the in-memory
+                ``seed`` is missing or does not correspond to that ``aeid``.
         """
         if not self.ks.opened:
             raise ClosedError("Attempt to setup Manager closed keystore"
@@ -780,17 +846,29 @@ def setup(self, aeid=None, pidx=None, algo=None, salt=None, tier=None):
         self.inited = True
 
     def updateAeid(self, aeid, seed):
-        """
-        Given seed belongs to aeid and encrypter, update aeid and re-encrypt all
-        secrets
-
-        Parameters:
-            aeid (Optional(str)): qb64 of new auth encrypt id  (public signing key)
-                        aeid may match current aeid no change innocuous
-                        aeid may be empty which unencrypts and removes aeid
-                        aeid may be different not empty which reencrypts
-            seed (str): qb64 of new seed from which new aeid is derived (private signing
-                        key seed)
+        """Update the authentication/encryption identifier and re-encrypt all stored secrets.
+
+        Verifies that ``seed`` corresponds to the current ``aeid`` (when one
+        is stored), verifies that ``seed`` corresponds to the new ``aeid``
+        (when one is provided), re-encrypts all secrets (root salt, prefix
+        salts, private signing keys) with the new encrypter, and persists
+        the updated ``aeid`` to the database.
+
+        Providing an empty ``aeid`` removes encryption at rest: all secrets
+        are stored as plain text and no ``aeid`` is persisted.
+
+        Args:
+            aeid (str): Fully qualified qb64 of the new authentication and
+                encryption identifier (public signing key), or empty string
+                to disable encryption at rest.
+            seed (str): Fully qualified qb64 private signing key seed
+                corresponding to ``aeid``.  Required when ``aeid`` is
+                non-empty.
+
+        Raises:
+            AuthError: If a current ``aeid`` is stored and the current
+                in-memory ``seed`` does not verify against it, or if the
+                provided ``seed`` does not verify against the new ``aeid``.
         """
         if self.aeid:  # check that last current seed matches last current .aeid
             # verifies seed belongs to aeid
@@ -842,29 +920,29 @@ def updateAeid(self, aeid, seed):
 
     @property
     def seed(self):
-        """
-        seed property getter from ._seed.
-        seed (str): qb64 from which aeid is derived
+        """str: In-memory qb64 private signing key seed associated with ``aeid``.
+
+        Never persisted to the database.
         """
         return self._seed
 
 
     @property
     def aeid(self):
-        """
-        aeid property getter from key store db.
-        Assumes db initialized.
-        aeid is qb64 auth encrypt id prefix
+        """str: Fully qualified qb64 non-transferable identifier prefix for authentication and encryption.
+
+        Read from the keeper database.  Empty string means no ``aeid`` is
+        configured and encryption at rest is disabled.
         """
         return self.ks.gbls.get('aeid')
 
 
     @property
     def pidx(self):
-        """
-        pidx property getter from key store db.
-        Assumes db initialized.
-        pidx is prefix index int for next new key sequence
+        """int | None: Index of the next prefix key-pair sequence to be incepted.
+
+        Read from the keeper database (stored as a hex string).  ``None``
+        when the database has not yet been initialized.
         """
         if (pidx := self.ks.gbls.get("pidx")) is not None:
             return int(pidx, 16)
@@ -873,38 +951,42 @@ def pidx(self):
 
     @pidx.setter
     def pidx(self, pidx):
-        """
-        pidx property setter to key store db.
-        pidx is prefix index int for next new key sequence
+        """Set the prefix index in the keeper database.
+
+        Args:
+            pidx (int): New prefix index value.  Stored as a hex string.
         """
         self.ks.gbls.pin("pidx", "%x" % pidx)
 
 
     @property
     def algo(self):
-        """
-        also property getter from key store db.
-        Assumes db initialized.
-        algo is default root algorithm for creating key pairs
+        """str | None: Default root algorithm code for creating key pairs.
+
+        Read from the keeper database.  ``None`` when the database has not
+        yet been initialized.
         """
         return self.ks.gbls.get('algo')
 
 
     @algo.setter
     def algo(self, algo):
-        """
-        algo property setter to key store db.
-        algo is default root algorithm for creating key pairs
+        """Set the default root algorithm code in the keeper database.
+
+        Args:
+            algo (str): Algorithm code to store (e.g. ``Algos.salty``).
         """
         self.ks.gbls.pin('algo', algo)
 
 
     @property
     def salt(self):
-        """
-        salt property getter from key store db.
-        Assumes db initialized.
-        salt is default root salt for new key sequence creation
+        """str | None: Fully qualified qb64 root salt for new key sequence creation.
+
+        Read from the keeper database.  When :attr:`decrypter` is set the
+        value is automatically decrypted before being returned, so callers
+        always receive plaintext qb64.  ``None`` when the database has not
+        yet been initialized.
         """
         salt = self.ks.gbls.get('salt')
         if self.decrypter:  # given .decrypt secret salt must be encrypted in db
@@ -914,11 +996,12 @@ def salt(self):
 
     @salt.setter
     def salt(self, salt):
-        """
-        salt property setter to key store db.
-        Parameters:
-            salt (str): qb64 default root salt for new key sequence creation
-                may be plain text or cipher text handled by updateAeid
+        """Store the root salt in the keeper database, encrypting it when configured.
+
+        Args:
+            salt (str): Fully qualified qb64 root salt.  Encrypted with the
+                current :attr:`encrypter` before storage when one is
+                configured; stored as plain qb64 otherwise.
         """
         if self.encrypter:
             salt = self.encrypter.encrypt(ser=salt, code=MtrDex.X25519_Cipher_Salt).qb64
@@ -927,19 +1010,21 @@ def salt(self, salt):
 
     @property
     def tier(self):
-        """
-        tier property getter from key store db.
-        Assumes db initialized.
-        tier is default root security tier for new key sequence creation
+        """str | None: Default security tier for the root salt.
+
+        Read from the keeper database.  ``None`` when the database has not
+        yet been initialized.
         """
         return self.ks.gbls.get('tier')
 
 
     @tier.setter
     def tier(self, tier):
-        """
-        tier property setter to key store db.
-        tier is default root security tier for new key sequence creation
+        """Set the default security tier in the keeper database.
+
+        Args:
+            tier (str): Security tier value (e.g. a member of
+                :class:`Tiers`).
         """
         self.ks.gbls.pin('tier', tier)
 
@@ -949,50 +1034,70 @@ def incept(self, icodes=None, icount=1, icode=MtrDex.Ed25519_Seed,
                      dcode=MtrDex.Blake3_256,
                      algo=None, salt=None, stem=None, tier=None, rooted=True,
                      transferable=True, temp=False):
-        """
-        Returns tuple (verfers, digers) for inception event where
-            verfers is list of current public key verfers
-                public key is verfer.qb64
-            digers is list of next public key digers
-                digest to xor is diger.raw
-
-        Incept a prefix. Use first public key as temporary prefix.
-        Must .repre later to move pubsit dict to correct permanent prefix.
-        Store the dictified PreSit in the keeper under the first public key
-
-
-        Parameters:
-            icodes is list of private key derivation codes qb64 str
-                one per incepting key pair
-            icount is int count of incepting public keys when icodes not provided
-            icode is str derivation code qb64  of all icount incepting private keys
-                when icodes list not provided
-            ncodes is list of private key derivation codes qb64 str
-                one per next key pair
-            ncount is int count of next public keys when ncodes not provided
-            ncode is str derivation code qb64  of all ncount next public keys
-                when ncodes not provided
-            dcode is str derivation code qb64 of next digers. Default is MtrDex.Blake3_256
-            algo is str key creation algorithm code
-            salt is str qb64 salt for randomization when salty algorithm used
-            stem is path modifier used with salt to derive private keys when using
-                salty agorithms. if stem is None then uses pidx
-            tier is str security criticality tier code when using salty algorithm
-            rooted is Boolean true means derive incept salt from root salt when
-                incept salt not provided. Otherwise use incept salt only
-            transferable is Boolean, True means each public key uses transferable
-                derivation code. Default is transferable. Special case is non-transferable
-                Use case for incept to use transferable = False is for basic
-                derivation of non-transferable identifier prefix.
-                When the derivation process of the identifier prefix is
-                transferable then one should not use non-transferable for the
-                associated public key(s).
-            temp is Boolean. True is temporary for testing. It modifies tier of salty algorithm
-
-        When both ncodes is empty and ncount is 0 then the nxt is null and will
-            not be rotatable. This makes the identifier non-transferable in effect
-            even when the identifier prefix is transferable.
+        """Create and store key pairs for a new identifier prefix inception event.
+
+        Generates the current signing key set (``isigners``) and the
+        pre-committed next key set (``nsigners``), stores all private keys
+        and public key sets in the database, initializes :class:`PrePrm` and
+        :class:`PreSit` records, and increments :attr:`pidx`.
+
+        Because the permanent identifier prefix is typically only known after
+        the inception event is constructed (e.g. self-addressing identifiers
+        require the event body to derive the prefix), the key material is
+        initially indexed under the first public key acting as a temporary
+        prefix.  Call :meth:`move` afterwards to migrate the records to the
+        permanent prefix.
+
+        Args:
+            icodes (list[str] | None): Derivation codes for each inception
+                key pair.  When ``None``, ``icount`` key pairs are created
+                using ``icode``.
+            icount (int): Number of inception key pairs to create when
+                ``icodes`` is ``None``.  Must be ``> 0``. Defaults to ``1``.
+            icode (str): Derivation code for all inception key pairs when
+                ``icodes`` is ``None``. Defaults to ``MtrDex.Ed25519_Seed``.
+            ncodes (list[str] | None): Derivation codes for each next key
+                pair.  When ``None``, ``ncount`` key pairs are created using
+                ``ncode``.
+            ncount (int): Number of next key pairs to create when ``ncodes``
+                is ``None``.  ``0`` produces an empty next key set, making
+                the prefix effectively non-transferable. Defaults to ``1``.
+            ncode (str): Derivation code for all next key pairs when
+                ``ncodes`` is ``None``. Defaults to ``MtrDex.Ed25519_Seed``.
+            dcode (str): Derivation code for the digest of each next public
+                key (used to build the pre-rotation commitment).
+                Defaults to ``MtrDex.Blake3_256``.
+            algo (str | None): Key-creation algorithm.  ``None`` inherits
+                the root algorithm from the database when ``rooted=True``.
+            salt (str | None): qb64 salt for the salty algorithm.  ``None``
+                inherits the root salt from the database when ``rooted=True``.
+            stem (str | None): Path stem for key derivation.  ``None`` causes
+                the creator to use the hex-encoded ``pidx`` as the stem.
+            tier (str | None): Security tier.  ``None`` inherits the root
+                tier from the database when ``rooted=True``.
+            rooted (bool): When ``True``, ``algo``, ``salt``, and ``tier``
+                default to the root values stored in the database.
+                Defaults to ``True``.
+            transferable (bool): When ``True`` each key pair uses a
+                transferable derivation code.  Set to ``False`` only for
+                basic non-transferable identifier derivation.
+                Defaults to ``True``.
+            temp (bool): When ``True`` bypasses the time-based key-stretching
+                work factor, for use in tests. Defaults to ``False``.
 
+        Returns:
+            tuple[list[Verfer], list[Diger]]: A two-element tuple:
+
+            * **verfers** — one :class:`Verfer` per inception signing key;
+              ``verfer.qb64`` is the public key.
+            * **digers** — one :class:`Diger` per next public key;
+              ``diger.raw`` is the pre-rotation digest used in the inception
+              event's ``n`` field.
+
+        Raises:
+            ValueError: If ``icount <= 0``, ``ncount < 0``, or if key
+                material for the derived prefix already exists in the
+                database.
         """
         # get root defaults to initialize key sequence
         if rooted and algo is None:  # use root algo from db as default
@@ -1078,16 +1183,32 @@ def incept(self, icodes=None, icount=1, icode=MtrDex.Ed25519_Seed,
 
 
     def move(self, old, new):
-        """
-        Assigns new pre to old default .pres at old
+        """Reassign key-pair database records from a temporary prefix to the permanent prefix.
+
+        After :meth:`incept`, records are stored under the first public key
+        as a temporary prefix.  Once the permanent identifier prefix is
+        known, this method copies the :class:`PrePrm`, :class:`PreSit`, and
+        all :class:`PubSet` records from ``old`` to ``new``, then removes
+        the ``prms`` and ``sits`` entries for ``old``.  Note that ``pubs``
+        entries for ``old`` are copied but not deleted from the database.
+
+        The ``pres`` entry for ``old`` is updated to point to ``new``, and a
+        new ``pres`` entry for ``new`` is inserted so that future move
+        attempts on ``new`` can be detected.
+
+        If ``old == new``, this method returns immediately without any
+        changes.
 
-        Moves PrePrm and PreSit dicts in keeper db from old default pre to new pre db key
-        The new pre is the newly derived prefix which may only be known some
-        time after the original creation of the associated key pairs.
+        Args:
+            old (str): The old (temporary) identifier prefix under which
+                records are currently stored.
+            new (str): The new (permanent) identifier prefix to migrate
+                records to.
 
-        Paraameters:
-           old is str for old prefix of pubsit dict in keeper db
-           new is str for new prefix to move pubsit dict to in keeper db
+        Raises:
+            ValueError: If ``old`` does not exist in ``ks.pres``, ``new``
+                already exists in ``ks.pres``, or if any database put/pin
+                operation fails.
         """
         if old == new:
             return
@@ -1141,38 +1262,59 @@ def rotate(self, pre, ncodes=None, ncount=1,
                      ncode=MtrDex.Ed25519_Seed,
                      dcode=MtrDex.Blake3_256,
                      transferable=True, temp=False, erase=True):
-        """
-        Returns tuple (verfers, digers) for rotation event of keys for pre where
-            verfers is list of current public key verfers
-                public key is verfer.qb64
-            digers is list of next public key digers
-                digest to xor is diger.raw
-
-        Rotate a prefix.
-        Store the updated dictified PreSit in the keeper under pre
-
-        Parameters:
-            pre (str) qb64 of prefix
-            ncodes (list): of private key derivation codes qb64 str
-                one per next key pair
-            ncount (int): count of next public keys when icodes not provided
-            ncode (str): derivation code qb64  of all ncount next public keys
-                when ncodes not provided
-            dcode i(str): derivation code qb64 of next key digest of digers
-                Default is MtrDex.Blake3_256
-            transferable (bool): True means each public key uses transferable
-                derivation code. Default is transferable. Special case is non-transferable
-                Normally no use case for rotation to use transferable = False.
-                When the derivation process of the identifier prefix is
-                transferable then one should not use transferable = False for the
-                associated public key(s).
-            temp (bool): True is temporary for testing. It modifies tier of salty algorithm
-            erase (bool): True means erase old private keys made stale by rotation
-
-        When both ncodes is empty and ncount is 0 then the nxt is null and will
-            not be rotatable. This makes the identifier non-transferable in effect
-            even when the identifier prefix is transferable.
+        """Rotate the signing keys for an existing identifier prefix.
+
+        Promotes the pre-committed next key set (``ps.nxt``) to become the
+        new current signing key set (``ps.new``), generates a fresh next key
+        set for the subsequent rotation, and persists all changes to the
+        database.
+
+        The three-slot :class:`PreSit` shifts by one on each call:
+
+        * Pre-call ``ps.old`` (keys from two rotations ago) — erased from
+          ``ks.pris`` when ``erase=True``.
+        * Pre-call ``ps.new`` (the just-superseded signing set) — becomes
+          the new ``ps.old``.
+        * Pre-call ``ps.nxt`` (the pre-committed set) — becomes the new
+          ``ps.new`` (active signers).
+        * Newly generated signers — stored as the new ``ps.nxt``.
+
+        Args:
+            pre (str): Fully qualified qb64 identifier prefix to rotate.
+            ncodes (list[str] | None): Derivation codes for each new next
+                key pair.  When ``None``, ``ncount`` key pairs are created
+                using ``ncode``.
+            ncount (int): Number of next key pairs to create when ``ncodes``
+                is ``None``.  ``0`` produces an empty next key set, making
+                the prefix non-transferable after this rotation.
+                Defaults to ``1``.
+            ncode (str): Derivation code for all next key pairs when
+                ``ncodes`` is ``None``. Defaults to ``MtrDex.Ed25519_Seed``.
+            dcode (str): Derivation code for the digest of each next public
+                key. Defaults to ``MtrDex.Blake3_256``.
+            transferable (bool): When ``True`` each key pair uses a
+                transferable derivation code. Defaults to ``True``.
+            temp (bool): When ``True`` bypasses the time-based key-stretching
+                work factor, for use in tests. Defaults to ``False``.
+            erase (bool): When ``True`` the private keys of the pre-call
+                ``ps.old`` set (keys from two rotations ago) are deleted
+                from ``ks.pris``. Defaults to ``True``.
+
+        Returns:
+            tuple[list[Verfer], list[Diger]]: A two-element tuple:
 
+            * **verfers** — one :class:`Verfer` per key in the newly active
+              signing key set; ``verfer.qb64`` is the public key.
+            * **digers** — one :class:`Diger` per key in the new next
+              (pre-committed) key set; ``diger.raw`` is the pre-rotation
+              digest.
+
+        Raises:
+            ValueError: If ``pre`` has no stored :class:`PrePrm` or
+                :class:`PreSit`, if the prefix is already non-transferable
+                (empty ``ps.nxt`` public keys), or if ``ncount < 0``.
+            DecryptError: If an ``aeid`` is configured but no decrypter is
+                available (i.e. ``seed`` was not provided).
         """
         # Secret to decrypt here
         if (pp := self.ks.prms.get(pre)) is None:
@@ -1248,89 +1390,72 @@ def rotate(self, pre, ncodes=None, ncount=1,
 
     def sign(self, ser, pubs=None, verfers=None, indexed=True,
              indices=None, ondices=None, pre=None, path=None):
-        """
-        Returns list of signatures of ser if indexed as Sigers else as Cigars with
-        .verfer assigned.
-
-        Parameters:
-            ser (bytes): serialization to sign
-            pubs (list[str] | None): of qb64 public keys to lookup private keys
-                one of pubs or verfers is required. If both then verfers is ignored.
-            verfers (list[Verfer] | None): Verfer instances of public keys
-                one of pubs or verfers is required. If both then verfers is ignored.
-                If not pubs then gets public key from verfer.qb64
-            indexed (bool):
-                True means use use indexed signatures and return
-                list of Siger instances.
-                False means do not use indexed signatures and return
-                list of Cigar instances
-
-                When indexed True, each index is an offset that maps the offset
-                in the coherent lists: pubs, verfers, signers (pris from keystore .ks)
-                onto the appropriate offset into the signing keys or prior next
-                keys lists of a key event as determined by the indices and ondices
-                lists, or appropriate defaults when indices and/or ondices are not
+        """Sign a serialization with one or more stored private keys.
+
+        Looks up the private key for each requested public key in the keeper
+        database and returns a list of signatures.  When ``indexed=True``
+        each signature is an indexed :class:`Siger`; when ``indexed=False``
+        each is a :class:`Cigar` with ``.verfer`` assigned.
+
+        Exactly one of ``pubs``, ``verfers``, or ``pre`` must be provided.
+        When both ``pubs`` and ``verfers`` are given, ``verfers`` is ignored.
+
+        The ``indices`` parameter allows the caller to specify the index
+        value embedded in each :class:`Siger`, decoupling it from the
+        position of the signer in ``pubs`` / ``verfers``.  This is necessary
+        for witness or multi-sig scenarios where different parties maintain
+        independent key stores with different key orderings.
+
+        The ``ondices`` parameter allows the caller to embed a second index
+        into each :class:`Siger`, used in partial-rotation or custodial key
+        management to indicate a key's position in the prior next list when
+        it differs from its position in the current signing list.  A ``None``
+        entry in ``ondices`` means no ondex for that signer.
+
+        Note:
+            The ``pre`` / ``path`` code path is currently unimplemented.
+            Providing ``pre`` without ``pubs`` or ``verfers`` will result in
+            a ``TypeError`` when the subsequent iteration over ``verfers``
+            (which is ``None``) is attempted.
+
+        Args:
+            ser (bytes): The serialized data to sign.
+            pubs (list[str] | None): Fully qualified qb64 public keys whose
+                private keys are looked up in the database.  Takes precedence
+                over ``verfers`` when both are provided.
+            verfers (list[Verfer] | None): :class:`Verfer` instances whose
+                ``.qb64`` public keys are used for lookup.  Ignored when
+                ``pubs`` is provided.
+            indexed (bool): When ``True`` return indexed :class:`Siger`
+                instances; when ``False`` return :class:`Cigar` instances.
+                Defaults to ``True``.
+            indices (list[int] | None): Explicit index values for each
+                returned :class:`Siger`, overriding the default positional
+                index.  Length must match the number of signers when
                 provided.
+            ondices (list[int | None] | None): Explicit other-index (ondex)
+                values for each returned :class:`Siger`.  A ``None`` entry
+                marks a signature as having no ondex (current-key-only).
+                Length must match the number of signers when provided.
+            pre (str | None): Reserved for future HDK salty-algorithm key
+                lookup.  Currently unimplemented; see Note above.
+            path (tuple | None): Reserved for future HDK path derivation
+                as ``(ridx, kidx)``.  Currently unimplemented.
 
-            indices (list[int] | None): indices (offsets) when indexed == True,
-                to use for indexed signatures whose offset into the current keys
-                or prior next list may differ from the order of appearance
-                in the provided coherent pubs, verfers, signers lists.
-                This allows witness indexed sigs or controller multi-sig
-                where the parties do not share the same manager or ordering so
-                the default ordering in pubs or verfers is wrong for the index.
-                This sets the value of the index property of the returned Siger.
-                When provided the length of indices must match the len of the
-                coherent lists: pubs, verfers, signers (pris from keystore .ks)
-                else raises ValueError.
-                When not provided and indexed is True then use default index that
-                is the offset into the coherent lists:
-                pubs, verfers, signers (pris from keystore .ks)
-
-            ondices (list[int | None] | None): other indices (offsets)
-                when indexed is True  for indexed signatures whose offset into
-                the prior next list may differ from the order of appearance
-                in the provided coherent pubs, verfers, signers lists.
-                This allows partial rotation with reserve or custodial key
-                management so that the index (hash of index) of the public key
-                for the signature appears at a different index in the
-                current key list from the prior next list.
-                This sets the value of the ondex property of the returned Siger.
-                When provided the length of indices must match the len of the
-                coherent lists: pubs, verfers, signers (pris from keystore .ks)
-                else raises ValueError.
-                When no ondex is applicable to a given signature then the value
-                of the entry in ondices MUST be None.
-                When  ondices is not provided then all sigers .ondex is None.
-
-            pre (str | None): identity prefix (aid) of signer. Used for HDK salty
-                algo key lookup or re-creation. Look up key path state and algo
-                from local keystore .ks
-
-            path (tuple | None): HDX randy algo signing key path tuple part of form
-                (ridx, kidx) where ridx is the optional rotation index and kidx
-                is the required zeroth key index of the key list. The fully HDK
-                path is formed by looking up the stem and pidx using the pre
-                and when provided the ridx and then computing the pidxes for each
-                signing key. When indices is provided then the kidxes are computed
-                by adding the index offset to the zeroth kidx. When indices is not
-                provided then the kidxes are assumed to be all the keys in the key
-                list computed from the local keystore by subtracting the kidx
-                of the zeroth element of the following key list. When path is not
-                provided then the default is all the kidxs of the key list from
-                the current .new key info.
-
-
-
-
-        if neither pubs or verfers provided then returns empty list of signatures
-        If pubs then ignores verfers otherwise uses verferss
-
-        Manager implement .sign method and tests
-        sign(self,ser,pubs,indexed=True)
-        checks for pris for pubs in db is not raises error
-        then signs ser with eah pub
-        returns list of sigers indexed else list of cigars if not
+        Returns:
+            list[Siger] | list[Cigar]: When ``indexed=True``, a list of
+            :class:`Siger` instances, one per signer.  When
+            ``indexed=False``, a list of :class:`Cigar` instances with
+            ``.verfer`` assigned.
+
+        Raises:
+            ValueError: If none of ``pubs`` or ``verfers`` is provided and
+                ``pre`` is also ``None``; if a public key has no
+                corresponding private key in the database; or if the length
+                of ``indices`` or ``ondices`` does not match the number of
+                signers.
+            DecryptError: If an ``aeid`` is configured but no decrypter is
+                available.
         """
         signers = []
 
@@ -1416,22 +1541,35 @@ def sign(self, ser, pubs=None, verfers=None, indexed=True,
 
 
     def decrypt(self, qb64, pubs=None, verfers=None):
-        """
-        Returns decrypted plaintext of encrypted qb64 ciphertext serialization.
-
-        Parameters:
-            qb64 (str | bytes | bytearray | memoryview): fully qualified base64
-                ciphertext serialization to decrypt
-            pubs (list[str] | None): of qb64 public keys to lookup private keys
-                one of pubs or verfers is required. If both then verfers is ignored.
-            verfers (list[Verfer] | None): Verfer instances of public keys
-                one of pubs or verfers is required. If both then verfers is ignored.
-                If not pubs then gets public key from verfer.qb64 used to lookup
-                private keys
+        """Decrypt a sealed ciphertext using a stored private key.
+
+        Looks up the private signing key for each provided public key,
+        converts it to a Curve25519 private decryption key via
+        :func:`pysodium.crypto_sign_sk_to_box_sk`, and attempts to open the
+        sealed box ciphertext ``qb64`` with each key pair in turn.  Only the
+        result of the final successful decryption is returned; provide a
+        single public key in the normal case.
+
+        Args:
+            qb64 (str | bytes | bytearray | memoryview): Fully qualified
+                Base64 sealed-box ciphertext to decrypt.
+            pubs (list[str] | None): Fully qualified qb64 public keys whose
+                corresponding private keys are used for decryption.  Takes
+                precedence over ``verfers`` when both are provided.
+            verfers (list[Verfer] | None): :class:`Verfer` instances whose
+                ``.qb64`` public keys are used for lookup.  Ignored when
+                ``pubs`` is provided.
 
         Returns:
-            plain (bytes): decrypted plaintext
+            bytes: The decrypted plaintext bytes from the last signer
+            attempted.
 
+        Raises:
+            ValueError: If a public key has no corresponding private key in
+                the database, or if the plaintext equals the ciphertext after
+                all decryption attempts (indicating failure).
+            DecryptError: If an ``aeid`` is configured but no decrypter is
+                available.
         """
         signers = []
         if pubs:
@@ -1475,66 +1613,85 @@ def ingest(self, secrecies, iridx=0, ncount=1, ncode=MtrDex.Ed25519_Seed,
                      dcode=MtrDex.Blake3_256,
                      algo=Algos.salty, salt=None, stem=None, tier=None,
                      rooted=True, transferable=True, temp=False):
-        """
-        Ingest secrecies as a list of lists of secrets organized in event order
-        to register the sets of secrets of associated externally generated keypair
-        lists into the database.
+        """Import an externally generated key sequence and register it in the database.
+
+        Ingests a list of lists of private key secrets (``secrecies``),
+        where each inner list corresponds to the signing key set for one
+        establishment event in order (inception first, then successive
+        rotations).  All ingested private keys are stored in the database
+        with encryption applied when configured.  After the last ingested
+        set, a new next key set is generated using the specified algorithm
+        and parameters, exactly as if a rotation had been performed.
+
+        Unlike :meth:`rotate`, ingest does not delete any of the ingested
+        private keys.  The caller is responsible for erasing stale keys if
+        desired.
+
+        Note:
+            The newly generated ``nsigners`` (keys after the ingested
+            sequence) are stored in ``ks.pris`` without applying the
+            :attr:`encrypter`, even when one is configured.  This is
+            inconsistent with the treatment of ingested keys and may be
+            addressed in a future revision.
+
+        The ``iridx`` parameter controls which ingested key set is treated
+        as the current active set (``ps.new``):
+
+        * The set at ``iridx - 1`` (or a default empty lot when
+          ``iridx == 0``) becomes ``ps.old``.
+        * The set at ``iridx`` becomes ``ps.new``.
+        * The set at ``iridx + 1`` (or the newly generated set when
+          ``len(secrecies) == iridx + 1``) becomes ``ps.nxt``.
+
+        Typical use cases are import from an external key store and recovery
+        from backup.
+
+        Args:
+            secrecies (list[list[str]]): Ordered list of lists of fully
+                qualified qb64 private key secrets.  The outer list is in
+                establishment event order; each inner list contains the
+                secrets for one establishment event's signing key set.
+            iridx (int): Rotation index into ``secrecies`` that marks the
+                currently active key set.  Must satisfy
+                ``0 <= iridx <= len(secrecies)``. Defaults to ``0``.
+            ncount (int): Number of next key pairs to generate after the
+                last ingested set. Defaults to ``1``.
+            ncode (str): Derivation code for each generated next key pair.
+                Defaults to ``MtrDex.Ed25519_Seed``.
+            dcode (str): Derivation code for next key digests.
+                Defaults to ``MtrDex.Blake3_256``.
+            algo (str): Key-creation algorithm for generating the new next
+                keys after the end of ``secrecies``.
+                Defaults to ``Algos.salty``.
+            salt (str | None): qb64 salt for key derivation.  ``None``
+                inherits the root salt when ``rooted=True``.
+            stem (str | None): Path stem for key derivation.  ``None``
+                causes the creator to use the hex-encoded ``pidx`` as the
+                stem.
+            tier (str | None): Security tier.  ``None`` inherits the root
+                tier when ``rooted=True``.
+            rooted (bool): When ``True`` ``salt`` and ``tier`` default to
+                the root values stored in the database.
+                Defaults to ``True``.
+            transferable (bool): When ``True`` each key pair uses a
+                transferable derivation code. Defaults to ``True``.
+            temp (bool): When ``True`` bypasses the time-based
+                key-stretching work factor, for use in tests.
+                Defaults to ``False``.
 
         Returns:
-            ret (tuple): (ipre, veferies) where:
-                ipre is prefix index of ingested key pairs needed to fetch later
-                   for replay
-
-                veferies is list of lists of all the verfers for the  public keys
-                from the private keys in secrecies in order of appearance.
-
-        Essentially ingest ends with the current keys as the
-        last key list in secrecies and the nxt keys are newly created as if a
-        rotation to the last set of keys was performed. Unlike rotate, however,
-        ingest does not delete any of the private keys it ingests. This must be
-        done separately if desired.
-
-        Each list in secrecies is an ordered list of private keys corresponding
-        to the public list in the key state for each establishment event in order.
-        The first list are the keys for the inception event, the next list for
-        the first rotation, and each subsequent list for the next rotation and
-        so on.
-
-        May be used for import or recovery from backup.
-        Method parameters specify the policy for generating new keys pairs for
-        rotations that follow the ingested list of lists. The parameters are used
-        to define how to rotate to new key pairs that follow the ingested sequence.
-
-
-
-        Parameters:
-            secrecies (list): list of lists of fully qualified secrets (private keys)
-            iridx (int): initial ridx at where set PubSit after ingestion
-                enables database to store where initial replay should start from
-
-            ncount (int): count of next public keys for next after end of secrecies
-            ncode (str): derivation code qb64  of all ncount next public keys
-                after end of secrecies
-            dcode is str derivation code qb64 of next digers after end of secrecies
-                Default is MtrDex.Blake3_256
-            algo is str key creation algorithm code for next after end of secrecies
-            salt is str qb64 salt for randomization when salty algorithm used
-                for next after end of secrecies
-            stem is path modifier used with salt to derive private keys when using
-                salty agorithms. if stem is None then uses pidx
-            tier is str security criticality tier code when using salty algorithm
-            rooted is Boolean true means derive incept salt from root salt when
-                incept salt not provided. Otherwise use incept salt only
-            transferable is Boolean, True means each public key uses transferable
-                derivation code. Default is transferable. Special case is non-transferable
-                Use case for incept to use transferable = False is for basic
-                derivation of non-transferable identifier prefix.
-                When the derivation process of the identifier prefix is
-                transferable then one should not use non-transferable for the
-                associated public key(s).
-            temp is Boolean. True is temporary for testing. It modifies strech
-                time of salty algorithm
+            tuple[str, list[list[Verfer]]]: A two-element tuple:
 
+            * **ipre** — the qb64 first public key of the ingested sequence,
+              used as the initial (temporary) prefix for subsequent
+              :meth:`move` calls.
+            * **verferies** — a list of lists of :class:`Verfer` instances,
+              one inner list per ingested establishment event, in order.
+
+        Raises:
+            ValueError: If ``iridx > len(secrecies)``, or if key material
+                for the derived prefix already exists in the database, or if
+                any database write fails.
         """
         if iridx > len(secrecies):
             raise ValueError(f"Initial ridx={iridx} beyond last secrecy.")
@@ -1650,30 +1807,55 @@ def ingest(self, secrecies, iridx=0, ncount=1, ncode=MtrDex.Ed25519_Seed,
 
 
     def replay(self, pre, dcode=MtrDex.Blake3_256, advance=True, erase=True):
-        """
-        Returns duple (verfers, digers) associated with public key set from
-        the key sequence for identifier prefix pre at rotation index ridx stored
-        in db .pubs. Inception is at ridx == 0.
-        Enables replay of preexisting public key sequence.
-
-        In returned duple:
-            verfers is list of current public key verfers. Public key is verfer.qb64.
-            digers is list of next public key digers. Digest to xor is diger.raw.
-
-        If key sequence at ridx does already exist in .pubs database for pre then
-            raises ValueError.
-
-        If  preexisting pubs for pre exist but .ridx is two large for preexisting
-            pubs then raises IndexError.
-
-        Parameters:
-            pre (str): fully qualified qb64 identifier prefix
-            dcode (str): derivation code for digers for next xor digest.
-                Default is MtrDex.Blake3_256
-            advance (bool): True means advance to next set of keys for replay
-            erase (bool): True means erase old private keys made stale by
-                advancement when advance is True otherwise ignore
+        """Replay the next establishment event's key set from pre-stored public keys.
+
+        Retrieves the public key set stored at the next rotation index from
+        the ``ks.pubs`` database and, when ``advance=True``, advances the
+        :class:`PreSit` so that:
+
+        * The current ``ps.new`` is moved to ``ps.old``.
+        * The current ``ps.nxt`` is moved to ``ps.new``.
+        * The public key set at the incremented rotation index is loaded as
+          the new ``ps.nxt``.
+
+        An :exc:`IndexError` is raised when no ``pubs`` entry exists at
+        ``ps.new.ridx + 1`` after the advance, signalling that all
+        pre-stored establishment events have been replayed.
+
+        When ``advance=False``, the current ``ps.new`` is returned without
+        any state update, providing a read-only view of the active key set.
+
+        Args:
+            pre (str): Fully qualified qb64 identifier prefix to replay.
+            dcode (str): Derivation code for computing next key digests.
+                Defaults to ``MtrDex.Blake3_256``.
+            advance (bool): When ``True`` advance the :class:`PreSit` state
+                and persist the update.  When ``False`` return the current
+                active key set without modifying state.
+                Defaults to ``True``.
+            erase (bool): When ``True`` and ``advance=True``, delete the
+                private keys of the pre-advance ``ps.old`` set from
+                ``ks.pris``. Defaults to ``True``.
 
+        Returns:
+            tuple[list[Verfer], list[Diger]]: A two-element tuple:
+
+            * **verfers** — one :class:`Verfer` per key in the current
+              active (``ps.new``) signing key set after any advance;
+              ``verfer.qb64`` is the public key.
+            * **digers** — one :class:`Diger` per key in the pre-committed
+              next (``ps.nxt``) key set, computed as the digest of each
+              public key's bytes.
+
+        Raises:
+            ValueError: If ``pre`` has no stored :class:`PrePrm` or
+                :class:`PreSit`, if a private key is missing from the
+                database, or if the :class:`PreSit` update fails.
+            IndexError: If ``advance=True`` and the ``ks.pubs`` database
+                has no entry at ``ps.new.ridx + 1`` (end of the pre-stored
+                replay sequence).
+            DecryptError: If an ``aeid`` is configured but no decrypter is
+                available.
         """
         if (pp := self.ks.prms.get(pre)) is None:
             raise ValueError("Attempt to replay nonexistent pre={}.".format(pre))
@@ -1729,65 +1911,60 @@ def replay(self, pre, dcode=MtrDex.Blake3_256, advance=True, erase=True):
 
 
 class ManagerDoer(doing.Doer):
-    """
-    Basic Manager Doer to initialize keystore database .ks
+    """Doer that defers and triggers :class:`Manager` initialization.
 
-    Inherited Attributes:
-        .done is Boolean completion state:
-            True means completed
-            Otherwise incomplete. Incompletion maybe due to close or abort.
+    Calls :meth:`Manager.setup` on enter if the manager has not yet been
+    initialized (i.e. :attr:`Manager.inited` is ``False``).  The exit
+    handler is a no-op because the :class:`Manager` does not own the
+    underlying :class:`Keeper` database lifecycle (that is managed by a
+    separate :class:`KeeperDoer`).
 
     Attributes:
-        .manager is Manager subclass
+        manager (Manager): The :class:`Manager` instance whose
+            initialization this doer triggers.
 
-    Inherited Properties:
-        .tyme is float relative cycle time of associated Tymist .tyme obtained
-            via injected .tymth function wrapper closure.
-
-        .tymth is function wrapper closure returned by Tymist .tymeth() method.
-            When .tymth is called it returns associated Tymist .tyme.
-            .tymth provides injected dependency on Tymist tyme base.
-
-        .tock is float, desired time in seconds between runs or until next run,
-            non negative, zero means run asap
-
-    Properties:
-
-    Methods:
-        .wind  injects ._tymth dependency from associated Tymist to get its .tyme
-
-        .__call__ makes instance callable
-            Appears as generator function that returns generator
+    Inherited Attributes:
+        done (bool): Completion state. ``True`` means the doer finished
+            normally; ``False`` means it is still running, was closed, or
+            was aborted.
 
-        .do is generator method that returns generator
-        .enter is enter context action method
-        .recur is recur context action method or generator method
-        .exit is exit context method
-        .close is close context method
-        .abort is abort context method
+        tyme (float): Relative cycle time supplied by the injected
+            :class:`Tymist`.
 
-    Hidden:
-        ._tymth is injected function wrapper closure returned by .tymen() of
-            associated Tymist instance that returns Tymist .tyme. when called.
+        tymth (callable): Closure that returns the associated
+            :class:`Tymist`'s ``.tyme`` when called.
 
-        ._tock is hidden attribute for .tock property
+        tock (float): Desired seconds between recur calls. ``0`` means run
+            as soon as possible.
     """
 
     def __init__(self, manager, **kwa):
-        """
-        Parameters:
-           manager (Manager): instance
+        """Initialize the ManagerDoer.
+
+        Args:
+            manager (Manager): The :class:`Manager` instance to initialize
+                on enter.
+            **kwa: Additional keyword arguments forwarded to
+                :class:`doing.Doer.__init__`.
         """
         super(ManagerDoer, self).__init__(**kwa)
         self.manager = manager
 
 
     def enter(self, *, temp=None):
-        """"""
+        """Call :meth:`Manager.setup` if the manager is not yet initialized.
+
+        Called automatically when the doer enters its execution context.
+        Passes the keyword arguments captured in ``manager._inits`` at
+        construction time to :meth:`Manager.setup`.
+
+        Args:
+            temp (bool | None): Unused; present for interface compatibility.
+        """
         if not self.manager.inited:
             self.manager.setup(**self.manager._inits)
 
 
     def exit(self):
-        """"""
+        """No-op exit handler."""
         pass
diff --git a/src/keri/app/notifying.py b/src/keri/app/notifying.py
index de0458643..0ed00a30b 100644
--- a/src/keri/app/notifying.py
+++ b/src/keri/app/notifying.py
@@ -2,6 +2,8 @@
 """
 keri.app.notifying module
 
+Provides data structures and persistence utilities for creating, storing,
+retrieving, and signaling agent notifications.
 """
 import os
 from collections.abc import Iterable
@@ -16,16 +18,19 @@
 
 
 def notice(attrs, dt=None, read=False):
-    """
+    """Create a Notice instance.
 
-    Parameters:
-        attrs (dict): payload of the notice
-        dt(Optional(str, datetime)): iso8601 formatted datetime of notice
-        read (bool): message read indicator
+    Args:
+        attrs (dict): Arbitrary payload describing the notification.
+        dt (str | datetime, optional): Datetime for the notice. If a
+            ``datetime`` object is provided it is converted to ISO 8601
+            format via its ``isoformat()`` method. Defaults to the current
+            time via ``nowIso8601()``.
+        read (bool, optional): Whether the notice is marked as read.
+            Defaults to ``False``.
 
     Returns:
-        Notice:  Notice instance
-
+        Notice: An initialized notice instance.
     """
     dt = dt if dt is not None else nowIso8601()
 
@@ -42,29 +47,35 @@ def notice(attrs, dt=None, read=False):
 
 
 class Notice(Dicter):
-    """ Notice is for creating notification messages for the controller of the agent
-
-    Sub class of Sadder that adds notification specific validation and properties
+    """Notification message container.
 
-    Inherited Properties:
-        .raw is bytes of serialized event only
-        .pad is key event dict
-
-    Properties:
-        .datetime (str): ISO8601 formatted datetime of notice
-        .pad (dict): payload of the notice
+    Extends :class:`Dicter` with notification-specific fields and validation.
+    A notice encapsulates a timestamp, a read status, and a metadata payload.
 
+    Attributes:
+        raw (bytes): Serialized representation of the notice, inherited from
+            :class:`Dicter`.
+        pad (dict): Underlying structured data dictionary, inherited from
+            :class:`Dicter`.
     """
 
     def __init__(self, raw=b'', pad=None, note=None):
-        """ Creates a serializer/deserializer for a ACDC Verifiable Credential in CESR Proof Format
+        """Initialize a notice instance.
 
-        Requires either raw or (crd and kind) to load credential from serialized form or in memory
+        Exactly one of ``raw``, ``pad``, or ``note`` should be provided.
+        The ``note`` argument is forwarded to the base class as the
+        ``dicter`` parameter.
 
-        Parameters:
-            raw (bytes): is raw credential
-            pad (dict): is populated data
+        Args:
+            raw (bytes, optional): Serialized notice data. Defaults to ``b''``.
+            pad (dict, optional): Structured notice data. Must contain at least
+                the key ``"a"`` (attributes). Defaults to ``None``.
+            note (Dicter, optional): An existing :class:`Dicter` instance used
+                to initialize the notice. Defaults to ``None``.
 
+        Raises:
+            ValueError: If the resolved data is missing the required ``"a"``
+                (attributes) key.
         """
         super(Notice, self).__init__(raw=raw, pad=pad, dicter=note)
 
@@ -76,150 +87,152 @@ def __init__(self, raw=b'', pad=None, note=None):
 
     @property
     def datetime(self):
-        """ issuer property getter"""
+        """str: The ISO 8601 formatted datetime of the notice."""
         return self._pad["dt"]
 
     @property
     def attrs(self):
-        """ pad property getter"""
+        """dict: The notification's arbitrary attributes payload."""
         return self._pad["a"]
 
     @property
     def read(self):
-        """ read property getter """
+        """bool: The read status flag of the notice."""
         return self._pad["r"]
 
     @read.setter
     def read(self, val):
-        """ read property setter """
+        """Set the read status flag.
+
+        Args:
+            val (bool): The new read state to apply.
+        """
         pad = self.pad
         pad["r"] = val
         self.pad = pad
 
 
 class DicterSuber(Suber):
-    """ Data serialization for Dicter and subclasses
+    """Sub-database for storing :class:`Dicter` instances.
 
-    Sub class of Suber where data is serialized Dicter instance or subclass
-    Automatically serializes and deserializes using Dicter methods
+    Serializes values using ``Dicter.raw`` and deserializes them back into
+    instances of a configurable subclass.
 
+    Attributes:
+        klas (Type[Dicter]): The class used to wrap raw bytes during retrieval.
     """
 
     def __init__(self, *pa, klas: Type[Dicter] = Dicter, **kwa):
-        """
-        Parameters:
-            db (LMDBer): base db
-            subkey (str):  LMDB sub database key
+        """Initialize the sub-database.
 
+        Args:
+            *pa: Positional arguments forwarded to :class:`Suber`.
+            klas (Type[Dicter], optional): Class used for deserializing stored
+                bytes. Defaults to :class:`Dicter`.
+            **kwa: Keyword arguments forwarded to :class:`Suber`.
         """
         super(DicterSuber, self).__init__(*pa, **kwa)
         self.klas = klas
 
     def put(self, keys: Union[str, Iterable], val: Dicter):
-        """ Puts val at key made from keys. Does not overwrite
+        """Store a value without overwriting an existing entry.
 
-        Parameters:
-            keys (tuple): of key strs to be combined in order to form key
-            val (Dicter): instance
+        Args:
+            keys (str | Iterable): Components used to construct the database
+                key.
+            val (Dicter): The :class:`Dicter` instance to store.
 
         Returns:
-            bool: True If successful, False otherwise, such as key
-                              already in database.
+            bool: ``True`` if the value was stored, ``False`` if the key
+                already exists.
         """
         return (self.db.putVal(db=self.sdb,
                                key=self._tokey(keys),
                                val=val.raw))
 
     def pin(self, keys: Union[str, Iterable], val: Dicter):
-        """ Pins (sets) val at key made from keys. Overwrites.
+        """Store a value, overwriting any existing entry.
 
-        Parameters:
-            keys (tuple): of key strs to be combined in order to form key
-            val (Sadder): instance
+        Args:
+            keys (str | Iterable): Components used to construct the database
+                key.
+            val (Dicter): The :class:`Dicter` instance to store.
 
         Returns:
-            bool: True If successful. False otherwise.
+            bool: ``True`` if the operation succeeded.
         """
         return (self.db.setVal(db=self.sdb,
                                key=self._tokey(keys),
                                val=val.raw))
 
     def get(self, keys: Union[str, Iterable]):
-        """ Gets Sadder at keys
+        """Retrieve and deserialize a value by key.
 
-        Parameters:
-            keys (tuple): of key strs to be combined in order to form key
+        Args:
+            keys (str | Iterable): Components used to construct the database
+                key.
 
         Returns:
-            Sadder: instance at keys
-            None: if no entry at keys
-
-        Usage:
-            Use walrus operator to catch and raise missing entry.
-            Example::
-
-                if (creder := mydb.get(keys)) is None:
-                    raise ExceptionHere
-                use creder here
-
+            Dicter | None: An instance of ``self.klas`` populated from the
+                stored bytes, or ``None`` if the key is not found.
         """
         val = self.db.getVal(db=self.sdb, key=self._tokey(keys))
         return self.klas(raw=bytes(val)) if val is not None else None
 
     def rem(self, keys: Union[str, Iterable]):
-        """ Removes entry at keys
+        """Remove an entry from the sub-database by key.
 
-        Parameters:
-            keys (tuple): of key strs to be combined in order to form key
+        Args:
+            keys (str | Iterable): Components used to construct the database
+                key.
 
         Returns:
-           bool: True if key exists so delete successful. False otherwise
+            bool: ``True`` if the entry existed and was removed, ``False``
+                otherwise.
         """
         return self.db.remVal(db=self.sdb, key=self._tokey(keys))
 
     def getTopItemIter(self, keys: Union[str, Iterable] = b""):
-        """ Return iterator over the all the items in subdb
-
-        Parameters:
-            keys (tuple): of key strs to be combined in order to form key
+        """Iterate over entries whose key matches a given prefix.
 
-        Returns:
-            iterator: of tuples of keys tuple and val serdering.SerderKERI for
-            each entry in db
+        Args:
+            keys (str | Iterable, optional): Prefix components used to filter
+                entries. Defaults to ``b""`` which matches all entries.
 
+        Yields:
+            tuple[tuple, Dicter]: A pair of the reconstructed key tuple and
+                the deserialized instance of ``self.klas``.
         """
         for key, val in self.db.getTopItemIter(db=self.sdb,
                                                top=self._tokey(keys)):
             yield self._tokeys(key), self.klas(raw=bytes(val))
 
     def cntAll(self):
-        """
-        Return count over the all the items in subdb
+        """Count the total number of entries in the sub-database.
 
         Returns:
-            count of all items
+            int: The total entry count.
         """
         return self.db.cntAll(db=self.sdb)
 
 
 class Noter(LMDBer):
-    """
-    Noter stores Notifications generated by the agent that are
-    intended to be read and dismissed by the controller of the agent.
+    """Persistent storage for :class:`Notice` objects and their signatures."""
 
-    """
     TailDirPath = os.path.join("keri", "not")
     AltTailDirPath = os.path.join(".keri", "not")
     TempPrefix = "keri_not_"
 
     def __init__(self, name="not", headDirPath=None, reopen=True, **kwa):
-        """
+        """Initialize the notifier database.
 
-        Parameters:
-            headDirPath:
-            perm:
-            reopen:
-            kwa:
+        Args:
+            name (str, optional): Database name. Defaults to ``"not"``.
+            headDirPath (str, optional): Base directory path. Defaults to
+                ``None``.
+            reopen (bool, optional): Whether to open the database immediately.
+                Defaults to ``True``.
+            **kwa: Additional keyword arguments forwarded to :class:`LMDBer`.
         """
         self.notes = None
         self.nidx = None
@@ -228,10 +241,16 @@ def __init__(self, name="not", headDirPath=None, reopen=True, **kwa):
         super(Noter, self).__init__(name=name, headDirPath=headDirPath, reopen=reopen, **kwa)
 
     def reopen(self, **kwa):
-        """
+        """Open or reopen the underlying LMDB environment.
+
+        Initializes the ``notes``, ``nidx``, and ``ncigs`` sub-databases.
 
-        :param kwa:
-        :return:
+        Args:
+            **kwa: Additional keyword arguments forwarded to
+                :meth:`LMDBer.reopen`.
+
+        Returns:
+            lmdb.Environment: The open LMDB environment.
         """
         super(Noter, self).reopen(**kwa)
 
@@ -242,13 +261,20 @@ def reopen(self, **kwa):
         return self.env
 
     def add(self, note, cigar):
-        """
-        Adds note to database, keyed by the datetime and said of the note.
+        """Add a new notice.
 
-        Parameters:
-            note (Notice): sad message content
-            cigar (Cigar): non-transferable signature over note
+        Indexes the notice by its ``rid`` under ``nidx``, stores the
+        signature under ``ncigs``, and stores the notice itself under the
+        composite key ``(dt, rid)`` in ``notes``. Does nothing if a notice
+        with the same ``rid`` already exists in the index.
 
+        Args:
+            note (Notice): The notice to store.
+            cigar (Cigar): Cryptographic signature over the serialized notice.
+
+        Returns:
+            bool: ``True`` if the notice was added, ``False`` if a notice with
+                the same ``rid`` already exists.
         """
         dt = note.datetime
         rid = note.rid
@@ -260,13 +286,19 @@ def add(self, note, cigar):
         return self.notes.pin(keys=(dt, rid), val=note)
 
     def update(self, note, cigar):
-        """
-        Adds note to database, keyed by the datetime and said of the note.
+        """Update an existing notice in place.
 
-        Parameters:
-            note (Notice): sad message content
-            cigar (Cigar): non-transferable signature over note
+        Overwrites the stored datetime index, signature, and notice body for
+        the given ``rid``. The notice must already exist in the index.
 
+        Args:
+            note (Notice): The updated notice. Its ``rid`` must match an
+                existing entry.
+            cigar (Cigar): Cryptographic signature over the serialized notice.
+
+        Returns:
+            bool: ``True`` if the notice was updated, ``False`` if no notice
+                with the given ``rid`` exists.
         """
         dt = note.datetime
         rid = note.rid
@@ -278,15 +310,15 @@ def update(self, note, cigar):
         return self.notes.pin(keys=(dt, rid), val=note)
 
     def get(self, rid):
-        """
-        Adds note to database, keyed by the datetime and said of the note.
+        """Retrieve a notice and its signature by identifier.
 
-        Parameters:
-            rid (str): qb64 random ID of note to get
+        Args:
+            rid (str): QB64 identifier of the notice.
 
         Returns:
-            (Notice, Cigar) = couple of notice object and accompanying signature
-
+            tuple[Notice, Cigar] | None: The notice and its signature, or
+                ``None`` if no notice with the given ``rid`` exists in the
+                index.
         """
         dt = self.nidx.get(keys=(rid,))
         if dt is None:
@@ -298,14 +330,17 @@ def get(self, rid):
         return note, cig
 
     def rem(self, rid):
-        """
-        Remove note from database if it exists
+        """Remove a notice by identifier.
+
+        Removes the notice from ``notes``, its index entry from ``nidx``,
+        and its signature from ``ncigs``.
 
-        Parameters:
-            rid (str): qb64 random ID of note to remove
+        Args:
+            rid (str): QB64 identifier of the notice.
 
         Returns:
-            bool:  True if deleted
+            bool: ``True`` if the notice was found and removed, ``False`` if
+                no notice with the given ``rid`` exists.
         """
         res = self.get(rid)
         if res is None:
@@ -319,23 +354,30 @@ def rem(self, rid):
         return self.notes.rem(keys=(dt, rid))
 
     def getNoteCnt(self):
-        """
-        Return count over the all Notes
+        """Return the total number of stored notices.
 
         Returns:
-            int: count of all items
-
+            int: Number of notices in the database.
         """
         return self.notes.cntAll()
 
     def getNotes(self, start=0, end=25):
-        """
-        Returns list of tuples (note, cigar) of notes for controller of agent
+        """Retrieve a slice of stored notices ordered by datetime.
 
-        Parameters:
-            start (int): number of item to start
-            end (int): number of last item to return
+        Skips the first ``start`` entries in iteration order, then collects
+        up to ``(end - start) + 1`` notices. Pass ``end=-1`` to collect all
+        remaining notices after ``start``.
 
+        Args:
+            start (int, optional): Zero-based count of notices to skip before
+                collecting. Defaults to ``0``.
+            end (int, optional): Inclusive upper bound controlling how many
+                notices are returned: ``(end - start) + 1`` notices are
+                collected. Pass ``-1`` to return all remaining notices after
+                ``start``. Defaults to ``25``.
+
+        Returns:
+            list[tuple[Notice, Cigar]]: Ordered list of notice/signature pairs.
         """
         if hasattr(start, "isoformat"):
             start = start.isoformat()
@@ -360,36 +402,38 @@ def getNotes(self, start=0, end=25):
 
 
 class Notifier:
-    """ Class for sending notifications to the controller of an agent.
-
-    The notifications are not just signals to reload data and not persistent messages that can be reread
-
-    """
+    """High-level interface for managing and signaling notifications."""
 
     def __init__(self, hby, signaler=None, noter=None):
-        """
-
-        Parameters:
-            hby (Habery): habery database environment with Signator
-            noter (Noter): database
-            signaler (Signaler): signaler for sending signals to controller that new data is available
+        """Initialize the notifier.
 
+        Args:
+            hby (Habery): Habitat environment providing a ``signator`` for
+                signing and verification.
+            signaler (Signaler, optional): Signaling interface used to push
+                notification events. Defaults to a new :class:`Signaler`
+                instance.
+            noter (Noter, optional): Persistent storage backend. Defaults to a
+                new :class:`Noter` instance scoped to ``hby.name``.
         """
         self.hby = hby
         self.signaler = signaler if signaler is not None else Signaler()
         self.noter = noter if noter is not None else Noter(name=hby.name, temp=hby.temp)
 
     def add(self, attrs):
-        """  Add unread notice to the end of the current list of notices
+        """Create and store a new unread notice.
+
+        Constructs a :class:`Notice` from ``attrs``, signs it with
+        ``hby.signator``, and persists it via :meth:`Noter.add`. On success,
+        pushes an ``"add"`` signal to the ``"/notification"`` topic.
 
         Args:
-            attrs (dict): body of a new unread notice to append to the current list of notices
+            attrs (dict): Notification payload.
 
         Returns:
-            bool: returns True if the notice was added
-
+            bool: ``True`` if the notice was created and stored, ``False`` if
+                a notice with the same identifier already exists.
         """
-
         note = notice(attrs, dt=nowIso8601())
         cig = self.hby.signator.sign(ser=note.raw)
         if self.noter.add(note, cig):
@@ -404,17 +448,19 @@ def add(self, attrs):
             return False
 
     def rem(self, rid):
-        """ Mark as Read
+        """Delete a notice by identifier.
 
-        Delete the note identified by the provided random ID
+        Retrieves the notice, removes it from storage, then verifies the
+        stored signature before pushing a ``"rem"`` signal. The signal is
+        only sent when both removal and signature verification succeed.
 
-        Parameters:
-            rid (str): qb64 random ID of the Note to delete
+        Args:
+            rid (str): QB64 identifier of the notice.
 
         Returns:
-            bool: True means the note was deleted, False otherwise
+            bool: ``True`` if the notice was found and removed, ``False`` if
+                no notice with the given ``rid`` exists.
         """
-
         res = self.noter.get(rid=rid)
         if res is None:
             return False
@@ -433,18 +479,22 @@ def rem(self, rid):
         return True
 
     def mar(self, rid):
-        """ Mark as Read
+        """Mark a notice as read.
 
-        Mark the note identified by the provided SAID as having been read by the controller of the agent
+        Retrieves the notice and verifies its stored signature before mutating
+        it. Sets the read flag to ``True``, re-signs the updated notice, and
+        persists it via :meth:`Noter.update`. Pushes a ``"mar"`` signal on
+        success.
 
-        Parameters:
-            rid (str): qb64 random ID of the Note to mark as read
+        Args:
+            rid (str): QB64 identifier of the notice.
 
         Returns:
-            bool: True means the note was marked as read, False otherwise
-
+            bool: ``True`` if the notice was found, verified, and marked as
+                read. ``False`` if the notice does not exist, the signature
+                verification fails, the notice was already marked as read, or
+                the update fails.
         """
-
         res = self.noter.get(rid=rid)
         if res is None:
             return False
@@ -474,23 +524,31 @@ def mar(self, rid):
         return False
 
     def getNoteCnt(self):
-        """
-        Return count over the all Notes
+        """Return the total number of notices.
 
         Returns:
-            int: count of all items
-
+            int: Number of notices in the backing store.
         """
         return self.noter.getNoteCnt()
 
     def getNotes(self, start=0, end=24):
-        """
-        Returns list of tuples (note, cigar) of notes for controller of agent
+        """Retrieve notices with signature verification.
 
-        Parameters:
-            start (int): number of item to start
-            end (int): number of last item to return
+        Delegates to :meth:`Noter.getNotes` and verifies the signature of
+        every returned notice.
+
+        Args:
+            start (int, optional): Zero-based count of notices to skip before
+                collecting. Defaults to ``0``.
+            end (int, optional): Inclusive upper bound passed to
+                :meth:`Noter.getNotes` controlling how many notices are
+                returned. Defaults to ``24``.
+
+        Returns:
+            list[Notice]: Verified notice instances.
 
+        Raises:
+            ValidationError: If the stored signature for any notice is invalid.
         """
         notesigs = self.noter.getNotes(start, end)
         notes = []
diff --git a/src/keri/app/oobiing.py b/src/keri/app/oobiing.py
index a0b637cd0..291f692fc 100644
--- a/src/keri/app/oobiing.py
+++ b/src/keri/app/oobiing.py
@@ -1,7 +1,9 @@
 # -*- encoding: utf-8 -*-
 """
-keri.kli.common.oobiing module
+keri.app.oobiing module
 
+Provides OOBI (Out-Of-Band Introduction) endpoint resources, handlers,
+and resolution workflows for discovering and verifying remote identifiers.
 """
 import datetime
 import json
@@ -14,7 +16,7 @@
 from hio.base import doing
 from hio.help import decking, ogler
 
-from .httping import Clienter,CESR_CONTENT_TYPE
+from .httping import Clienter, CESR_CONTENT_TYPE
 from .organizing import Organizer
 from .. import (Vrsn_1_0, Roles, Schemes, Ilks,
                 ValidationError, UnverifiedReplyError,
@@ -33,75 +35,65 @@
 
 
 def loadEnds(app, *, hby, prefix=""):
+    """Register OOBI HTTP endpoints on the Falcon application.
+
+    Args:
+        app (falcon.App): Falcon WSGI application instance.
+        hby (Habery): Identifier database environment.
+        prefix (str, optional): Route prefix for mounting endpoints.
+
+    Returns:
+        list: Empty list (no doers registered).
+    """
     oobiEnd = OobiResource(hby=hby)
     app.add_route(prefix + "/oobi", oobiEnd)
     return []
 
 
 def loadHandlers(hby, exc, notifier):
-    """ Load handlers for the peer-to-peer delegation protocols
+    """Load handlers for the peer-to-peer delegation protocols
 
-    Parameters:
+    Args:
         hby (Habery): Database and keystore for environment
         exc (Exchanger): Peer-to-peer message router
         notifier (Notifier): Outbound notifications
-
     """
     oobireq = OobiRequestHandler(hby=hby, notifier=notifier)
     exc.addHandler(oobireq)
 
 
 class OobiResource:
-    """
-    Resource for managing OOBIs
-
-    """
+    """Falcon resource for managing OOBI generation and resolution."""
 
     def __init__(self, hby):
-        """ Create Endpoints for discovery and resolution of OOBIs
-
-        Parameters:
-            hby (Habery): identifier database environment
+        """Initialize OOBI resource endpoints.
 
+        Args:
+            hby (Habery): Identifier database environment.
         """
         self.hby = hby
 
     def on_get_alias(self, req, rep, alias=None):
-        """ OOBI GET endpoint
-
-        Parameters:
-            req: falcon.Request HTTP request
-            rep: falcon.Response HTTP response
-            alias: option route parameter for specific identifier to get
-
-        .. code-block:: none
-
-            ---
-            summary:  Get OOBI for specific identifier
-            description:  Generate OOBI for the identifier of the specified alias and role
-            tags:
-               - OOBIs
-            parameters:
-              - in: path
-                name: alias
-                schema:
-                  type: string
-                required: true
-                description: human readable alias for the identifier generate OOBI for
-              - in: query
-                name: role
-                schema:
-                  type: string
-                required: true
-                description: role for which to generate OOBI
-            responses:
-                200:
-                  description: An array of Identifier key state information
-                  content:
-                      application/json:
-                        schema:
-                            description: Key state information for current identifiers
-                            type: object
+        """Handle GET requests to generate OOBIs for an identifier.
+
+        Args:
+            req (falcon.Request): HTTP request object.
+            rep (falcon.Response): HTTP response object.
+            alias (str, optional): Human-readable alias of the identifier.
+
+        Query Parameters:
+            role (str): Role for which to generate OOBIs. Supported values
+                include witness and controller.
+
+        Behavior:
+            - Resolves the identifier associated with the alias.
+            - Generates OOBI URLs for the requested role.
+            - Returns URLs for witnesses or controller endpoints.
+
+        Responses:
+            200: JSON object containing generated OOBIs.
+            400: Invalid alias.
+            404: Missing endpoints or unsupported role.
         """
 
         hab = self.hby.habByName(alias)
@@ -146,41 +138,26 @@ def on_get_alias(self, req, rep, alias=None):
         rep.data = json.dumps(res).encode("utf-8")
 
     def on_post(self, req, rep):
-        """ Resolve OOBI endpoint.
-
-        Parameters:
-            req: falcon.Request HTTP request
-            rep: falcon.Response HTTP response
-
-        .. code-block:: none
-
-            ---
-            summary: Resolve OOBI and assign an alias for the remote identifier
-            description: Resolve OOBI URL or rpy message by process results of request
-                and assign alias in contact data for resolved identifier
-            tags:
-               - OOBIs
-            requestBody:
-                required: true
-                content:
-                  application/json:
-                    schema:
-                        description: OOBI
-                        properties:
-                            oobialias:
-                              type: string
-                              description: alias to assign to the identifier resolved from this OOBI
-                              required: false
-                            url:
-                              type: string
-                              description:  URL OOBI
-                            rpy:
-                              type: object
-                              description: unsigned KERI rpy event message with endpoints
-            responses:
-               202:
-                  description: OOBI resolution to key state successful
-
+        """Handle POST requests to resolve an OOBI.
+
+        Args:
+            req (falcon.Request): HTTP request object.
+            rep (falcon.Response): HTTP response object.
+
+        Request Body:
+            application/json:
+                url (str, optional): OOBI URL to resolve.
+                rpy (dict, optional): Unsigned KERI reply message (not implemented).
+                oobialias (str, optional): Alias to assign to resolved identifier.
+
+        Behavior:
+            - Stores OOBI URL for asynchronous resolution.
+            - Optionally associates an alias with the resolved identifier.
+
+        Responses:
+            202: OOBI accepted for processing.
+            400: Invalid request body.
+            501: `rpy` support not implemented.
         """
         body = req.get_media()
 
@@ -207,30 +184,33 @@ def on_post(self, req, rep):
 
 
 class OobiRequestHandler:
-    """
-    Handler for oobi notification EXN messages
-
-    """
+    """Handler for processing OOBI request EXN messages."""
     resource = "/oobis"
 
     def __init__(self, hby, notifier):
-        """
-
-        Parameters:
-            hby (Habery) database environment of the controller
-            notifier (Notifier) notifier to convert OOBI request exn messages to controller notifications
+        """Initialize OOBI request handler.
 
+        Args:
+            hby (Habery): Identifier database environment.
+            notifier (Notifier): Notification dispatcher for outbound events.
         """
         self.hby = hby
         self.notifier = notifier
 
     def handle(self, serder, attachments=None):
-        """  Do route specific processsing of OOBI request messages
+        """Process an incoming OOBI request EXN message.
 
-        Parameters:
-            serder (Serder): Serder of the exn OOBI request message
-            attachments (list): list of tuples of pather, CESR SAD path attachments to the exn event
+        Args:
+            serder (Serder): Serialized EXN message.
+            attachments (list, optional): CESR attachments associated with the message.
 
+        Behavior:
+            - Extracts OOBI URL from the message payload.
+            - Stores the OOBI for later resolution.
+            - Emits a notification for UI or downstream processing.
+
+        Notes:
+            Invalid messages missing an ``oobi`` field are ignored.
         """
         src = serder.pre
         pay = serder.ked['a']
@@ -257,6 +237,22 @@ def handle(self, serder, attachments=None):
 
 
 def oobiRequestExn(hab, dest, oobi):
+    """Create an EXN message requesting OOBI resolution.
+
+    Args:
+        hab (Hab): Local habitat initiating the request.
+        dest (str): Recipient identifier prefix.
+        oobi (str): OOBI URL.
+
+    Returns:
+        tuple:
+            - Serder: Constructed EXN message.
+            - bytearray: Attachments for transmission.
+
+    Behavior:
+        - Constructs a peer-to-peer EXN message.
+        - Endorses the message for sending.
+    """
     data = dict(
         dest=dest,
         oobi=oobi
@@ -272,19 +268,27 @@ def oobiRequestExn(hab, dest, oobi):
 
 
 class Oobiery:
-    """ Resolver for OOBIs
+    """Resolver and processor for OOBIs.
 
+    Coordinates retrieval, parsing, verification, and persistence
+    of OOBI data from remote endpoints.
     """
 
     RetryDelay = 30
 
     def __init__(self, hby, rvy=None, clienter=None, cues=None):
-        """  DoDoer to handle the request and parsing of OOBIs
-
-        Parameters:
-            hby (Habery): database environment
-            clienter (Clienter): DoDoer client provider responsible for managing HTTP client requests
-            cues (decking.Deck): outbound cues from processing oobis
+        """Initialize OOBI resolver.
+
+        Args:
+            hby (Habery): Identifier database environment.
+            rvy (Revery, optional): Reply verifier for processing `rpy` messages.
+            clienter (Clienter, optional): HTTP client manager.
+            cues (decking.Deck, optional): Output queue for resolution events.
+
+        Behavior:
+            - Sets up HTTP client handling.
+            - Initializes parser for CESR and KERI message streams.
+            - Registers reply routes if a verifier is provided.
         """
 
         self.hby = hby
@@ -307,54 +311,52 @@ def __init__(self, hby, rvy=None, clienter=None, cues=None):
         self.doers = [self.clienter, doing.doify(self.scoobiDo)]
 
     def registerReplyRoutes(self, router):
-        """ Register the routes for processing messages embedded in `rpy` event messages
+        """Register reply routes for OOBI-related messages.
 
-        The Oobiery handles rpy messages with the /introduce route by processing the contained oobi
-
-        Parameters:
-            router(Router): reply message router
+        Args:
+            router (Router): Reply message router.
 
+        Behavior:
+            Adds support for processing ``/introduce`` reply messages.
         """
         router.addRoute("/introduce", self)
 
     def processReply(self, *, serder, diger, route, cigars=None, tsgs=None, **kwargs):
-        """
-        Process one reply message for route = /introduce
-        with either attached nontrans receipt couples in cigars or attached trans
-        indexed sig groups in tsgs.
-        Assumes already validated diger, dater, and route from serder.ked
-
-        Parameters:
-            serder (SerderKERI): instance of reply msg (SAD)
-            diger (Diger): instance from said in serder (SAD)
-            route (str): reply route
-            cigars (list): of Cigar instances that contain nontrans signing couple
-                signature in .raw and public key in .verfer
-            tsgs (list): tuples (quadruples) of form
-                (prefixer, seqner, diger, [sigers]) where:
-                prefixer is pre of trans endorser
-                seqner is sequence number of trans endorser's est evt for keys for sigs
-                diger is digest of trans endorser's est evt for keys for sigs
-                [sigers] is list of indexed sigs from trans endorser's keys from est evt
-
-        OobiRecord:
-            date: str = date time of reply message of the introduction
+        """Process a reply message for OOBI introduction.
 
-        Reply Message::
+        Args:
+            serder (SerderKERI): Parsed reply message.
+            diger (Diger): Digest of the message.
+            route (str): Reply route (must be ``/introduce``).
+            cigars (list, optional): Non-transferable signature attachments.
+            tsgs (list, optional): Transferable signature groups.
+
+        Raises:
+            ValidationError: Invalid route or missing required fields.
+            ConfigurationError: Resolver not configured for reply handling.
+            UnverifiedReplyError: Reply signature verification failed.
+
+        Behavior:
+            - Validates message structure and required fields.
+            - Verifies signatures using the reply verifier.
+            - Stores OOBI record if verification succeeds.
 
-            {
-              "v" : "KERI10JSON00011c_",
-              "t" : "rpy",
-              "d": "EZ-i0d8JZAoTNZH3ULaU6JR2nmwyvYAfSVPzhzS6b5CM",
-              "dt": "2020-08-22T17:50:12.988921+00:00",
-              "r" : "/introduce",
-              "a" :
-              {
-                 "cid": "ENcOes8_t2C7tck4X4j61fSm0sWkLbZrEZffq7mSn8On",
-                 "oobi":  "http://localhost:5632/oobi/ENcOes8_t2C7tck4X4j61fSm0sWkLbZrEZffq7mSn8On/witness",
-              }
-            }
+        Reply Message::
 
+            .. code-block:: json
+
+                {
+                "v" : "KERI10JSON00011c_",
+                "t" : "rpy",
+                "d": "EZ-i0d8JZAoTNZH3ULaU6JR2nmwyvYAfSVPzhzS6b5CM",
+                "dt": "2020-08-22T17:50:12.988921+00:00",
+                "r" : "/introduce",
+                "a" :
+                {
+                    "cid": "ENcOes8_t2C7tck4X4j61fSm0sWkLbZrEZffq7mSn8On",
+                    "oobi":  "http://localhost:5632/oobi/ENcOes8_t2C7tck4X4j61fSm0sWkLbZrEZffq7mSn8On/witness"
+                }
+                }
         """
         if route != "/introduce":
             raise ValidationError(f"Usupported route={route} in {Ilks.rpy} "
@@ -391,17 +393,17 @@ def processReply(self, *, serder, diger, route, cigars=None, tsgs=None, **kwargs
         self.hby.db.oobis.put(keys=(oobi,), val=obr)
 
     def scoobiDo(self, tymth=None, tock=0.0, **kwa):
-        """
-        Returns doifiable Doist compatibile generator method (doer dog) to process
-            .exc responses and pass them on to the HTTPRespondant
+        """Generator for periodic OOBI processing.
+
+        Args:
+            tymth (Callable, optional): Time function from scheduler.
+            tock (float, optional): Scheduling interval.
 
-        Parameters:
-            tymth (function): injected function wrapper closure returned by .tymen() of
-                Tymist instance. Calling tymth() returns associated Tymist .tyme.
-            tock (float): injected initial tock value
+        Yields:
+            float: Next scheduling interval.
 
-        Usage:
-            add result of doify on this method to doers list
+        Behavior:
+            Continuously processes OOBI workflows.
         """
         _ = (yield tock)
 
@@ -410,9 +412,13 @@ def scoobiDo(self, tymth=None, tock=0.0, **kwa):
             yield tock
 
     def processFlows(self):
-        """
-        Process OOBI URLs by requesting from the endpoint and parsing the results
+        """Execute all OOBI processing stages.
 
+        Stages include:
+            - OOBI discovery
+            - Client response handling
+            - Retry scheduling
+            - Multi-OOBI resolution
         """
         self.processOobis()
         self.processClients()
@@ -420,10 +426,15 @@ def processFlows(self):
         self.processMOOBIs()
 
     def processOobis(self):
-        """ Process OOBI records loaded for discovery
+        """Process pending OOBI records for discovery.
 
-        There should be only one OOBIERY that minds the OOBI table, this should read from the table like an escrow
+        Behavior:
+            - Iterates over stored OOBIs.
+            - Determines type (standard, data, well-known).
+            - Initiates HTTP requests for resolution.
 
+        Notes:
+            Acts as the primary escrow processor for OOBI records.
         """
         for (url,), obr in self.hby.db.oobis.getTopItemIter():
             try:
@@ -475,8 +486,12 @@ def processOobis(self):
                 print(f"error requesting invalid OOBI URL {ex}", url)
 
     def processClients(self):
-        """ Process Client responses by parsing the messages and removing the client/doer
+        """Process HTTP client responses for OOBI requests.
 
+        Behavior:
+            - Parses responses based on content type.
+            - Handles CESR streams, schema responses, and JSON replies.
+            - Updates resolution state and persists results.
         """
         for (url,), obr in self.hby.db.coobi.getTopItemIter():
             if url not in self.clients:
@@ -581,8 +596,11 @@ def processClients(self):
                 self.cues.append(dict(kin=obr.state, oobi=url))
 
     def processMOOBIs(self):
-        """ Process Client responses by parsing the messages and removing the client/doer
+        """Process multi-OOBI (MOOBI) resolution results.
 
+        Behavior:
+            - Aggregates results from multiple OOBI URLs.
+            - Marks overall resolution state when complete.
         """
         for (url,), obr in self.hby.db.moobi.getTopItemIter():
             result = Result.resolved
@@ -601,8 +619,10 @@ def processMOOBIs(self):
                 self.hby.db.roobi.put(keys=(url,), val=obr)
 
     def processRetries(self):
-        """ Process Client responses by parsing the messages and removing the client/doer
+        """Retry failed OOBI resolutions after delay.
 
+        Behavior:
+            - Moves expired retry records back into active processing.
         """
         for (url,), obr in self.hby.db.eoobi.getTopItemIter():
             last = fromIso8601(obr.date)
@@ -613,6 +633,16 @@ def processRetries(self):
                 self.hby.db.oobis.pin(keys=(url,), val=obr)
 
     def request(self, url, obr):
+        """Initiate HTTP request for an OOBI.
+
+        Args:
+            url (str): OOBI URL.
+            obr (OobiRecord): Associated OOBI record.
+
+        Behavior:
+            - Creates HTTP client request.
+            - Moves record into client-processing escrow.
+        """
         client = self.clienter.request("GET", url=url)
         if client is None:
             self.hby.db.oobis.rem(keys=(url,))
@@ -624,6 +654,20 @@ def request(self, url, obr):
         self.hby.db.coobi.pin(keys=(url,), val=obr)
 
     def processMultiOobiRpy(self, url, serder, mobr):
+        """Process multi-OOBI reply message.
+
+        Args:
+            url (str): Source OOBI URL.
+            serder (SerderKERI): Parsed reply message.
+            mobr (OobiRecord): Multi-OOBI record.
+
+        Returns:
+            str: Resolution result state.
+
+        Behavior:
+            - Validates identifier consistency.
+            - Expands into multiple OOBI requests.
+        """
         data = serder.ked["a"]
         cid = data["aid"]
 
@@ -644,13 +688,14 @@ def processMultiOobiRpy(self, url, serder, mobr):
 
 
 class Authenticator:
+    """Handler for well-known OOBI-based authentication workflows."""
 
     def __init__(self, hby, clienter=None):
-        """
+        """Initialize authenticator.
 
-        Parameters:
-            hby (Habery): Identifier database environment
-            clienter (Clienter): DoDoer client provider responsible for managing HTTP client requests
+        Args:
+            hby (Habery): Identifier database environment.
+            clienter (Clienter, optional): HTTP client manager.
         """
         self.hby = hby
         self.clienter = clienter if clienter is not None else Clienter()
@@ -658,6 +703,12 @@ def __init__(self, hby, clienter=None):
         self.doers = [self.clienter, doing.doify(self.authzDo)]
 
     def request(self, wurl, obr):
+        """Initiate request for well-known OOBI authentication.
+
+        Args:
+            wurl (str): Well-known OOBI URL.
+            obr (OobiRecord): Associated record.
+        """
         client = self.clienter.request("GET", wurl)
 
         self.clients[wurl] = client
@@ -665,22 +716,25 @@ def request(self, wurl, obr):
         self.hby.db.mfa.pin(keys=(wurl,), val=obr)
 
     def addAuthToAid(self, cid, url):
+        """Associate authentication URL with an identifier.
+
+        Args:
+            cid (str): Controller identifier prefix.
+            url (str): Authentication endpoint URL.
+        """
         now = nowIso8601()
         wkan = WellKnownAuthN(url=url, dt=now)
         self.hby.db.wkas.add(keys=(cid,), val=wkan)
 
     def authzDo(self, tymth=None, tock=0.0, **kwa):
-        """
-        Returns doifiable Doist compatibile generator method (doer dog) to process
-            .exc responses and pass them on to the HTTPRespondant
+        """Generator for authentication processing loop.
 
-        Parameters:
-            tymth (function): injected function wrapper closure returned by .tymen() of
-                Tymist instance. Calling tymth() returns associated Tymist .tyme.
-            tock (float): injected initial tock value
+        Args:
+            tymth (Callable, optional): Time function from scheduler.
+            tock (float, optional): Scheduling interval.
 
-        Usage:
-            add result of doify on this method to doers list
+        Yields:
+            float: Next scheduling interval.
         """
         _ = (yield tock)
 
@@ -689,16 +743,17 @@ def authzDo(self, tymth=None, tock=0.0, **kwa):
             yield tock
 
     def processFlows(self):
-        """ Process well-known authentication URLs """
+        """Process well-known authentication URLs """
 
         self.processWoobis()
         self.processMultiFactorAuth()
 
     def processWoobis(self):
-        """ Process well-known OOBIs saved as multi-factor auth records
-
-        Process wOOBI URLs by requesting from the endpoint and confirming the results
+        """Process well-known OOBI records for authentication.
 
+        Behavior:
+            - Filters valid well-known OOBIs.
+            - Initiates authentication requests for known identifiers.
         """
         for (wurl,), obr in self.hby.db.woobi.getTopItemIter():
             # Find any woobis that match and can be used to perform MFA for this resolved AID
@@ -714,8 +769,13 @@ def processWoobis(self):
                 self.hby.db.woobi.rem(keys=(wurl,))
 
     def processMultiFactorAuth(self):
-        """ Process Client responses by parsing the messages and removing the client
+        """Processes responses for multi-factor authentication requests.
+
+        Iterates through pending MFA items, matches them with client responses,
+        and updates the database state based on the HTTP status code.
 
+        Note:
+            This method modifies the internal `hby.db` and `clients` collection.
         """
         for (wurl,), obr in self.hby.db.mfa.getTopItemIter():
             if wurl not in self.clients: