diff --git a/README.md b/README.md index 540223ab..44897e86 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,12 @@ All Agent db access is through the associated Agent. make build-keria ``` +#### Run with docker +* Specify an entrypoint with proper configuration, for instance if you want to use the demo-witness-oobis that is under the scripts/keri/cf dir: +``` +ENTRYPOINT ["keria", "start", "--config-file", "demo-witness-oobis", "--config-dir", "./scripts"] +``` +You can see a [working example here](https://github.com/WebOfTrust/signify-ts/blob/main/docker-compose.yaml). ### Running Tests diff --git a/images/keria.dockerfile b/images/keria.dockerfile index 5d2f0bb5..fa500481 100644 --- a/images/keria.dockerfile +++ b/images/keria.dockerfile @@ -1,55 +1,10 @@ -# Builder stage -FROM python:3.12-alpine3.19 as builder +FROM weboftrust/keri:1.2.0-dev6 -# Install compilation dependencies -RUN apk --no-cache add \ - bash \ - alpine-sdk \ - libffi-dev \ - libsodium \ - libsodium-dev +WORKDIR /usr/local/var -SHELL ["/bin/bash", "-c"] +RUN mkdir keria +COPY . /usr/local/var/keria -# Install Rust for blake3 dependency build -RUN curl https://sh.rustup.rs -sSf | bash -s -- -y +WORKDIR /usr/local/var/keria -WORKDIR /keria - -RUN python -m venv venv -ENV PATH=/keria/venv/bin:${PATH} -RUN pip install --upgrade pip - -# Copy in Python dependency files -COPY requirements.txt setup.py ./ -# "src/" dir required for installation of dependencies with setup.py -RUN mkdir /keria/src -# Install Python dependencies -RUN . "$HOME/.cargo/env" && \ - pip install -r requirements.txt - -# Runtime stage -FROM python:3.12-alpine3.19 - -# Install runtime dependencies -RUN apk --no-cache add \ - bash \ - alpine-sdk \ - libsodium-dev - -WORKDIR /keria - -# Copy over compiled dependencies -COPY --from=builder /keria /keria -# Copy in KERIA source files - enables near instantaneous builds for source only changes -RUN mkdir -p /usr/local/var/keri -ENV KERI_AGENT_CORS=${KERI_AGENT_CORS:-false} -ENV PATH=/keria/venv/bin:${PATH} - -EXPOSE 3901 -EXPOSE 3902 -EXPOSE 3903 - -COPY src/ src/ - -ENTRYPOINT ["keria", "start", "--config-file", "demo-witness-oobis", "--config-dir", "./scripts"] +RUN pip install -r requirements.txt \ No newline at end of file diff --git a/setup.py b/setup.py index 53918fbf..3e05e7dc 100644 --- a/setup.py +++ b/setup.py @@ -76,7 +76,7 @@ python_requires='>=3.12.2', install_requires=[ 'hio>=0.6.12', - 'keri @ git+https://github.com/WebOfTrust/keripy.git@main', + 'keri>=1.2.0.dev6', 'mnemonic>=0.20', 'multicommand>=1.0.0', 'falcon>=3.1.3', diff --git a/src/keria/app/aiding.py b/src/keria/app/aiding.py index 45e49b03..5f575d62 100644 --- a/src/keria/app/aiding.py +++ b/src/keria/app/aiding.py @@ -23,6 +23,7 @@ logger = ogler.getLogger() + def loadEnds(app, agency, authn): groupEnd = AgentResourceEnd(agency=agency, authn=authn) app.add_route("/agent/{caid}", groupEnd) @@ -70,14 +71,14 @@ def loadEnds(app, agency, authn): class AgentResourceEnd: - """ Resource class for getting agent specific launch information """ + """Resource class for getting agent specific launch information""" def __init__(self, agency, authn): self.agency = agency self.authn = authn def on_get(self, _, rep, caid): - """ GET endpoint for Keystores + """GET endpoint for Keystores Get keystore status @@ -108,13 +109,19 @@ def on_get(self, _, rep, caid): """ agent = self.agency.get(caid) if agent is None: - raise falcon.HTTPNotFound(description=f"not agent found for controller {caid}") + raise falcon.HTTPNotFound( + description=f"not agent found for controller {caid}" + ) if agent.pre not in agent.hby.kevers: - raise falcon.HTTPBadRequest(description=f"invalid agent configuration, {agent.pre} not found") + raise falcon.HTTPBadRequest( + description=f"invalid agent configuration, {agent.pre} not found" + ) if agent.caid not in agent.hby.kevers: - raise falcon.HTTPBadRequest(description=f"invalid controller configuration, {agent.caid} not found") + raise falcon.HTTPBadRequest( + description=f"invalid controller configuration, {agent.caid} not found" + ) pidx = 0 for name, _ in agent.hby.db.names.getItemIter(): @@ -124,17 +131,14 @@ def on_get(self, _, rep, caid): # pidx = agent.hby.db.habs.cntAll() state = asdict(agent.hby.kevers[agent.caid].state()) - key = dbing.dgKey(state['i'], state['ee']['d']) # digest key + key = dbing.dgKey(state["i"], state["ee"]["d"]) # digest key msg = agent.hby.db.getEvt(key) eserder = serdering.SerderKERI(raw=bytes(msg)) body = dict( agent=asdict(agent.hby.kevers[agent.pre].state()), - controller=dict( - state=state, - ee=eserder.ked - ), - pidx=pidx + controller=dict(state=state, ee=eserder.ked), + pidx=pidx, ) if (sxlt := agent.mgr.sxlt) is not None: @@ -213,16 +217,24 @@ def on_put(self, req, rep, caid): body = req.get_media() if "rot" not in body: - raise falcon.HTTPBadRequest(description="required field 'rot' missing from body") + raise falcon.HTTPBadRequest( + description="required field 'rot' missing from body" + ) if "sigs" not in body: - raise falcon.HTTPBadRequest(description="required field 'sigs' missing from body") + raise falcon.HTTPBadRequest( + description="required field 'sigs' missing from body" + ) if "sxlt" not in body: - raise falcon.HTTPBadRequest(description="required field 'sxlt' missing from body") + raise falcon.HTTPBadRequest( + description="required field 'sxlt' missing from body" + ) if "keys" not in body: - raise falcon.HTTPBadRequest(description="required field 'keys' missing from body") + raise falcon.HTTPBadRequest( + description="required field 'keys' missing from body" + ) rot = serdering.SerderKERI(sad=body["rot"]) sigs = body["sigs"] @@ -240,12 +252,18 @@ def on_put(self, req, rep, caid): for pre, val in keys.items(): if "sxlt" in val: if (sp := agent.mgr.rb.sprms.get(pre)) is None: - raise ValueError("Attempt to update sxlt for nonexistent or invalid pre={}.".format(pre)) + raise ValueError( + "Attempt to update sxlt for nonexistent or invalid pre={}.".format( + pre + ) + ) sp.sxlt = val["sxlt"] if not agent.mgr.rb.sprms.pin(pre, val=sp): - raise ValueError("Unable to update sxlt prms for pre={}.".format(pre)) + raise ValueError( + "Unable to update sxlt prms for pre={}.".format(pre) + ) elif "prxs" in val: hab = agent.hby.habs[pre] @@ -260,7 +278,9 @@ def on_put(self, req, rep, caid): if "nxts" in val: nxts = val["nxts"] if len(nxts) != len(digers): - raise ValueError("If encrypted private next keys are provided, must match digers") + raise ValueError( + "If encrypted private next keys are provided, must match digers" + ) for idx, prx in enumerate(nxts): cipher = core.Cipher(qb64=prx) @@ -275,13 +295,17 @@ def interact(req, rep, agent, caid): body = req.get_media() if "ixn" not in body: - raise falcon.HTTPBadRequest(description="required field 'ixn' missing from body") + raise falcon.HTTPBadRequest( + description="required field 'ixn' missing from body" + ) if "sigs" not in body: - raise falcon.HTTPBadRequest(description="required field 'sigs' missing from body") + raise falcon.HTTPBadRequest( + description="required field 'sigs' missing from body" + ) - ked = body['ixn'] - sigs = body['sigs'] + ked = body["ixn"] + sigs = body["sigs"] ixn = serdering.SerderKERI(sad=ked) sigers = [core.Siger(qb64=sig) for sig in sigs] @@ -300,11 +324,11 @@ def anchorSeals(agent, ixn): if len(a) == 0: return - delegator = coring.Saider(qb64=ixn.ked['d']) - delegatorsn = coring.Seqner(snh=ixn.ked['s']) + delegator = coring.Saider(qb64=ixn.ked["d"]) + delegatorsn = coring.Seqner(snh=ixn.ked["s"]) seal = a[0] - prefixer = coring.Prefixer(qb64=seal['i']) + prefixer = coring.Prefixer(qb64=seal["i"]) saider = coring.Saider(qb64=seal["d"]) couple = delegatorsn.qb64b + delegator.qb64b @@ -313,7 +337,7 @@ def anchorSeals(agent, ixn): class IdentifierCollectionEnd: - """ Resource class for creating and managing identifiers """ + """Resource class for creating and managing identifiers""" @staticmethod def on_options(req, rep): @@ -322,7 +346,7 @@ def on_options(req, rep): @staticmethod def on_get(req, rep): - """ Identifier List GET endpoint + """Identifier List GET endpoint Parameters: req: falcon.Request HTTP request @@ -386,7 +410,7 @@ def on_get(req, rep): @staticmethod def on_post(req, rep): - """ Inception event POST endpoint + """Inception event POST endpoint Parameters: req (Request): falcon.Request HTTP request object @@ -444,58 +468,91 @@ def on_post(req, rep): sigers = [core.Siger(qb64=sig) for sig in sigs] if agent.hby.habByName(name) is not None: - raise falcon.HTTPBadRequest(title=f"AID with name {name} already incepted") + raise falcon.HTTPBadRequest( + title=f"AID with name {name} already incepted" + ) - if 'b' in icp: - for wit in icp['b']: + if "b" in icp: + for wit in icp["b"]: urls = agent.agentHab.fetchUrls(eid=wit, scheme=kering.Schemes.http) if not urls and wit not in agent.hby.kevers: - raise falcon.HTTPBadRequest(description=f'unknown witness {wit}') + raise falcon.HTTPBadRequest( + description=f"unknown witness {wit}" + ) - if 'di' in icp and icp["di"] not in agent.hby.kevers: - raise falcon.HTTPBadRequest(description=f'unknown delegator {icp["di"]}') + if "di" in icp and icp["di"] not in agent.hby.kevers: + raise falcon.HTTPBadRequest( + description=f'unknown delegator {icp["di"]}' + ) # client is requesting agent to join multisig group if "group" in body: group = body["group"] if "mhab" not in group: - raise falcon.HTTPBadRequest(description=f'required field "mhab" missing from body.group') + raise falcon.HTTPBadRequest( + description=f'required field "mhab" missing from body.group' + ) mpre = group["mhab"]["prefix"] if mpre not in agent.hby.habs: - raise falcon.HTTPBadRequest(description=f'signing member {mpre} not a local AID') + raise falcon.HTTPBadRequest( + description=f"signing member {mpre} not a local AID" + ) mhab = agent.hby.habs[mpre] if "keys" not in group: - raise falcon.HTTPBadRequest(description=f'required field "keys" missing from body.group') + raise falcon.HTTPBadRequest( + description=f'required field "keys" missing from body.group' + ) keys = group["keys"] verfers = [coring.Verfer(qb64=key) for key in keys] if mhab.kever.fetchLatestContribTo(verfers=verfers) is None: - raise falcon.HTTPBadRequest(description=f"Member hab={mhab.pre} not a participant in " - f"event for this group hab.") + raise falcon.HTTPBadRequest( + description=f"Member hab={mhab.pre} not a participant in " + f"event for this group hab." + ) if "ndigs" not in group: - raise falcon.HTTPBadRequest(description=f'required field "ndigs" missing from body.group') + raise falcon.HTTPBadRequest( + description=f'required field "ndigs" missing from body.group' + ) ndigs = group["ndigs"] digers = [coring.Diger(qb64=ndig) for ndig in ndigs] states = httping.getRequiredParam(body, "smids") rstates = httping.getRequiredParam(body, "rmids") - smids = [state['i'] for state in states] - rmids = [rstate['i'] for rstate in rstates] - hab = agent.hby.makeSignifyGroupHab(name, mhab=mhab, smids=smids, rmids=rmids, serder=serder, - sigers=sigers) + + hab = agent.hby.makeSignifyGroupHab( + name, + mhab=mhab, + smids=states, + rmids=rstates, + serder=serder, + sigers=sigers, + ) try: - agent.inceptGroup(pre=serder.pre, mpre=mhab.pre, verfers=verfers, digers=digers) + agent.inceptGroup( + pre=serder.pre, mpre=mhab.pre, verfers=verfers, digers=digers + ) except ValueError as e: agent.hby.deleteHab(name=name) raise falcon.HTTPInternalServerError(description=f"{e.args[0]}") # Generate response, a long running operaton indicator for the type - agent.groups.append(dict(pre=hab.pre, serder=serder, sigers=sigers, smids=states, rmids=rstates)) - op = agent.monitor.submit(serder.pre, longrunning.OpTypes.group, metadata=dict(sn=0)) + agent.groups.append( + dict( + pre=hab.pre, + serder=serder, + sigers=sigers, + smids=states, + rmids=rstates, + ) + ) + op = agent.monitor.submit( + serder.pre, longrunning.OpTypes.group, metadata=dict(sn=0) + ) rep.content_type = "application/json" rep.status = falcon.HTTP_202 @@ -517,7 +574,12 @@ def on_post(req, rep): rand = body[Algos.randy] hab = agent.hby.makeSignifyHab(name, serder=serder, sigers=sigers) try: - agent.inceptRandy(pre=serder.pre, verfers=serder.verfers, digers=serder.ndigers, **rand) + agent.inceptRandy( + pre=serder.pre, + verfers=serder.verfers, + digers=serder.ndigers, + **rand, + ) except ValueError as e: agent.hby.deleteHab(name=name) raise falcon.HTTPInternalServerError(description=f"{e.args[0]}") @@ -526,47 +588,63 @@ def on_post(req, rep): extern = body[Algos.extern] hab = agent.hby.makeSignifyHab(name, serder=serder, sigers=sigers) try: - agent.inceptExtern(pre=serder.pre, verfers=serder.verfers, digers=serder.ndigers, **extern) + agent.inceptExtern( + pre=serder.pre, + verfers=serder.verfers, + digers=serder.ndigers, + **extern, + ) except ValueError as e: agent.hby.deleteHab(name=name) raise falcon.HTTPInternalServerError(description=f"{e.args[0]}") else: raise falcon.HTTPBadRequest( - description="invalid request: one of group, rand or salt field required") + description="invalid request: one of group, rand or salt field required" + ) # create Hab and incept the key store (if any) # Generate response, either the serder or a long running operaton indicator for the type rep.content_type = "application/json" if hab.kever.delpre: agent.anchors.append(dict(pre=hab.pre, sn=0)) - op = agent.monitor.submit(hab.kever.prefixer.qb64, longrunning.OpTypes.delegation, - metadata=dict(pre=hab.pre, sn=0)) + op = agent.monitor.submit( + hab.kever.prefixer.qb64, + longrunning.OpTypes.delegation, + metadata=dict(pre=hab.pre, sn=0), + ) rep.status = falcon.HTTP_202 rep.data = op.to_json().encode("utf-8") elif hab.kever.wits: agent.witners.append(dict(serder=serder)) - op = agent.monitor.submit(hab.kever.prefixer.qb64, longrunning.OpTypes.witness, - metadata=dict(sn=0)) + op = agent.monitor.submit( + hab.kever.prefixer.qb64, + longrunning.OpTypes.witness, + metadata=dict(sn=0), + ) rep.status = falcon.HTTP_202 rep.data = op.to_json().encode("utf-8") else: rep.status = falcon.HTTP_202 - op = agent.monitor.submit(hab.kever.prefixer.qb64, longrunning.OpTypes.done, - metadata=dict(response=serder.ked)) + op = agent.monitor.submit( + hab.kever.prefixer.qb64, + longrunning.OpTypes.done, + metadata=dict(response=serder.ked), + ) rep.data = op.to_json().encode("utf-8") except (kering.AuthError, ValueError) as e: raise falcon.HTTPBadRequest(description=e.args[0]) + class IdentifierResourceEnd: - """ Resource class for updating and deleting identifiers """ + """Resource class for updating and deleting identifiers""" @staticmethod def on_get(req, rep, name): - """ Identifier GET endpoint + """Identifier GET endpoint Parameters: req: falcon.Request HTTP request @@ -599,7 +677,9 @@ def on_get(req, rep, name): agent = req.context.agent hab = agent.hby.habByName(name) if hab is None: - raise falcon.HTTPNotFound(description=f"{name} is not a valid identifier name") + raise falcon.HTTPNotFound( + description=f"{name} is not a valid identifier name" + ) data = info(hab, agent.mgr, full=True) rep.status = falcon.HTTP_200 @@ -607,7 +687,7 @@ def on_get(req, rep, name): rep.data = json.dumps(data).encode("utf-8") def on_put(self, req, rep, name): - """ Identifier rename endpoint + """Identifier rename endpoint Parameters: req (Request): falcon.Request HTTP request object @@ -656,8 +736,7 @@ def on_put(self, req, rep, name): newName = body.get("name") habord = hab.db.habs.get(keys=(hab.pre,)) habord.name = newName - hab.db.habs.pin(keys=(hab.pre,), - val=habord) + hab.db.habs.pin(keys=(hab.pre,), val=habord) hab.db.names.pin(keys=("", newName), val=hab.pre) hab.db.names.rem(keys=("", name)) hab.name = newName @@ -668,7 +747,7 @@ def on_put(self, req, rep, name): rep.data = json.dumps(data).encode("utf-8") def on_delete(self, req, rep, name): - """ Identifier delete endpoint + """Identifier delete endpoint Parameters: req (Request): falcon.Request HTTP request object @@ -704,7 +783,7 @@ def on_delete(self, req, rep, name): rep.status = falcon.HTTP_200 def on_post(self, req, rep, name): - """ Identifier events endpoint + """Identifier events endpoint Parameters: req (Request): falcon.Request HTTP request object @@ -756,8 +835,10 @@ def on_post(self, req, rep, name): elif body.get("ixn") is not None: op = self.interact(agent, name, body) else: - raise falcon.HTTPBadRequest(title="invalid request", - description=f"required field 'rot' or 'ixn' missing from request") + raise falcon.HTTPBadRequest( + title="invalid request", + description=f"required field 'rot' or 'ixn' missing from request", + ) rep.status = falcon.HTTP_200 rep.content_type = "application/json" @@ -774,19 +855,23 @@ def rotate(agent, name, body): rot = body.get("rot") if rot is None: - raise falcon.HTTPBadRequest(title="invalid rotation", - description=f"required field 'rot' missing from request") + raise falcon.HTTPBadRequest( + title="invalid rotation", + description=f"required field 'rot' missing from request", + ) - if 'ba' in rot: - for wit in rot['ba']: + if "ba" in rot: + for wit in rot["ba"]: urls = agent.agentHab.fetchUrls(eid=wit, scheme=kering.Schemes.http) if not urls and wit not in agent.hby.kevers: - raise falcon.HTTPBadRequest(description=f'unknown witness {wit}') + raise falcon.HTTPBadRequest(description=f"unknown witness {wit}") sigs = body.get("sigs") if sigs is None or len(sigs) == 0: - raise falcon.HTTPBadRequest(title="invalid rotation", - description=f"required field 'sigs' missing from request") + raise falcon.HTTPBadRequest( + title="invalid rotation", + description=f"required field 'sigs' missing from request", + ) serder = serdering.SerderKERI(sad=rot) sigers = [core.Siger(qb64=sig) for sig in sigs] @@ -809,7 +894,9 @@ def rotate(agent, name, body): rand = body[Algos.randy] keeper = agent.mgr.get(Algos.randy) - keeper.rotate(pre=serder.pre, verfers=serder.verfers, digers=serder.ndigers, **rand) + keeper.rotate( + pre=serder.pre, verfers=serder.verfers, digers=serder.ndigers, **rand + ) elif Algos.group in body: smids = httping.getRequiredParam(body, "smids") @@ -821,25 +908,40 @@ def rotate(agent, name, body): keeper.rotate(pre=serder.pre, verfers=serder.verfers, digers=serder.ndigers) - agent.groups.append(dict(pre=hab.pre, serder=serder, sigers=sigers, smids=smids, rmids=rmids)) - op = agent.monitor.submit(serder.pre, longrunning.OpTypes.group, metadata=dict(sn=serder.sn)) + agent.groups.append( + dict( + pre=hab.pre, serder=serder, sigers=sigers, smids=smids, rmids=rmids + ) + ) + op = agent.monitor.submit( + serder.pre, longrunning.OpTypes.group, metadata=dict(sn=serder.sn) + ) return op if hab.kever.delpre: agent.anchors.append(dict(alias=name, pre=hab.pre, sn=serder.sn)) - op = agent.monitor.submit(hab.kever.prefixer.qb64, longrunning.OpTypes.delegation, - metadata=dict(pre=hab.pre, sn=serder.sn)) + op = agent.monitor.submit( + hab.kever.prefixer.qb64, + longrunning.OpTypes.delegation, + metadata=dict(pre=hab.pre, sn=serder.sn), + ) return op if hab.kever.wits: agent.witners.append(dict(serder=serder)) - op = agent.monitor.submit(hab.kever.prefixer.qb64, longrunning.OpTypes.witness, - metadata=dict(sn=serder.sn)) + op = agent.monitor.submit( + hab.kever.prefixer.qb64, + longrunning.OpTypes.witness, + metadata=dict(sn=serder.sn), + ) return op - op = agent.monitor.submit(hab.kever.prefixer.qb64, longrunning.OpTypes.done, - metadata=dict(response=serder.ked)) + op = agent.monitor.submit( + hab.kever.prefixer.qb64, + longrunning.OpTypes.done, + metadata=dict(response=serder.ked), + ) return op @staticmethod @@ -850,13 +952,17 @@ def interact(agent, name, body): ixn = body.get("ixn") if ixn is None: - raise falcon.HTTPBadRequest(title="invalid interaction", - description=f"required field 'ixn' missing from request") + raise falcon.HTTPBadRequest( + title="invalid interaction", + description=f"required field 'ixn' missing from request", + ) sigs = body.get("sigs") if sigs is None or len(sigs) == 0: - raise falcon.HTTPBadRequest(title="invalid interaction", - description=f"required field 'sigs' missing from request") + raise falcon.HTTPBadRequest( + title="invalid interaction", + description=f"required field 'sigs' missing from request", + ) serder = serdering.SerderKERI(sad=ixn) sigers = [core.Siger(qb64=sig) for sig in sigs] @@ -865,18 +971,26 @@ def interact(agent, name, body): if "group" in body: agent.groups.append(dict(pre=hab.pre, serder=serder, sigers=sigers)) - op = agent.monitor.submit(serder.pre, longrunning.OpTypes.group, metadata=dict(sn=serder.sn)) + op = agent.monitor.submit( + serder.pre, longrunning.OpTypes.group, metadata=dict(sn=serder.sn) + ) return op if hab.kever.wits: agent.witners.append(dict(serder=serder)) - op = agent.monitor.submit(hab.kever.prefixer.qb64, longrunning.OpTypes.witness, - metadata=dict(sn=serder.sn)) + op = agent.monitor.submit( + hab.kever.prefixer.qb64, + longrunning.OpTypes.witness, + metadata=dict(sn=serder.sn), + ) return op - op = agent.monitor.submit(hab.kever.prefixer.qb64, longrunning.OpTypes.done, - metadata=dict(response=serder.ked)) + op = agent.monitor.submit( + hab.kever.prefixer.qb64, + longrunning.OpTypes.done, + metadata=dict(response=serder.ked), + ) return op @@ -886,8 +1000,12 @@ def info(hab, rm, full=False): prefix=hab.pre, ) - if not isinstance(hab, habbing.SignifyHab) and not isinstance(hab, habbing.SignifyGroupHab): - raise kering.ConfigurationError(f"agent only allows SignifyHab instances, {type(hab)}") + if not isinstance(hab, habbing.SignifyHab) and not isinstance( + hab, habbing.SignifyGroupHab + ): + raise kering.ConfigurationError( + f"agent only allows SignifyHab instances, {type(hab)}" + ) keeper = rm.get(pre=hab.pre) data.update(keeper.params(pre=hab.pre)) @@ -907,13 +1025,13 @@ def info(hab, rm, full=False): class IdentifierOOBICollectionEnd: """ - This class represents the OOBI subresource collection endpoint for identifiers + This class represents the OOBI subresource collection endpoint for identifiers """ @staticmethod def on_get(req, rep, name): - """ Identifier GET endpoint + """Identifier GET endpoint Parameters: req: falcon.Request HTTP request @@ -962,33 +1080,48 @@ def on_get(req, rep, name): if role in (kering.Roles.witness,): # Fetch URL OOBIs for all witnesses oobis = [] for wit in hab.kever.wits: - urls = hab.fetchUrls(eid=wit, scheme=kering.Schemes.http) or hab.fetchUrls(eid=wit, - scheme=kering.Schemes.https) + urls = hab.fetchUrls( + eid=wit, scheme=kering.Schemes.http + ) or hab.fetchUrls(eid=wit, scheme=kering.Schemes.https) if not urls: - raise falcon.HTTPNotFound(description=f"unable to query witness {wit}, no http endpoint") - - url = urls[kering.Schemes.http] if kering.Schemes.http in urls else urls[kering.Schemes.https] + raise falcon.HTTPNotFound( + description=f"unable to query witness {wit}, no http endpoint" + ) + + url = ( + urls[kering.Schemes.http] + if kering.Schemes.http in urls + else urls[kering.Schemes.https] + ) up = urlparse(url) oobis.append(urljoin(up.geturl(), f"/oobi/{hab.pre}/witness/{wit}")) res["oobis"] = oobis elif role in (kering.Roles.controller,): # Fetch any controller URL OOBIs oobis = [] - urls = hab.fetchUrls(eid=hab.pre, scheme=kering.Schemes.http) or hab.fetchUrls(eid=hab.pre, - scheme=kering.Schemes.https) + urls = hab.fetchUrls( + eid=hab.pre, scheme=kering.Schemes.http + ) or hab.fetchUrls(eid=hab.pre, scheme=kering.Schemes.https) if not urls: - raise falcon.HTTPNotFound(description=f"unable to query controller {hab.pre}, no http endpoint") - - url = urls[kering.Schemes.http] if kering.Schemes.http in urls else urls[kering.Schemes.https] + raise falcon.HTTPNotFound( + description=f"unable to query controller {hab.pre}, no http endpoint" + ) + + url = ( + urls[kering.Schemes.http] + if kering.Schemes.http in urls + else urls[kering.Schemes.https] + ) up = urlparse(url) oobis.append(urljoin(up.geturl(), f"/oobi/{hab.pre}/controller")) res["oobis"] = oobis elif role in (kering.Roles.agent,): # Fetch URL OOBIs for all witnesses - roleUrls = hab.fetchRoleUrls(cid=hab.pre, role=kering.Roles.agent, - scheme=kering.Schemes.http) or hab.fetchRoleUrls(cid=hab.pre, - role=kering.Roles.agent, - scheme=kering.Schemes.https) + roleUrls = hab.fetchRoleUrls( + cid=hab.pre, role=kering.Roles.agent, scheme=kering.Schemes.http + ) or hab.fetchRoleUrls( + cid=hab.pre, role=kering.Roles.agent, scheme=kering.Schemes.https + ) if kering.Roles.agent not in roleUrls: - res['oobis'] = [] + res["oobis"] = [] else: aoobis = roleUrls[kering.Roles.agent] @@ -1003,14 +1136,19 @@ def on_get(req, rep, name): urls.extend(murl.naball(kering.Schemes.https)) for url in urls: up = urlparse(url) - oobis.append(urljoin(up.geturl(), f"/oobi/{hab.pre}/agent/{agent}")) + oobis.append( + urljoin(up.geturl(), f"/oobi/{hab.pre}/agent/{agent}") + ) res["oobis"] = oobis elif role in (kering.Roles.mailbox,): # Fetch URL OOBIs for all witnesses - roleUrls = (hab.fetchRoleUrls(cid=hab.pre, role=kering.Roles.mailbox, scheme=kering.Schemes.http) or - hab.fetchRoleUrls(cid=hab.pre, role=kering.Roles.mailbox, scheme=kering.Schemes.https)) + roleUrls = hab.fetchRoleUrls( + cid=hab.pre, role=kering.Roles.mailbox, scheme=kering.Schemes.http + ) or hab.fetchRoleUrls( + cid=hab.pre, role=kering.Roles.mailbox, scheme=kering.Schemes.https + ) if kering.Roles.mailbox not in roleUrls: - res['oobis'] = [] + res["oobis"] = [] else: aoobis = roleUrls[kering.Roles.mailbox] @@ -1025,11 +1163,17 @@ def on_get(req, rep, name): urls.extend(murl.naball(kering.Schemes.https)) for url in urls: up = urlparse(url) - oobis.append(urljoin(up.geturl(), f"/oobi/{hab.pre}/mailbox/{mailbox}")) + oobis.append( + urljoin( + up.geturl(), f"/oobi/{hab.pre}/mailbox/{mailbox}" + ) + ) res["oobis"] = oobis else: - raise falcon.HTTPBadRequest(description=f"unsupport role type {role} for oobi request") + raise falcon.HTTPBadRequest( + description=f"unsupport role type {role} for oobi request" + ) rep.status = falcon.HTTP_200 rep.content_type = "application/json" @@ -1040,7 +1184,7 @@ class EndRoleCollectionEnd: @staticmethod def on_get(req, rep, name=None, aid=None, role=None): - """ GET endpoint for end role collection + """GET endpoint for end role collection Parameters: req (Request): falcon HTTP request object @@ -1092,10 +1236,15 @@ def on_get(req, rep, name=None, aid=None, role=None): elif aid is not None: pre = aid else: - raise falcon.HTTPBadRequest(description="either `aid` or `name` are required in the path") + raise falcon.HTTPBadRequest( + description="either `aid` or `name` are required in the path" + ) if role is not None: - keys = (pre, role,) + keys = ( + pre, + role, + ) else: keys = (pre,) @@ -1109,7 +1258,7 @@ def on_get(req, rep, name=None, aid=None, role=None): @staticmethod def on_post(req, rep, name, aid=None, role=None): - """ POST endpoint for end role collection + """POST endpoint for end role collection Args: req (Request): Falcon HTTP request object @@ -1168,10 +1317,10 @@ def on_post(req, rep, name, aid=None, role=None): rsigs = httping.getRequiredParam(body, "sigs") rserder = serdering.SerderKERI(sad=rpy) - data = rserder.ked['a'] - pre = data['cid'] - role = data['role'] - eid = data['eid'] + data = rserder.ked["a"] + pre = data["cid"] + role = data["role"] + eid = data["eid"] hab = agent.hby.habByName(name) if hab is None: @@ -1179,17 +1328,25 @@ def on_post(req, rep, name, aid=None, role=None): if pre != hab.pre: raise falcon.errors.HTTPBadRequest( - description=f"error trying to create end role for unknown local AID {pre}") + description=f"error trying to create end role for unknown local AID {pre}" + ) rsigers = [core.Siger(qb64=rsig) for rsig in rsigs] - tsg = (hab.kever.prefixer, coring.Seqner(sn=hab.kever.sn), coring.Saider(qb64=hab.kever.serder.said), rsigers) + tsg = ( + hab.kever.prefixer, + coring.Seqner(sn=hab.kever.sn), + coring.Saider(qb64=hab.kever.serder.said), + rsigers, + ) try: agent.hby.rvy.processReply(rserder, tsgs=[tsg]) except kering.UnverifiedReplyError: pass oid = ".".join([pre, role, eid]) - op = agent.monitor.submit(oid, longrunning.OpTypes.endrole, metadata=dict(cid=pre, role=role, eid=eid)) + op = agent.monitor.submit( + oid, longrunning.OpTypes.endrole, metadata=dict(cid=pre, role=role, eid=eid) + ) rep.content_type = "application/json" rep.status = falcon.HTTP_202 @@ -1247,11 +1404,11 @@ def on_get(req, rep): class ChallengeCollectionEnd: - """ Resource for Challenge/Response Endpoints """ + """Resource for Challenge/Response Endpoints""" @staticmethod def on_get(req, rep): - """ Challenge GET endpoint + """Challenge GET endpoint Parameters: req: falcon.Request HTTP request @@ -1285,7 +1442,7 @@ def on_get(req, rep): type: string """ - mnem = mnemonic.Mnemonic(language='english') + mnem = mnemonic.Mnemonic(language="english") s = req.params.get("strength") strength = int(s) if s is not None else 128 @@ -1297,11 +1454,11 @@ def on_get(req, rep): class ChallengeResourceEnd: - """ Resource for Challenge/Response Endpoints """ + """Resource for Challenge/Response Endpoints""" @staticmethod def on_post(req, rep, name): - """ Challenge POST endpoint + """Challenge POST endpoint Parameters: req: falcon.Request HTTP request @@ -1347,7 +1504,9 @@ def on_post(req, rep, name): body = req.get_media() if "exn" not in body or "sig" not in body or "recipient" not in body: - raise falcon.HTTPBadRequest(description="challenge response requires 'words', 'sig' and 'recipient'") + raise falcon.HTTPBadRequest( + description="challenge response requires 'words', 'sig' and 'recipient'" + ) exn = body["exn"] sig = body["sig"] @@ -1359,17 +1518,19 @@ def on_post(req, rep, name): agent.hby.psr.parseOne(ims=bytearray(ims)) - agent.exchanges.append(dict(said=serder.said, pre=hab.pre, rec=recpt, topic='challenge')) + agent.exchanges.append( + dict(said=serder.said, pre=hab.pre, rec=recpt, topic="challenge") + ) rep.status = falcon.HTTP_202 class ChallengeVerifyResourceEnd: - """ Resource for Challenge/Response Verification Endpoints """ + """Resource for Challenge/Response Verification Endpoints""" @staticmethod def on_post(req, rep, source): - """ Challenge POST endpoint + """Challenge POST endpoint Parameters: req: falcon.Request HTTP request @@ -1413,7 +1574,9 @@ def on_post(req, rep, source): body = req.get_media() words = httping.getRequiredParam(body, "words") if source not in agent.hby.kevers: - raise falcon.HTTPNotFound(description=f"challenge response source={source} not found") + raise falcon.HTTPNotFound( + description=f"challenge response source={source} not found" + ) meta = dict(words=words) op = agent.monitor.submit(source, longrunning.OpTypes.challenge, metadata=meta) @@ -1425,7 +1588,7 @@ def on_post(req, rep, source): @staticmethod def on_put(req, rep, source): - """ Challenge PUT accept endpoint + """Challenge PUT accept endpoint Parameters: req: falcon.Request HTTP request @@ -1466,10 +1629,14 @@ def on_put(req, rep, source): agent = req.context.agent body = req.get_media() if "said" not in body: - raise falcon.HTTPBadRequest(description="challenge response acceptance requires 'aid' and 'said'") + raise falcon.HTTPBadRequest( + description="challenge response acceptance requires 'aid' and 'said'" + ) if source not in agent.hby.kevers: - raise falcon.HTTPNotFound(description=f"challenge response source={source} not found") + raise falcon.HTTPNotFound( + description=f"challenge response source={source} not found" + ) said = body["said"] saider = coring.Saider(qb64=said) @@ -1481,7 +1648,7 @@ def on_put(req, rep, source): class ContactCollectionEnd: def on_get(self, req, rep): - """ Contact plural GET endpoint + """Contact plural GET endpoint Parameters: req: falcon.Request HTTP request @@ -1535,7 +1702,9 @@ def on_get(self, req, rep): elif field is not None: val = req.params.get("filter_value") if val is None: - raise falcon.HTTPBadRequest(description="filter_value if required if field_field is specified") + raise falcon.HTTPBadRequest( + description="filter_value if required if field_field is specified" + ) contacts = agent.org.find(field=field, val=val) self.authn(agent, contacts) @@ -1558,10 +1727,10 @@ def on_get(self, req, rep): @staticmethod def authn(agent, contacts): for contact in contacts: - aid = contact['id'] + aid = contact["id"] ends = agent.agentHab.endsFor(aid) - contact['ends'] = ends + contact["ends"] = ends accepted = [saider.qb64 for saider in agent.hby.db.chas.get(keys=(aid,))] received = [saider.qb64 for saider in agent.hby.db.reps.get(keys=(aid,))] @@ -1569,8 +1738,14 @@ def authn(agent, contacts): challenges = [] for said in received: exn = agent.hby.db.exns.get(keys=(said,)) - challenges.append(dict(dt=exn.ked['dt'], words=exn.ked['a']['words'], said=said, - authenticated=said in accepted)) + challenges.append( + dict( + dt=exn.ked["dt"], + words=exn.ked["a"]["words"], + said=said, + authenticated=said in accepted, + ) + ) contact["challenges"] = challenges @@ -1622,7 +1797,9 @@ def on_post(req, rep, prefix): """ agent = req.context.agent if prefix not in agent.hby.kevers: - raise falcon.HTTPNotFound(description=f"{prefix} is not a known identifier.") + raise falcon.HTTPNotFound( + description=f"{prefix} is not a known identifier." + ) if req.content_length > 1000000: raise falcon.HTTPBadRequest(description="image too big to save") @@ -1632,47 +1809,49 @@ def on_post(req, rep, prefix): @staticmethod def on_get(req, rep, prefix): - """ Contact image GET endpoint + """Contact image GET endpoint - Parameters: - req: falcon.Request HTTP request - rep: falcon.Response HTTP response - prefix: qb64 identifier prefix of contact information to get + Parameters: + req: falcon.Request HTTP request + rep: falcon.Response HTTP response + prefix: qb64 identifier prefix of contact information to get - --- - summary: Get contact image for identifer prefix - description: Get contact image for identifer prefix - tags: - - Contacts - parameters: - - in: path - name: prefix - schema: - type: string - required: true - description: qb64 identifier prefix of contact image to get - responses: - 200: - description: Contact information successfully retrieved for prefix - content: - image/jpg: - schema: - description: Image - type: binary - 404: - description: No contact information found for prefix + --- + summary: Get contact image for identifer prefix + description: Get contact image for identifer prefix + tags: + - Contacts + parameters: + - in: path + name: prefix + schema: + type: string + required: true + description: qb64 identifier prefix of contact image to get + responses: + 200: + description: Contact information successfully retrieved for prefix + content: + image/jpg: + schema: + description: Image + type: binary + 404: + description: No contact information found for prefix """ agent = req.context.agent if prefix not in agent.hby.kevers: - raise falcon.HTTPNotFound(description=f"{prefix} is not a known identifier.") + raise falcon.HTTPNotFound( + description=f"{prefix} is not a known identifier." + ) data = agent.org.getImgData(pre=prefix) if data is None: raise falcon.HTTPNotFound(description=f"no image available for {prefix}.") rep.status = falcon.HTTP_200 - rep.set_header('Content-Type', data["type"]) - rep.set_header('Content-Length', data["length"]) + rep.set_header("Content-Type", data["type"]) + rep.set_header("Content-Length", data["length"]) rep.stream = agent.org.getImg(pre=prefix) @@ -1680,35 +1859,37 @@ class ContactResourceEnd: @staticmethod def on_get(req, rep, prefix): - """ Contact GET endpoint + """Contact GET endpoint - Parameters: - req: falcon.Request HTTP request - rep: falcon.Response HTTP response - prefix: qb64 identifier prefix of contact information to get + Parameters: + req: falcon.Request HTTP request + rep: falcon.Response HTTP response + prefix: qb64 identifier prefix of contact information to get - --- - summary: Get contact information associated with single remote identifier - description: Get contact information associated with single remote identifier. All - information is meta-data and kept in local storage only - tags: - - Contacts - parameters: - - in: path - name: prefix - schema: - type: string - required: true - description: qb64 identifier prefix of contact to get - responses: - 200: - description: Contact information successfully retrieved for prefix - 404: - description: No contact information found for prefix + --- + summary: Get contact information associated with single remote identifier + description: Get contact information associated with single remote identifier. All + information is meta-data and kept in local storage only + tags: + - Contacts + parameters: + - in: path + name: prefix + schema: + type: string + required: true + description: qb64 identifier prefix of contact to get + responses: + 200: + description: Contact information successfully retrieved for prefix + 404: + description: No contact information found for prefix """ agent = req.context.agent if prefix not in agent.hby.kevers: - raise falcon.HTTPNotFound(description=f"{prefix} is not a known identifier.") + raise falcon.HTTPNotFound( + description=f"{prefix} is not a known identifier." + ) contact = agent.org.get(prefix) if contact is None: @@ -1719,57 +1900,63 @@ def on_get(req, rep, prefix): @staticmethod def on_post(req, rep, prefix): - """ Contact plural GET endpoint + """Contact plural GET endpoint - Parameters: - req: falcon.Request HTTP request - rep: falcon.Response HTTP response - prefix: human readable name of identifier to replace contact information + Parameters: + req: falcon.Request HTTP request + rep: falcon.Response HTTP response + prefix: human readable name of identifier to replace contact information - --- - summary: Create new contact information for an identifier - description: Creates new information for an identifier, overwriting all existing - information for that identifier - tags: - - Contacts - parameters: - - in: path - name: prefix - schema: - type: string - required: true - description: qb64 identifier prefix to add contact metadata to - requestBody: - required: true - content: - application/json: - schema: - description: Contact information - type: object + --- + summary: Create new contact information for an identifier + description: Creates new information for an identifier, overwriting all existing + information for that identifier + tags: + - Contacts + parameters: + - in: path + name: prefix + schema: + type: string + required: true + description: qb64 identifier prefix to add contact metadata to + requestBody: + required: true + content: + application/json: + schema: + description: Contact information + type: object - responses: - 200: - description: Updated contact information for remote identifier - 400: - description: Invalid identifier used to update contact information - 404: - description: Prefix not found in identifier contact information + responses: + 200: + description: Updated contact information for remote identifier + 400: + description: Invalid identifier used to update contact information + 404: + description: Prefix not found in identifier contact information """ agent = req.context.agent body = req.get_media() if prefix not in agent.hby.kevers: - raise falcon.HTTPNotFound(description="{prefix} is not a known identifier. oobi required before contact " - "information") + raise falcon.HTTPNotFound( + description="{prefix} is not a known identifier. oobi required before contact " + "information" + ) if prefix in agent.hby.prefixes: - raise falcon.HTTPBadRequest(description=f"{prefix} is a local identifier, contact information only for " - f"remote identifiers") + raise falcon.HTTPBadRequest( + description=f"{prefix} is a local identifier, contact information only for " + f"remote identifiers" + ) if "id" in body: del body["id"] if agent.org.get(prefix): - raise falcon.HTTPBadRequest(description=f"contact data for {prefix} already exists") + raise falcon.HTTPBadRequest( + description=f"contact data for {prefix} already exists" + ) agent.org.replace(prefix, body) contact = agent.org.get(prefix) @@ -1779,7 +1966,7 @@ def on_post(req, rep, prefix): @staticmethod def on_put(req, rep, prefix): - """ Contact PUT endpoint + """Contact PUT endpoint Parameters: req: falcon.Request HTTP request @@ -1819,11 +2006,13 @@ def on_put(req, rep, prefix): body = req.get_media() if prefix not in agent.hby.kevers: raise falcon.HTTPNotFound( - description=f"{prefix} is not a known identifier. oobi required before contact information") + description=f"{prefix} is not a known identifier. oobi required before contact information" + ) if prefix in agent.hby.prefixes: raise falcon.HTTPBadRequest( - description=f"{prefix} is a local identifier, contact information only for remote identifiers") + description=f"{prefix} is a local identifier, contact information only for remote identifiers" + ) if "id" in body: del body["id"] @@ -1836,7 +2025,7 @@ def on_put(req, rep, prefix): @staticmethod def on_delete(req, rep, prefix): - """ Contact plural GET endpoint + """Contact plural GET endpoint Parameters: req: falcon.Request HTTP request @@ -1864,7 +2053,9 @@ def on_delete(req, rep, prefix): agent = req.context.agent deleted = agent.org.rem(prefix) if not deleted: - raise falcon.HTTPNotFound(description=f"no contact information to delete for {prefix}") + raise falcon.HTTPNotFound( + description=f"no contact information to delete for {prefix}" + ) rep.status = falcon.HTTP_202 @@ -1907,7 +2098,9 @@ def on_get(req, rep, name): raise falcon.errors.HTTPNotFound(description=f"invalid alias {name}") if not isinstance(hab, habbing.SignifyGroupHab): - raise falcon.HTTPBadRequest(description="members endpoint only available for group AIDs") + raise falcon.HTTPBadRequest( + description="members endpoint only available for group AIDs" + ) smids = hab.db.signingMembers(hab.pre) rmids = hab.db.rotationMembers(hab.pre) diff --git a/src/keria/app/delegating.py b/src/keria/app/delegating.py index 45d80c97..da624ed4 100644 --- a/src/keria/app/delegating.py +++ b/src/keria/app/delegating.py @@ -209,14 +209,15 @@ def on_post(self, req, rep, name): raise falcon.HTTPNotFound(title=f"No AID with name {name} found") body = req.get_media() - - op = self.identifierResource.interact(agent, name, body) anc = httping.getRequiredParam(body, "ixn") + if not agent.hby.db.findAnchoringSealEvent(hab.pre, seal=anc): + op = self.identifierResource.interact(agent, name, body) + # successful approval returns the delegatee prefix - teepre = self.approveDelegation(agent, hab, anc) + teepre = approveDelegation(hab, anc) adop = agent.monitor.submit(hab.kever.prefixer.qb64, longrunning.OpTypes.delegation, - metadata=dict(teepre=teepre, depends=op)) + metadata=dict(teepre=teepre, anchor=anc, depends=op)) try: rep.status = falcon.HTTP_200 @@ -226,23 +227,22 @@ def on_post(self, req, rep, name): except (kering.AuthError, ValueError) as e: raise falcon.HTTPBadRequest(description=e.args[0]) - @staticmethod - def approveDelegation(agent, hab, anc) -> str: - serder = serdering.SerderKERI(sad=anc) +def approveDelegation(hab, anc) -> str: + serder = serdering.SerderKERI(sad=anc) + + teepre = anc['a'][0]['i'] + teesaid = anc['a'][0]['d'] + + for (pre, sn), dig in hab.db.delegables.getItemIter(): + if pre == teepre: + seqner = coring.Seqner(sn=serder.sn) + couple = seqner.qb64b + serder.saidb + dgkey = dbing.dgKey(coring.Saider(qb64=teepre).qb64b, coring.Saider(qb64=teesaid).qb64b) + # the dip event should have been received from the delegatee via a postman call + # and will be sitting in the delegator escrows (hence the hab.db.delegables above) + # adding the authorize event seal will allow the dip to be processed + # and added to the delegator kever + hab.db.setAes(dgkey, couple) # authorizer event seal (delegator/issuer) - teepre = anc['a'][0]['i'] - teesaid = anc['a'][0]['d'] - - for (pre, sn), dig in hab.db.delegables.getItemIter(): - if pre == teepre: - seqner = coring.Seqner(sn=serder.sn) - couple = seqner.qb64b + serder.saidb - dgkey = dbing.dgKey(coring.Saider(qb64=teepre).qb64b, coring.Saider(qb64=teesaid).qb64b) - # the dip event should have been received from the delegatee via a postman call - # and will be sitting in the delegator escrows (hence the hab.db.delegables above) - # adding the authorize event seal will allow the dip to be processed - # and added to the delegator kever - hab.db.setAes(dgkey, couple) # authorizer event seal (delegator/issuer) - return teepre - - raise falcon.HTTPBadRequest(title=f"No delegables found for delegator {hab.pre} to approve delegatee {teepre}") \ No newline at end of file + return teepre + # raise falcon.HTTPBadRequest(title=f"No delegables found for delegator {hab.pre} to approve delegatee {teepre}") \ No newline at end of file diff --git a/src/keria/app/grouping.py b/src/keria/app/grouping.py index d9503850..489d9794 100644 --- a/src/keria/app/grouping.py +++ b/src/keria/app/grouping.py @@ -10,10 +10,12 @@ from keri import core from keri.app import habbing from keri.core import coring, eventing, serdering +from keri.help import ogler from keri.kering import SerializeError from keria.core import httping, longrunning +logger = ogler.getLogger() def loadEnds(app): msrCol = MultisigRequestCollectionEnd() @@ -70,8 +72,10 @@ def on_post(req, rep, name): # now get rid of the event so we can pass it as atc to send del ims[:serder.size] - smids = hab.db.signingMembers(pre=hab.pre) - smids.remove(hab.mhab.pre) + slist = hab.db.signingMembers(pre=hab.pre) + smids = slist + if hab.mhab.pre in smids: + smids.remove(hab.mhab.pre) agent.exchanges.append(dict(said=serder.said, pre=hab.pre, rec=smids, topic='multisig')) @@ -149,6 +153,12 @@ def on_post(req, rep, name): # Get the rot, sigs and recipients from the request rot = httping.getRequiredParam(body, "rot") + serder = None + try: + serder = serdering.SerderKERI(sad=rot) + except(SerializeError) as e: + raise falcon.HTTPBadRequest(description=f"{e.args[0]}") + sigs = httping.getRequiredParam(body, "sigs") # Get group specific values @@ -178,13 +188,11 @@ def on_post(req, rep, name): hab = agent.hby.joinSignifyGroupHab(gid, name=name, mhab=mhab, smids=smids, rmids=rmids) try: - hab.make(serder=serdering.SerderKERI(sad=rot), sigers=sigers) - agent.inceptGroup(pre=gid, mpre=mhab.pre, verfers=verfers, digers=digers) - except (ValueError, SerializeError) as e: - agent.hby.deleteHab(name=name) - raise falcon.HTTPBadRequest(description=f"{e.args[0]}") + hab.make(serder=serder, sigers=sigers) + except (ValueError) as e: + logger.info("Already incepted group, continuing...") - serder = serdering.SerderKERI(sad=rot) + agent.inceptGroup(pre=gid, mpre=mhab.pre, verfers=verfers, digers=digers) agent.groups.append(dict(pre=hab.pre, serder=serder, sigers=sigers, smids=smids, rmids=rmids)) op = agent.monitor.submit(serder.pre, longrunning.OpTypes.group, metadata=dict(sn=serder.sn)) diff --git a/src/keria/core/longrunning.py b/src/keria/core/longrunning.py index 45012e53..dec4b0fc 100644 --- a/src/keria/core/longrunning.py +++ b/src/keria/core/longrunning.py @@ -17,6 +17,8 @@ from keri.db import dbing, koming from keri.help import helping +from keria.app import delegating + # long running operation types Typeage = namedtuple("Tierage", 'oobi witness delegation group query registry credential endrole challenge exchange ' 'done') @@ -253,6 +255,7 @@ def status(self, op): reqsn = "sn" reqtee = "teepre" + anchor = "anchor" required = [reqsn, reqtee] if reqsn in op.metadata: #delegatee detects successful delegation sn = op.metadata["sn"] @@ -269,11 +272,13 @@ def status(self, op): operation.done = False elif reqtee in op.metadata: #delegator detects delegatee delegation success teepre = op.metadata[reqtee] - # Once the delegatee dip is processed by the delegator, the - if teepre in self.hby.kevers: + anc = op.metadata[anchor] + if teepre in self.hby.kevers: # delegatee dip has been processed by the delegator operation.done = True operation.response = op.metadata[reqtee] else: + hab = self.hby.habByPre(kever.prefixer.qb64) + delegating.approveDelegation(hab,anc) operation.done = False else: raise falcon.HTTPBadRequest(description=f"longrunning operation type {op.type} requires one of {required}, but are missing from request") diff --git a/src/keria/testing/testing_helper.py b/src/keria/testing/testing_helper.py index 42a122e4..444d1f6f 100644 --- a/src/keria/testing/testing_helper.py +++ b/src/keria/testing/testing_helper.py @@ -422,6 +422,35 @@ def interact(pre, bran, pidx, ridx, sn, dig, data): signers = creator.create(pidx=pidx, ridx=ridx, tier=coring.Tiers.low, temp=False, count=1) sigers = [signer.sign(ser=serder.raw, index=0).qb64 for signer in signers] return serder, sigers + + @staticmethod + def createRotate(aid, salt, signers, pidx, ridx, kidx, wits, toad): + salter = core.Salter(raw=salt) + creator = keeping.SaltyCreator(salt=salter.qb64, stem="signify:aid", tier=coring.Tiers.low) + encrypter = core.Encrypter(verkey=signers[0].verfer.qb64) + sxlt = encrypter.encrypt(salter.qb64).qb64 + + rsigners = creator.create(pidx=pidx, ridx=ridx, tier=coring.Tiers.low, temp=False, count=1) + rnsigners = creator.create(pidx=pidx, ridx=ridx+1, tier=coring.Tiers.low, temp=False, count=1) + + rkeys = [signer.verfer.qb64 for signer in rsigners] + rndigs = [coring.Diger(ser=nsigner.verfer.qb64b) for nsigner in rnsigners] + + serder = eventing.rotate(pre=aid["prefix"], + keys=rkeys, + dig=aid["prefix"], + ndigs=[diger.qb64 for diger in rndigs], + wits=wits, + toad=toad + ) + sigers = [signer.sign(ser=serder.raw, index=0).qb64 for signer in rsigners] + body = { + 'rot': serder.ked, + 'sigs': sigers, + 'salty': {'stem': 'signify:aid', 'pidx': pidx, 'tier': 'low', 'sxlt': sxlt, 'transferable': True, 'kidx': kidx, + 'icodes': [MtrDex.Ed25519_Seed], 'ncodes': [MtrDex.Ed25519_Seed]} + } + return body @staticmethod def sign(bran, pidx, ridx, ser): diff --git a/tests/app/test_aiding.py b/tests/app/test_aiding.py index af29237c..96d2c4de 100644 --- a/tests/app/test_aiding.py +++ b/tests/app/test_aiding.py @@ -522,13 +522,14 @@ def test_identifier_collection_end(helpers): sigers = [signer0.sign(ser=serder.raw, index=0).qb64, p1.sign(ser=serder.raw, indices=[1])[0].qb64, p2.sign(ser=serder.raw, indices=[2])[0].qb64] states = nstates = [agent0, asdict(p1.kever.state()), asdict(p2.kever.state())] + smids = rmids = [state['i'] for state in states if 'i' in state] body = { 'name': 'multisig', 'icp': serder.ked, 'sigs': sigers, - "smids": states, - "rmids": nstates, + "smids": smids, + "rmids": rmids, 'group': { "keys": keys, "ndigs": ndigs @@ -546,8 +547,8 @@ def test_identifier_collection_end(helpers): 'name': 'multisig', 'icp': serder.ked, 'sigs': sigers, - "smids": states, - "rmids": nstates, + "smids": smids, + "rmids": rmids, 'group': { "mhab": bad, "keys": keys, @@ -564,8 +565,8 @@ def test_identifier_collection_end(helpers): 'name': 'multisig', 'icp': serder.ked, 'sigs': sigers, - "smids": states, - "rmids": nstates, + "smids": smids, + "rmids": rmids, 'group': { "mhab": mhab, "ndigs": ndigs @@ -581,8 +582,8 @@ def test_identifier_collection_end(helpers): 'name': 'multisig', 'icp': serder.ked, 'sigs': sigers, - "smids": states, - "rmids": nstates, + "smids": smids, + "rmids": rmids, 'group': { "mhab": mhab, "keys": keys, @@ -599,8 +600,8 @@ def test_identifier_collection_end(helpers): 'name': 'multisig', 'icp': serder.ked, 'sigs': sigers, - "smids": states, - "rmids": nstates, + "smids": smids, + "rmids": rmids, 'group': { "mhab": mhab, "keys": keys, @@ -1565,62 +1566,152 @@ def test_approve_delegation(helpers): def test_rotation(helpers): - caid = "ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose" salt = b'0123456789abcdef' salter = core.Salter(raw=salt) - cf = configing.Configer(name="keria", headDirPath=SCRIPTS_DIR, temp=True, reopen=True, clear=False) - with habbing.openHby(name="keria", salt=salter.qb64, temp=True, cf=cf) as hby: - hab = hby.makeHab(name="test") - agency = agenting.Agency(name="agency", bran=None, temp=True) - agentHab = hby.makeHab(caid, ns="agent", transferable=True, data=[caid]) + with helpers.openKeria() as (agency, agent, app, client), \ + habbing.openHby(name="p1", temp=True, salt=salter.qb64) as p1hby, \ + habbing.openHby(name="p2", temp=True, salt=salter.qb64) as p2hby: + end = aiding.IdentifierCollectionEnd() + resend = aiding.IdentifierResourceEnd() + app.add_route("/identifiers", end) + app.add_route("/identifiers/{name}", resend) + app.add_route("/identifiers/{name}/events", resend) - rgy = credentialing.Regery(hby=hby, name=agentHab.name, base=hby.base, temp=True) - agent = agenting.Agent(hby=hby, rgy=rgy, agentHab=agentHab, agency=agency, caid=caid) + groupEnd = aiding.GroupMemberCollectionEnd() + app.add_route("/identifiers/{name}/members", groupEnd) - doist = doing.Doist(limit=1.0, tock=0.03125, real=True) - doist.enter(doers=[agency]) + opColEnd = longrunning.OperationCollectionEnd() + app.add_route("/operations", opColEnd) + opResEnd = longrunning.OperationResourceEnd() + app.add_route("/operations/{name}", opResEnd) - end = agenting.KeyStateCollectionEnd() + client = testing.TestClient(app) - app = falcon.App() - app.add_middleware(helpers.middleware(agent)) - app.add_route("/states", end) + salt = b'0123456789abcdef' + serder1, signers1 = helpers.incept(salt, "signify:aid", pidx=0) + assert len(signers1) == 1 - client = testing.TestClient(app) + sigers1 = [signer.sign(ser=serder1.raw, index=0).qb64 for signer in signers1] - bran = b'abcdefghijk0123456789' - serder, signers = helpers.incept(bran=bran, stem="signify:controller", pidx=0) - sigers = [signers[0].sign(ser=serder.raw, index=0)] - controllerAID = "EEnUbC_nMt-2uGQMp7jq_xUS9nc8SQ0q7eX_w49CG7jb" - assert serder.pre == controllerAID + salter = core.Salter(raw=salt) + encrypter = core.Encrypter(verkey=signers1[0].verfer.qb64) + sxlt = encrypter.encrypt(salter.qb64).qb64 - bootEnd = agenting.BootEnd(agency) - app.add_route("/boot", bootEnd) - agentEnd = aiding.AgentResourceEnd(agency=agency, authn=None) - app.add_route("/agent/{caid}", agentEnd) - end = aiding.IdentifierCollectionEnd() - app.add_route("/identifiers", end) - idResEnd = aiding.IdentifierResourceEnd() - app.add_route("/identifiers/{name}", idResEnd) + bodyid1 = {'name': 'aid1', + 'icp': serder1.ked, + 'sigs': sigers1, + "salty": { + 'stem': 'signify:aid', 'pidx': 0, 'tier': 'low', 'sxlt': sxlt, 'transferable': True, 'kidx': 0, + 'icodes': [MtrDex.Ed25519_Seed], 'ncodes': [MtrDex.Ed25519_Seed]} + } - body = dict( - icp=serder.ked, - sig=sigers[0].qb64, - salty=dict( - stem='signify:aid', pidx=0, tier='low', sxlt='OBXYZ', - icodes=[MtrDex.Ed25519_Seed], ncodes=[MtrDex.Ed25519_Seed] - ) - ) + res = client.simulate_post(path="/identifiers", body=json.dumps(bodyid1)) + assert res.status_code == 202 - rep = client.simulate_post("/boot", body=json.dumps(body).encode("utf-8")) - assert rep.status_code == 202 + res = client.simulate_get(path="/identifiers") + assert res.status_code == 200 + assert len(res.json) == 1 + aid = res.json[0] + assert aid["name"] == "aid1" + assert aid["prefix"] == "EHgwVwQT15OJvilVvW57HE4w0-GPs_Stj2OFoAHZSysY" - res = client.simulate_get(path=f"/agent/{serder.pre}") + serder2, signers2 = helpers.incept(salt, "signify:aid", pidx=1, count=3) + sigers2 = [signer.sign(ser=serder2.raw, index=0).qb64 for signer in signers2] + + bodyid2 = {'name': 'aid2', + 'icp': serder2.ked, + 'sigs': sigers2, + 'salty': {'stem': 'signify:aid', 'pidx': 1, 'tier': 'low', 'sxlt': sxlt, + 'icodes': [MtrDex.Ed25519_Seed], 'ncodes': [MtrDex.Ed25519_Seed]}} + res = client.simulate_post(path="/identifiers", body=json.dumps(bodyid2)) + assert res.status_code == 202 + + res = client.simulate_get(path="/identifiers") assert res.status_code == 200 - ctrl = res.json["controller"] - assert ctrl["state"]["i"] == controllerAID + assert len(res.json) == 2 + aid1 = res.json[0] + assert aid1["name"] == "aid1" + assert aid1["prefix"] == "EHgwVwQT15OJvilVvW57HE4w0-GPs_Stj2OFoAHZSysY" + ss = aid1[Algos.salty] + assert ss["pidx"] == 0 - op = helpers.createAid(client, name="salty_aid", salt=bran) - aid = op["response"] - assert aid['i'] == "EKYCAqyMMllSeGowQJUGMbRJpvLnhNMbF1qEIPCSpmOM" + aid2 = res.json[1] + assert aid2["name"] == "aid2" + assert aid2["prefix"] == "ECL8abFVW_0RTZXFhiiA4rkRobNvjTfJ6t-T8UdBRV1e" + ss = aid2[Algos.salty] + assert ss["pidx"] == 1 + + # Rotate aid1 + bodyrot1 = helpers.createRotate(aid1, salt, signers1, pidx=0, ridx=1, kidx=1, wits=[], toad=0) + res = client.simulate_post(path=f"/identifiers/{aid1['name']}/events", body=json.dumps(bodyrot1)) + assert res.status_code == 200 + + # Try with missing arguments + bodybad = { + 'rot': serder1.ked, + 'sigs': sigers1, + 'salty': {'stem': 'signify:aid', 'pidx': 0, 'tier': 'low', 'sxlt': sxlt, + 'icodes': [MtrDex.Ed25519_Seed], 'ncodes': [MtrDex.Ed25519_Seed]} + } + res = client.simulate_post(path="/identifiers/aid1/events", body=json.dumps(bodybad)) + assert res.status_code == 500 + + # Test with witnesses + url = "http://127.0.0.1:9999" + agent.hby.db.locs.put(keys=("BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha", kering.Schemes.http), + val=basing.LocationRecord(url=url)) + agent.hby.db.locs.put(keys=("BLskRTInXnMxWaGqcpSyMgo0nYbalW99cGZESrz3zapM", kering.Schemes.http), + val=basing.LocationRecord(url=url)) + agent.hby.db.locs.put(keys=("BIKKuvBwpmDVA4Ds-EpL5bt9OqPzWPja2LigFYZN2YfX", kering.Schemes.http), + val=basing.LocationRecord(url=url)) + wits3 = ["BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha", + "BLskRTInXnMxWaGqcpSyMgo0nYbalW99cGZESrz3zapM", + "BIKKuvBwpmDVA4Ds-EpL5bt9OqPzWPja2LigFYZN2YfX", ] + toad3 = "2" + serder3, signers3 = helpers.incept(salt, "signify:aid", pidx=3, + wits=wits3, + toad=toad3) + sigers3 = [signer.sign(ser=serder3.raw, index=0).qb64 for signer in signers3] + + bodyid3 = {'name': 'aid3', + 'icp': serder3.ked, + 'sigs': sigers3, + 'salty': {'stem': 'signify:aid', 'pidx': 3, 'tier': 'low', 'sxlt': sxlt, + 'icodes': [MtrDex.Ed25519_Seed], 'ncodes': [MtrDex.Ed25519_Seed]}} + + res = client.simulate_post(path="/identifiers", body=json.dumps(bodyid3)) + assert res.status_code == 202 + + op = res.json + name = op['name'] + + res = client.simulate_get(path=f"/operations/{name}") + assert res.status_code == 200 + assert res.json['done'] is False + + assert len(agent.witners) == 1 + res = client.simulate_get(path="/identifiers") + assert res.status_code == 200 + assert len(res.json) == 3 + aid3 = res.json[2] + assert aid3["name"] == "aid3" + assert aid3["prefix"] == serder3.pre + ss = aid3[Algos.salty] + assert ss["pidx"] == 3 + + # Add fake witness receipts to test satified witnessing + dgkey = dbing.dgKey(serder3.preb, serder3.preb) + agent.hby.db.putWigs(dgkey, vals=[b'A', b'B', b'C']) + res = client.simulate_get(path=f"/operations/{name}") + assert res.status_code == 200 + assert res.json['done'] is True + + res = client.simulate_get(path=f"/identifiers/{aid1['name']}") + mhab = res.json + agent0 = mhab["state"] + + # rotate aid3 + body = helpers.createRotate(aid=aid3, salt=salt, signers=signers3, pidx=3, ridx=1, kidx=3, wits=wits3, toad=toad3) + res = client.simulate_post(path=f"/identifiers/{aid3['name']}/events", body=json.dumps(body)) + assert res.status_code == 200 \ No newline at end of file diff --git a/tests/app/test_grouping.py b/tests/app/test_grouping.py index 3849d500..0d9e0acd 100644 --- a/tests/app/test_grouping.py +++ b/tests/app/test_grouping.py @@ -42,19 +42,19 @@ def test_multisig_request_ends(helpers): app.add_route("/multisig/request/{said}", msrRes) # First create participants (aid0, aid1) in a multisig AID - salt0 = b'0123456789abcdef' + salt0 = b"0123456789abcdef" op = helpers.createAid(client, "aid0", salt0) aid0 = op["response"] - pre0 = aid0['i'] + pre0 = aid0["i"] assert pre0 == "EHgwVwQT15OJvilVvW57HE4w0-GPs_Stj2OFoAHZSysY" serder, signers0 = helpers.incept(salt0, "signify:aid", pidx=0) assert serder.pre == pre0 signer0 = signers0[0] - salt1 = b'abcdef0123456789' + salt1 = b"abcdef0123456789" op = helpers.createAid(client, "aid1", salt1) aid1 = op["response"] - pre1 = aid1['i'] + pre1 = aid1["i"] assert pre1 == "EMgdjM1qALk3jlh4P2YyLRSTcjSOjLXD3e_uYpxbdbg6" serder, signers1 = helpers.incept(salt1, "signify:aid", pidx=0) assert serder.pre == pre1 @@ -67,34 +67,35 @@ def test_multisig_request_ends(helpers): assert m0["prefix"] == "EHgwVwQT15OJvilVvW57HE4w0-GPs_Stj2OFoAHZSysY" assert m1["prefix"] == "EMgdjM1qALk3jlh4P2YyLRSTcjSOjLXD3e_uYpxbdbg6" - keys = [m0['state']['k'][0], m1['state']['k'][0]] - ndigs = [m0['state']['n'][0], m1['state']['n'][0]] + keys = [m0["state"]["k"][0], m1["state"]["k"][0]] + ndigs = [m0["state"]["n"][0], m1["state"]["n"][0]] # Create the mutlsig inception event - serder = eventing.incept(keys=keys, - isith="2", - nsith="2", - ndigs=ndigs, - code=coring.MtrDex.Blake3_256, - toad=0, - wits=[]) + serder = eventing.incept( + keys=keys, + isith="2", + nsith="2", + ndigs=ndigs, + code=coring.MtrDex.Blake3_256, + toad=0, + wits=[], + ) assert serder.said == "EG8p1Zb4BfyKYkA9SkpyTvCo9xoCsISlOl7YlsB5b1Vt" # Send in all signatures as if we are joining the inception event - sigers = [signer0.sign(ser=serder.raw, index=0).qb64, signer1.sign(ser=serder.raw, index=1).qb64] - states = nstates = [m0['state'], m1['state']] + sigers = [ + signer0.sign(ser=serder.raw, index=0).qb64, + signer1.sign(ser=serder.raw, index=1).qb64, + ] + states = nstates = [m0["state"], m1["state"]] body = { - 'name': 'multisig', - 'icp': serder.ked, - 'sigs': sigers, + "name": "multisig", + "icp": serder.ked, + "sigs": sigers, "smids": states, "rmids": nstates, - 'group': { - "mhab": m0, - "keys": keys, - "ndigs": ndigs - } + "group": {"mhab": m0, "keys": keys, "ndigs": ndigs}, } res = client.simulate_post(path="/identifiers", body=json.dumps(body)) @@ -102,41 +103,44 @@ def test_multisig_request_ends(helpers): # Get the multisig AID hab dict m2 = client.simulate_get(path="/identifiers/multisig").json - pre2 = m2['prefix'] + pre2 = m2["prefix"] assert pre2 == "EG8p1Zb4BfyKYkA9SkpyTvCo9xoCsISlOl7YlsB5b1Vt" - payload = dict(i=pre2, words="these are the words being signed for this response") - cexn, _ = exchanging.exchange(route="/challenge/response", payload=payload, sender=agent.agentHab.pre) + payload = dict( + i=pre2, words="these are the words being signed for this response" + ) + cexn, _ = exchanging.exchange( + route="/challenge/response", payload=payload, sender=agent.agentHab.pre + ) # Signing this with agentHab because I'm lazing. Nothing will be done with this signature cha = agent.agentHab.endorse(serder=cexn, last=False, pipelined=False) - embeds = dict( - exn=cha + embeds = dict(exn=cha) + exn, end = exchanging.exchange( + route="/multisig/exn", payload=dict(gid=pre2), embeds=embeds, sender=pre0 ) - exn, end = exchanging.exchange(route="/multisig/exn", payload=dict(gid=pre2), embeds=embeds, - sender=pre0) sig = signer0.sign(exn.raw, index=0).qb64 - body = dict( - exn=exn.ked, - sigs=[sig], - atc=end.decode("utf-8") - ) + body = dict(exn=exn.ked, sigs=[sig], atc=end.decode("utf-8")) - res = client.simulate_post(path="/identifiers/badaid/multisig/request", json=body) + res = client.simulate_post( + path="/identifiers/badaid/multisig/request", json=body + ) assert res.status_code == 404 res = client.simulate_post(path="/identifiers/aid1/multisig/request", json=body) assert res.status_code == 400 - res = client.simulate_post(path="/identifiers/multisig/multisig/request", json=body) + res = client.simulate_post( + path="/identifiers/multisig/multisig/request", json=body + ) assert res.status_code == 200 assert res.json == exn.ked said = exn.said # Fudge this because we won't be able to save a message from someone else: - esaid = exn.ked['e']['d'] + esaid = exn.ked["e"]["d"] agent.hby.db.meids.add(keys=(esaid,), val=coring.Saider(qb64=exn.said)) res = client.simulate_get(path=f"/multisig/request/BADSAID") @@ -148,9 +152,9 @@ def test_multisig_request_ends(helpers): req = res.json[0] - assert req['exn'] == exn.ked - path = req['paths']['exn'] - assert '-LA35AACAA-e-exn' + path == end.decode("utf-8") + assert req["exn"] == exn.ked + path = req["paths"]["exn"] + assert "-LA35AACAA-e-exn" + path == end.decode("utf-8") # We've send this one exn to our other participants assert len(agent.exchanges) == 1 @@ -165,7 +169,7 @@ def test_join(helpers, monkeypatch): app.add_route("/identifiers", end) app.add_route("/identifiers/{name}", resend) - salt = b'0123456789abcdef' + salt = b"0123456789abcdef" op = helpers.createAid(client, "recipient", salt) aid = op["response"] @@ -187,8 +191,8 @@ def test_join(helpers, monkeypatch): "EBFg-5SGDCv5YfwpkArWRBdTxNRUXU8uVcDKNzizOQZc", "EBmW2bXbgsP3HITwW3FmITzAb3wVmHlxCusZ46vgGgP5", "EL4RpdS2Atb2Syu5xLdpz9CcNNYoFUUDlLHxHD09vcgh", - "EAiBVuuhCZrgckeHc9KzROVGJpmGbk2-e1B25GaeRrJs" - ] + "EAiBVuuhCZrgckeHc9KzROVGJpmGbk2-e1B25GaeRrJs", + ], ), sigs=[], gid="EDWg3-rB5FTpcckaYdBcexGmbLIO6AvAwjaJTBlXUn_I", @@ -198,7 +202,7 @@ def test_join(helpers, monkeypatch): "EBFg-5SGDCv5YfwpkArWRBdTxNRUXU8uVcDKNzizOQZc", "EBmW2bXbgsP3HITwW3FmITzAb3wVmHlxCusZ46vgGgP5", "EL4RpdS2Atb2Syu5xLdpz9CcNNYoFUUDlLHxHD09vcgh", - "EAiBVuuhCZrgckeHc9KzROVGJpmGbk2-e1B25GaeRrJs" + "EAiBVuuhCZrgckeHc9KzROVGJpmGbk2-e1B25GaeRrJs", ], rmids=[ "EKYLUMmNPZeEs77Zvclf0bSN5IN-mLfLpx2ySb-HDlk4", @@ -206,83 +210,74 @@ def test_join(helpers, monkeypatch): "EBFg-5SGDCv5YfwpkArWRBdTxNRUXU8uVcDKNzizOQZc", "EBmW2bXbgsP3HITwW3FmITzAb3wVmHlxCusZ46vgGgP5", "EL4RpdS2Atb2Syu5xLdpz9CcNNYoFUUDlLHxHD09vcgh", - "EAiBVuuhCZrgckeHc9KzROVGJpmGbk2-e1B25GaeRrJs" - ] + "EAiBVuuhCZrgckeHc9KzROVGJpmGbk2-e1B25GaeRrJs", + ], ) res = client.simulate_post("/identifiers/mms/multisig/join", json=body) assert res.status_code == 400 - for smid in body['smids']: + for smid in body["smids"]: agent.hby.kevers[smid] = {} - for rmid in body['rmids']: + for rmid in body["rmids"]: agent.hby.kevers[rmid] = {} res = client.simulate_post("/identifiers/mms/multisig/join", json=body) assert res.status_code == 400 - assert res.json == {'description': 'Invalid multisig group rotation request, signing member list ' - "must contain a local identifier'", - 'title': '400 Bad Request'} - - body['smids'][0] = aid["i"] + assert res.json == { + "description": "Missing version string field in {'k': " + "['DNp1NUbUEgei6KOlIfT5evXueOi3TDFZkUXgJQWNvegf', " + "'DLsXs0-dxqrM4hugX7NkfZUzET13ngfRhWC9GgXvX9my', " + "'DE2W_yGSF-m44vXPuQ5_wHJ9EK59N-OIT3hABgdAcCKs', " + "'DKFKNK7s0xLhazlmL3xH9YEl9sc3fVoqUSsQxK6DZ3oC', " + "'DEyEcy5NzjqA3KQ1DTE0BJs-XMIdWIvPWligyq6y1TxS', " + "'DGhflVckn2wVLJH6wq94gGQxmpvsFdsZvd61Owj3Qhjl'], 'n': " + "['EKYLUMmNPZeEs77Zvclf0bSN5IN-mLfLpx2ySb-HDlk4', " + "'EJccSRTfXYF6wrUVuenAIHzwcx3hJugeiJsEKmndi5q1', " + "'EBFg-5SGDCv5YfwpkArWRBdTxNRUXU8uVcDKNzizOQZc', " + "'EBmW2bXbgsP3HITwW3FmITzAb3wVmHlxCusZ46vgGgP5', " + "'EL4RpdS2Atb2Syu5xLdpz9CcNNYoFUUDlLHxHD09vcgh', " + "'EAiBVuuhCZrgckeHc9KzROVGJpmGbk2-e1B25GaeRrJs']}.", + "title": "400 Bad Request", + } res = client.simulate_post("/identifiers/mms/multisig/join", json=body) assert res.status_code == 400 - assert res.json == {'description': "Missing version string field in {'k': " - "['DNp1NUbUEgei6KOlIfT5evXueOi3TDFZkUXgJQWNvegf', " - "'DLsXs0-dxqrM4hugX7NkfZUzET13ngfRhWC9GgXvX9my', " - "'DE2W_yGSF-m44vXPuQ5_wHJ9EK59N-OIT3hABgdAcCKs', " - "'DKFKNK7s0xLhazlmL3xH9YEl9sc3fVoqUSsQxK6DZ3oC', " - "'DEyEcy5NzjqA3KQ1DTE0BJs-XMIdWIvPWligyq6y1TxS', " - "'DGhflVckn2wVLJH6wq94gGQxmpvsFdsZvd61Owj3Qhjl'], 'n': " - "['EKYLUMmNPZeEs77Zvclf0bSN5IN-mLfLpx2ySb-HDlk4', " - "'EJccSRTfXYF6wrUVuenAIHzwcx3hJugeiJsEKmndi5q1', " - "'EBFg-5SGDCv5YfwpkArWRBdTxNRUXU8uVcDKNzizOQZc', " - "'EBmW2bXbgsP3HITwW3FmITzAb3wVmHlxCusZ46vgGgP5', " - "'EL4RpdS2Atb2Syu5xLdpz9CcNNYoFUUDlLHxHD09vcgh', " - "'EAiBVuuhCZrgckeHc9KzROVGJpmGbk2-e1B25GaeRrJs']}.", - 'title': '400 Bad Request'} - - body['rot'] = { + assert res.json == { + "title": "400 Bad Request", + "description": "Missing version string field in {'k': ['DNp1NUbUEgei6KOlIfT5evXueOi3TDFZkUXgJQWNvegf', 'DLsXs0-dxqrM4hugX7NkfZUzET13ngfRhWC9GgXvX9my', 'DE2W_yGSF-m44vXPuQ5_wHJ9EK59N-OIT3hABgdAcCKs', 'DKFKNK7s0xLhazlmL3xH9YEl9sc3fVoqUSsQxK6DZ3oC', 'DEyEcy5NzjqA3KQ1DTE0BJs-XMIdWIvPWligyq6y1TxS', 'DGhflVckn2wVLJH6wq94gGQxmpvsFdsZvd61Owj3Qhjl'], 'n': ['EKYLUMmNPZeEs77Zvclf0bSN5IN-mLfLpx2ySb-HDlk4', 'EJccSRTfXYF6wrUVuenAIHzwcx3hJugeiJsEKmndi5q1', 'EBFg-5SGDCv5YfwpkArWRBdTxNRUXU8uVcDKNzizOQZc', 'EBmW2bXbgsP3HITwW3FmITzAb3wVmHlxCusZ46vgGgP5', 'EL4RpdS2Atb2Syu5xLdpz9CcNNYoFUUDlLHxHD09vcgh', 'EAiBVuuhCZrgckeHc9KzROVGJpmGbk2-e1B25GaeRrJs']}.", + } + + body["smids"][0] = aid["i"] + + body["rot"] = { "v": "KERI10JSON00030c_", "t": "rot", "d": "EPKCBT0rSgFKTDRjynYzOTsYWo7fDNElTxFbRZZW9f6R", "i": "EDWg3-rB5FTpcckaYdBcexGmbLIO6AvAwjaJTBlXUn_I", "s": "3", "p": "EM2OaIZuLWyGGyxf4Tzs6yeoENvjP47i1Dn88GGxw3_Z", - "kt": [ - "0", - "0", - "1/2", - "1/2", - "1/2", - "1/2" - ], + "kt": ["0", "0", "1/2", "1/2", "1/2", "1/2"], "k": [ "DNp1NUbUEgei6KOlIfT5evXueOi3TDFZkUXgJQWNvegf", "DLsXs0-dxqrM4hugX7NkfZUzET13ngfRhWC9GgXvX9my", "DE2W_yGSF-m44vXPuQ5_wHJ9EK59N-OIT3hABgdAcCKs", "DKFKNK7s0xLhazlmL3xH9YEl9sc3fVoqUSsQxK6DZ3oC", "DEyEcy5NzjqA3KQ1DTE0BJs-XMIdWIvPWligyq6y1TxS", - "DGhflVckn2wVLJH6wq94gGQxmpvsFdsZvd61Owj3Qhjl" - ], - "nt": [ - "1/2", - "1/2", - "1/2", - "1/2" + "DGhflVckn2wVLJH6wq94gGQxmpvsFdsZvd61Owj3Qhjl", ], + "nt": ["1/2", "1/2", "1/2", "1/2"], "n": [ "EDr0gf60BDB9cZyVoz_Os55Ma49muyCNTZoWG-VWAe6g", "EIM3hKH1VBG_ofS7hD-XMfTG-dP1ziJwloFhrNx34G7o", "EOi609MGQlByLPdaUgqGQn_IOEE4cf6u7zCW-J3E82Qz", - "ECQF1Tdpcqew6dqN6nHNpz4jhYTZtojl7EpqVJhXRBav" + "ECQF1Tdpcqew6dqN6nHNpz4jhYTZtojl7EpqVJhXRBav", ], "bt": "3", "br": [], "ba": [], - "a": [] + "a": [], } def make(self, serder, sigers): @@ -292,13 +287,17 @@ def make(self, serder, sigers): res = client.simulate_post("/identifiers/mms/multisig/join", json=body) assert res.status_code == 202 - assert res.json == {'done': False, - 'error': None, - 'metadata': {'sn': 3}, - 'name': 'group.EDWg3-rB5FTpcckaYdBcexGmbLIO6AvAwjaJTBlXUn_I', - 'response': None} + assert res.json == { + "done": False, + "error": None, + "metadata": {"sn": 3}, + "name": "group.EDWg3-rB5FTpcckaYdBcexGmbLIO6AvAwjaJTBlXUn_I", + "response": None, + } res = client.simulate_post("/identifiers/mms/multisig/join", json=body) assert res.status_code == 400 - assert res.json == {'description': 'attempt to create identifier with an already used alias=mms', - 'title': '400 Bad Request'} + assert res.json == { + "description": "attempt to create identifier with an already used alias=mms", + "title": "400 Bad Request", + } diff --git a/tests/app/test_ipexing.py b/tests/app/test_ipexing.py index d44df504..75b20c25 100644 --- a/tests/app/test_ipexing.py +++ b/tests/app/test_ipexing.py @@ -194,13 +194,14 @@ def test_ipex_grant(helpers, mockHelpingNowIso8601, seeder): # Send in all signatures as if we are joining the inception event sigers = [signer0.sign(ser=serder.raw, index=0).qb64, signer1.sign(ser=serder.raw, index=1).qb64] states = nstates = [m0['state'], m1['state']] + smids = rmids = [state['i'] for state in states if 'i' in state] body = { 'name': 'multisig', 'icp': serder.ked, 'sigs': sigers, - "smids": states, - "rmids": nstates, + "smids": smids, + "rmids": rmids, 'group': { "mhab": m0, "keys": keys, @@ -430,13 +431,14 @@ def test_multisig_grant_admit(seeder, helpers): sigers = [issuerSigner0.sign(ser=serder.raw, index=0).qb64, issuerSigner1.sign(ser=serder.raw, index=1).qb64] states = nstates = [ip0['state'], ip1['state']] + smids = rmids = [state['i'] for state in states if 'i' in state] body = { 'name': 'issuer', 'icp': serder.ked, 'sigs': sigers, - "smids": states, - "rmids": nstates, + "smids": smids, + "rmids": rmids, 'group': { "mhab": ip0, "keys": ikeys, @@ -451,8 +453,8 @@ def test_multisig_grant_admit(seeder, helpers): 'name': 'issuer', 'icp': serder.ked, 'sigs': sigers, - "smids": states, - "rmids": nstates, + "smids": smids, + "rmids": rmids, 'group': { "mhab": ip1, "keys": ikeys, @@ -530,13 +532,14 @@ def test_multisig_grant_admit(seeder, helpers): # Send in all signatures as if we are joining the inception event sigers = [holderSigner0.sign(ser=serder.raw, index=0).qb64, holderSigner1.sign(ser=serder.raw, index=1).qb64] states = nstates = [hp0['state'], hp1['state']] - + smids = rmids = [state['i'] for state in states if 'i' in state] + body = { 'name': 'holder', 'icp': serder.ked, 'sigs': sigers, - "smids": states, - "rmids": nstates, + "smids": smids, + "rmids": rmids, 'group': { "mhab": hp0, "keys": keys, @@ -551,8 +554,8 @@ def test_multisig_grant_admit(seeder, helpers): 'name': 'holder', 'icp': serder.ked, 'sigs': sigers, - "smids": states, - "rmids": nstates, + "smids": smids, + "rmids": rmids, 'group': { "mhab": hp1, "keys": keys, diff --git a/tests/app/test_specing.py b/tests/app/test_specing.py index 4683fae4..f6331690 100644 --- a/tests/app/test_specing.py +++ b/tests/app/test_specing.py @@ -1,6 +1,5 @@ import json -from keria.app import agenting, aiding, delegating, notifying, indirecting, specing from keria.app import agenting, aiding, delegating, notifying, indirecting, specing from keria.end import ending @@ -11,13 +10,12 @@ def test_spec_resource(helpers): agenting.loadEnds(app) aiding.loadEnds(app, agency, authn=None) delegating.loadEnds(app=app, identifierResource=aiding.IdentifierResourceEnd()) + delegating.loadEnds(app=app, identifierResource=aiding.IdentifierResourceEnd()) ending.loadEnds(agency=agency, app=app) indirecting.loadEnds(agency=agency, app=app) notifying.loadEnds(app) - - specRes = specing.AgentSpecResource( - app, title="KERIA Interactive Web Interface API" - ) + + specRes = specing.AgentSpecResource(app, title='KERIA Interactive Web Interface API') sd = specRes.spec.to_dict() @@ -51,10 +49,4 @@ def test_spec_resource(helpers): js = json.dumps(sd) print(js) # Assert on the entire JSON to ensure we are getting all the docs - assert ( - js - == '{"paths": {"/operations": {"get": {"summary": "Get list of long running operations", "parameters": [{"in": "query", "name": "type", "schema": {"type": "string"}, "required": false, "description": "filter list of long running operations by type"}], "responses": {"200": {"description": "list of long running operations", "content": {"application/json": {"schema": {"type": "array", "items": {"properties": {"name": {"type": "string"}, "metadata": {"type": "object"}, "done": {"type": "boolean"}, "error": {"type": "object"}, "response": {"type": "object"}}}}}}}}}}, "/oobis": {"post": {"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", "oneOf": [{"type": "object", "properties": {"oobialias": {"type": "string", "description": "alias to assign to the identifier resolved from this OOBI"}, "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"}}}}, "/states": {"get": {"summary": "Display key event log (KEL) for given identifier prefix", "description": "If provided qb64 identifier prefix is in Kevers, return the current state of the identifier along with the KEL and all associated signatures and receipts", "tags": ["Key Event Log"], "parameters": [{"in": "path", "name": "pre", "description": "qb64 identifier prefix of KEL to load", "schema": {"type": "string"}, "required": true}], "responses": {"200": {"description": "Key event log and key state of identifier"}, "404": {"description": "Identifier not found in Key event database"}}}}, "/events": {"get": {"summary": "Display key event log (KEL) for given identifier prefix", "description": "If provided qb64 identifier prefix is in Kevers, return the current state of the identifier along with the KEL and all associated signatures and receipts", "tags": ["Key Event Log"], "parameters": [{"in": "path", "name": "pre", "schema": {"type": "string"}, "required": true, "description": "qb64 identifier prefix of KEL to load"}], "responses": {"200": {"description": "Key event log and key state of identifier"}, "404": {"description": "Identifier not found in Key event database"}}}}, "/queries": {"post": {"summary": "Display key event log (KEL) for given identifier prefix", "description": "If provided qb64 identifier prefix is in Kevers, return the current state of the identifier along with the KEL and all associated signatures and receipts", "tags": ["Query"], "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "required": ["pre"], "properties": {"pre": {"type": "string", "description": "qb64 identifier prefix of KEL to load"}, "anchor": {"type": "string", "description": "Anchor"}, "sn": {"type": "string", "description": "Serial number"}}}}}}, "responses": {"200": {"description": "Key event log and key state of identifier"}, "404": {"description": "Identifier not found in Key event database"}}}}, "/identifiers": {"get": {"summary": "Retrieve a list of identifiers associated with the agent.", "description": "This endpoint retrieves a list of identifiers associated with the agent. It supports pagination through the \'Range\' header.", "tags": ["Identifier"], "parameters": [{"in": "header", "name": "Range", "schema": {"type": "string"}, "required": false, "description": "The \'Range\' header is used for pagination. The default range is 0-9."}], "responses": {"200": {"description": "Successfully retrieved identifiers."}, "206": {"description": "Successfully retrieved identifiers within the specified range."}}}, "options": {}, "post": {"summary": "Create an identifier.", "description": "This endpoint creates an identifier with the provided inception event, name, and signatures.", "tags": ["Identifier"], "requestBody": {"content": {"application/json": {"schema": {"type": "object", "properties": {"icp": {"type": "object", "description": "The inception event for the identifier."}, "name": {"type": "string", "description": "The name of the identifier."}, "sigs": {"type": "array", "items": {"type": "string"}, "description": "The signatures for the inception event."}, "group": {"type": "object", "description": "Multisig group information."}, "salty": {"type": "object", "description": "Salty parameters."}, "randy": {"type": "object", "description": "Randomly generated materials."}, "extern": {"type": "object", "description": "External parameters."}}}}}}, "responses": {"202": {"description": "Identifier creation is in progress. The response is a long running operation."}, "400": {"description": "Bad request. This could be due to missing or invalid parameters."}}}}, "/challenges": {"get": {"summary": "Get random list of words for a 2 factor auth challenge", "description": "Get the list of identifiers associated with this agent", "tags": ["Challenge/Response"], "parameters": [{"in": "query", "name": "strength", "schema": {"type": "integer"}, "description": "cryptographic strength of word list", "required": false}], "responses": {"200": {"description": "An array of random words", "content": {"application/json": {"schema": {"description": "Random word list", "type": "object", "properties": {"words": {"type": "array", "description": "random challenge word list", "items": {"type": "string"}}}}}}}}}}, "/contacts": {"get": {"summary": "Get list of contact information associated with remote identifiers", "description": "Get list of contact information associated with remote identifiers. All information is metadata and kept in local storage only", "tags": ["Contacts"], "parameters": [{"in": "query", "name": "group", "schema": {"type": "string"}, "required": false, "description": "field name to group results by"}, {"in": "query", "name": "filter_field", "schema": {"type": "string"}, "description": "field name to search", "required": false}, {"in": "query", "name": "filter_value", "schema": {"type": "string"}, "description": "value to search for", "required": false}], "responses": {"200": {"description": "List of contact information for remote identifiers"}}}}, "/oobi": {"get": {"summary": "Retrieve OOBI resource.", "description": "This endpoint retrieves the OOBI resource based on the provided aid, role, and eid.", "tags": ["OOBI Resource"], "parameters": [{"in": "path", "name": "aid", "schema": {"type": "string"}, "required": true, "description": "The qb64 identifier prefix of OOBI."}, {"in": "path", "name": "role", "schema": {"type": "string"}, "required": true, "description": "The requested role for OOBI rpy message."}, {"in": "path", "name": "eid", "schema": {"type": "string"}, "required": true, "description": "The qb64 identifier prefix of participant in role."}], "responses": {"200": {"description": "Successfully retrieved the OOBI resource."}, "400": {"description": "Bad request. This could be due to invalid or missing parameters."}, "404": {"description": "The requested OOBI resource was not found."}}}}, "/": {"post": {"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": {"204": {"description": "KEL EXN, QRY, RPY event accepted."}}}, "put": {"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."}}}}, "/notifications": {"get": {"summary": "Get list of notifications for the controller of the agent", "description": "Get list of notifications for the controller of the agent. Notifications will be sorted by creation date/time", "parameters": [{"in": "header", "name": "Range", "schema": {"type": "string"}, "required": false, "description": "size of the result list. Defaults to 25"}], "tags": ["Notifications"], "responses": {"200": {"description": "List of contact information for remote identifiers"}}}}, "/operations/{name}": {"delete": {"summary": "Remove a specific long running operation.", "description": "This endpoint removes a long running operation by its name.", "tags": ["Operation"], "parameters": [{"in": "path", "name": "name", "schema": {"type": "string"}, "required": true, "description": "The name of the long running operation to remove."}], "responses": {"204": {"description": "Successfully removed the long running operation."}, "404": {"description": "The requested long running operation was not found."}, "500": {"description": "Internal server error. This could be due to an issue with removing the operation."}}}, "get": {"summary": "Retrieve a specific long running operation.", "description": "This endpoint retrieves the status of a long running operation by its name.", "tags": ["Operation"], "parameters": [{"in": "path", "name": "name", "schema": {"type": "string"}, "required": true, "description": "The name of the long running operation to retrieve."}], "responses": {"200": {"description": "Successfully retrieved the status of the long running operation."}, "404": {"description": "The requested long running operation was not found."}}}}, "/oobis/{alias}": {"get": {"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"}}}}}}}, "/agent/{caid}": {"get": {"summary": "Retrieve key state record of an agent by controller AID.", "description": "This endpoint retrieves the key state record for a given controller of an agent.", "tags": ["Agent"], "parameters": [{"in": "path", "name": "caid", "schema": {"type": "string"}, "required": true, "description": "The qb64 identifier prefix of Controller."}], "responses": {"200": {"description": "Successfully retrieved the key state record."}, "400": {"description": "Bad request. This could be due to an invalid agent or controller configuration."}, "404": {"description": "The requested controller or agent was not found."}}}, "put": {"summary": "Update agent configuration by controller AID.", "description": "This endpoint updates the agent configuration based on the provided request parameters and body.", "tags": ["Agent"], "parameters": [{"in": "path", "name": "caid", "schema": {"type": "string"}, "required": true, "description": "The qb64 identifier prefix of Controller."}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "required": ["rot", "sigs", "sxlt", "kyes"], "properties": {"rot": {"type": "object", "description": "The rotation event."}, "sigs": {"type": "array", "items": {"type": "string"}, "description": "The signatures."}, "sxlt": {"type": "string", "description": "The salty parameters."}, "keys": {"type": "object", "description": "The keys."}}}}}}, "responses": {"204": {"description": "Successfully updated the agent configuration."}, "400": {"description": "Bad request. This could be due to missing or invalid parameters."}, "404": {"description": "The requested agent was not found."}, "500": {"description": "Internal server error. This could be due to an issue with updating the agent configuration."}}}}, "/identifiers/{name}": {"delete": {"summary": "Delete an identifier.", "description": "This endpoint deletes an identifier by its name.", "tags": ["Identifier"], "parameters": [{"in": "path", "name": "name", "schema": {"type": "string"}, "required": true, "description": "The human-readable name of the identifier."}], "responses": {"200": {"description": "Successfully deleted the identifier."}, "400": {"description": "Bad request. This could be due to a missing or invalid name parameter."}, "404": {"description": "The requested identifier was not found."}}}, "get": {"summary": "Retrieve an identifier.", "description": "This endpoint retrieves an identifier by its human-readable name.", "tags": ["Identifier"], "parameters": [{"in": "path", "name": "name", "schema": {"type": "string"}, "required": true, "description": "The human-readable name of the identifier."}], "responses": {"200": {"description": "Successfully retrieved the identifier details."}, "400": {"description": "Bad request. This could be due to a missing or invalid name parameter."}, "404": {"description": "The requested identifier was not found."}}}, "post": {"summary": "Process identifier events.", "description": "This endpoint handles the \'rot\' or \'ixn\' events of an identifier based on the provided request.", "tags": ["Identifier"], "parameters": [{"in": "path", "name": "name", "schema": {"type": "string"}, "required": true, "description": "The human-readable name of the identifier."}], "requestBody": {"content": {"application/json": {"schema": {"type": "object", "properties": {"rot": {"type": "object", "description": "The rotation event details."}, "ixn": {"type": "object", "description": "The interaction event details."}}, "oneOf": [{"required": ["rot"]}, {"required": ["ixn"]}]}}}}, "responses": {"200": {"description": "Successfully processed the identifier\'s event."}, "400": {"description": "Bad request. This could be due to missing or invalid parameters."}}}, "put": {"summary": "Rename an identifier.", "description": "This endpoint renames an identifier with the provided new name.", "tags": ["Identifier"], "parameters": [{"in": "path", "name": "name", "schema": {"type": "string"}, "required": true, "description": "The current human-readable name of the identifier."}], "requestBody": {"content": {"application/json": {"schema": {"type": "object", "properties": {"name": {"type": "string", "description": "The new name for the identifier."}}, "required": ["name"]}}}}, "responses": {"200": {"description": "Successfully renamed the identifier and returns the updated information."}, "400": {"description": "Bad request. This could be due to a missing or invalid name parameter."}, "404": {"description": "The requested identifier was not found."}}}}, "/endroles/{aid}": {"get": {"summary": "Retrieve end roles.", "description": "This endpoint retrieves the end roles associated with AID or human-readable name. It can also filter the end roles based on a specific role.", "tags": ["End Role"], "parameters": [{"in": "path", "name": "name", "schema": {"type": "string"}, "required": true, "description": "The human-readable name of the identifier."}, {"in": "path", "name": "aid", "schema": {"type": "string"}, "required": true, "description": "The identifier (AID)."}, {"in": "path", "name": "role", "schema": {"type": "string"}, "required": true, "description": "The specific role to filter the end roles."}], "responses": {"200": {"description": "Successfully retrieved the end roles. The response body contains the end roles."}, "400": {"description": "Bad request. This could be due to missing or invalid parameters."}, "404": {"description": "The requested identifier was not found."}}}, "post": {"summary": "Create an end role.", "description": "This endpoint creates an end role associated with a given identifier (AID) or name.", "tags": ["End Role"], "parameters": [{"in": "path", "name": "name", "schema": {"type": "string"}, "required": true, "description": "The human-readable name of the identifier."}, {"in": "path", "name": "aid", "schema": {"type": "string"}, "required": true, "description": "Not supported for POST. If provided, a 404 is returned."}], "requestBody": {"content": {"application/json": {"schema": {"type": "object", "properties": {"rpy": {"type": "object", "description": "The reply object."}, "sigs": {"type": "array", "items": {"type": "string"}, "description": "The signatures."}}}}}}, "responses": {"202": {"description": "Accepted. The end role creation is in progress."}, "400": {"description": "Bad request. This could be due to missing or invalid parameters."}, "404": {"description": "Not found. The requested identifier was not found."}}}}, "/escrows/rpy": {"get": {"summary": "Retrieve reply escrows.", "description": "This endpoint retrieves the reply escrows and can filter the collection based on a specific route.", "tags": ["Reply Escrow"], "parameters": [{"in": "query", "name": "route", "schema": {"type": "string"}, "required": false, "description": "The specific route to filter the reply escrow collection."}], "responses": {"200": {"description": "Successfully retrieved the reply escrows."}, "400": {"description": "Bad request. This could be due to missing or invalid parameters."}}}}, "/challenges/{name}": {"post": {"summary": "Sign challenge message and forward to peer identifier", "description": "Sign a challenge word list received out of bands and send `exn` peer to peer message to recipient", "tags": ["Challenge/Response"], "parameters": [{"in": "path", "name": "name", "schema": {"type": "string"}, "required": true, "description": "Human readable alias for the identifier to create"}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"description": "Challenge response", "properties": {"recipient": {"type": "string", "description": "human readable alias recipient identifier to send signed challenge to"}, "words": {"type": "array", "description": "challenge in form of word list", "items": {"type": "string"}}}}}}}, "responses": {"202": {"description": "Success submission of signed challenge/response"}}}}, "/challenges_verify/{source}": {"post": {"summary": "Sign challenge message and forward to peer identifier", "description": "Sign a challenge word list received out of bands and send `exn` peer to peer message to recipient", "tags": ["Challenge/Response"], "parameters": [{"in": "path", "name": "source", "schema": {"type": "string"}, "required": true, "description": "Human readable alias for the identifier to create"}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"description": "Challenge response", "properties": {"recipient": {"type": "string", "description": "human readable alias recipient identifier to send signed challenge to"}, "words": {"type": "array", "description": "challenge in form of word list", "items": {"type": "string"}}}}}}}, "responses": {"202": {"description": "Success submission of signed challenge/response"}}}, "put": {"summary": "Mark challenge response exn message as signed", "description": "Mark challenge response exn message as signed", "tags": ["Challenge/Response"], "parameters": [{"in": "path", "name": "source", "schema": {"type": "string"}, "required": true, "description": "Human readable alias for the identifier to create"}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"description": "Challenge response", "properties": {"aid": {"type": "string", "description": "aid of signer of accepted challenge response"}, "said": {"type": "array", "description": "SAID of challenge message signed", "items": {"type": "string"}}}}}}}, "responses": {"202": {"description": "Success submission of signed challenge/response"}}}}, "/contacts/{prefix}": {"delete": {"summary": "Delete contact information associated with remote identifier", "description": "Delete contact information associated with remote identifier", "tags": ["Contacts"], "parameters": [{"in": "path", "name": "prefix", "schema": {"type": "string"}, "required": true, "description": "qb64 identifier prefix of contact to delete"}], "responses": {"202": {"description": "Contact information successfully deleted for prefix"}, "404": {"description": "No contact information found for prefix"}}}, "get": {"summary": "Get contact information associated with single remote identifier", "description": "Get contact information associated with single remote identifier. All information is meta-data and kept in local storage only", "tags": ["Contacts"], "parameters": [{"in": "path", "name": "prefix", "schema": {"type": "string"}, "required": true, "description": "qb64 identifier prefix of contact to get"}], "responses": {"200": {"description": "Contact information successfully retrieved for prefix"}, "404": {"description": "No contact information found for prefix"}}}, "post": {"summary": "Create new contact information for an identifier", "description": "Creates new information for an identifier, overwriting all existing information for that identifier", "tags": ["Contacts"], "parameters": [{"in": "path", "name": "prefix", "schema": {"type": "string"}, "required": true, "description": "qb64 identifier prefix to add contact metadata to"}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"description": "Contact information", "type": "object"}}}}, "responses": {"200": {"description": "Updated contact information for remote identifier"}, "400": {"description": "Invalid identifier used to update contact information"}, "404": {"description": "Prefix not found in identifier contact information"}}}, "put": {"summary": "Update provided fields in contact information associated with remote identifier prefix", "description": "Update provided fields in contact information associated with remote identifier prefix. All information is metadata and kept in local storage only", "tags": ["Contacts"], "parameters": [{"in": "path", "name": "prefix", "schema": {"type": "string"}, "required": true, "description": "qb64 identifier prefix to add contact metadata to"}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"description": "Contact information", "type": "object"}}}}, "responses": {"200": {"description": "Updated contact information for remote identifier"}, "400": {"description": "Invalid identifier used to update contact information"}, "404": {"description": "Prefix not found in identifier contact information"}}}}, "/oobi/{aid}": {"get": {"summary": "Retrieve OOBI resource.", "description": "This endpoint retrieves the OOBI resource based on the provided aid, role, and eid.", "tags": ["OOBI Resource"], "parameters": [{"in": "path", "name": "aid", "schema": {"type": "string"}, "required": true, "description": "The qb64 identifier prefix of OOBI."}, {"in": "path", "name": "role", "schema": {"type": "string"}, "required": true, "description": "The requested role for OOBI rpy message."}, {"in": "path", "name": "eid", "schema": {"type": "string"}, "required": true, "description": "The qb64 identifier prefix of participant in role."}], "responses": {"200": {"description": "Successfully retrieved the OOBI resource."}, "400": {"description": "Bad request. This could be due to invalid or missing parameters."}, "404": {"description": "The requested OOBI resource was not found."}}}}, "/notifications/{said}": {"delete": {"summary": "Delete notification", "description": "Delete notification", "tags": ["Notifications"], "parameters": [{"in": "path", "name": "said", "schema": {"type": "string"}, "required": true, "description": "qb64 said of note to delete"}], "responses": {"202": {"description": "Notification successfully deleted for prefix"}, "404": {"description": "No notification information found for prefix"}}}, "put": {"summary": "Mark notification as read", "description": "Mark notification as read", "tags": ["Notifications"], "parameters": [{"in": "path", "name": "said", "schema": {"type": "string"}, "required": true, "description": "qb64 said of note to mark as read"}], "responses": {"202": {"description": "Notification successfully marked as read for prefix"}, "404": {"description": "No notification information found for SAID"}}}}, "/identifiers/{name}/events": {"delete": {"summary": "Delete an identifier.", "description": "This endpoint deletes an identifier by its name.", "tags": ["Identifier"], "parameters": [{"in": "path", "name": "name", "schema": {"type": "string"}, "required": true, "description": "The human-readable name of the identifier."}], "responses": {"200": {"description": "Successfully deleted the identifier."}, "400": {"description": "Bad request. This could be due to a missing or invalid name parameter."}, "404": {"description": "The requested identifier was not found."}}}, "get": {"summary": "Retrieve an identifier.", "description": "This endpoint retrieves an identifier by its human-readable name.", "tags": ["Identifier"], "parameters": [{"in": "path", "name": "name", "schema": {"type": "string"}, "required": true, "description": "The human-readable name of the identifier."}], "responses": {"200": {"description": "Successfully retrieved the identifier details."}, "400": {"description": "Bad request. This could be due to a missing or invalid name parameter."}, "404": {"description": "The requested identifier was not found."}}}, "post": {"summary": "Process identifier events.", "description": "This endpoint handles the \'rot\' or \'ixn\' events of an identifier based on the provided request.", "tags": ["Identifier"], "parameters": [{"in": "path", "name": "name", "schema": {"type": "string"}, "required": true, "description": "The human-readable name of the identifier."}], "requestBody": {"content": {"application/json": {"schema": {"type": "object", "properties": {"rot": {"type": "object", "description": "The rotation event details."}, "ixn": {"type": "object", "description": "The interaction event details."}}, "oneOf": [{"required": ["rot"]}, {"required": ["ixn"]}]}}}}, "responses": {"200": {"description": "Successfully processed the identifier\'s event."}, "400": {"description": "Bad request. This could be due to missing or invalid parameters."}}}, "put": {"summary": "Rename an identifier.", "description": "This endpoint renames an identifier with the provided new name.", "tags": ["Identifier"], "parameters": [{"in": "path", "name": "name", "schema": {"type": "string"}, "required": true, "description": "The current human-readable name of the identifier."}], "requestBody": {"content": {"application/json": {"schema": {"type": "object", "properties": {"name": {"type": "string", "description": "The new name for the identifier."}}, "required": ["name"]}}}}, "responses": {"200": {"description": "Successfully renamed the identifier and returns the updated information."}, "400": {"description": "Bad request. This could be due to a missing or invalid name parameter."}, "404": {"description": "The requested identifier was not found."}}}}, "/identifiers/{name}/oobis": {"get": {"summary": "Fetch OOBI URLs of an identifier.", "description": "This endpoint fetches the OOBI URLs for a specific role associated with an identifier.", "tags": ["Identifier"], "parameters": [{"in": "path", "name": "name", "schema": {"type": "string"}, "required": true, "description": "The human-readable name of the identifier."}, {"in": "query", "name": "role", "schema": {"type": "string"}, "required": true, "description": "The role for which to fetch the OOBI URLs. Can be a witness, controller, agent, or mailbox."}], "responses": {"200": {"description": "Successfully fetched the OOBI URLs. The response body contains the OOBI URLs."}, "400": {"description": "Bad request. This could be due to missing or invalid parameters."}, "404": {"description": "The requested identifier was not found."}}}}, "/identifiers/{name}/endroles": {"get": {"summary": "Retrieve end roles.", "description": "This endpoint retrieves the end roles associated with AID or human-readable name. It can also filter the end roles based on a specific role.", "tags": ["End Role"], "parameters": [{"in": "path", "name": "name", "schema": {"type": "string"}, "required": true, "description": "The human-readable name of the identifier."}, {"in": "path", "name": "aid", "schema": {"type": "string"}, "required": true, "description": "The identifier (AID)."}, {"in": "path", "name": "role", "schema": {"type": "string"}, "required": true, "description": "The specific role to filter the end roles."}], "responses": {"200": {"description": "Successfully retrieved the end roles. The response body contains the end roles."}, "400": {"description": "Bad request. This could be due to missing or invalid parameters."}, "404": {"description": "The requested identifier was not found."}}}, "post": {"summary": "Create an end role.", "description": "This endpoint creates an end role associated with a given identifier (AID) or name.", "tags": ["End Role"], "parameters": [{"in": "path", "name": "name", "schema": {"type": "string"}, "required": true, "description": "The human-readable name of the identifier."}, {"in": "path", "name": "aid", "schema": {"type": "string"}, "required": true, "description": "Not supported for POST. If provided, a 404 is returned."}], "requestBody": {"content": {"application/json": {"schema": {"type": "object", "properties": {"rpy": {"type": "object", "description": "The reply object."}, "sigs": {"type": "array", "items": {"type": "string"}, "description": "The signatures."}}}}}}, "responses": {"202": {"description": "Accepted. The end role creation is in progress."}, "400": {"description": "Bad request. This could be due to missing or invalid parameters."}, "404": {"description": "Not found. The requested identifier was not found."}}}}, "/identifiers/{name}/members": {"get": {"summary": "Fetch group member information.", "description": "This endpoint retrieves the signing and rotation members for a specific group associated with an identifier.", "tags": ["Group Member"], "parameters": [{"in": "path", "name": "name", "schema": {"type": "string"}, "required": true, "description": "The human-readable name of the identifier."}], "responses": {"200": {"description": "Successfully fetched the group member information."}, "400": {"description": "Bad request. This could be due to missing or invalid parameters."}, "404": {"description": "The requested identifier was not found."}}}}, "/identifiers/{name}/delegation": {"post": {}}, "/endroles/{aid}/{role}": {"get": {"summary": "Retrieve end roles.", "description": "This endpoint retrieves the end roles associated with AID or human-readable name. It can also filter the end roles based on a specific role.", "tags": ["End Role"], "parameters": [{"in": "path", "name": "name", "schema": {"type": "string"}, "required": true, "description": "The human-readable name of the identifier."}, {"in": "path", "name": "aid", "schema": {"type": "string"}, "required": true, "description": "The identifier (AID)."}, {"in": "path", "name": "role", "schema": {"type": "string"}, "required": true, "description": "The specific role to filter the end roles."}], "responses": {"200": {"description": "Successfully retrieved the end roles. The response body contains the end roles."}, "400": {"description": "Bad request. This could be due to missing or invalid parameters."}, "404": {"description": "The requested identifier was not found."}}}, "post": {"summary": "Create an end role.", "description": "This endpoint creates an end role associated with a given identifier (AID) or name.", "tags": ["End Role"], "parameters": [{"in": "path", "name": "name", "schema": {"type": "string"}, "required": true, "description": "The human-readable name of the identifier."}, {"in": "path", "name": "aid", "schema": {"type": "string"}, "required": true, "description": "Not supported for POST. If provided, a 404 is returned."}], "requestBody": {"content": {"application/json": {"schema": {"type": "object", "properties": {"rpy": {"type": "object", "description": "The reply object."}, "sigs": {"type": "array", "items": {"type": "string"}, "description": "The signatures."}}}}}}, "responses": {"202": {"description": "Accepted. The end role creation is in progress."}, "400": {"description": "Bad request. This could be due to missing or invalid parameters."}, "404": {"description": "Not found. The requested identifier was not found."}}}}, "/contacts/{prefix}/img": {"get": {"summary": "Get contact image for identifer prefix", "description": "Get contact image for identifer prefix", "tags": ["Contacts"], "parameters": [{"in": "path", "name": "prefix", "schema": {"type": "string"}, "required": true, "description": "qb64 identifier prefix of contact image to get"}], "responses": {"200": {"description": "Contact information successfully retrieved for prefix", "content": {"image/jpg": {"schema": {"description": "Image", "type": "binary"}}}}, "404": {"description": "No contact information found for prefix"}}}, "post": {"summary": "Uploads an image to associate with identifier.", "description": "Uploads an image to associate with identifier.", "tags": ["Contacts"], "parameters": [{"in": "path", "name": "prefix", "schema": {"type": "string"}, "description": "identifier prefix to associate image to", "required": true}], "requestBody": {"required": true, "content": {"image/jpg": {"schema": {"type": "string", "format": "binary"}}, "image/png": {"schema": {"type": "string", "format": "binary"}}}}, "responses": {"200": {"description": "Image successfully uploaded"}}}}, "/oobi/{aid}/{role}": {"get": {"summary": "Retrieve OOBI resource.", "description": "This endpoint retrieves the OOBI resource based on the provided aid, role, and eid.", "tags": ["OOBI Resource"], "parameters": [{"in": "path", "name": "aid", "schema": {"type": "string"}, "required": true, "description": "The qb64 identifier prefix of OOBI."}, {"in": "path", "name": "role", "schema": {"type": "string"}, "required": true, "description": "The requested role for OOBI rpy message."}, {"in": "path", "name": "eid", "schema": {"type": "string"}, "required": true, "description": "The qb64 identifier prefix of participant in role."}], "responses": {"200": {"description": "Successfully retrieved the OOBI resource."}, "400": {"description": "Bad request. This could be due to invalid or missing parameters."}, "404": {"description": "The requested OOBI resource was not found."}}}}, "/identifiers/{name}/endroles/{role}": {"get": {"summary": "Retrieve end roles.", "description": "This endpoint retrieves the end roles associated with AID or human-readable name. It can also filter the end roles based on a specific role.", "tags": ["End Role"], "parameters": [{"in": "path", "name": "name", "schema": {"type": "string"}, "required": true, "description": "The human-readable name of the identifier."}, {"in": "path", "name": "aid", "schema": {"type": "string"}, "required": true, "description": "The identifier (AID)."}, {"in": "path", "name": "role", "schema": {"type": "string"}, "required": true, "description": "The specific role to filter the end roles."}], "responses": {"200": {"description": "Successfully retrieved the end roles. The response body contains the end roles."}, "400": {"description": "Bad request. This could be due to missing or invalid parameters."}, "404": {"description": "The requested identifier was not found."}}}, "post": {"summary": "Create an end role.", "description": "This endpoint creates an end role associated with a given identifier (AID) or name.", "tags": ["End Role"], "parameters": [{"in": "path", "name": "name", "schema": {"type": "string"}, "required": true, "description": "The human-readable name of the identifier."}, {"in": "path", "name": "aid", "schema": {"type": "string"}, "required": true, "description": "Not supported for POST. If provided, a 404 is returned."}], "requestBody": {"content": {"application/json": {"schema": {"type": "object", "properties": {"rpy": {"type": "object", "description": "The reply object."}, "sigs": {"type": "array", "items": {"type": "string"}, "description": "The signatures."}}}}}}, "responses": {"202": {"description": "Accepted. The end role creation is in progress."}, "400": {"description": "Bad request. This could be due to missing or invalid parameters."}, "404": {"description": "Not found. The requested identifier was not found."}}}}, "/oobi/{aid}/{role}/{eid}": {"get": {"summary": "Retrieve OOBI resource.", "description": "This endpoint retrieves the OOBI resource based on the provided aid, role, and eid.", "tags": ["OOBI Resource"], "parameters": [{"in": "path", "name": "aid", "schema": {"type": "string"}, "required": true, "description": "The qb64 identifier prefix of OOBI."}, {"in": "path", "name": "role", "schema": {"type": "string"}, "required": true, "description": "The requested role for OOBI rpy message."}, {"in": "path", "name": "eid", "schema": {"type": "string"}, "required": true, "description": "The qb64 identifier prefix of participant in role."}], "responses": {"200": {"description": "Successfully retrieved the OOBI resource."}, "400": {"description": "Bad request. This could be due to invalid or missing parameters."}, "404": {"description": "The requested OOBI resource was not found."}}}}, "/identifiers/{name}/endroles/{role}/{eid}": {"delete": {}}}, "info": {"title": "KERIA Interactive Web Interface API", "version": "1.0.1"}, "openapi": "3.1.0"}' - ) - - -"" + assert js == '{"paths": {"/operations": {"get": {"summary": "Get list of long running operations", "parameters": [{"in": "query", "name": "type", "schema": {"type": "string"}, "required": false, "description": "filter list of long running operations by type"}], "responses": {"200": {"description": "list of long running operations", "content": {"application/json": {"schema": {"type": "array", "items": {"properties": {"name": {"type": "string"}, "metadata": {"type": "object"}, "done": {"type": "boolean"}, "error": {"type": "object"}, "response": {"type": "object"}}}}}}}}}}, "/oobis": {"post": {"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", "oneOf": [{"type": "object", "properties": {"oobialias": {"type": "string", "description": "alias to assign to the identifier resolved from this OOBI"}, "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"}}}}, "/states": {"get": {"summary": "Display key event log (KEL) for given identifier prefix", "description": "If provided qb64 identifier prefix is in Kevers, return the current state of the identifier along with the KEL and all associated signatures and receipts", "tags": ["Key Event Log"], "parameters": [{"in": "path", "name": "pre", "description": "qb64 identifier prefix of KEL to load", "schema": {"type": "string"}, "required": true}], "responses": {"200": {"description": "Key event log and key state of identifier"}, "404": {"description": "Identifier not found in Key event database"}}}}, "/events": {"get": {"summary": "Display key event log (KEL) for given identifier prefix", "description": "If provided qb64 identifier prefix is in Kevers, return the current state of the identifier along with the KEL and all associated signatures and receipts", "tags": ["Key Event Log"], "parameters": [{"in": "path", "name": "pre", "schema": {"type": "string"}, "required": true, "description": "qb64 identifier prefix of KEL to load"}], "responses": {"200": {"description": "Key event log and key state of identifier"}, "404": {"description": "Identifier not found in Key event database"}}}}, "/queries": {"post": {"summary": "Display key event log (KEL) for given identifier prefix", "description": "If provided qb64 identifier prefix is in Kevers, return the current state of the identifier along with the KEL and all associated signatures and receipts", "tags": ["Query"], "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "required": ["pre"], "properties": {"pre": {"type": "string", "description": "qb64 identifier prefix of KEL to load"}, "anchor": {"type": "string", "description": "Anchor"}, "sn": {"type": "string", "description": "Serial number"}}}}}}, "responses": {"200": {"description": "Key event log and key state of identifier"}, "404": {"description": "Identifier not found in Key event database"}}}}, "/identifiers": {"get": {"summary": "Retrieve a list of identifiers associated with the agent.", "description": "This endpoint retrieves a list of identifiers associated with the agent. It supports pagination through the \'Range\' header.", "tags": ["Identifier"], "parameters": [{"in": "header", "name": "Range", "schema": {"type": "string"}, "required": false, "description": "The \'Range\' header is used for pagination. The default range is 0-9."}], "responses": {"200": {"description": "Successfully retrieved identifiers."}, "206": {"description": "Successfully retrieved identifiers within the specified range."}}}, "options": {}, "post": {"summary": "Create an identifier.", "description": "This endpoint creates an identifier with the provided inception event, name, and signatures.", "tags": ["Identifier"], "requestBody": {"content": {"application/json": {"schema": {"type": "object", "properties": {"icp": {"type": "object", "description": "The inception event for the identifier."}, "name": {"type": "string", "description": "The name of the identifier."}, "sigs": {"type": "array", "items": {"type": "string"}, "description": "The signatures for the inception event."}, "group": {"type": "object", "description": "Multisig group information."}, "salty": {"type": "object", "description": "Salty parameters."}, "randy": {"type": "object", "description": "Randomly generated materials."}, "extern": {"type": "object", "description": "External parameters."}}}}}}, "responses": {"202": {"description": "Identifier creation is in progress. The response is a long running operation."}, "400": {"description": "Bad request. This could be due to missing or invalid parameters."}}}}, "/challenges": {"get": {"summary": "Get random list of words for a 2 factor auth challenge", "description": "Get the list of identifiers associated with this agent", "tags": ["Challenge/Response"], "parameters": [{"in": "query", "name": "strength", "schema": {"type": "integer"}, "description": "cryptographic strength of word list", "required": false}], "responses": {"200": {"description": "An array of random words", "content": {"application/json": {"schema": {"description": "Random word list", "type": "object", "properties": {"words": {"type": "array", "description": "random challenge word list", "items": {"type": "string"}}}}}}}}}}, "/contacts": {"get": {"summary": "Get list of contact information associated with remote identifiers", "description": "Get list of contact information associated with remote identifiers. All information is metadata and kept in local storage only", "tags": ["Contacts"], "parameters": [{"in": "query", "name": "group", "schema": {"type": "string"}, "required": false, "description": "field name to group results by"}, {"in": "query", "name": "filter_field", "schema": {"type": "string"}, "description": "field name to search", "required": false}, {"in": "query", "name": "filter_value", "schema": {"type": "string"}, "description": "value to search for", "required": false}], "responses": {"200": {"description": "List of contact information for remote identifiers"}}}}, "/oobi": {"get": {"summary": "Retrieve OOBI resource.", "description": "This endpoint retrieves the OOBI resource based on the provided aid, role, and eid.", "tags": ["OOBI Resource"], "parameters": [{"in": "path", "name": "aid", "schema": {"type": "string"}, "required": true, "description": "The qb64 identifier prefix of OOBI."}, {"in": "path", "name": "role", "schema": {"type": "string"}, "required": true, "description": "The requested role for OOBI rpy message."}, {"in": "path", "name": "eid", "schema": {"type": "string"}, "required": true, "description": "The qb64 identifier prefix of participant in role."}], "responses": {"200": {"description": "Successfully retrieved the OOBI resource."}, "400": {"description": "Bad request. This could be due to invalid or missing parameters."}, "404": {"description": "The requested OOBI resource was not found."}}}}, "/": {"post": {"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": {"204": {"description": "KEL EXN, QRY, RPY event accepted."}}}, "put": {"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."}}}}, "/notifications": {"get": {"summary": "Get list of notifications for the controller of the agent", "description": "Get list of notifications for the controller of the agent. Notifications will be sorted by creation date/time", "parameters": [{"in": "header", "name": "Range", "schema": {"type": "string"}, "required": false, "description": "size of the result list. Defaults to 25"}], "tags": ["Notifications"], "responses": {"200": {"description": "List of contact information for remote identifiers"}}}}, "/operations/{name}": {"delete": {"summary": "Remove a specific long running operation.", "description": "This endpoint removes a long running operation by its name.", "tags": ["Operation"], "parameters": [{"in": "path", "name": "name", "schema": {"type": "string"}, "required": true, "description": "The name of the long running operation to remove."}], "responses": {"204": {"description": "Successfully removed the long running operation."}, "404": {"description": "The requested long running operation was not found."}, "500": {"description": "Internal server error. This could be due to an issue with removing the operation."}}}, "get": {"summary": "Retrieve a specific long running operation.", "description": "This endpoint retrieves the status of a long running operation by its name.", "tags": ["Operation"], "parameters": [{"in": "path", "name": "name", "schema": {"type": "string"}, "required": true, "description": "The name of the long running operation to retrieve."}], "responses": {"200": {"description": "Successfully retrieved the status of the long running operation."}, "404": {"description": "The requested long running operation was not found."}}}}, "/oobis/{alias}": {"get": {"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"}}}}}}}, "/agent/{caid}": {"get": {"summary": "Retrieve key state record of an agent by controller AID.", "description": "This endpoint retrieves the key state record for a given controller of an agent.", "tags": ["Agent"], "parameters": [{"in": "path", "name": "caid", "schema": {"type": "string"}, "required": true, "description": "The qb64 identifier prefix of Controller."}], "responses": {"200": {"description": "Successfully retrieved the key state record."}, "400": {"description": "Bad request. This could be due to an invalid agent or controller configuration."}, "404": {"description": "The requested controller or agent was not found."}}}, "put": {"summary": "Update agent configuration by controller AID.", "description": "This endpoint updates the agent configuration based on the provided request parameters and body.", "tags": ["Agent"], "parameters": [{"in": "path", "name": "caid", "schema": {"type": "string"}, "required": true, "description": "The qb64 identifier prefix of Controller."}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "required": ["rot", "sigs", "sxlt", "kyes"], "properties": {"rot": {"type": "object", "description": "The rotation event."}, "sigs": {"type": "array", "items": {"type": "string"}, "description": "The signatures."}, "sxlt": {"type": "string", "description": "The salty parameters."}, "keys": {"type": "object", "description": "The keys."}}}}}}, "responses": {"204": {"description": "Successfully updated the agent configuration."}, "400": {"description": "Bad request. This could be due to missing or invalid parameters."}, "404": {"description": "The requested agent was not found."}, "500": {"description": "Internal server error. This could be due to an issue with updating the agent configuration."}}}}, "/identifiers/{name}": {"delete": {"summary": "Delete an identifier.", "description": "This endpoint deletes an identifier by its name.", "tags": ["Identifier"], "parameters": [{"in": "path", "name": "name", "schema": {"type": "string"}, "required": true, "description": "The human-readable name of the identifier."}], "responses": {"200": {"description": "Successfully deleted the identifier."}, "400": {"description": "Bad request. This could be due to a missing or invalid name parameter."}, "404": {"description": "The requested identifier was not found."}}}, "get": {"summary": "Retrieve an identifier.", "description": "This endpoint retrieves an identifier by its human-readable name.", "tags": ["Identifier"], "parameters": [{"in": "path", "name": "name", "schema": {"type": "string"}, "required": true, "description": "The human-readable name of the identifier."}], "responses": {"200": {"description": "Successfully retrieved the identifier details."}, "400": {"description": "Bad request. This could be due to a missing or invalid name parameter."}, "404": {"description": "The requested identifier was not found."}}}, "post": {"summary": "Process identifier events.", "description": "This endpoint handles the \'rot\' or \'ixn\' events of an identifier based on the provided request.", "tags": ["Identifier"], "parameters": [{"in": "path", "name": "name", "schema": {"type": "string"}, "required": true, "description": "The human-readable name of the identifier."}], "requestBody": {"content": {"application/json": {"schema": {"type": "object", "properties": {"rot": {"type": "object", "description": "The rotation event details."}, "ixn": {"type": "object", "description": "The interaction event details."}}, "oneOf": [{"required": ["rot"]}, {"required": ["ixn"]}]}}}}, "responses": {"200": {"description": "Successfully processed the identifier\'s event."}, "400": {"description": "Bad request. This could be due to missing or invalid parameters."}}}, "put": {"summary": "Rename an identifier.", "description": "This endpoint renames an identifier with the provided new name.", "tags": ["Identifier"], "parameters": [{"in": "path", "name": "name", "schema": {"type": "string"}, "required": true, "description": "The current human-readable name of the identifier."}], "requestBody": {"content": {"application/json": {"schema": {"type": "object", "properties": {"name": {"type": "string", "description": "The new name for the identifier."}}, "required": ["name"]}}}}, "responses": {"200": {"description": "Successfully renamed the identifier and returns the updated information."}, "400": {"description": "Bad request. This could be due to a missing or invalid name parameter."}, "404": {"description": "The requested identifier was not found."}}}}, "/endroles/{aid}": {"get": {"summary": "Retrieve end roles.", "description": "This endpoint retrieves the end roles associated with AID or human-readable name. It can also filter the end roles based on a specific role.", "tags": ["End Role"], "parameters": [{"in": "path", "name": "name", "schema": {"type": "string"}, "required": true, "description": "The human-readable name of the identifier."}, {"in": "path", "name": "aid", "schema": {"type": "string"}, "required": true, "description": "The identifier (AID)."}, {"in": "path", "name": "role", "schema": {"type": "string"}, "required": true, "description": "The specific role to filter the end roles."}], "responses": {"200": {"description": "Successfully retrieved the end roles. The response body contains the end roles."}, "400": {"description": "Bad request. This could be due to missing or invalid parameters."}, "404": {"description": "The requested identifier was not found."}}}, "post": {"summary": "Create an end role.", "description": "This endpoint creates an end role associated with a given identifier (AID) or name.", "tags": ["End Role"], "parameters": [{"in": "path", "name": "name", "schema": {"type": "string"}, "required": true, "description": "The human-readable name of the identifier."}, {"in": "path", "name": "aid", "schema": {"type": "string"}, "required": true, "description": "Not supported for POST. If provided, a 404 is returned."}], "requestBody": {"content": {"application/json": {"schema": {"type": "object", "properties": {"rpy": {"type": "object", "description": "The reply object."}, "sigs": {"type": "array", "items": {"type": "string"}, "description": "The signatures."}}}}}}, "responses": {"202": {"description": "Accepted. The end role creation is in progress."}, "400": {"description": "Bad request. This could be due to missing or invalid parameters."}, "404": {"description": "Not found. The requested identifier was not found."}}}}, "/escrows/rpy": {"get": {"summary": "Retrieve reply escrows.", "description": "This endpoint retrieves the reply escrows and can filter the collection based on a specific route.", "tags": ["Reply Escrow"], "parameters": [{"in": "query", "name": "route", "schema": {"type": "string"}, "required": false, "description": "The specific route to filter the reply escrow collection."}], "responses": {"200": {"description": "Successfully retrieved the reply escrows."}, "400": {"description": "Bad request. This could be due to missing or invalid parameters."}}}}, "/challenges/{name}": {"post": {"summary": "Sign challenge message and forward to peer identifier", "description": "Sign a challenge word list received out of bands and send `exn` peer to peer message to recipient", "tags": ["Challenge/Response"], "parameters": [{"in": "path", "name": "name", "schema": {"type": "string"}, "required": true, "description": "Human readable alias for the identifier to create"}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"description": "Challenge response", "properties": {"recipient": {"type": "string", "description": "human readable alias recipient identifier to send signed challenge to"}, "words": {"type": "array", "description": "challenge in form of word list", "items": {"type": "string"}}}}}}}, "responses": {"202": {"description": "Success submission of signed challenge/response"}}}}, "/challenges_verify/{source}": {"post": {"summary": "Sign challenge message and forward to peer identifier", "description": "Sign a challenge word list received out of bands and send `exn` peer to peer message to recipient", "tags": ["Challenge/Response"], "parameters": [{"in": "path", "name": "source", "schema": {"type": "string"}, "required": true, "description": "Human readable alias for the identifier to create"}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"description": "Challenge response", "properties": {"recipient": {"type": "string", "description": "human readable alias recipient identifier to send signed challenge to"}, "words": {"type": "array", "description": "challenge in form of word list", "items": {"type": "string"}}}}}}}, "responses": {"202": {"description": "Success submission of signed challenge/response"}}}, "put": {"summary": "Mark challenge response exn message as signed", "description": "Mark challenge response exn message as signed", "tags": ["Challenge/Response"], "parameters": [{"in": "path", "name": "source", "schema": {"type": "string"}, "required": true, "description": "Human readable alias for the identifier to create"}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"description": "Challenge response", "properties": {"aid": {"type": "string", "description": "aid of signer of accepted challenge response"}, "said": {"type": "array", "description": "SAID of challenge message signed", "items": {"type": "string"}}}}}}}, "responses": {"202": {"description": "Success submission of signed challenge/response"}}}}, "/contacts/{prefix}": {"delete": {"summary": "Delete contact information associated with remote identifier", "description": "Delete contact information associated with remote identifier", "tags": ["Contacts"], "parameters": [{"in": "path", "name": "prefix", "schema": {"type": "string"}, "required": true, "description": "qb64 identifier prefix of contact to delete"}], "responses": {"202": {"description": "Contact information successfully deleted for prefix"}, "404": {"description": "No contact information found for prefix"}}}, "get": {"summary": "Get contact information associated with single remote identifier", "description": "Get contact information associated with single remote identifier. All information is meta-data and kept in local storage only", "tags": ["Contacts"], "parameters": [{"in": "path", "name": "prefix", "schema": {"type": "string"}, "required": true, "description": "qb64 identifier prefix of contact to get"}], "responses": {"200": {"description": "Contact information successfully retrieved for prefix"}, "404": {"description": "No contact information found for prefix"}}}, "post": {"summary": "Create new contact information for an identifier", "description": "Creates new information for an identifier, overwriting all existing information for that identifier", "tags": ["Contacts"], "parameters": [{"in": "path", "name": "prefix", "schema": {"type": "string"}, "required": true, "description": "qb64 identifier prefix to add contact metadata to"}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"description": "Contact information", "type": "object"}}}}, "responses": {"200": {"description": "Updated contact information for remote identifier"}, "400": {"description": "Invalid identifier used to update contact information"}, "404": {"description": "Prefix not found in identifier contact information"}}}, "put": {"summary": "Update provided fields in contact information associated with remote identifier prefix", "description": "Update provided fields in contact information associated with remote identifier prefix. All information is metadata and kept in local storage only", "tags": ["Contacts"], "parameters": [{"in": "path", "name": "prefix", "schema": {"type": "string"}, "required": true, "description": "qb64 identifier prefix to add contact metadata to"}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"description": "Contact information", "type": "object"}}}}, "responses": {"200": {"description": "Updated contact information for remote identifier"}, "400": {"description": "Invalid identifier used to update contact information"}, "404": {"description": "Prefix not found in identifier contact information"}}}}, "/oobi/{aid}": {"get": {"summary": "Retrieve OOBI resource.", "description": "This endpoint retrieves the OOBI resource based on the provided aid, role, and eid.", "tags": ["OOBI Resource"], "parameters": [{"in": "path", "name": "aid", "schema": {"type": "string"}, "required": true, "description": "The qb64 identifier prefix of OOBI."}, {"in": "path", "name": "role", "schema": {"type": "string"}, "required": true, "description": "The requested role for OOBI rpy message."}, {"in": "path", "name": "eid", "schema": {"type": "string"}, "required": true, "description": "The qb64 identifier prefix of participant in role."}], "responses": {"200": {"description": "Successfully retrieved the OOBI resource."}, "400": {"description": "Bad request. This could be due to invalid or missing parameters."}, "404": {"description": "The requested OOBI resource was not found."}}}}, "/notifications/{said}": {"delete": {"summary": "Delete notification", "description": "Delete notification", "tags": ["Notifications"], "parameters": [{"in": "path", "name": "said", "schema": {"type": "string"}, "required": true, "description": "qb64 said of note to delete"}], "responses": {"202": {"description": "Notification successfully deleted for prefix"}, "404": {"description": "No notification information found for prefix"}}}, "put": {"summary": "Mark notification as read", "description": "Mark notification as read", "tags": ["Notifications"], "parameters": [{"in": "path", "name": "said", "schema": {"type": "string"}, "required": true, "description": "qb64 said of note to mark as read"}], "responses": {"202": {"description": "Notification successfully marked as read for prefix"}, "404": {"description": "No notification information found for SAID"}}}}, "/identifiers/{name}/events": {"delete": {"summary": "Delete an identifier.", "description": "This endpoint deletes an identifier by its name.", "tags": ["Identifier"], "parameters": [{"in": "path", "name": "name", "schema": {"type": "string"}, "required": true, "description": "The human-readable name of the identifier."}], "responses": {"200": {"description": "Successfully deleted the identifier."}, "400": {"description": "Bad request. This could be due to a missing or invalid name parameter."}, "404": {"description": "The requested identifier was not found."}}}, "get": {"summary": "Retrieve an identifier.", "description": "This endpoint retrieves an identifier by its human-readable name.", "tags": ["Identifier"], "parameters": [{"in": "path", "name": "name", "schema": {"type": "string"}, "required": true, "description": "The human-readable name of the identifier."}], "responses": {"200": {"description": "Successfully retrieved the identifier details."}, "400": {"description": "Bad request. This could be due to a missing or invalid name parameter."}, "404": {"description": "The requested identifier was not found."}}}, "post": {"summary": "Process identifier events.", "description": "This endpoint handles the \'rot\' or \'ixn\' events of an identifier based on the provided request.", "tags": ["Identifier"], "parameters": [{"in": "path", "name": "name", "schema": {"type": "string"}, "required": true, "description": "The human-readable name of the identifier."}], "requestBody": {"content": {"application/json": {"schema": {"type": "object", "properties": {"rot": {"type": "object", "description": "The rotation event details."}, "ixn": {"type": "object", "description": "The interaction event details."}}, "oneOf": [{"required": ["rot"]}, {"required": ["ixn"]}]}}}}, "responses": {"200": {"description": "Successfully processed the identifier\'s event."}, "400": {"description": "Bad request. This could be due to missing or invalid parameters."}}}, "put": {"summary": "Rename an identifier.", "description": "This endpoint renames an identifier with the provided new name.", "tags": ["Identifier"], "parameters": [{"in": "path", "name": "name", "schema": {"type": "string"}, "required": true, "description": "The current human-readable name of the identifier."}], "requestBody": {"content": {"application/json": {"schema": {"type": "object", "properties": {"name": {"type": "string", "description": "The new name for the identifier."}}, "required": ["name"]}}}}, "responses": {"200": {"description": "Successfully renamed the identifier and returns the updated information."}, "400": {"description": "Bad request. This could be due to a missing or invalid name parameter."}, "404": {"description": "The requested identifier was not found."}}}}, "/identifiers/{name}/oobis": {"get": {"summary": "Fetch OOBI URLs of an identifier.", "description": "This endpoint fetches the OOBI URLs for a specific role associated with an identifier.", "tags": ["Identifier"], "parameters": [{"in": "path", "name": "name", "schema": {"type": "string"}, "required": true, "description": "The human-readable name of the identifier."}, {"in": "query", "name": "role", "schema": {"type": "string"}, "required": true, "description": "The role for which to fetch the OOBI URLs. Can be a witness, controller, agent, or mailbox."}], "responses": {"200": {"description": "Successfully fetched the OOBI URLs. The response body contains the OOBI URLs."}, "400": {"description": "Bad request. This could be due to missing or invalid parameters."}, "404": {"description": "The requested identifier was not found."}}}}, "/identifiers/{name}/endroles": {"get": {"summary": "Retrieve end roles.", "description": "This endpoint retrieves the end roles associated with AID or human-readable name. It can also filter the end roles based on a specific role.", "tags": ["End Role"], "parameters": [{"in": "path", "name": "name", "schema": {"type": "string"}, "required": true, "description": "The human-readable name of the identifier."}, {"in": "path", "name": "aid", "schema": {"type": "string"}, "required": true, "description": "The identifier (AID)."}, {"in": "path", "name": "role", "schema": {"type": "string"}, "required": true, "description": "The specific role to filter the end roles."}], "responses": {"200": {"description": "Successfully retrieved the end roles. The response body contains the end roles."}, "400": {"description": "Bad request. This could be due to missing or invalid parameters."}, "404": {"description": "The requested identifier was not found."}}}, "post": {"summary": "Create an end role.", "description": "This endpoint creates an end role associated with a given identifier (AID) or name.", "tags": ["End Role"], "parameters": [{"in": "path", "name": "name", "schema": {"type": "string"}, "required": true, "description": "The human-readable name of the identifier."}, {"in": "path", "name": "aid", "schema": {"type": "string"}, "required": true, "description": "Not supported for POST. If provided, a 404 is returned."}], "requestBody": {"content": {"application/json": {"schema": {"type": "object", "properties": {"rpy": {"type": "object", "description": "The reply object."}, "sigs": {"type": "array", "items": {"type": "string"}, "description": "The signatures."}}}}}}, "responses": {"202": {"description": "Accepted. The end role creation is in progress."}, "400": {"description": "Bad request. This could be due to missing or invalid parameters."}, "404": {"description": "Not found. The requested identifier was not found."}}}}, "/identifiers/{name}/members": {"get": {"summary": "Fetch group member information.", "description": "This endpoint retrieves the signing and rotation members for a specific group associated with an identifier.", "tags": ["Group Member"], "parameters": [{"in": "path", "name": "name", "schema": {"type": "string"}, "required": true, "description": "The human-readable name of the identifier."}], "responses": {"200": {"description": "Successfully fetched the group member information."}, "400": {"description": "Bad request. This could be due to missing or invalid parameters."}, "404": {"description": "The requested identifier was not found."}}}}, "/identifiers/{name}/delegation": {"post": {}}, "/endroles/{aid}/{role}": {"get": {"summary": "Retrieve end roles.", "description": "This endpoint retrieves the end roles associated with AID or human-readable name. It can also filter the end roles based on a specific role.", "tags": ["End Role"], "parameters": [{"in": "path", "name": "name", "schema": {"type": "string"}, "required": true, "description": "The human-readable name of the identifier."}, {"in": "path", "name": "aid", "schema": {"type": "string"}, "required": true, "description": "The identifier (AID)."}, {"in": "path", "name": "role", "schema": {"type": "string"}, "required": true, "description": "The specific role to filter the end roles."}], "responses": {"200": {"description": "Successfully retrieved the end roles. The response body contains the end roles."}, "400": {"description": "Bad request. This could be due to missing or invalid parameters."}, "404": {"description": "The requested identifier was not found."}}}, "post": {"summary": "Create an end role.", "description": "This endpoint creates an end role associated with a given identifier (AID) or name.", "tags": ["End Role"], "parameters": [{"in": "path", "name": "name", "schema": {"type": "string"}, "required": true, "description": "The human-readable name of the identifier."}, {"in": "path", "name": "aid", "schema": {"type": "string"}, "required": true, "description": "Not supported for POST. If provided, a 404 is returned."}], "requestBody": {"content": {"application/json": {"schema": {"type": "object", "properties": {"rpy": {"type": "object", "description": "The reply object."}, "sigs": {"type": "array", "items": {"type": "string"}, "description": "The signatures."}}}}}}, "responses": {"202": {"description": "Accepted. The end role creation is in progress."}, "400": {"description": "Bad request. This could be due to missing or invalid parameters."}, "404": {"description": "Not found. The requested identifier was not found."}}}}, "/contacts/{prefix}/img": {"get": {"summary": "Get contact image for identifer prefix", "description": "Get contact image for identifer prefix", "tags": ["Contacts"], "parameters": [{"in": "path", "name": "prefix", "schema": {"type": "string"}, "required": true, "description": "qb64 identifier prefix of contact image to get"}], "responses": {"200": {"description": "Contact information successfully retrieved for prefix", "content": {"image/jpg": {"schema": {"description": "Image", "type": "binary"}}}}, "404": {"description": "No contact information found for prefix"}}}, "post": {"summary": "Uploads an image to associate with identifier.", "description": "Uploads an image to associate with identifier.", "tags": ["Contacts"], "parameters": [{"in": "path", "name": "prefix", "schema": {"type": "string"}, "description": "identifier prefix to associate image to", "required": true}], "requestBody": {"required": true, "content": {"image/jpg": {"schema": {"type": "string", "format": "binary"}}, "image/png": {"schema": {"type": "string", "format": "binary"}}}}, "responses": {"200": {"description": "Image successfully uploaded"}}}}, "/oobi/{aid}/{role}": {"get": {"summary": "Retrieve OOBI resource.", "description": "This endpoint retrieves the OOBI resource based on the provided aid, role, and eid.", "tags": ["OOBI Resource"], "parameters": [{"in": "path", "name": "aid", "schema": {"type": "string"}, "required": true, "description": "The qb64 identifier prefix of OOBI."}, {"in": "path", "name": "role", "schema": {"type": "string"}, "required": true, "description": "The requested role for OOBI rpy message."}, {"in": "path", "name": "eid", "schema": {"type": "string"}, "required": true, "description": "The qb64 identifier prefix of participant in role."}], "responses": {"200": {"description": "Successfully retrieved the OOBI resource."}, "400": {"description": "Bad request. This could be due to invalid or missing parameters."}, "404": {"description": "The requested OOBI resource was not found."}}}}, "/identifiers/{name}/endroles/{role}": {"get": {"summary": "Retrieve end roles.", "description": "This endpoint retrieves the end roles associated with AID or human-readable name. It can also filter the end roles based on a specific role.", "tags": ["End Role"], "parameters": [{"in": "path", "name": "name", "schema": {"type": "string"}, "required": true, "description": "The human-readable name of the identifier."}, {"in": "path", "name": "aid", "schema": {"type": "string"}, "required": true, "description": "The identifier (AID)."}, {"in": "path", "name": "role", "schema": {"type": "string"}, "required": true, "description": "The specific role to filter the end roles."}], "responses": {"200": {"description": "Successfully retrieved the end roles. The response body contains the end roles."}, "400": {"description": "Bad request. This could be due to missing or invalid parameters."}, "404": {"description": "The requested identifier was not found."}}}, "post": {"summary": "Create an end role.", "description": "This endpoint creates an end role associated with a given identifier (AID) or name.", "tags": ["End Role"], "parameters": [{"in": "path", "name": "name", "schema": {"type": "string"}, "required": true, "description": "The human-readable name of the identifier."}, {"in": "path", "name": "aid", "schema": {"type": "string"}, "required": true, "description": "Not supported for POST. If provided, a 404 is returned."}], "requestBody": {"content": {"application/json": {"schema": {"type": "object", "properties": {"rpy": {"type": "object", "description": "The reply object."}, "sigs": {"type": "array", "items": {"type": "string"}, "description": "The signatures."}}}}}}, "responses": {"202": {"description": "Accepted. The end role creation is in progress."}, "400": {"description": "Bad request. This could be due to missing or invalid parameters."}, "404": {"description": "Not found. The requested identifier was not found."}}}}, "/oobi/{aid}/{role}/{eid}": {"get": {"summary": "Retrieve OOBI resource.", "description": "This endpoint retrieves the OOBI resource based on the provided aid, role, and eid.", "tags": ["OOBI Resource"], "parameters": [{"in": "path", "name": "aid", "schema": {"type": "string"}, "required": true, "description": "The qb64 identifier prefix of OOBI."}, {"in": "path", "name": "role", "schema": {"type": "string"}, "required": true, "description": "The requested role for OOBI rpy message."}, {"in": "path", "name": "eid", "schema": {"type": "string"}, "required": true, "description": "The qb64 identifier prefix of participant in role."}], "responses": {"200": {"description": "Successfully retrieved the OOBI resource."}, "400": {"description": "Bad request. This could be due to invalid or missing parameters."}, "404": {"description": "The requested OOBI resource was not found."}}}}, "/identifiers/{name}/endroles/{role}/{eid}": {"delete": {}}}, "info": {"title": "KERIA Interactive Web Interface API", "version": "1.0.1"}, "openapi": "3.1.0"}'