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 0ce9edff..af34d84f 100644 --- a/setup.py +++ b/setup.py @@ -76,7 +76,7 @@ python_requires='>=3.12.2', install_requires=[ 'hio>=0.6.12', - 'keri==1.2.0-dev4', + 'keri>=1.2.0.dev8', 'mnemonic>=0.20', 'multicommand>=1.0.0', 'falcon>=3.1.3', diff --git a/src/keria/app/agenting.py b/src/keria/app/agenting.py index caae9c3f..da140056 100644 --- a/src/keria/app/agenting.py +++ b/src/keria/app/agenting.py @@ -8,6 +8,7 @@ import os from dataclasses import asdict from urllib.parse import urlparse, urljoin +from types import MappingProxyType import falcon from falcon import media @@ -90,6 +91,7 @@ def setup(name, bran, adminPort, bootPort, base='', httpPort=None, configFile=No loadEnds(app=app) aidEnd = aiding.loadEnds(app=app, agency=agency, authn=authn) credentialing.loadEnds(app=app, identifierResource=aidEnd) + delegating.loadEnds(app=app, identifierResource=aidEnd) notifying.loadEnds(app=app) keriagrouping.loadEnds(app=app) keriaexchanging.loadEnds(app=app) @@ -288,6 +290,8 @@ def __init__(self, hby, rgy, agentHab, agency, caid, **opts): self.agentHab = agentHab self.agency = agency self.caid = caid + self.cfd = MappingProxyType(dict(self.hby.cf.get()) if self.hby.cf is not None else dict()) + self.tocks = MappingProxyType(self.cfd.get("tocks", {})) self.swain = delegating.Anchorer(hby=hby, proxy=agentHab) self.counselor = Counselor(hby=hby, swain=self.swain, proxy=agentHab) @@ -363,19 +367,25 @@ def __init__(self, hby, rgy, agentHab, agency, caid, **opts): local=True) # disable misfit escrow until we can add another parser for remote. doers.extend([ - Initer(agentHab=agentHab, caid=caid), - Querier(hby=hby, agentHab=agentHab, kvy=self.kvy, queries=self.queries), + Initer(agentHab=agentHab, caid=caid, tock=self.tocks.get("initer", 0.0)), + Querier(hby=hby, agentHab=agentHab, kvy=self.kvy, queries=self.queries, + tock=self.tocks.get("querier", 0.0)), Escrower(kvy=self.kvy, rgy=self.rgy, rvy=self.rvy, tvy=self.tvy, exc=self.exc, vry=self.verifier, - registrar=self.registrar, credentialer=self.credentialer), - ParserDoer(kvy=self.kvy, parser=self.parser), - Witnesser(receiptor=receiptor, witners=self.witners), - Delegator(agentHab=agentHab, swain=self.swain, anchors=self.anchors), - ExchangeSender(hby=hby, agentHab=agentHab, exc=self.exc, exchanges=self.exchanges), - Granter(hby=hby, rgy=rgy, agentHab=agentHab, exc=self.exc, grants=self.grants), - Admitter(hby=hby, witq=self.witq, psr=self.parser, agentHab=agentHab, exc=self.exc, admits=self.admits), - GroupRequester(hby=hby, agentHab=agentHab, counselor=self.counselor, groups=self.groups), - SeekerDoer(seeker=self.seeker, cues=self.verifier.cues), - ExchangeCueDoer(seeker=self.exnseeker, cues=self.exc.cues, queries=self.queries) + registrar=self.registrar, credentialer=self.credentialer, tock=self.tocks.get("escrower", 0.0)), + ParserDoer(kvy=self.kvy, parser=self.parser, tock=self.tocks.get("parser", 0.0)), + Witnesser(receiptor=receiptor, witners=self.witners, tock=self.tocks.get("witnesser", 0.0)), + Delegator(agentHab=agentHab, swain=self.swain, anchors=self.anchors, tock=self.tocks.get("delegator", 0.0)), + ExchangeSender(hby=hby, agentHab=agentHab, exc=self.exc, exchanges=self.exchanges, + tock=self.tocks.get("exchangeSender", 0.0)), + Granter(hby=hby, rgy=rgy, agentHab=agentHab, exc=self.exc, grants=self.grants, + tock=self.tocks.get("granter", 0.0)), + Admitter(hby=hby, witq=self.witq, psr=self.parser, agentHab=agentHab, exc=self.exc, admits=self.admits, + tock=self.tocks.get("admitter", 0.0)), + GroupRequester(hby=hby, agentHab=agentHab, counselor=self.counselor, groups=self.groups, + tock=self.tocks.get("groupRequester", 0.0)), + SeekerDoer(seeker=self.seeker, cues=self.verifier.cues, tock=self.tocks.get("seeker", 0.0)), + ExchangeCueDoer(seeker=self.exnseeker, cues=self.exc.cues, queries=self.queries, + tock=self.tocks.get("exchangecue", 0.0)), ]) super(Agent, self).__init__(doers=doers, always=True, **opts) @@ -411,10 +421,11 @@ def inceptExtern(self, pre, verfers, digers, **kwargs): class ParserDoer(doing.Doer): - def __init__(self, kvy, parser): + def __init__(self, kvy, parser, tock=0.0): self.kvy = kvy self.parser = parser - super(ParserDoer, self).__init__() + self.tock = tock + super(ParserDoer, self).__init__(tock=self.tock) def recur(self, tyme=None): if self.parser.ims: @@ -425,10 +436,11 @@ def recur(self, tyme=None): class Witnesser(doing.Doer): - def __init__(self, receiptor, witners): + def __init__(self, receiptor, witners, tock=0.0): self.receiptor = receiptor self.witners = witners - super(Witnesser, self).__init__() + self.tock = tock + super(Witnesser, self).__init__(tock=self.tock) def recur(self, tyme=None): while True: @@ -449,11 +461,12 @@ def recur(self, tyme=None): class Delegator(doing.Doer): - def __init__(self, agentHab, swain, anchors): + def __init__(self, agentHab, swain, anchors, tock=0.0): self.agentHab = agentHab self.swain = swain self.anchors = anchors - super(Delegator, self).__init__() + self.tock = tock + super(Delegator, self).__init__(tock=self.tock) def recur(self, tyme=None): if self.anchors: @@ -466,12 +479,13 @@ def recur(self, tyme=None): class ExchangeSender(doing.DoDoer): - def __init__(self, hby, agentHab, exc, exchanges): + def __init__(self, hby, agentHab, exc, exchanges, tock=0.0): self.hby = hby self.agentHab = agentHab self.exc = exc self.exchanges = exchanges - super(ExchangeSender, self).__init__(always=True) + self.tock = tock + super(ExchangeSender, self).__init__(always=True, tock=self.tock) def recur(self, tyme, deeds=None): if self.exchanges: @@ -506,13 +520,14 @@ def recur(self, tyme, deeds=None): class Granter(doing.DoDoer): - def __init__(self, hby, rgy, agentHab, exc, grants): + def __init__(self, hby, rgy, agentHab, exc, grants, tock=0.0): self.hby = hby self.rgy = rgy self.agentHab = agentHab self.exc = exc self.grants = grants - super(Granter, self).__init__(always=True) + self.tock = tock + super(Granter, self).__init__(always=True, tock=self.tock) def recur(self, tyme, deeds=None): if self.grants: @@ -552,14 +567,15 @@ def recur(self, tyme, deeds=None): class Admitter(doing.Doer): - def __init__(self, hby, witq, psr, agentHab, exc, admits): + def __init__(self, hby, witq, psr, agentHab, exc, admits, tock=0.0): self.hby = hby self.agentHab = agentHab self.witq = witq self.psr = psr self.exc = exc self.admits = admits - super(Admitter, self).__init__() + self.tock = tock + super(Admitter, self).__init__(tock=self.tock) def recur(self, tyme): if self.admits: @@ -599,11 +615,11 @@ def recur(self, tyme): class SeekerDoer(doing.Doer): - def __init__(self, seeker, cues): + def __init__(self, seeker, cues, tock=0.0): self.seeker = seeker self.cues = cues - - super(SeekerDoer, self).__init__() + self.tock = tock + super(SeekerDoer, self).__init__(tock=self.tock) def recur(self, tyme=None): if self.cues: @@ -622,12 +638,12 @@ def recur(self, tyme=None): class ExchangeCueDoer(doing.Doer): - def __init__(self, seeker, cues, queries): + def __init__(self, seeker, cues, queries, tock=0.0): self.seeker = seeker self.cues = cues self.queries = queries - - super(ExchangeCueDoer, self).__init__() + self.tock = tock + super(ExchangeCueDoer, self).__init__(tock=self.tock) def recur(self, tyme=None): if self.cues: @@ -648,10 +664,11 @@ def recur(self, tyme=None): class Initer(doing.Doer): - def __init__(self, agentHab, caid): + def __init__(self, agentHab, caid, tock=0.0): self.agentHab = agentHab self.caid = caid - super(Initer, self).__init__() + self.tock = tock + super(Initer, self).__init__(tock=self.tock) def recur(self, tyme): """ Prints Agent name and prefix """ @@ -664,13 +681,13 @@ def recur(self, tyme): class GroupRequester(doing.Doer): - def __init__(self, hby, agentHab, counselor, groups): + def __init__(self, hby, agentHab, counselor, groups, tock=0.0): self.hby = hby self.agentHab = agentHab self.counselor = counselor self.groups = groups - - super(GroupRequester, self).__init__() + self.tock = tock + super(GroupRequester, self).__init__(tock=self.tock) def recur(self, tyme): """ Checks cue for group proceccing requests and processes any with Counselor """ @@ -690,13 +707,13 @@ def recur(self, tyme): class Querier(doing.DoDoer): - def __init__(self, hby, agentHab, queries, kvy): + def __init__(self, hby, agentHab, queries, kvy, tock=0.0): self.hby = hby self.agentHab = agentHab self.queries = queries self.kvy = kvy - - super(Querier, self).__init__(always=True) + self.tock = tock + super(Querier, self).__init__(always=True, tock=self.tock) def recur(self, tyme, deeds=None): """ Processes query reqests submitting any on the cue""" @@ -723,7 +740,7 @@ def recur(self, tyme, deeds=None): class Escrower(doing.Doer): - def __init__(self, kvy, rgy, rvy, tvy, exc, vry, registrar, credentialer): + def __init__(self, kvy, rgy, rvy, tvy, exc, vry, registrar, credentialer, tock=0.0): """ Recuring process or escrows for all components in an Agent Parameters: @@ -744,8 +761,9 @@ def __init__(self, kvy, rgy, rvy, tvy, exc, vry, registrar, credentialer): self.vry = vry self.registrar = registrar self.credentialer = credentialer + self.tock = tock - super(Escrower, self).__init__() + super(Escrower, self).__init__(tock=self.tock) def recur(self, tyme): """ Process all escrows once per loop. """ @@ -759,7 +777,6 @@ def recur(self, tyme): self.vry.processEscrows() self.registrar.processEscrows() self.credentialer.processEscrows() - return False @@ -904,11 +921,11 @@ def on_get(req, rep): - Key Event Log parameters: - in: path - name: prefix + name: pre + description: qb64 identifier prefix of KEL to load schema: type: string required: true - description: qb64 identifier prefix of KEL to load responses: 200: description: Key event log and key state of identifier @@ -955,7 +972,7 @@ def on_get(req, rep): - Key Event Log parameters: - in: path - name: prefix + name: pre schema: type: string required: true @@ -1013,12 +1030,13 @@ def on_post(req, rep): content: application/json: schema: - description: OOBI - properties: + description: OOBI + oneOf: + - type: object + properties: oobialias: type: string description: alias to assign to the identifier resolved from this OOBI - required: false url: type: string description: URL OOBI @@ -1165,13 +1183,24 @@ def on_post(req, rep): identifier along with the KEL and all associated signatures and receipts tags: - Query - parameters: - - in: body - name: pre - schema: - type: string - required: true - description: qb64 identifier prefix of KEL to load + 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 diff --git a/src/keria/app/aiding.py b/src/keria/app/aiding.py index fa8ef21a..77955030 100644 --- a/src/keria/app/aiding.py +++ b/src/keria/app/aiding.py @@ -30,6 +30,7 @@ def loadEnds(app, agency, authn): aidsEnd = IdentifierCollectionEnd() app.add_route("/identifiers", aidsEnd) + aidEnd = IdentifierResourceEnd() app.add_route("/identifiers/{name}", aidEnd) app.add_route("/identifiers/{name}/events", aidEnd) @@ -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 @@ -86,16 +87,41 @@ def on_get(self, _, rep, caid): rep: falcon.Response HTTP response caid(str): qb64 identifier prefix of Controller + --- + 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. """ 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(): @@ -105,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: @@ -133,6 +156,53 @@ def on_put(self, req, rep, caid): rep (Response): falcon.Response HTTP response caid(str): qb64 identifier prefix of Controller + --- + 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. """ agent = self.agency.get(caid) if agent is None: @@ -147,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"] @@ -174,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] @@ -194,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) @@ -209,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] @@ -234,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 @@ -247,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): @@ -256,12 +346,30 @@ 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 rep: falcon.Response HTTP response + --- + 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. """ agent = req.context.agent res = [] @@ -302,12 +410,51 @@ 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 rep (Response): falcon.Response HTTP response object + --- + 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. """ agent = req.context.agent try: @@ -321,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 @@ -394,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]}") @@ -403,36 +588,51 @@ 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: @@ -440,17 +640,36 @@ def on_post(req, rep): 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 rep: falcon.Response HTTP response - name (str): human readable name for Hab to GET + name (str): human-readable name for Hab to 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. """ if not name: raise falcon.HTTPBadRequest(description="name is required") @@ -458,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 @@ -466,13 +687,43 @@ 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 rep (Response): falcon.Response HTTP response object name (str): human readable name for Hab to rename + --- + 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. """ if not name: raise falcon.HTTPBadRequest(description="name is required") @@ -485,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 @@ -496,32 +746,48 @@ def on_put(self, req, rep, name): rep.content_type = "application/json" rep.data = json.dumps(data).encode("utf-8") - def on_delete(self, req, rep, name): - """ Identifier delete endpoint - - Parameters: - req (Request): falcon.Request HTTP request object - rep (Response): falcon.Response HTTP response object - name (str): human readable name for Hab to delete - - """ - if not name: - raise falcon.HTTPBadRequest(description="name is required") - agent = req.context.agent - hab = agent.hby.habByName(name) - if hab is None: - raise falcon.HTTPNotFound(title=f"No AID with name {name} found") - agent.hby.deleteHab(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 rep (Response): falcon.Response HTTP response object - name (str): human readable name for Hab to rotate or interact + name (str): human-readable name for Hab to rotate or interact + --- + 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. """ if not name: raise falcon.HTTPBadRequest(description="name is required") @@ -533,8 +799,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" @@ -551,19 +819,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] @@ -586,7 +858,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") @@ -598,25 +872,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 @@ -627,13 +916,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] @@ -642,18 +935,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 @@ -663,8 +964,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)) @@ -684,19 +989,43 @@ 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 rep: falcon.Response HTTP response - name (str): human readable name for Hab to GET - + name (str): human-readable name for Hab to 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. """ agent = req.context.agent if not name: @@ -715,33 +1044,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] @@ -756,14 +1100,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] @@ -778,11 +1127,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" @@ -793,7 +1148,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 @@ -802,6 +1157,38 @@ def on_get(req, rep, name=None, aid=None, role=None): aid (str): aid to use instead of name role (str): optional role to search for + --- + 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: false + description: The human-readable name of the identifier. + - in: path + name: aid + schema: + type: string + required: false + description: The identifier (AID). + - in: path + name: role + schema: + type: string + required: false + 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. """ agent = req.context.agent @@ -813,10 +1200,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,) @@ -830,7 +1222,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 @@ -839,6 +1231,45 @@ def on_post(req, rep, name, aid=None, role=None): aid (str): Not supported for POST. If provided, a 404 is returned role (str): Not supported for POST. If provided, a 404 is returned + --- + 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: false + 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. """ if role is not None or aid is not None: raise falcon.HTTPNotFound(description="route not found") @@ -850,10 +1281,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: @@ -861,17 +1292,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 @@ -888,6 +1327,31 @@ class RpyEscrowCollectionEnd: @staticmethod def on_get(req, rep): + """ + GET endpoint for reply escrow collection + + Parameters: + req (falcon.Request): The request object. + rep (falcon.Response): The response object. + + --- + 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. + """ agent = req.context.agent # Optional Route parameter @@ -904,11 +1368,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 @@ -923,7 +1387,7 @@ def on_get(req, rep): - in: query name: strength schema: - type: int + type: integer description: cryptographic strength of word list required: false responses: @@ -942,7 +1406,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 @@ -954,11 +1418,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 @@ -1004,7 +1468,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"] @@ -1016,17 +1482,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 @@ -1041,7 +1509,7 @@ def on_post(req, rep, source): - Challenge/Response parameters: - in: path - name: name + name: source schema: type: string required: true @@ -1070,7 +1538,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) @@ -1082,7 +1552,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 @@ -1095,12 +1565,12 @@ def on_put(req, rep, source): tags: - Challenge/Response parameters: - - in: path - name: name - schema: - type: string - required: true - description: Human readable alias for the identifier to create + - in: path + name: source + schema: + type: string + required: true + description: Human readable alias for the identifier to create requestBody: required: true content: @@ -1123,10 +1593,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) @@ -1138,7 +1612,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 @@ -1192,7 +1666,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) @@ -1215,10 +1691,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,))] @@ -1226,8 +1702,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 @@ -1279,7 +1761,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") @@ -1289,47 +1773,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) @@ -1337,35 +1823,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: @@ -1376,57 +1864,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) @@ -1436,7 +1930,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 @@ -1476,11 +1970,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"] @@ -1493,7 +1989,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 @@ -1521,7 +2017,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 @@ -1530,6 +2028,33 @@ class GroupMemberCollectionEnd: @staticmethod def on_get(req, rep, name): + """ + GET endpoint for group members + Parameters: + req (falcon.Request): The request object. + rep (falcon.Response): The response object. + name (str): The human-readable name of the identifier. + + --- + 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. + """ agent = req.context.agent hab = agent.hby.habByName(name) @@ -1537,7 +2062,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/credentialing.py b/src/keria/app/credentialing.py index 8201b3d7..62bdba77 100644 --- a/src/keria/app/credentialing.py +++ b/src/keria/app/credentialing.py @@ -207,10 +207,24 @@ def on_get(req, rep, name, registryName): description: Get a single credential issuance and revocation registy tags: - Registries + parameters: + - in: path + name: name + schema: + type: string + required: true + description: The human-readable name of the identifier. + - in: path + name: registryName + schema: + type: string + required: true + description: The human-readable name of the registry. responses: 200: description: credential issuance and revocation registy - + 404: + description: The requested registry was not found. """ agent = req.context.agent @@ -250,10 +264,35 @@ def on_put(req, rep, name, registryName): description: Get a single credential issuance and revocation registy tags: - Registries + parameters: + - in: path + name: name + schema: + type: string + required: true + description: The human-readable name of the identifier. + - in: path + name: registryName + schema: + type: string + required: true + description: The human-readable name of the registry. + requestBody: + content: + application/json: + schema: + type: object + properties: + name: + type: string + description: The new name for the registry. responses: 200: - description: credential issuance and revocation registy - + description: credential issuance and revocation registy + 400: + description: Bad request. This could be due to missing or invalid parameters. + 404: + description: The requested registry was not found. """ agent = req.context.agent @@ -393,12 +432,6 @@ def on_post(req, rep): tags: - Credentials parameters: - - in: path - name: aid - schema: - type: string - required: true - description: identifier to load credentials for - in: query name: type schema: @@ -604,12 +637,6 @@ def on_get(req, rep, said): tags: - Credentials parameters: - - in: path - name: aid - schema: - type: string - required: true - description: The identifier to create - in: path name: said schema: @@ -624,7 +651,8 @@ def on_get(req, rep, said): schema: description: Credential type: object - + 400: + description: The requested credential was not found. """ agent = req.context.agent accept = req.get_header("accept") @@ -719,7 +747,49 @@ def on_delete(self, req, rep, name, said): sigs (list): list of signatures for the revocation event --- summary: Perform credential revocation - description: Perform credential revocation + description: Initiates a credential revocation for a given identifier and SAID. + tags: + - Credentials + parameters: + - in: path + name: name + schema: + type: string + required: true + description: The human-readable alias for the AID to use as issuer. + - in: path + name: said + schema: + type: string + required: true + description: The SAID of the credential to revoke. + requestBody: + content: + application/json: + schema: + type: object + properties: + rev: + type: string + description: Serialized revocation event. + ixn: + type: string + description: Serialized interaction event. + rot: + type: string + description: Serialized rotation event. + sigs: + type: array + items: + type: string + description: List of signatures for the revocation event. + responses: + 200: + description: Credential revocation initiated successfully. + 400: + description: Bad request. This could be due to invalid revocation event or other invalid parameters. + 404: + description: The requested identifier or credential was not found. """ agent = req.context.agent diff --git a/src/keria/app/delegating.py b/src/keria/app/delegating.py index 8bd5bf74..da624ed4 100644 --- a/src/keria/app/delegating.py +++ b/src/keria/app/delegating.py @@ -1,9 +1,18 @@ +import falcon + from hio.base import doing from keri import kering from keri.app import forwarding, agenting, habbing from keri.core import coring, serdering from keri.db import dbing +from keria.core import httping, longrunning + +DELEGATION_ROUTE = "/identifiers/{name}/delegation" + +def loadEnds(app, identifierResource): + gatorEnd = DelegatorEnd(identifierResource) + app.add_route(DELEGATION_ROUTE, gatorEnd) class Anchorer(doing.DoDoer): """ @@ -109,10 +118,7 @@ def processEscrows(self): def processUnanchoredEscrow(self): """ - Process escrow of partially signed multisig group KEL events. Message - processing will send this local controllers signature to all other participants - then this escrow waits for signatures from all other participants - + Process escrow of unacnchored events that have been delegated and are waiting for delegator anchor/approval. """ for (pre, said), serder in self.hby.db.dune.getItemIter(): # group partial witness escrow kever = self.hby.kevers[pre] @@ -172,4 +178,71 @@ def processPartialWitnessEscrow(self): self.hby.db.dpwe.rem(keys=(pre, said)) self.hby.db.dune.pin(keys=(srdr.pre, srdr.said), val=srdr) + +class DelegatorEnd: + """ Resource class for for handling delegator events""" + + def __init__(self, identifierResource) -> None: + """ + + Parameters: + identifierResource (IdentifierResourceEnd): endpoint class for creating rotation and interaction events + """ + self.identifierResource = identifierResource + + def on_post(self, req, rep, name): + """ Identifier delegator enpoint POST to create the ixn anchor and approve the delegation + + Parameters: + req (Request): falcon.Request HTTP request object + rep (Response): falcon.Response HTTP response object + name (str): human readable name for Hab to rename + + """ + if not name: + raise falcon.HTTPBadRequest(description="name is required") + agent = req.context.agent + hab = agent.hby.habByName(name) + + if hab is None: + raise falcon.HTTPNotFound(title=f"No AID with name {name} found") + + body = req.get_media() + 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 = approveDelegation(hab, anc) + adop = agent.monitor.submit(hab.kever.prefixer.qb64, longrunning.OpTypes.delegation, + metadata=dict(teepre=teepre, anchor=anc, depends=op)) + + try: + rep.status = falcon.HTTP_200 + rep.content_type = "application/json" + rep.data = adop.to_json().encode("utf-8") + return rep + except (kering.AuthError, ValueError) as e: + raise falcon.HTTPBadRequest(description=e.args[0]) + +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) + + 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 3b7ae821..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')) @@ -90,7 +94,52 @@ def on_post(req, rep, name): req (falcon.Request): HTTP request object rep (falcon.Response): HTTP response object name (str): AID of Hab to load credentials for - + --- + summary: Create a multisig group request. + description: This endpoint creates a multisig request based on the provided name. + tags: + - Multisig Request + parameters: + - in: path + name: name + schema: + type: string + required: true + description: The AID of Hab to load credentials for. + requestBody: + content: + application/json: + schema: + type: object + properties: + rot: + type: object + description: The rotation event. + sigs: + type: array + items: + type: string + description: List of signatures for the rotation event. + gid: + type: string + description: The group identifier. + smids: + type: array + items: + type: string + description: List of signing member identifiers. + rmids: + type: array + items: + type: string + description: List of recipient member identifiers. + responses: + 202: + description: Successfully created the multisig request. + 400: + description: Bad request. Bad request. This could be due to missing or invalid parameters. + 404: + description: The requested identifier was not found. """ agent = req.context.agent @@ -104,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 @@ -133,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)) @@ -159,7 +212,25 @@ def on_get(req, rep, said): req (falcon.Request): HTTP request object rep (falcon.Response): HTTP response object said (str): qb64 SAID of EXN multisig message. - + --- + summary: Retrieve a specific multisig resource. + description: This endpoint retrieves the multisig resources based on the provided SAID. + tags: + - Multisig Resource + parameters: + - in: path + name: said + schema: + type: string + required: true + description: The qb64 SAID of the multisig resource to retrieve. + responses: + 200: + description: Successfully retrieved the multisig resource. + 400: + description: Bad request. This could be due to missing or invalid parameters. + 404: + description: The requested multisig resource was not found. """ agent = req.context.agent exn = agent.hby.db.exns.get(keys=(said,)) diff --git a/src/keria/core/longrunning.py b/src/keria/core/longrunning.py index 06016184..dec4b0fc 100644 --- a/src/keria/core/longrunning.py +++ b/src/keria/core/longrunning.py @@ -17,7 +17,9 @@ from keri.db import dbing, koming from keri.help import helping -# long running operationt types +from keria.app import delegating + +# long running operation types Typeage = namedtuple("Tierage", 'oobi witness delegation group query registry credential endrole challenge exchange ' 'done') @@ -249,22 +251,37 @@ def status(self, op): if op.oid not in self.hby.kevers: raise kering.ValidationError(f"long running {op.type} operation identifier {op.oid} not found") - if "sn" not in op.metadata: - raise kering.ValidationError(f"invalid long running {op.type} operation, metadata missing 'sn' field") - kever = self.hby.kevers[op.oid] - sn = op.metadata["sn"] - seqner = coring.Seqner(sn=sn) - sdig = self.hby.db.getKeLast(key=dbing.snKey(pre=op.oid, sn=sn)) - - if self.swain.complete(kever.prefixer, seqner): - evt = self.hby.db.getEvt(dbing.dgKey(pre=kever.prefixer.qb64, dig=bytes(sdig))) - serder = serdering.SerderKERI(raw=bytes(evt)) + + reqsn = "sn" + reqtee = "teepre" + anchor = "anchor" + required = [reqsn, reqtee] + if reqsn in op.metadata: #delegatee detects successful delegation + sn = op.metadata["sn"] + seqner = coring.Seqner(sn=sn) + sdig = self.hby.db.getKeLast(key=dbing.snKey(pre=op.oid, sn=sn)) + + if self.swain.complete(kever.prefixer, seqner): + evt = self.hby.db.getEvt(dbing.dgKey(pre=kever.prefixer.qb64, dig=bytes(sdig))) + serder = serdering.SerderKERI(raw=bytes(evt)) - operation.done = True - operation.response = serder.ked + operation.done = True + operation.response = serder.ked + else: + operation.done = False + elif reqtee in op.metadata: #delegator detects delegatee delegation success + teepre = op.metadata[reqtee] + 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: - operation.done = False + raise falcon.HTTPBadRequest(description=f"longrunning operation type {op.type} requires one of {required}, but are missing from request") elif op.type in (OpTypes.group, ): if "sn" not in op.metadata: @@ -427,11 +444,23 @@ def on_get(req, rep): 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 """ agent = req.context.agent type = req.params.get("type") @@ -454,6 +483,23 @@ def on_get(req, rep, name): req (Request): Falcon HTTP Request object rep (Response): Falcon HTTP Response object name (str): Long running operation resource name to load + --- + 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. """ agent = req.context.agent @@ -472,7 +518,25 @@ def on_delete(req, rep, name): req (Request): Falcon HTTP Request object rep (Response): Falcon HTTP Response object name (str): Long running operation resource name to load - + --- + 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. """ agent = req.context.agent diff --git a/src/keria/end/ending.py b/src/keria/end/ending.py index bee8fa6a..32f531fb 100644 --- a/src/keria/end/ending.py +++ b/src/keria/end/ending.py @@ -46,7 +46,37 @@ def on_get(self, _, rep, aid=None, role=None, eid=None): aid: qb64 identifier prefix of OOBI role: requested role for OOBI rpy message eid: qb64 identifier prefix of participant in role - + --- + 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: false + description: The qb64 identifier prefix of OOBI. + - in: path + name: role + schema: + type: string + required: false + description: The requested role for OOBI rpy message. + - in: path + name: eid + schema: + type: string + required: false + 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. """ if not aid: if self.default is None: diff --git a/src/keria/peer/exchanging.py b/src/keria/peer/exchanging.py index 3a98cf7e..d6409faf 100644 --- a/src/keria/peer/exchanging.py +++ b/src/keria/peer/exchanging.py @@ -35,7 +35,50 @@ def on_post(req, rep, name): req (Request): falcon HTTP request object rep (Response): falcon HTTP response object name (str): human readable alias for AID context - + --- + summary: Post an exchange message for an identifier. + description: This endpoint posts an exchange message to a specific named identifier. + tags: + - Exchange Message + parameters: + - in: path + name: name + schema: + type: string + required: true + description: The human-readable alias for the AID context. + requestBody: + content: + application/json: + schema: + type: object + properties: + exn: + type: object + description: The exchange message event. + sigs: + type: array + items: + type: string + description: The signatures for the exn message. + atc: + type: object + description: The additional attachments for the exn message. + rec: + type: array + items: + type: string + description: The recipients of the exn message. + tpc: + type: string + description: The topic of the exn message. + responses: + 202: + description: Successfully posted the exchange message. + 400: + description: Bad request. This could be due to missing or invalid parameters. + 404: + description: The requested identifier was not found. """ agent = req.context.agent @@ -91,6 +134,34 @@ def on_post(req, rep): req (Request): falcon HTTP request object rep (Response): falcon HTTP response object + --- + summary: Query exchange message collection. + description: This endpoint retrieves the exchange messages based on the provided query parameters. + tags: + - Exchange Message + requestBody: + content: + application/json: + schema: + type: object + properties: + filter: + type: object + description: The filter criteria to apply on the exchange messages. + sort: + type: object + description: The sorting criteria to apply on the exchange messages. + skip: + type: integer + description: The number of exchange messages to skip. (default=0) + limit: + type: integer + description: The maximum number of exchange messages to return. (default=25) + responses: + 200: + description: Successfully retrieved the exchange messages. + 400: + description: Bad request. This could be due to missing or invalid parameters. """ agent = req.context.agent @@ -146,6 +217,23 @@ def on_get(req, rep, said): rep (Response): falcon HTTP response object said (str): qb64 SAID of exchange message to retrieve + --- + summary: Retrieve a specific exchange message. + description: This endpoint retrieves a specific exchange message based on the provided SAID. + tags: + - Exchange Message + parameters: + - in: path + name: said + schema: + type: string + required: true + description: The qb64 SAID of the exchange message to retrieve. + responses: + 200: + description: Successfully retrieved the exchange message. + 404: + description: The requested exchange message was not found. """ agent = req.context.agent serder, pathed = exchanging.cloneMessage(agent.hby, said) 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_agenting.py b/tests/app/test_agenting.py index c93acdb9..af4d9f55 100644 --- a/tests/app/test_agenting.py +++ b/tests/app/test_agenting.py @@ -9,6 +9,8 @@ import os import shutil +import pytest + import falcon import hio from falcon import testing @@ -62,6 +64,52 @@ def test_load_ends(helpers): assert isinstance(end, agenting.QueryCollectionEnd) +def test_load_tocks_config(helpers): + with helpers.openKeria() as (agency, agent, app, client): + agenting.loadEnds(app=app) + assert app._router is not None + + assert agent.cfd == { + "dt": "2022-01-20T12:57:59.823350+00:00", + "keria": { + "dt": "2022-01-20T12:57:59.823350+00:00", + "curls": ["http://127.0.0.1:3902/"] + }, + "EK35JRNdfVkO4JwhXaSTdV4qzB_ibk_tGJmSVcY4pZqx": { + "dt": "2022-01-20T12:57:59.823350+00:00", + "curls": ["http://127.0.0.1:3902/"] + }, + "EI7AkI40M11MS7lkTCb10JC9-nDt-tXwQh44OHAFlv_9": { + "dt": "2022-01-20T12:57:59.823350+00:00", + "curls": ["http://127.0.0.1:3902/"] + }, + "tocks": { + "initer": 0.0, + "escrower": 1.0 + } + } + + assert agent.tocks == { + "initer": 0.0, + "escrower": 1.0 + } + + escrower_doer = next((doer for doer in agent.doers if isinstance(doer, agenting.Escrower)), None) + assert escrower_doer is not None + assert escrower_doer.tock == 1.0 + + initer_doer = next((doer for doer in agent.doers if isinstance(doer, agenting.Initer)), None) + assert initer_doer is not None + assert initer_doer.tock == 0.0 + + querier_doer = next((doer for doer in agent.doers if isinstance(doer, agenting.Querier)), None) + assert querier_doer is not None + assert querier_doer.tock == 0.0 + + with pytest.raises(TypeError): + agent.tocks["initer"] = 1.0 # agent.tocks is read-only + + def test_agency(): salt = b'0123456789abcdef' salter = core.Salter(raw=salt) diff --git a/tests/app/test_aiding.py b/tests/app/test_aiding.py index 267fe3f0..ec81c8b6 100644 --- a/tests/app/test_aiding.py +++ b/tests/app/test_aiding.py @@ -5,10 +5,11 @@ Testing the Mark II Agent """ -import json -import os from builtins import isinstance from dataclasses import asdict +import json +import os +import pytest import falcon from falcon import testing @@ -24,6 +25,7 @@ from keri.vdr import credentialing from hio.base import doing + from keria.app import aiding, agenting from keria.app.aiding import IdentifierOOBICollectionEnd, RpyEscrowCollectionEnd from keria.core import longrunning @@ -483,13 +485,6 @@ def test_identifier_collection_end(helpers): aid = res.json assert aid["name"] == "aid3Renamed" - # delete aid3renamed - res = client.simulate_delete(path="/identifiers/aid3Renamed") - assert res.status_code == 200 - res = client.simulate_get(path="/identifiers") - assert res.status_code == 200 - assert len(res.json) == 2 - # create member habs for group AID p1 = p1hby.makeHab(name="p1") assert p1.pre == "EBPtjiAY9ITdvScWFGeeCu3Pf6_CFFr57siQqffVt9Of" @@ -520,13 +515,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 @@ -544,8 +540,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, @@ -562,8 +558,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 @@ -579,8 +575,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, @@ -597,8 +593,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, @@ -616,8 +612,8 @@ def test_identifier_collection_end(helpers): res = client.simulate_get(path="/identifiers") assert res.status_code == 200 - assert len(res.json) == 3 - aid = res.json[2] + assert len(res.json) == 4 + aid = res.json[3] assert aid["name"] == "multisig" assert aid["prefix"] == serder.pre group = aid["group"] @@ -685,8 +681,8 @@ def test_identifier_collection_end(helpers): # Make sure keys got rotated res = client.simulate_get(path="/identifiers") assert res.status_code == 200 - assert len(res.json) == 3 - aid = res.json[2] + assert len(res.json) == 4 + aid = res.json[3] assert aid["name"] == "multisig" assert aid["prefix"] == rserder.pre group = aid["group"] @@ -958,7 +954,6 @@ def test_identifier_collection_end(helpers): res = client.simulate_post(path="/identifiers", body=json.dumps(body)) assert res.status_code == 202 - def test_challenge_ends(helpers): with helpers.openKeria() as (agency, agent, app, client): end = aiding.IdentifierCollectionEnd() @@ -1564,62 +1559,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_delegating.py b/tests/app/test_delegating.py index bf353869..b7c9a866 100644 --- a/tests/app/test_delegating.py +++ b/tests/app/test_delegating.py @@ -5,14 +5,19 @@ Testing the Mark II Agent Anchorer """ +import json +import time import pytest + from hio.base import doing from keri import kering from keri.app import habbing -from keri.core import coring, eventing - -from keria.app import delegating +from keri.core import coring, eventing, parsing, serdering +from keria.app import aiding, delegating +from keria.core import longrunning +from keria.end import ending +from keria.app import agenting def test_sealer(): with habbing.openHby(name="p1", temp=True) as hby: @@ -54,7 +59,117 @@ def test_sealer(): anchorer.complete(prefixer=prefixer, seqner=seqner, saider=saider) assert anchorer.complete(prefixer=prefixer, seqner=seqner) is True + +def test_delegator_end(helpers): + torname = "delegator" + teename = "delegatee" + saltb = b"0123456789abcdef" + + with habbing.openHby(name="p1", temp=True) as hby, \ + helpers.openKeria() as (toragency, toragent, torapp, torclient): + # Create Anchorer to test + anchorer = delegating.Anchorer(hby=hby) + escrower = next((doer for doer in toragent.doers if isinstance(doer, agenting.Escrower)), None) + assert escrower is not None + dipEvtProcDelay = escrower.tock * 60 + 10 # 60 seconds * escrower.tock + 10 seconds + + #setup agency endpoints + ending.loadEnds(app=torapp, agency=toragency) + end = aiding.IdentifierCollectionEnd() + resend = aiding.IdentifierResourceEnd() + torapp.add_route("/identifiers", end) + torapp.add_route("/identifiers/{name}", resend) + torapp.add_route("/identifiers/{name}/events", resend) + torend = delegating.DelegatorEnd(resend) + torapp.add_route(delegating.DELEGATION_ROUTE, torend) + + # Create delegator + salt = b"0123456789abcdef" + op = helpers.createAid(torclient, torname, salt) + aid = op["response"] + torpre = aid["i"] + assert torpre == "EHgwVwQT15OJvilVvW57HE4w0-GPs_Stj2OFoAHZSysY" + + # setup delegatee + fakeproxy = hby.makeHab("proxy") + teehab = hby.makeHab(teename, delpre=torpre) + + # Use valid AID, role and EID + toroobi = torclient.simulate_get(path=f"/oobi/{torpre}/agent/{toragent.agentHab.pre}") + assert toroobi.status_code == 200 + assert toroobi.headers['Content-Type'] == "application/json+cesr" + + # Try before knowing delegator key state + with pytest.raises(kering.ValidationError): + anchorer.delegation(pre=teehab.pre, proxy=fakeproxy) + + #introduce delegator to delegatee + teehab.psr.parse(ims=toroobi.content) + + # Doer hierarchy + doist = doing.Doist(tock=0.03125, real=True) + deeds = doist.enter(doers=([anchorer, toragent])) + # Run delegation to escrow inception event + anchorer.delegation(pre=teehab.pre) + doist.recur(deeds=deeds) + + # normally postman would take care of this but we can do it manually here + teeser = teehab.kever.serder + for msg in teehab.db.clonePreIter(pre=teehab.pre): + parsing.Parser().parse(ims=bytearray(msg), kvy=toragent.kvy, local=True) # Local true otherwise it will be a misfit + + # Delegatee hasn't seen delegator anchor + prefixer = coring.Prefixer(qb64=teehab.pre) + seqner = coring.Seqner(sn=0) + assert anchorer.complete(prefixer=prefixer, seqner=seqner) is False + # Delegator still hasn't processed the delegatee dip event + doist.recur(deeds=deeds) + assert teehab.pre not in toragent.agentHab.kevers + + # Anchor the seal in delegator's KEL and approve the escrowed dip event + seal = dict(i=prefixer.qb64, s="0", d=prefixer.qb64) + iserder, isigers = helpers.interact(pre=aid["i"], bran=saltb, pidx=0, ridx=0, dig=aid["d"], sn='1', data=[seal]) + appDelBody = {"ixn": iserder.ked, "sigs": isigers} + apprDelRes = torclient.simulate_post(path=f"/identifiers/{torname}/delegation", body=json.dumps(appDelBody)) + assert apprDelRes.status_code == 200 + op = apprDelRes.json + assert op["metadata"]["teepre"] == iserder.ked['a'][0]['i'] + + # Delegator still hasn't processed the delegatee dip event + assert teehab.pre not in toragent.agentHab.kevers + + # Monitor long running operation indicating escrowed delegatee + # dip event was successfully processed + opColEnd = longrunning.OperationCollectionEnd() + torapp.add_route("/operations", opColEnd) + opResEnd = longrunning.OperationResourceEnd() + torapp.add_route("/operations/{name}", opResEnd) + count=0 + while not op or not "done" in op or not op["done"]: + doist.recur(deeds=deeds) + time.sleep(1) + res = torclient.simulate_get(path=f'/operations/{op["name"]}') + assert res.status_code == 200 + op = res.json + count += 1 + if count > dipEvtProcDelay: + raise Exception("Delegator never processed the delegatee dip event") + + # Delegator escrows completed and now aknowledges the delegatee dip event + assert teehab.pre in toragent.agentHab.kevers + + # Delegatee hasn't seen the anchor yet + assert anchorer.complete(prefixer=prefixer, seqner=seqner) is False + + # update delegatee with delegator KEL w/ interaction event + toroobi = torclient.simulate_get(path=f"/oobi/{torpre}/agent/{toragent.agentHab.pre}") + teehab.psr.parse(ims=toroobi.content) + count = 0 + while anchorer.complete(prefixer=prefixer, seqner=seqner) is False: + doist.recur(deeds=deeds) + if count > 10: + raise Exception("Delegatee never saw the successful anchor") \ 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 946e88bd..75b20c25 100644 --- a/tests/app/test_ipexing.py +++ b/tests/app/test_ipexing.py @@ -77,15 +77,16 @@ def test_ipex_admit(helpers, mockHelpingNowIso8601): recipient=pre1, date=helping.nowIso8601()) assert admitSerder.ked == {'a': {'i': 'EFnYGvF_ENKJ_4PGsWsvfd_R6m5cN-3KYsz_0mAuNpCm'}, - 'd': 'EBrMlfQbJRS9RYuP90t2PPPV24Qynmtu7BefWAqWzb0Q', + 'd': 'EEsFX0BFd58i84TBnq4S4Z_5XZuuz1HGtDC5Hb7NdU1P', 'dt': '2021-06-27T21:26:21.233257+00:00', 'e': {}, 'i': 'EHgwVwQT15OJvilVvW57HE4w0-GPs_Stj2OFoAHZSysY', + 'rp': 'EFnYGvF_ENKJ_4PGsWsvfd_R6m5cN-3KYsz_0mAuNpCm', 'p': 'EB_Lr3fHezn1ygn-wbBT5JjzaCMxTmhUoegXeZzWC2eT', 'q': {}, 'r': '/ipex/admit', 't': 'exn', - 'v': 'KERI10JSON00013d_'} + 'v': 'KERI10JSON000171_'} assert end == b'' sigs = ["AAAa70b4QnTOtGOsMqcezMtVzCFuRJHGeIMkWYHZ5ZxGIXM0XDVAzkYdCeadfPfzlKC6dkfiwuJ0IzLOElaanUgH"] @@ -193,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, @@ -236,7 +238,7 @@ def test_ipex_grant(helpers, mockHelpingNowIso8601, seeder): recipient=verifier['i'], date=helping.nowIso8601()) assert exn.ked == {'a': {'i': 'EEtaMHCGi83N3IJN05DRDhkpIo5S03LOX5_8IgdvMaVq'}, - 'd': 'EHwjDEsub6XT19ISLft1m1xMNvVXnSfH0IsDGllox4Y8', + 'd': 'ELkQART3yXFd8C6ImzGyqlDrgVUDtCfh1Goqr1PCbi9r', 'dt': '2021-06-27T21:26:21.233257+00:00', 'e': {'acdc': {'a': {'LEI': '78I9GKEFM361IFY3PIN0', 'd': 'ELJ7Emhi0Bhxz3s7HyhZ45qcsgpvsT8p8pxwWkG362n3', @@ -265,11 +267,12 @@ def test_ipex_grant(helpers, mockHelpingNowIso8601, seeder): 't': 'iss', 'v': 'KERI10JSON0000ed_'}}, 'i': 'EFnYGvF_ENKJ_4PGsWsvfd_R6m5cN-3KYsz_0mAuNpCm', + 'rp': 'EEtaMHCGi83N3IJN05DRDhkpIo5S03LOX5_8IgdvMaVq', 'p': '', 'q': {}, 'r': '/ipex/grant', 't': 'exn', - 'v': 'KERI10JSON000517_'} + 'v': 'KERI10JSON00054b_'} assert end == (b'-LAg4AACA-e-acdc-IABEBg1YzKmwZIDzZsMslTFwQARB6nUN85sRJF5oywlJr3N' b'0AAAAAAAAAAAAAAAAAAAAAAAEO83mwXWqiGxovpTXE6QQUBP05xkP9c1xc88xvMw' b'kWWZ-LAW5AACAA-e-iss-VAS-GAB0AAAAAAAAAAAAAAAAAAAAAACEKZtbklUNPLO' @@ -308,7 +311,7 @@ def test_ipex_grant(helpers, mockHelpingNowIso8601, seeder): assert res.status_code == 200 assert res.json == {'done': False, 'error': None, - 'metadata': {'said': 'EHwjDEsub6XT19ISLft1m1xMNvVXnSfH0IsDGllox4Y8'}, + 'metadata': {'said': 'ELkQART3yXFd8C6ImzGyqlDrgVUDtCfh1Goqr1PCbi9r'}, 'name': 'exchange.EFnYGvF_ENKJ_4PGsWsvfd_R6m5cN-3KYsz_0mAuNpCm', 'response': None} assert len(agent.exchanges) == 1 @@ -428,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, @@ -449,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, @@ -528,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, @@ -549,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, @@ -958,15 +963,16 @@ def test_ipex_apply(helpers, mockHelpingNowIso8601): recipient=pre1, date=helping.nowIso8601()) assert applySerder.ked == {'a': {'i': 'EFnYGvF_ENKJ_4PGsWsvfd_R6m5cN-3KYsz_0mAuNpCm', 'm': 'Applying for a credential', 's': 'EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao', 'a': {'LEI': '78I9GKEFM361IFY3PIN0'}}, - 'd': 'EJq6zSDUWw6iaBz8n1cY5cAW3Rrgp4E3sUsoz5JkoMZc', + 'd': 'EPAThHL_ExMdhQoLTxsMWdsDo-aunDFZPkK_UKlCVe2d', 'dt': '2021-06-27T21:26:21.233257+00:00', 'e': {}, 'i': 'EHgwVwQT15OJvilVvW57HE4w0-GPs_Stj2OFoAHZSysY', + 'rp': 'EFnYGvF_ENKJ_4PGsWsvfd_R6m5cN-3KYsz_0mAuNpCm', 'p': '', 'q': {}, 'r': '/ipex/apply', 't': 'exn', - 'v': 'KERI10JSON000187_'} + 'v': 'KERI10JSON0001bb_'} assert end == b'' sigs = ["AAAa70b4QnTOtGOsMqcezMtVzCFuRJHGeIMkWYHZ5ZxGIXM0XDVAzkYdCeadfPfzlKC6dkfiwuJ0IzLOElaanUgH"] @@ -1003,7 +1009,7 @@ def test_ipex_apply(helpers, mockHelpingNowIso8601): res = client.simulate_post(path="/identifiers/test/ipex/apply", body=data) assert res.json == {'done': False, 'error': None, - 'metadata': {'said': 'EJq6zSDUWw6iaBz8n1cY5cAW3Rrgp4E3sUsoz5JkoMZc'}, + 'metadata': {'said': 'EPAThHL_ExMdhQoLTxsMWdsDo-aunDFZPkK_UKlCVe2d'}, 'name': 'exchange.EHgwVwQT15OJvilVvW57HE4w0-GPs_Stj2OFoAHZSysY', 'response': None} @@ -1045,7 +1051,7 @@ def test_ipex_offer(helpers, mockHelpingNowIso8601): recipient=pre1, date=helping.nowIso8601()) assert offer0Serder.ked == {'a': {'i': 'EFnYGvF_ENKJ_4PGsWsvfd_R6m5cN-3KYsz_0mAuNpCm', 'm': 'Offering this'}, - 'd': 'EDY-IFIMBR4umlYATxAqEAcT5jiHEMn5EyL6i1sUwxDO', + 'd': 'ECa9XU2648ryO8PXKEcWkS7V-hvpj86Nh3rjGv93g6jT', 'dt': '2021-06-27T21:26:21.233257+00:00', 'e': {'acdc': {'a': {'d': 'ELJ7Emhi0Bhxz3s7HyhZ45qcsgpvsT8p8pxwWkG362n3', 'dt': '2021-06-27T21:26:21.233257+00:00', @@ -1058,11 +1064,12 @@ def test_ipex_offer(helpers, mockHelpingNowIso8601): 'v': 'ACDC10JSON000197_'}, 'd': 'EEcYZMP-zilz2w1w2hEFm6tF0eaX_1KaPEWhNfY3kf8i'}, 'i': 'EHgwVwQT15OJvilVvW57HE4w0-GPs_Stj2OFoAHZSysY', + 'rp': 'EFnYGvF_ENKJ_4PGsWsvfd_R6m5cN-3KYsz_0mAuNpCm', 'p': '', 'q': {}, 'r': '/ipex/offer', 't': 'exn', - 'v': 'KERI10JSON0002f6_'} + 'v': 'KERI10JSON00032a_'} assert end0 == b'' sigs = ["AAAa70b4QnTOtGOsMqcezMtVzCFuRJHGeIMkWYHZ5ZxGIXM0XDVAzkYdCeadfPfzlKC6dkfiwuJ0IzLOElaanUgH"] @@ -1099,7 +1106,7 @@ def test_ipex_offer(helpers, mockHelpingNowIso8601): res = client.simulate_post(path="/identifiers/test/ipex/offer", body=data) assert res.json == {'done': False, 'error': None, - 'metadata': {'said': 'EDY-IFIMBR4umlYATxAqEAcT5jiHEMn5EyL6i1sUwxDO'}, + 'metadata': {'said': 'ECa9XU2648ryO8PXKEcWkS7V-hvpj86Nh3rjGv93g6jT'}, 'name': 'exchange.EHgwVwQT15OJvilVvW57HE4w0-GPs_Stj2OFoAHZSysY', 'response': None} @@ -1116,7 +1123,7 @@ def test_ipex_offer(helpers, mockHelpingNowIso8601): recipient=pre1, date=helping.nowIso8601()) assert offer1Serder.ked == {'a': {'i': 'EFnYGvF_ENKJ_4PGsWsvfd_R6m5cN-3KYsz_0mAuNpCm', 'm': 'How about this'}, - 'd': 'EDT7go7TfCTzeFnhNBl19JJqdabBfBx8tjBvi_asFCwT', + 'd': 'EM79tlKrG142-jcaglGnIXKRfLW_DKOK5pnTwN60yz5U', 'dt': '2021-06-27T21:26:21.233257+00:00', 'e': {'acdc': {'a': {'d': 'ELJ7Emhi0Bhxz3s7HyhZ45qcsgpvsT8p8pxwWkG362n3', 'dt': '2021-06-27T21:26:21.233257+00:00', @@ -1129,11 +1136,12 @@ def test_ipex_offer(helpers, mockHelpingNowIso8601): 'v': 'ACDC10JSON000197_'}, 'd': 'EEcYZMP-zilz2w1w2hEFm6tF0eaX_1KaPEWhNfY3kf8i'}, 'i': 'EHgwVwQT15OJvilVvW57HE4w0-GPs_Stj2OFoAHZSysY', + 'rp': 'EFnYGvF_ENKJ_4PGsWsvfd_R6m5cN-3KYsz_0mAuNpCm', 'p': 'EB_Lr3fHezn1ygn-wbBT5JjzaCMxTmhUoegXeZzWC2eT', 'q': {}, 'r': '/ipex/offer', 't': 'exn', - 'v': 'KERI10JSON000323_'} + 'v': 'KERI10JSON000357_'} assert end1 == b'' sigs = ["AAAa70b4QnTOtGOsMqcezMtVzCFuRJHGeIMkWYHZ5ZxGIXM0XDVAzkYdCeadfPfzlKC6dkfiwuJ0IzLOElaanUgH"] @@ -1148,7 +1156,7 @@ def test_ipex_offer(helpers, mockHelpingNowIso8601): res = client.simulate_post(path="/identifiers/test/ipex/offer", body=data) assert res.json == {'done': False, 'error': None, - 'metadata': {'said': 'EDT7go7TfCTzeFnhNBl19JJqdabBfBx8tjBvi_asFCwT'}, + 'metadata': {'said': 'EM79tlKrG142-jcaglGnIXKRfLW_DKOK5pnTwN60yz5U'}, 'name': 'exchange.EHgwVwQT15OJvilVvW57HE4w0-GPs_Stj2OFoAHZSysY', 'response': None} @@ -1187,15 +1195,16 @@ def test_ipex_agree(helpers, mockHelpingNowIso8601): recipient=pre1, date=helping.nowIso8601()) assert offerSerder.ked == {'a': {'i': 'EFnYGvF_ENKJ_4PGsWsvfd_R6m5cN-3KYsz_0mAuNpCm', 'm': 'Agreed'}, - 'd': 'ECxQe2TgUCRjbbxyCaXMEp6EtSMaqPmDstetoi4bEUrG', + 'd': 'ENMBCgTGXxiMuTMcfGWp4uqnsiso1Jm3tAAn1x7ZPRox', 'dt': '2021-06-27T21:26:21.233257+00:00', 'e': {}, 'i': 'EHgwVwQT15OJvilVvW57HE4w0-GPs_Stj2OFoAHZSysY', + 'rp': 'EFnYGvF_ENKJ_4PGsWsvfd_R6m5cN-3KYsz_0mAuNpCm', 'p': 'EB_Lr3fHezn1ygn-wbBT5JjzaCMxTmhUoegXeZzWC2eT', 'q': {}, 'r': '/ipex/agree', 't': 'exn', - 'v': 'KERI10JSON00014a_'} + 'v': 'KERI10JSON00017e_'} assert end == b'' sigs = ["AAAa70b4QnTOtGOsMqcezMtVzCFuRJHGeIMkWYHZ5ZxGIXM0XDVAzkYdCeadfPfzlKC6dkfiwuJ0IzLOElaanUgH"] @@ -1232,7 +1241,7 @@ def test_ipex_agree(helpers, mockHelpingNowIso8601): res = client.simulate_post(path="/identifiers/test/ipex/agree", body=data) assert res.json == {'done': False, 'error': None, - 'metadata': {'said': 'ECxQe2TgUCRjbbxyCaXMEp6EtSMaqPmDstetoi4bEUrG'}, + 'metadata': {'said': 'ENMBCgTGXxiMuTMcfGWp4uqnsiso1Jm3tAAn1x7ZPRox'}, 'name': 'exchange.EHgwVwQT15OJvilVvW57HE4w0-GPs_Stj2OFoAHZSysY', 'response': None} diff --git a/tests/app/test_specing.py b/tests/app/test_specing.py index 77115bd1..e50e84ae 100644 --- a/tests/app/test_specing.py +++ b/tests/app/test_specing.py @@ -1,6 +1,6 @@ import json -from keria.app import agenting, aiding, notifying, indirecting, specing +from keria.app import agenting, aiding, delegating, notifying, indirecting, specing from keria.end import ending @@ -9,10 +9,12 @@ def test_spec_resource(helpers): # Add all the endpoints similar to the agenting.setup function agenting.loadEnds(app) aiding.loadEnds(app, agency, authn=None) - notifying.loadEnds(app) + 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') sd = specRes.spec.to_dict() @@ -25,6 +27,7 @@ def test_spec_resource(helpers): assert "/challenges/{name}" in paths assert "/contacts/{prefix}" in paths assert "/contacts/{prefix}/img" in paths + assert delegating.DELEGATION_ROUTE in paths assert "/events" in paths assert "/identifiers" in paths assert "/identifiers/{name}" in paths @@ -46,7 +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": {"content": {"application/json": {"schema": {"type": "array"}}}}}}}, "/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", "properties": {"oobialias": {"type": "string", "description": "alias to assign to the identifier resolved from this OOBI", "required": false}, "url": {"type": "string", "description": "URL OOBI"}, "rpy": {"type": "object", "description": "unsigned KERI `rpy` event message with endpoints"}}}}}}, "responses": {"202": {"description": "OOBI resolution to key state successful"}}}}, "/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": "prefix", "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"}}}}, "/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": "prefix", "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"], "parameters": [{"in": "body", "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"}}}}, "/identifiers": {"get": {}, "options": {}, "post": {}}, "/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": "int"}, "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"}}}}, "/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"}}}}, "/oobi": {"get": {}}, "/": {"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."}}}}, "/operations/{name}": {"delete": {}, "get": {}}, "/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": {}, "put": {}}, "/identifiers/{name}": {"delete": {}, "get": {}, "post": {}, "put": {}}, "/endroles/{aid}": {"get": {}, "post": {}}, "/escrows/rpy": {"get": {}}, "/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": "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"}}}, "put": {"summary": "Mark challenge response exn message as signed", "description": "Mark challenge response exn message as signed", "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": {"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"}}}}, "/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"}}}}, "/oobi/{aid}": {"get": {}}, "/identifiers/{name}/events": {"delete": {}, "get": {}, "post": {}, "put": {}}, "/identifiers/{name}/oobis": {"get": {}}, "/identifiers/{name}/endroles": {"get": {}, "post": {}}, "/identifiers/{name}/members": {"get": {}}, "/endroles/{aid}/{role}": {"get": {}, "post": {}}, "/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": {}}, "/identifiers/{name}/endroles/{role}": {"get": {}, "post": {}}, "/oobi/{aid}/{role}/{eid}": {"get": {}}, "/identifiers/{name}/endroles/{role}/{eid}": {"delete": {}}}, "info": {"title": "KERIA Interactive Web Interface API", "version": "1.0.1"}, "openapi": "3.1.0"}""" - - -"" \ No newline at end of file + 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}": {"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": {"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"}' diff --git a/tests/scripts/keri/cf/main/keria.json b/tests/scripts/keri/cf/main/keria.json index 93faf9c9..9ccd32fe 100755 --- a/tests/scripts/keri/cf/main/keria.json +++ b/tests/scripts/keri/cf/main/keria.json @@ -11,5 +11,9 @@ "EI7AkI40M11MS7lkTCb10JC9-nDt-tXwQh44OHAFlv_9": { "dt": "2022-01-20T12:57:59.823350+00:00", "curls": ["http://127.0.0.1:3902/"] + }, + "tocks": { + "initer": 0.0, + "escrower": 1.0 } } \ No newline at end of file