From d324157b245db913610acf4e0748e5cc9c71638f Mon Sep 17 00:00:00 2001 From: PatStLouis Date: Sun, 6 Oct 2024 19:43:28 +0000 Subject: [PATCH 1/2] oca and dcc mapping Signed-off-by: PatStLouis --- backend/app/__init__.py | 11 +- .../PetroleumAndNaturalGasTitle copy.json | 46 ++++++ .../app/data/PetroleumAndNaturalGasTitle.json | 12 ++ backend/app/models/options.py | 5 + backend/app/models/registrations.py | 59 +++++-- backend/app/models/untp.py | 117 +++++++------- backend/app/models/web_schemas.py | 74 +++++---- backend/app/plugins/__init__.py | 6 + backend/app/plugins/askar.py | 11 +- backend/app/plugins/registrar.py | 148 ++++++++++++++++++ backend/app/plugins/soup.py | 24 +++ backend/app/plugins/traction.py | 69 ++++++++ backend/app/plugins/untp.py | 90 ++++++----- .../PetroleumAndNaturalGasTitle copy.json | 74 +++++++++ .../PetroleumAndNaturalGasTitle.json | 53 +++---- backend/app/routers/credentials.py | 105 ++++--------- backend/app/routers/issuers.py | 54 ++++++- backend/app/routers/registrations.py | 79 ++++++++++ backend/app/routers/utilities.py | 14 ++ backend/app/security.py | 16 ++ backend/app/utilities.py | 34 +++- backend/config.py | 23 +-- backend/requirements.txt | 17 +- frontend/app/__init__.py | 4 +- frontend/app/plugins/__init__.py | 0 frontend/app/plugins/agent.py | 12 -- frontend/app/plugins/db.py | 39 +++++ frontend/app/plugins/traction.py | 84 ++++++++++ frontend/app/routes/auth/routes.py | 25 ++- frontend/config.py | 5 +- 30 files changed, 1001 insertions(+), 309 deletions(-) create mode 100644 backend/app/data/PetroleumAndNaturalGasTitle copy.json create mode 100644 backend/app/data/PetroleumAndNaturalGasTitle.json create mode 100644 backend/app/plugins/registrar.py create mode 100644 backend/app/plugins/soup.py create mode 100644 backend/app/related_resources/oca_bundles/PetroleumAndNaturalGasTitle copy.json create mode 100644 backend/app/routers/registrations.py create mode 100644 backend/app/routers/utilities.py create mode 100644 backend/app/security.py create mode 100644 frontend/app/plugins/__init__.py delete mode 100644 frontend/app/plugins/agent.py create mode 100644 frontend/app/plugins/db.py create mode 100644 frontend/app/plugins/traction.py diff --git a/backend/app/__init__.py b/backend/app/__init__.py index eae70ee..57e314b 100644 --- a/backend/app/__init__.py +++ b/backend/app/__init__.py @@ -1,11 +1,12 @@ from fastapi import FastAPI, APIRouter from fastapi.responses import JSONResponse from fastapi.middleware.cors import CORSMiddleware -from app.routers import issuers, credentials, related_resources +from app.routers import registrations, credentials from config import settings app = FastAPI(title=settings.PROJECT_TITLE, version=settings.PROJECT_VERSION) + app.add_middleware( CORSMiddleware, allow_origins=["*"], @@ -16,14 +17,12 @@ api_router = APIRouter() -api_router.include_router(issuers.router, tags=["Issuers"]) -api_router.include_router(credentials.router, tags=["Credentials"]) -api_router.include_router(related_resources.router, tags=["Related Resources"]) - - @api_router.get("/server/status", tags=["Server"], include_in_schema=False) async def server_status(): return JSONResponse(status_code=200, content={"status": "ok"}) +api_router.include_router(credentials.router, tags=["Credentials"]) +api_router.include_router(registrations.router, tags=["Registrations"]) + app.include_router(api_router) diff --git a/backend/app/data/PetroleumAndNaturalGasTitle copy.json b/backend/app/data/PetroleumAndNaturalGasTitle copy.json new file mode 100644 index 0000000..6cadb3a --- /dev/null +++ b/backend/app/data/PetroleumAndNaturalGasTitle copy.json @@ -0,0 +1,46 @@ +{ + "type": "Petroleum&NaturalGasTitle", + "titleType": "PETROLEUM AND NATURAL GAS LEASE", + "titleNumber": 745, + "titleHolderType": "TitleHolder", + "titleHolder": "PACIFIC CANBRIAM ENERGY LIMITED", + "titleHolderInterest": 100.00, + "titleCaveats": [], + "tractType": "Tract", + "tractCommodities": [ + [ + "PETROLEUM", + "NATURAL GAS" + ] + ], + "tractLocations": [ + [ + "NTS 094-B-08 BLK I UNITS 92 93", + "NTS 094-B-09 BLK A UNITS 2 3" + ] + ], + "tractRights": [ + [ + "INCLUDES: PETROLEUM AND NATURAL GAS DOWN TO BASE OF 32505 BELLOY ZONE", + "EXCLUDES: NATURAL GAS IN 34002 ARTEX-HALFWAY-DOIG ZONE" + ] + ], + "tractNotes": [ + [ + "32505 BELLOY ZONE DEFINED IN THE INTERVAL 6537'-6626' MD ON THE GAMMA RAY-NEUTRON LOG OF THE WELL W.A. 141 d-94-I/94-B-8", + "34002 ARTEX-HALFWAY-DOIG ZONE DEFINED IN THE INTERVAL 5970.7'-6952.7' ON THE GAMMA RAY NEUTRON LOG OF THE WELL W.A. 238 C-53-D/94-B-09" + ] + ], + "wellType": "Well", + "wellNames": [ + [ + "PACIFIC CANBRIAM" + ] + ], + "wellLocations": [ + [ + "KOBES A- 003-A/094-B-09" + ] + ] +} + \ No newline at end of file diff --git a/backend/app/data/PetroleumAndNaturalGasTitle.json b/backend/app/data/PetroleumAndNaturalGasTitle.json new file mode 100644 index 0000000..e011e59 --- /dev/null +++ b/backend/app/data/PetroleumAndNaturalGasTitle.json @@ -0,0 +1,12 @@ +{ + "type": "Petroleum&NaturalGasTitle", + "titleType": "PETROLEUM AND NATURAL GAS LEASE", + "titleNumber": 745, + "originType": "PETROLEUM AND NATURAL GAS LEASE", + "originNumber": 745, + "titleHolderType": "TitleHolder", + "titleHolder": "PACIFIC CANBRIAM ENERGY LIMITED", + "titleHolderInterest": 100.00, + "titleCaveats": [] +} + \ No newline at end of file diff --git a/backend/app/models/options.py b/backend/app/models/options.py index b18c83b..193da03 100644 --- a/backend/app/models/options.py +++ b/backend/app/models/options.py @@ -13,6 +13,11 @@ class IssuanceOptions(BaseModel): credentialType: str = Field() +class PublishCredentialOptions(BaseModel): + entityId: str = Field() + credentialType: str = Field() + + class ProofOptions(BaseModel): type: SkipJsonSchema[str] = Field("DataIntegrityProof") cryptosuite: SkipJsonSchema[str] = Field("eddsa-jcs-2022") diff --git a/backend/app/models/registrations.py b/backend/app/models/registrations.py index f927eca..15e6166 100644 --- a/backend/app/models/registrations.py +++ b/backend/app/models/registrations.py @@ -1,6 +1,5 @@ from typing import Dict, Any, List -from pydantic import BaseModel, Field -from .did_document import DidDocument +from pydantic import BaseModel, Field, field_validator from config import settings @@ -9,16 +8,52 @@ def model_dump(self, **kwargs) -> Dict[str, Any]: return super().model_dump(by_alias=True, exclude_none=True, **kwargs) -class RelatedResource(BaseModel): - id: str = Field() - name: str = Field() +class IssuerRegistration(BaseModel): + name: str = Field(example="Director of Petroleum Lands") + scope: str = Field(example="Petroleum and Natural Gas Act") + description: str = Field( + example="An officer or employee of the ministry who is designated as the Director of Petroleum Lands by the minister." + ) + url: str = Field(None, example="https://www2.gov.bc.ca/gov/content/governments/organizational-structure/ministries-organizations/ministries/energy-mines-and-petroleum-resources") + # image: str = Field(None, example="https://") +class RelatedResources(BaseModel): + context: str = Field(example="https://bcgov.github.io/digital-trust-toolkit/contexts/BCPetroleumAndNaturalGasTitle/v1.jsonld") + legalAct: str = Field(None, example="https://www.bclaws.gov.bc.ca/civix/document/id/complete/statreg/00_96361_01") + ocaBundle: str = Field(None, example="") + governance: str = Field(None, example="https://bcgov.github.io/digital-trust-toolkit/docs/governance/pilots/bc-petroleum-and-natural-gas-title") + class CredentialRegistration(BaseModel): - type: str = Field() - untpType: str = Field(None) - version: str = Field() - issuer: str = Field() - context: str = Field() - ocaBundle: str = Field() - relatedResources: dict = Field(None) + type: str = Field('BCPetroleumAndNaturalGasTitleCredential') + untpType: str = Field(None, example='DigitalConformityCredential') + version: str = Field(example='v1.0') + issuer: str = Field(example=f'did:web:{settings.TDW_SERVER_URL.split("//")[-1]}:petroleum-and-natural-gas-act:director-of-petroleum-lands') + mappings: Dict[str, str] = Field(example={ + "type": "$.credentialSubject.type", + "titleType": "$.credentialSubject.titleType", + "titleNumber": '$.credentialSubject.titleNumber', + "originType": '$.credentialSubject.originType', + "originNumber": '$.credentialSubject.originNumber', + "titleHolderType": '$.credentialSubject.issuedToParty.type', + "titleHolder": '$.credentialSubject.issuedToParty.name', + "titleHolderInterest": '$.credentialSubject.issuedToParty.interest', + "titleCaveats": '$.credentialSubject.caveats' + }) + relatedResources: RelatedResources = Field() + + @field_validator("untpType") + @classmethod + def validate_untp_type(cls, value): + if value not in ['DigitalConformityCredential']: + raise ValueError(f"Unsupported UNTP type {value}.") + return value + + @field_validator("relatedResources") + @classmethod + def validate_related_resources(cls, value): + if not value.context: + raise ValueError("Context is required.") + # if not value.ocaBundle: + # raise ValueError("OCA Bundle is required.") + return value diff --git a/backend/app/models/untp.py b/backend/app/models/untp.py index c8c3bd2..695c836 100644 --- a/backend/app/models/untp.py +++ b/backend/app/models/untp.py @@ -5,31 +5,40 @@ class BaseModel(BaseModel): + type: List[str] = None + id: str = None + name: str = None + description: str = None + def model_dump(self, **kwargs) -> Dict[str, Any]: return super().model_dump(by_alias=True, exclude_none=True, **kwargs) -class IdentifierScheme(BaseModel): - # https://jargon.sh/user/unece/ConformityCredential/v/0.3.10/artefacts/readme/render#identifierscheme - type: str = "IdentifierScheme" - - id: AnyUrl # from vocabulary.uncefact.org/identifierSchemes - name: str - -class Entity(BaseModel): - # https://jargon.sh/user/unece/ConformityCredential/v/0.3.10/artefacts/readme/render#entity - type: str = "Entity" - - id: str - name: str +class IdentifierScheme(BaseModel): + type: List[str] = ["IdentifierScheme"] + +class Identifier(BaseModel): + type: List[str] = ["Identifier"] + + registeredId: str + idScheme: IdentifierScheme + + +class Party(BaseModel): + type: List[str] = ["Party"] + registeredId: Optional[str] = None idScheme: Optional[IdentifierScheme] = None + registrationCountry: Optional[IdentifierScheme] = None + organisationWebsite: Optional[IdentifierScheme] = None + industryCategory: Optional[IdentifierScheme] = None + otherIdentifier: Optional[Identifier] = None + class BinaryFile(BaseModel): - # https://jargon.sh/user/unece/ConformityCredential/v/0.3.10/artefacts/readme/render#binaryfile - type: str = "BinaryFile" + type: List[str] = ["BinaryFile"] fileName: str fileType: str # https://mimetype.io/all-types @@ -37,8 +46,7 @@ class BinaryFile(BaseModel): class Link(BaseModel): - # https://jargon.sh/user/unece/ConformityCredential/v/0.3.10/artefacts/readme/render#link - type: str = "Link" + type: List[str] = ["Link"] linkURL: AnyUrl linkName: str @@ -46,8 +54,7 @@ class Link(BaseModel): class SecureLink(BaseModel): - # https://jargon.sh/user/unece/ConformityCredential/v/0.3.10/artefacts/readme/render#securelink - type: str = "SecureLink" + type: List[str] = ["SecureLink"] linkUrl: AnyUrl linkName: str @@ -58,8 +65,7 @@ class SecureLink(BaseModel): class Measure(BaseModel): - # https://jargon.sh/user/unece/ConformityCredential/v/0.3.10/artefacts/readme/render#measure - type: str = "Measure" + type: List[str] = ["Measure"] value: float unit: str = Field( @@ -68,41 +74,37 @@ class Measure(BaseModel): class Endorsement(BaseModel): - # https://jargon.sh/user/unece/ConformityCredential/v/0.3.10/artefacts/readme/render#endorsement type: str = "Endorsement" id: AnyUrl name: str trustmark: Optional[BinaryFile] = None - issuingAuthority: Entity + issuingAuthority: Party accreditationCertification: Optional[Link] = None class Standard(BaseModel): - # https://jargon.sh/user/unece/ConformityCredential/v/0.3.10/artefacts/readme/render#standard type: str = "Standard" id: AnyUrl name: str - issuingParty: Entity + issuingParty: Party issueDate: str # iso8601 datetime string class Regulation(BaseModel): - # https://jargon.sh/user/unece/ConformityCredential/v/0.3.10/artefacts/readme/render#regulation - type: str = "Regulation" + type: List[str] = ["Regulation"] - id: AnyUrl - name: str + id: str = None + name: str = None jurisdictionCountry: ( str # countryCode from https://vocabulary.uncefact.org/CountryId ) - administeredBy: Entity - effectiveDate: str # iso8601 datetime string + administeredBy: Party + effectiveDate: str = None # iso8601 datetime string class Metric(BaseModel): - # https://jargon.sh/user/unece/ConformityCredential/v/0.3.10/artefacts/readme/render#metric type: str = "Metric" metricName: str @@ -111,7 +113,6 @@ class Metric(BaseModel): class Criterion(BaseModel): - # https://jargon.sh/user/unece/ConformityCredential/v/0.3.10/artefacts/readme/render#criterion type: str = "Criterion" id: AnyUrl @@ -120,11 +121,10 @@ class Criterion(BaseModel): class Facility(BaseModel): - # https://jargon.sh/user/unece/ConformityCredential/v/0.3.10/artefacts/readme/render#facility - type: str = "Facility" + type: List[str] = ["Facility"] # this looks wrongs - id: AnyUrl # The globally unique ID of the entity as a resolvable URL according to ISO 18975. + id: AnyUrl # The globally unique ID of the Party as a resolvable URL according to ISO 18975. name: str registeredId: Optional[str] = None idScheme: Optional[IdentifierScheme] = None @@ -132,61 +132,56 @@ class Facility(BaseModel): class Product(BaseModel): - # https://jargon.sh/user/unece/ConformityCredential/v/0.3.10/artefacts/readme/render#product - type: str = "Product" + type: List[str] = ["Product"] - id: AnyUrl # The globally unique ID of the entity as a resolvable URL according to ISO 18975. - name: str + id: AnyUrl = None # The globally unique ID of the Party as a resolvable URL according to ISO 18975. + name: str = None registeredId: Optional[str] = None idScheme: Optional[IdentifierScheme] = None - IDverifiedByCAB: bool + IDverifiedByCAB: bool = None class ConformityAssessment(BaseModel): - # https://jargon.sh/user/unece/ConformityCredential/v/0.3.10/artefacts/readme/render#conformityassessment - type: str = "ConformityAssessment" + type: List[str] = ["ConformityAssessment"] - id: AnyUrl + id: str = None referenceStandard: Optional[Standard] = None # defines the specification referenceRegulation: Optional[Regulation] = None # defines the regulation assessmentCriterion: Optional[Criterion] = None # defines the criteria declaredValues: Optional[List[Metric]] = None compliance: Optional[bool] = False - # conformityTopic: ConformityTopicCode + + conformityTopic: str = None - assessedProducts: Optional[List[Product]] = None - assessedFacilities: Optional[List[Facility]] = None + assessedProduct: Optional[List[Product]] = [] + assessedFacility: Optional[List[Facility]] = [] class ConformityAssessmentScheme(BaseModel): - # https://jargon.sh/user/unece/ConformityCredential/v/0.3.10/artefacts/readme/render#conformityassessmentscheme type: str = "ConformityAssessmentScheme" id: str name: str - issuingParty: Optional[Entity] = None + issuingParty: Optional[Party] = None issueDate: Optional[str] = None # ISO8601 datetime string trustmark: Optional[BinaryFile] = None class ConformityAttestation(BaseModel): - # https://jargon.sh/user/unece/ConformityCredential/v/0.3.10/artefacts/readme/render#ConformityAttestation - type: list = ["ConformityAttestation"] + type: List[str] = ["ConformityAttestation"] # id: str - # assessorLevel: Optional[AssessorLevelCode] = None - # assessmentLevel: AssessmentLevelCode - # attestationType: AttestationType - attestationDescription: Optional[str] = None # missing from context file - issuedToParty: Entity + assessorLevel: Optional[str] = None + assessmentLevel: str = None + attestationType: str = None + issuedToParty: Party = None authorisations: Optional[Endorsement] = None conformityCertificate: Optional[SecureLink] = None auditableEvidence: Optional[SecureLink] = None - # scope: ConformityAssessmentScheme - assessments: List[ConformityAssessment] = None + scope: ConformityAssessmentScheme = None + assessment: List[ConformityAssessment] = None class AssessorLevelCode(str, Enum): - # https://jargon.sh/user/unece/ConformityCredential/v/0.3.10/artefacts/readme/render#assessorLevelCode Self = "Self" Commercial = "Commercial" Buyer = "Buyer" @@ -196,7 +191,6 @@ class AssessorLevelCode(str, Enum): class AssessmentLevelCode(str, Enum): - # https://jargon.sh/user/unece/ConformityCredential/v/0.3.10/artefacts/readme/render#assessmentlevelcode GovtApproval = "GovtApproval" GlobalMLA = "GlobalMLA" Accredited = "Accredited" @@ -206,7 +200,6 @@ class AssessmentLevelCode(str, Enum): class AttestationType(str, Enum): - # https://uncefact.github.io/spec-untp/docs/specification/ConformityCredential/#attestationtype Certification = "Certification" Declaration = "Declaration" Inspection = "Inspection" @@ -217,7 +210,6 @@ class AttestationType(str, Enum): class HashMethod(str, Enum): - # https://jargon.sh/user/unece/ConformityCredential/v/0.3.10/artefacts/readme/render#hashmethodcode SHA256 = "SHA-256" SHA1 = "SHA-1" @@ -228,7 +220,6 @@ class EncryptionMethod(str, Enum): class ConformityTopicCode(str, Enum): - # https://jargon.sh/user/unece/ConformityCredential/v/0.3.10/artefacts/readme/render#conformityTopicCode Environment_Energy = "Environment.Energy" Environment_Emissions = "Environment.Emissions" Environment_Water = "Environment.Water" diff --git a/backend/app/models/web_schemas.py b/backend/app/models/web_schemas.py index 3895ed3..1e7290f 100644 --- a/backend/app/models/web_schemas.py +++ b/backend/app/models/web_schemas.py @@ -14,38 +14,6 @@ def model_dump(self, **kwargs) -> Dict[str, Any]: return super().model_dump(by_alias=True, exclude_none=True, **kwargs) -class RegisterIssuer(BaseModel): - # namespace: str = Field(example="petroleum-and-natural-gas-act") - # identifier: str = Field(example="director-of-petroleum-lands") - name: str = Field(example="Director of Petroleum Lands") - scope: str = Field(example="Petroleum and Natrual Gas Act") - description: str = Field( - example="An officer or employee of the ministry who is designated as the Director of Petroleum Lands by the minister." - ) - - # @field_validator("namespace") - # @classmethod - # def validate_namespace(cls, value): - # return value - - # @field_validator("identifier") - # @classmethod - # def validate_identifer(cls, value): - # return value - - -with open( - "app/related_resources/credential_types/PetroleumAndNaturalGasTitle.json" -) as f: - EXAMPLE_CREDENTIAL_TYPE = json.loads(f.read(), object_pairs_hook=OrderedDict) - - -class RegisterCredential(BaseModel): - credentialRegistration: CredentialRegistration = Field( - example=EXAMPLE_CREDENTIAL_TYPE - ) - - with open("app/related_resources/credentials/PetroleumAndNaturalGasTitle.json") as f: EXAMPLE_CREDENTIAL = json.loads(f.read(), object_pairs_hook=OrderedDict) @@ -57,11 +25,39 @@ class IssueCredential(BaseModel): options: IssuanceOptions = Field(example=EXAMPLE_ISSUANCE_OPTIONS) -class PublishCredential(BaseModel): - validFrom: str = Field(None) - validUntil: str = Field(None) - credentialType: str = Field(example="BCPetroleum&NaturalGasTitle") - credentialSubject: dict = Field(example={}) +example_subject = { + 'type': 'Petroleum&NaturalGasTitle', + 'products': [], + 'facilities': [], +} - def model_dump(self, **kwargs) -> Dict[str, Any]: - return super().model_dump(by_alias=True, exclude_none=True, **kwargs) +example_data = { + 'titleType': '', + 'titleNumber': '', + 'titleHolder': '', + 'originType': '', + 'originNumber': '', + 'caveats': [], + 'tracts': [], + 'wells': [], +} + +with open("app/data/PetroleumAndNaturalGasTitle.json") as f: + EXAMPLE_CREDENTIAL_DATA = json.loads(f.read(), object_pairs_hook=OrderedDict) + +class DataToPublish(BaseModel): + pass + +# class CredentialToPublish(BaseModel): +# credentialSubject: dict = Field(example=example_subject) + +class PublishingOptions(BaseModel): + validFrom: str = Field(None, example='2024-01-01T00:00:00Z') + validUntil: str = Field(None, example='2025-01-01T00:00:00Z') + entityId: str = Field(example='A0131571') + credentialType: str = Field(example="BCPetroleumAndNaturalGasTitleCredential") + +class PublishCredential(BaseModel): + data: DataToPublish = Field(example=EXAMPLE_CREDENTIAL_DATA) + # credential: CredentialToPublish = Field() + options: PublishingOptions = Field() diff --git a/backend/app/plugins/__init__.py b/backend/app/plugins/__init__.py index 0e6890b..061b7ed 100644 --- a/backend/app/plugins/__init__.py +++ b/backend/app/plugins/__init__.py @@ -4,6 +4,9 @@ from .orgbook import OrgbookPublisher from .untp import DigitalConformityCredential from .status_list import BitstringStatusList +from .registrar import PublisherRegistrar +from .traction import TractionController +from .soup import Soup __all__ = [ "AgentController", @@ -13,5 +16,8 @@ "DidWebEndorser", "OrgbookPublisher", "BitstringStatusList", + "PublisherRegistrar", + "Soup", + "TractionController", "DigitalConformityCredential", ] diff --git a/backend/app/plugins/askar.py b/backend/app/plugins/askar.py index f5d8a50..037c42f 100644 --- a/backend/app/plugins/askar.py +++ b/backend/app/plugins/askar.py @@ -28,11 +28,11 @@ async def provision(self, recreate=False): await Store.provision(self.db, "raw", self.store_key, recreate=recreate) # Register the endorser - await self.create_key(kid=None, seed=None) + # await self.create_key(kid=None, seed=None) # Register the issuers - for issuer in settings.ISSUERS: - await self.create_key(kid=issuer["id"], seed=None) + # for issuer in settings.ISSUERS: + # await self.create_key(kid=issuer["id"], seed=None) # TODO create status list @@ -53,7 +53,6 @@ async def create_key(self, kid=None, seed=None): multikey = self._to_multikey(key.get_public_bytes()) if not kid: kid = f"did:key:{multikey}" - print(kid) try: async with store.session() as session: await session.insert( @@ -162,10 +161,10 @@ async def fetch(self, category, data_key): async def replace(self, category, data_key, data): try: - self.store(category, data_key, data) + await self.store(category, data_key, data) except: try: - self.update(category, data_key, data) + await self.update(category, data_key, data) except: raise HTTPException(status_code=400, detail="Couldn't replace record.") diff --git a/backend/app/plugins/registrar.py b/backend/app/plugins/registrar.py new file mode 100644 index 0000000..29e330d --- /dev/null +++ b/backend/app/plugins/registrar.py @@ -0,0 +1,148 @@ +from config import settings +from fastapi import HTTPException +import requests +from app.models import DidDocument, VerificationMethod, Service +from app.plugins.traction import TractionController +from app.utilities import multikey_to_jwk + + +class PublisherRegistrar: + + def __init__(self): + self.tdw_server = settings.TDW_SERVER_URL + self.endorser_multikey = settings.TDW_ENDORSER_MULTIKEY + + def register_issuer(self, name, scope, url, description): + namespace = scope.replace(" ", "-").lower() + identifier = name.replace(" ", "-").lower() + r = requests.get(f'{self.tdw_server}?namespace={namespace}&identifier={identifier}') + try: + did = r.json()['didDocument']['id'] + except: + raise HTTPException(status_code=r.status_code, detail=r.json()) + + + multikey_kid = f'{did}#multikey-01' + jwk_kid = f'{did}#jwk-01' + + traction = TractionController() + # traction.authorize() + multikey = traction.create_did_key() + # traction.bind_key(multikey, multikey_kid) + + verification_method_multikey = VerificationMethod( + id=multikey_kid, + type='Multikey', + controller=did, + publicKeyMultibase=multikey + ) + + verification_method_jwk = VerificationMethod( + id=jwk_kid, + type='JsonWebKey', + controller=did, + publicKeyJwk=multikey_to_jwk(multikey) + ) + + service = Service( + id=f'{did}#bcgov-website', + type='LinkedDomains', + serviceEndpoint=url, + ) if url else None + + did_document = DidDocument( + id=did, + name=name, + description=description, + authentication=[verification_method_multikey.id], + assertionMethod=[verification_method_multikey.id], + verificationMethod=[ + verification_method_multikey, + verification_method_jwk + ], + service=[service] if service else None + ).model_dump() + + client_proof_options = r.json()['proofOptions'].copy() + client_proof_options['verificationMethod'] = f'did:key:{multikey}#{multikey}' + signed_did_document = traction.add_di_proof(did_document, client_proof_options) + + endorser_proof_options = r.json()['proofOptions'].copy() + endorser_proof_options['verificationMethod'] = f'did:key:{self.endorser_multikey}#{self.endorser_multikey}' + endorsed_did_document = traction.add_di_proof(signed_did_document, endorser_proof_options) + + r = requests.post(f'{self.tdw_server}/{namespace}/{identifier}', json={ + 'didDocument': endorsed_did_document + }) + try: + return r.json()['didDocument'] + except: + raise HTTPException(status_code=r.status_code, detail=r.json()) + + def register_credential(self): + pass + + def publish_credential(self, credential_data): + credential_registration = {} + credential = { + 'credentialSubject': {} + } + if 'untpType' in credential_registration: + if credential_registration['untpType'] == 'DigitalConformityCredential': + type = ['ConformityAttestation'] + attestationType = "Certification" + assessmentLevel = "GovtApproval" + scope = { + "id": "https://bcgov.github.io/digital-trust-toolkit/docs/governance/pilots/bc-petroleum-and-natural-gas-title/governance", + "name": "B.C. Petroleum & Natural Gas Title - DRAFT" + } + issuedToParty = { + "id": "https://orgbook.gov.bc.ca/entity/A0131571", + "idScheme": { + "id": "https://www.bcregistry.gov.bc.ca/", + "name": "BC Registry", + "type": "IdentifierScheme" + }, + "name": "PACIFIC CANBRIAM ENERGY LIMITED", + "registeredId": credential_data['entityId'], + "type": [ + "Entity", + ] + } + assessment = { + "type": ["ConformityAssessment"], + "conformityTopic": "Governance.Compliance", + "compliance": True, + "referenceRegulation": { + "administeredBy": { + "id": "https://www2.gov.bc.ca/gov/content/home", + "idScheme": { + "id": "https://www2.gov.bc.ca/gov/content/home", + "name": "BC-GOV", + "type": "IdentifierScheme" + }, + "name": "Government of British Columbia", + "registeredId": "BC-GOV", + "type": [ + "Entity" + ] + }, + "effectiveDate": act_soup['date'], + "id": credential_registration['legalAct'], + "jurisdictionCountry": "CA", + "name": act_soup['title'], + "type": [ + "Regulation" + ] + } + } + assessedFacility = [] + assessedProduct = [] + facility = { + "type": ["Facility"] + } + product = { + "type": ["Product"] + } + + act_soup = {} \ No newline at end of file diff --git a/backend/app/plugins/soup.py b/backend/app/plugins/soup.py new file mode 100644 index 0000000..dce67f7 --- /dev/null +++ b/backend/app/plugins/soup.py @@ -0,0 +1,24 @@ +from bs4 import BeautifulSoup +import requests + +class Soup: + def __init__(self, url): + self.url = url + r = requests.get(url) + self.soup = BeautifulSoup(r.text, 'html.parser') + + def governance_info(self): + title = self.soup.title.name + return { + 'id': self.url, + 'name': title + } + + def legal_act_info(self): + date = self.soup.find("div", {"id": "act:currency"}).find('td', {"class": "currencysingle"}).get_text() + title = self.soup.find("div", {"id": "title"}).find('h2').get_text() + return { + 'id': self.url, + 'name': title, + 'date': date.split('current to')[-1].strip() + } \ No newline at end of file diff --git a/backend/app/plugins/traction.py b/backend/app/plugins/traction.py index e69de29..3462ec7 100644 --- a/backend/app/plugins/traction.py +++ b/backend/app/plugins/traction.py @@ -0,0 +1,69 @@ +from config import settings +import requests +from fastapi import HTTPException + + +class TractionController: + + def __init__(self): + self.endpoint = settings.TRACTION_API_URL + self.tenant_id = settings.TRACTION_TENANT_ID + self.api_key = settings.TRACTION_API_KEY + self.headers = {} + + def _try_response(self, response, response_key=None): + try: + return response.json()[response_key] + except: + raise HTTPException(status_code=response.status_code, detail=response.json()) + + def authorize(self): + r = requests.post(f'{self.endpoint}/multitenance/tenant/{self.tenant_id}/token', json={ + 'api_key': self.api_key + }) + token = self._try_response(r, 'token') + self.headers = { + 'Authorization': f'Bearer {token}' + } + + def create_did_key(self): + r = requests.post( + f'{self.endpoint}/wallet/did/create', + headers=self.headers, + json={ + 'method': 'key', + 'options': { + 'key_type': 'ed25519' + } + } + ) + did_info = self._try_response(r, 'result') + return did_info['did'].split(':')[-1] + + def create_key(self, kid=None): + r = requests.post( + f'{self.endpoint}/wallet/keys', + headers=self.headers, + json={'kid': kid} if kid else {} + ) + return self._try_response(r, 'multikey') + + def bind_key(self, multikey, kid): + r = requests.put(f'{self.endpoint}/wallet/keys', headers=self.headers, json={ + 'multikey': multikey, + 'kid': kid + }) + return self._try_response(r, 'kid') + + def add_di_proof(self, document, options): + r = requests.post(f'{self.endpoint}/vc/di/add-proof', headers=self.headers, json={ + 'document': document, + 'options': options, + }) + return self._try_response(r, 'securedDocument') + + def verify_di_proof(self, secured_document): + r = requests.post(f'{self.endpoint}/vc/di/verify', headers=self.headers, json={ + 'securedDocument': secured_document, + }) + return self._try_response(r, 'verified') \ No newline at end of file diff --git a/backend/app/plugins/untp.py b/backend/app/plugins/untp.py index 694a9db..16c827c 100644 --- a/backend/app/plugins/untp.py +++ b/backend/app/plugins/untp.py @@ -1,5 +1,4 @@ import app.models.untp as untp -from untp_models.conformity_credential import ConformityAttestation, ConformityAssessment, Regulation, Entity, Product, Facility UNTP_CONTEXTS = { "DigitalConformityCredential": "https://test.uncefact.org/vocabulary/untp/dcc/0.4.1/" @@ -8,50 +7,55 @@ class DigitalConformityCredential: def __init__(self): - self.context = "https://test.uncefact.org/vocabulary/untp/dcc/0.4.1" + self.context = "https://test.uncefact.org/vocabulary/untp/dcc/0.4.2/" self.type = "DigitalConformityCredential" - def vc_to_dcc(self, credential_subject, credential_registration): - conformity_attestation = ConformityAttestation( - type=[], - ) - credential_subject["assessments"]["type"].append("ConformityAttestation") - credential_subject["assessments"]["issuedToParty"]["idScheme"] = { - "type": ["IdentifierScheme"], - "id": "https://www.bcregistry.gov.bc.ca/", - "name": "BC Registry", - } - credential_subject["assessments"]["assessmentLevel"] = "GovtApproval" - credential_subject["assessments"]["attestationType"] = "Certification" - credential_subject["assessments"]["scope"] = { - "id": credential_registration["relatedResources"]["governance"]["id"], - "name": credential_registration["relatedResources"]["governance"]["name"], - } - for idx, assessment in enumerate(credential_subject["assessments"]): - credential_subject["assessments"][idx] = ConformityAssessment( - referenceRegulation=Regulation( - id=credential_registration["relatedResources"]["legalAct"]["id"], - name=credential_registration["relatedResources"]["legalAct"]["name"], - jurisdictionCountry="CA", - administeredBy=Entity( - id="https://gov.bc.ca", - name="Government of British Columbia" - ) - ), - compliance=True, - conformityTopic="Governance.Compliance", - assessedProducts=[ - Product() for product in assessment['assessedProducts'] - ], - assessedFacilities=[ - Facility() for facility in assessment['assessedFacilities'] - ], + def attestation(self, credential_registration=None, products=None, facilities=None): + conformity_attestation = untp.ConformityAttestation( + scope = untp.ConformityAssessmentScheme( + id=credential_registration["relatedResources"]["governance"], + name=credential_registration["relatedResources"]["governance"], + ), + issuedToParty = untp.Party( + idScheme=untp.IdentifierScheme( + id="https://www.bcregistry.gov.bc.ca/", + name="BC Registry" + ) ) - return credential_subject + ) + conformity_attestation.assessment = [self.add_assessment( + # credential_registration["relatedResources"]["legalAct"], + # products, + # facilities, + )] + return conformity_attestation - def add_subject_party(self, entity_id): - self.credential["credentialSubject"]["issuedTo"] = {"id": entity_id} + # def add_subject_party(self, entity_id): + # self.credential["credentialSubject"]["issuedTo"] = {"id": entity_id} - def add_assessment(self, credential, assessment): - credential["credentialSubject"]["assessments"] = [assessment] - return credential + def add_assessment(self, regulation=None, products=[], facilities=[]): + assessment = untp.ConformityAssessment( + compliance=True, + conformityTopic="Governance.Compliance", + assessmentLevel='GovtApproval', + attestationType='Certification', + referenceRegulation=untp.Regulation( + # id=regulation["id"], + # name=regulation["name"], + # effectiveDate=regulation["effectiveDate"], + jurisdictionCountry="CA", + administeredBy=untp.Party( + id="https://gov.bc.ca", + name="Government of British Columbia" + ) + ) + ) + for product in products: + assessed_product = untp.Product() + assessed_product.type.append(product['type']) + assessment.assessedProduct.append(assessed_product) + for facility in facilities: + assessed_facility = untp.Facility() + assessed_facility.type.append(facility['type']) + assessment.assessedFacility.append(assessed_facility) + return assessment diff --git a/backend/app/related_resources/oca_bundles/PetroleumAndNaturalGasTitle copy.json b/backend/app/related_resources/oca_bundles/PetroleumAndNaturalGasTitle copy.json new file mode 100644 index 0000000..760f502 --- /dev/null +++ b/backend/app/related_resources/oca_bundles/PetroleumAndNaturalGasTitle copy.json @@ -0,0 +1,74 @@ +[ + { + "capture_base": { + "attributes": { + "entityId": "Text", + "entityName": "Text", + "interest": "Numeric", + "titleType": "Text", + "titleNumber": "Numeric" + }, + "classification": "", + "digest": "", + "flagged_attributes": [], + "type": "spec/capture_base/1.0" + }, + "overlays": [ + { + "attribute_mapping": { + "entityId": "$.credentialSubject.issuedToParty.registeredId", + "entityName": "$.credentialSubject.issuedToParty.name", + "interest": "$.credentialSubject.issuedToParty.interest", + "titleType": "$.credentialSubject.titleType", + "titleNumber": "$.credentialSubject.titleNumber" + }, + "capture_base": "", + "digest": "", + "type": "spec/overlays/mapping/1.0" + }, + { + "attribute_character_encoding": {}, + "capture_base": "", + "default_character_encoding": "utf-8", + "digest": "", + "type": "spec/overlays/character_encoding/1.0" + }, + { + "attribute_categories": [], + "attribute_labels": { + "entityId": "Entity Id", + "entityName": "Entity Name", + "interest": "Interest", + "titleType": "Title Type", + "titleNumber": "Title Number" + }, + "capture_base": "", + "category_labels": {}, + "digest": "", + "language": "en", + "type": "spec/overlays/label/1.0" + }, + { + "capture_base": "", + "description": "The majority of subsurface petroleum and natural gas (PNG) resources in British Columbia (B.C.) are owned by the Province. By entering into a tenure agreement with the Province, private industry can develop these resources. Tenure agreements are the mechanism used by the Province to give rights to petroleum and natural gas resources through issuance of Petroleum and Natural Gas Titles.", + "digest": "", + "issuer": "Director of Petroleum Lands", + "language": "en", + "name": "B.C. Petroleum & Natural Gas Title", + "type": "spec/overlays/meta/1.0" + }, + { + "logo": "", + "background_image_slice": "", + "background_image": "", + "primary_background_color": "#003366", + "secondary_background_color": "#00264D", + "capture_base": "", + "digest": "", + "primary_attribute": "titleType", + "secondary_attribute": "titleNumber", + "type": "aries/overlays/branding/1.0" + } + ] + } + ] \ No newline at end of file diff --git a/backend/app/related_resources/oca_bundles/PetroleumAndNaturalGasTitle.json b/backend/app/related_resources/oca_bundles/PetroleumAndNaturalGasTitle.json index 760f502..deadfd6 100644 --- a/backend/app/related_resources/oca_bundles/PetroleumAndNaturalGasTitle.json +++ b/backend/app/related_resources/oca_bundles/PetroleumAndNaturalGasTitle.json @@ -1,40 +1,38 @@ [ { "capture_base": { + "type": "spec/capture_base/1.0", "attributes": { - "entityId": "Text", - "entityName": "Text", - "interest": "Numeric", "titleType": "Text", - "titleNumber": "Numeric" - }, - "classification": "", - "digest": "", - "flagged_attributes": [], - "type": "spec/capture_base/1.0" + "titleNumber": "Numeric", + "titleHolder": "Text", + "tractComodity": "List[Text]", + "tractRights": "List[Text]", + "tractNotes": "List[Text]" + } }, "overlays": [ { - "attribute_mapping": { - "entityId": "$.credentialSubject.issuedToParty.registeredId", - "entityName": "$.credentialSubject.issuedToParty.name", - "interest": "$.credentialSubject.issuedToParty.interest", + "type": "vc/overlays/paths/1.0", + "attribute_paths": { + "type": "$.credentialSubject.type", "titleType": "$.credentialSubject.titleType", - "titleNumber": "$.credentialSubject.titleNumber" - }, - "capture_base": "", - "digest": "", - "type": "spec/overlays/mapping/1.0" + "titleNumber": "$.credentialSubject.titleNumber", + "titleHolderType": "$.credentialSubject.issuedToParty.type", + "titleHolder": "$.credentialSubject.issuedToParty.name", + "titleHolderInterest": "$.credentialSubject.issuedToParty.interest", + "titleCaveats": "$.credentialSubject.caveats", + "tractType": "$.credentialSubject.assessment[0]assessedProduct[*]type", + "tractCommodities": "$.credentialSubject.assessment[0]assessedProduct[*]commodities", + "tractLocations": "$.credentialSubject.assessment[0]assessedProduct[*]locations", + "tractRights": "$.credentialSubject.assessment[0]assessedProduct[*]rights", + "tractNotes": "$.credentialSubject.assessment[0]assessedProduct[*]notes", + "wellType": "$.credentialSubject.assessment[0]assessedFacility[*]type", + "wellNames": "$.credentialSubject.assessment[0]assessedFacility[*]commodities", + "wellLocations": "$.credentialSubject.assessment[0]assessedFacility[*]rights" + } }, { - "attribute_character_encoding": {}, - "capture_base": "", - "default_character_encoding": "utf-8", - "digest": "", - "type": "spec/overlays/character_encoding/1.0" - }, - { - "attribute_categories": [], "attribute_labels": { "entityId": "Entity Id", "entityName": "Entity Name", @@ -42,9 +40,6 @@ "titleType": "Title Type", "titleNumber": "Title Number" }, - "capture_base": "", - "category_labels": {}, - "digest": "", "language": "en", "type": "spec/overlays/label/1.0" }, diff --git a/backend/app/routers/credentials.py b/backend/app/routers/credentials.py index 98a5466..0934c0d 100644 --- a/backend/app/routers/credentials.py +++ b/backend/app/routers/credentials.py @@ -1,7 +1,6 @@ from fastapi import APIRouter, Depends, HTTPException, Request from fastapi.responses import JSONResponse from app.models.web_schemas import ( - RegisterCredential, IssueCredential, PublishCredential, ) @@ -11,58 +10,23 @@ AskarVerifier, AskarStorage, AskarWallet, - DidWebEndorser, OrgbookPublisher, BitstringStatusList, DigitalConformityCredential, ) -from app.utilities import freeze_ressource_digest import uuid from datetime import datetime, timezone, timedelta import json -import base64 - router = APIRouter() - -@router.post("/credentials/register") -async def register_credential(request_body: RegisterCredential): - credential_registration = request_body.model_dump()["credentialRegistration"] - - # Create a new status list for this type of credential - status_list = await BitstringStatusList().create(credential_registration) - credential_registration["statusList"] = [status_list] - - await AskarStorage().replace( - "credentialRegistration", - credential_registration["type"], - credential_registration, - ) - - return JSONResponse( - status_code=201, - content=credential_registration, - ) - - # TODO, find another way to get verificationMethod - verification_method = credential_registration["issuer"] + "#multikey-01" - credential_type = await OrgbookPublisher().create_credential_type( - credential_registration, verification_method - ) - - return JSONResponse( - status_code=201, - content=credential_type, - ) - - -@router.post("/credentials/publish") +@router.post("/credentials") async def publish_credential(request_body: PublishCredential): # valid_from = request_body.model_dump()['validFrom'] # valid_until = request_body.model_dump()['validUntil'] - credential_type = request_body.model_dump()["credentialType"] - credential_subject = request_body.model_dump()["credentialSubject"] + data = request_body.model_dump()["data"] + options = request_body.model_dump()["options"] + credential_type = options["credentialType"] try: credential_registration = await AskarStorage().fetch( @@ -70,6 +34,7 @@ async def publish_credential(request_body: PublishCredential): ) except: raise HTTPException(status_code=404, detail="Unknown credential type.") + # return JSONResponse(status_code=200, content=credential_registration) credential = {} @@ -77,8 +42,8 @@ async def publish_credential(request_body: PublishCredential): contexts = ["https://www.w3.org/ns/credentials/v2"] types = ["VerifiableCredential"] - # credential['validFrom'] = '' - # credential['validUntil'] = '' + # credential['validFrom'] = options['validFrom'] + # credential['validUntil'] = options['validUntil'] # UNTP type and context if "untpType" in credential_registration: @@ -87,49 +52,47 @@ async def publish_credential(request_body: PublishCredential): # DigitalConformityCredential template if credential_registration["untpType"] == "DigitalConformityCredential": - credential_subject = DigitalConformityCredential().vc_to_dcc( - credential_subject, credential_registration - ) + governance = {} + legal_act = {} + credential_subject = DigitalConformityCredential().attestation( + credential_registration + ).model_dump() credential_status = {} # BCGov type and context - contexts.append(credential_registration["ressources"]["context"]) + contexts.append(credential_registration["relatedResources"]["context"]) types.append(credential_registration["type"]) credential_id = str(uuid.uuid4()) - issuer = next( - ( - issuer - for issuer in settings.ISSUERS - if issuer["id"] == credential_registration["issuer"] - ), - None, - ) + issuer = await AskarStorage().fetch('didRegistration', credential_registration['issuer']) credential = { "@context": contexts, "type": types, "id": f"https://{settings.DOMAIN}/credentials/{credential_id}", "issuer": issuer, - "name": credential_registration["name"], - "description": credential_registration["description"], - "credentialSubject": credential_subject, - "credentialStatus": credential_status, + "credentialSubject": credential_subject + # "name": credential_registration["name"], + # "description": credential_registration["description"], + # "credentialStatus": credential_status, } # TODO, find a better way to get verification method - verification_method = credential_registration["issuer"] + "#multikey-01" + verification_method = issuer["id"] + "#multikey-01" proof_options = { "type": "DataIntegrityProof", "cryptosuite": "eddsa-jcs-2022", - # 'proofPurpose': 'authentication', "proofPurpose": "assertionMethod", "verificationMethod": verification_method, "created": str(datetime.now().isoformat("T", "seconds")), } + return JSONResponse(status_code=200, content={ + 'credential': credential, + 'options': proof_options + }) @router.post("/credentials/issue") async def issue_credential(request_body: IssueCredential): @@ -193,17 +156,17 @@ async def issue_credential(request_body: IssueCredential): return JSONResponse(status_code=201, content=vc) -# @router.get("/credentials/{credential_id}") -# async def get_credential(credential_id: str, envelope: bool = False): -# headers = {"Content-Type": "application/ld+json"} -# vc = await AskarStorage().fetch('credential', credential_id) -# if envelope: -# vc = await AskarWallet().sign_vc_jose(vc) -# return JSONResponse( -# status_code=200, -# content=vc, -# headers=headers -# ) +@router.get("/credentials/{credential_id}") +async def get_credential(credential_id: str, envelope: bool = False): + headers = {"Content-Type": "application/ld+json"} + vc = await AskarStorage().fetch('credential', credential_id) + if envelope: + vc = await AskarWallet().sign_vc_jose(vc) + return JSONResponse( + status_code=200, + content=vc, + headers=headers + ) # @router.post("/credentials/status") diff --git a/backend/app/routers/issuers.py b/backend/app/routers/issuers.py index 0ea01c8..784719d 100644 --- a/backend/app/routers/issuers.py +++ b/backend/app/routers/issuers.py @@ -1,6 +1,6 @@ from fastapi import APIRouter, Depends, HTTPException, Request from fastapi.responses import JSONResponse -from app.models.web_schemas import RegisterIssuer +from app.models.registrations import IssuerRegistration from config import settings from app.plugins import AskarStorage, DidWebEndorser, AskarWallet from app.models import DidDocument, VerificationMethod, Service @@ -9,7 +9,8 @@ @router.post("/issuers", summary="Register issuer.") -async def register_issuer(request_body: RegisterIssuer): +async def register_issuer(request_body: IssuerRegistration): + url = vars(request_body)["url"] name = vars(request_body)["name"] scope = vars(request_body)["scope"] description = vars(request_body)["description"] @@ -17,9 +18,54 @@ async def register_issuer(request_body: RegisterIssuer): identifier = name.replace(" ", "-").lower() did = f"did:web:{settings.DOMAIN}:{namespace}:{identifier}" # did_document = await DidWebEndorser().did_registration(namespace, identifier) - did_document = DidDocument(id=did, name=name, description=description).model_dump() + multikey = await AskarWallet().create_key() + + verification_method_multikey = VerificationMethod( + id=f'{did}#multikey-01', + type='Multikey', + controller=did, + publicKeyMultibase=multikey + ) + + verification_method_jwk = VerificationMethod( + id=f'{did}#jwk-01', + type='JsonWebKey', + controller=did, + publicKeyJwk={ + "kty": "OKP", + "crv": "Ed25519", + "x": multikey + } + ) + + service = Service( + id=f'{did}#bcgov-website', + type='LinkedDomains', + serviceEndpoint=url, + ) if url else None + + did_document = DidDocument( + id=did, + name=name, + description=description, + authentication=[verification_method_multikey.id], + assertionMethod=[verification_method_multikey.id], + verificationMethod=[ + verification_method_multikey, + verification_method_jwk + ], + service=[service] if service else None + ).model_dump() + options = { + 'type': 'DataIntegrityProof', + 'cryptosuite': 'eddsa-jcs-2022', + 'proofPurpose': 'authentication', + 'verificationMethod': f'did:key:{multikey}#{multikey}', + } + signed_did_doc = await AskarWallet().add_proof(did_document, options) + # await AskarStorage().store('didRegistration', did_document['id'], did_document) - return JSONResponse(status_code=201, content=did_document) + return JSONResponse(status_code=201, content=signed_did_doc) # @router.get("/issuers") diff --git a/backend/app/routers/registrations.py b/backend/app/routers/registrations.py new file mode 100644 index 0000000..a47ee03 --- /dev/null +++ b/backend/app/routers/registrations.py @@ -0,0 +1,79 @@ +from typing import Annotated +from fastapi import APIRouter, Depends, HTTPException, Request +from fastapi.responses import JSONResponse +from app.models.registrations import IssuerRegistration, CredentialRegistration +from config import settings +from app.plugins import AskarStorage, BitstringStatusList, PublisherRegistrar, Soup +from app.security import check_api_key_header + +router = APIRouter(prefix='/registrar') + + +@router.post("/issuers") +async def register_issuer(request_body: IssuerRegistration, authorized = Depends(check_api_key_header)): + namespace = vars(request_body)["scope"].replace(" ", "-").lower() + identifier = vars(request_body)["name"].replace(" ", "-").lower() + issuer = { + 'id': f'did:web:{settings.TDW_SERVER_URL.split("//")[-1]}:{namespace}:{identifier}', + 'name': vars(request_body)["name"], + 'description': vars(request_body)["description"] + } + await AskarStorage().store('didRegistration', issuer['id'], issuer) + return JSONResponse(status_code=201, content=issuer) + + did_document = PublisherRegistrar().register_issuer( + vars(request_body)["name"], + vars(request_body)["scope"], + vars(request_body)["url"], + vars(request_body)["description"] + ) + + await AskarStorage().store('didRegistration', did_document['id'], did_document) + return JSONResponse(status_code=201, content=did_document) + +# @router.post("/issuers/{did}") +# async def approve_pending_issuer_registration(did: str): +# did_document = await AskarStorage().fetch('didRegistration', did) +# await AskarStorage().store('didDocument', did, did_document) +# # await AskarStorage().remove('didRegistration', did) +# return JSONResponse( +# status_code=200, +# content=did_document, +# ) + + +@router.delete("/issuers/{did}") +async def register_issuer(did: str, authorized = Depends(check_api_key_header)): + + await AskarStorage().remove('didRegistration', did) + return JSONResponse(status_code=200, content={}) + + +@router.post("/credentials") +async def register_credential_type(request_body: CredentialRegistration, authorized = Depends(check_api_key_header)): + credential_registration = request_body.model_dump() + + untp_type = credential_registration.get('untpType') + if untp_type == 'DigitalConformityCredential': + if not credential_registration['relatedResources'].get('governance') \ + or not credential_registration['relatedResources'].get('legalAct'): + pass + governance = { + 'id': credential_registration['relatedResources'].get('governance').lstrip('/'), + 'name': credential_registration['relatedResources'].get('governance').lstrip('/').split('/')[-1].replace('-', ' ').title() + } + legal_act = Soup( + credential_registration['relatedResources'].get('legalAct') + ).legal_act_info() + + # Create a new status list for this type of credential + # status_list = await BitstringStatusList().create(credential_registration) + # credential_registration["statusList"] = [status_list] + + await AskarStorage().replace( + "credentialRegistration", + credential_registration["type"], + credential_registration, + ) + + return JSONResponse(status_code=201, content=credential_registration) diff --git a/backend/app/routers/utilities.py b/backend/app/routers/utilities.py new file mode 100644 index 0000000..0de5653 --- /dev/null +++ b/backend/app/routers/utilities.py @@ -0,0 +1,14 @@ +from fastapi import APIRouter, HTTPException +from fastapi.responses import JSONResponse +from app.models.web_schemas import RegisterIssuer +from config import settings +from app.plugins import AskarStorage +from app.models import DidDocument +import json + +router = APIRouter() + + +@router.post("/utilities/oca-bundle-generator") +async def generate_oca_bundle(request_body: CredentialRegistration): + pass diff --git a/backend/app/security.py b/backend/app/security.py new file mode 100644 index 0000000..e488fa3 --- /dev/null +++ b/backend/app/security.py @@ -0,0 +1,16 @@ +from fastapi import Depends, HTTPException +from fastapi.security import APIKeyHeader +from starlette import status +from config import settings + +X_API_KEY = APIKeyHeader(name='X-API-Key') + +def check_api_key_header(x_api_key: str = Depends(X_API_KEY)): + """ takes the X-API-Key header and converts it into the matching user object from the database """ + + if x_api_key == settings.TRACTION_API_KEY: + return True + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid API Key", + ) \ No newline at end of file diff --git a/backend/app/utilities.py b/backend/app/utilities.py index 98a118b..7dfc997 100644 --- a/backend/app/utilities.py +++ b/backend/app/utilities.py @@ -1,17 +1,47 @@ import requests import hashlib +import base64 +import base58 +from multiformats import multibase +MULTIKEY = [ + { + 'alg': 'ed25519', + 'crv': 'Ed25519', + 'prefix': 'z6M', + 'hex_prefix': 'ed01', + 'bytes_prefix_lenght': 2 + } +] def create_did_doc(did): return {} - def freeze_ressource_digest(url): r = requests.get(url) mb = "" return {"url": url, "digestMultibase": mb} - def verkey_to_multikey(verkey, format="ed25519"): multitable = {"ed25519": "ed01"} pass + +def bytes_prefix_lenght(multikey): + return next((item['bytes_prefix_lenght'] for item in MULTIKEY if multikey.startswith(item['prefix'])), None) + +def alg_from_multikey(multikey): + return next((item['alg'] for item in MULTIKEY if multikey.startswith(item['prefix'])), None) + +def crv_from_multikey(multikey): + return next((item['crv'] for item in MULTIKEY if multikey.startswith(item['prefix'])), None) + +def get_coordinates(multikey): + if alg_from_multikey(multikey) == 'ed25519': + key_bytes = multibase.decode(multikey)[bytes_prefix_lenght(multikey):] + return {'x': base64.urlsafe_b64encode(key_bytes).decode().rstrip('=')} + +def multikey_to_jwk(multikey): + return { + "kty": "OKP", + "crv": crv_from_multikey(multikey) + } | get_coordinates(multikey) diff --git a/backend/config.py b/backend/config.py index ef73216..e45f8cc 100644 --- a/backend/config.py +++ b/backend/config.py @@ -12,15 +12,16 @@ class Settings(BaseSettings): DOMAIN: str = os.environ["DOMAIN"] - # TRACTION_API_URL: str = os.environ["TRACTION_API_URL"] - # TRACTION_API_KEY: str = os.environ["TRACTION_API_KEY"] - # TRACTION_TENANT_ID: str = os.environ["TRACTION_TENANT_ID"] + TRACTION_API_URL: str = os.environ["TRACTION_API_URL"] + TRACTION_API_KEY: str = os.environ["TRACTION_API_KEY"] + TRACTION_TENANT_ID: str = os.environ["TRACTION_TENANT_ID"] ORGBOOK_URL: str = os.environ["ORGBOOK_URL"] ORGBOOK_API_URL: str = f"{ORGBOOK_URL}/api/v4" ORGBOOK_VC_SERVICE: str = f"{ORGBOOK_URL}/api/vc" TDW_SERVER_URL: str = os.environ["TDW_SERVER_URL"] + TDW_ENDORSER_MULTIKEY: str = os.environ["TDW_ENDORSER_MULTIKEY"] # AGENT_ADMIN_URL: str = os.environ["AGENT_ADMIN_URL"] # AGENT_ADMIN_API_KEY: str = TRACTION_API_KEY @@ -33,14 +34,14 @@ class Settings(BaseSettings): else "sqlite://app.db" ) - ISSUERS: list = [ - { - "id": f"did:web:{DOMAIN}:petroleum-and-natural-gas-act:director-of-petroleum-lands", - "name": "Director of Petroleum Lands", - "description": "An officer or employee of the ministry who is designated as the Director of Petroleum Lands by the minister.", - "url": "https://www2.gov.bc.ca/gov/content/governments/organizational-structure/ministries-organizations/ministries/energy-mines-and-petroleum-resources", - } - ] + # ISSUERS: list = [ + # { + # "id": f"did:web:{DOMAIN}:petroleum-and-natural-gas-act:director-of-petroleum-lands", + # "name": "Director of Petroleum Lands", + # "description": "An officer or employee of the ministry who is designated as the Director of Petroleum Lands by the minister.", + # "url": "https://www2.gov.bc.ca/gov/content/governments/organizational-structure/ministries-organizations/ministries/energy-mines-and-petroleum-resources", + # } + # ] settings = Settings() diff --git a/backend/requirements.txt b/backend/requirements.txt index d9e1237..d79ffd1 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,26 +1,39 @@ annotated-types==0.7.0 anyio==4.4.0 aries-askar==0.3.2 +base58==2.1.1 bases==0.3.0 +bitarray==2.9.2 +bitstring==4.2.3 cached-property==1.5.2 canonicaljson==2.0.0 certifi==2024.8.30 +cffi==1.17.1 charset-normalizer==3.3.2 click==8.1.7 +cryptography==43.0.1 +Deprecated==1.2.14 exceptiongroup==1.2.2 fastapi==0.112.2 h11==0.14.0 idna==3.8 multiformats==0.3.1.post4 multiformats-config==0.3.1 -pydantic==2.8.2 +pycparser==2.22 +pydantic==2.7.1 pydantic-settings==2.4.0 -pydantic_core==2.20.1 +pydantic_core==2.18.2 +PyGithub==2.4.0 +PyJWT==2.9.0 +PyNaCl==1.5.0 python-dotenv==1.0.1 requests==2.32.3 +ruff==0.6.8 sniffio==1.3.1 starlette==0.38.4 typing-validation==1.2.11.post4 typing_extensions==4.12.2 +untp_models==0.0.2 urllib3==2.2.2 uvicorn==0.30.6 +wrapt==1.16.0 diff --git a/frontend/app/__init__.py b/frontend/app/__init__.py index 25cd0fa..5bc425a 100644 --- a/frontend/app/__init__.py +++ b/frontend/app/__init__.py @@ -11,13 +11,13 @@ def create_app(config_class=Config): app = Flask(__name__) - app.secret_key = 's3cret' + app.config.from_object(config_class) CORS(app) QRcode(app) # Session(app) - app.register_blueprint(errors_bp) + # app.register_blueprint(errors_bp) app.register_blueprint(main_bp) app.register_blueprint(auth_bp) diff --git a/frontend/app/plugins/__init__.py b/frontend/app/plugins/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/frontend/app/plugins/agent.py b/frontend/app/plugins/agent.py deleted file mode 100644 index bed3a5a..0000000 --- a/frontend/app/plugins/agent.py +++ /dev/null @@ -1,12 +0,0 @@ - - - - - -class AgentController: - def __init__(self): - self.endpoint = '' - self.api_key = '' - - def new_presentation_request(self): - pass \ No newline at end of file diff --git a/frontend/app/plugins/db.py b/frontend/app/plugins/db.py new file mode 100644 index 0000000..749507c --- /dev/null +++ b/frontend/app/plugins/db.py @@ -0,0 +1,39 @@ +import sqlite3 +import json + +class SQLite: + def __init__(self): + self.connection = sqlite3.connect(':memory:') + self.cursor = self.connection.cursor() + + def provision(self): + sql = """ +DROP TABLE IF EXISTS invitations; + +CREATE TABLE invitations ( + id TEXT PRIMARY KEY NOT NULL, + data TEXT NOT NULL, + created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, +); +""" + self.connection.executescript(sql) + self.connection.commit() + self.connection.close() + + def new_invitation(self, invitation_id, invitation): + self.cursor.execute("INSERT INTO invitations (id, invitation) VALUES (?, ?)", + (invitation_id, json.dumps(invitation)) + ) + self.connection.commit() + self.connection.close() + + def get_invitation(self, invitation_id): + self.connection.row_factory = sqlite3.Row + invitation = self.connection.execute('SELECT FROM invitations WHERE id = ?', (invitation_id,)).fetchone() + if invitation is None: + pass + invitation = self.connection.execute('DELETE FROM invitations WHERE id = ?', (invitation_id,)) + self.connection.commit() + self.connection.close() + return json.loads(invitation.data) + \ No newline at end of file diff --git a/frontend/app/plugins/traction.py b/frontend/app/plugins/traction.py new file mode 100644 index 0000000..64dd2a7 --- /dev/null +++ b/frontend/app/plugins/traction.py @@ -0,0 +1,84 @@ +from flask import current_app +from datetime import datetime +import random +import requests +import json + + +class TractionController: + def __init__(self, token=None): + self.endpoint = current_app.config['TRACTION_API_URL'] + self.tenant_id = current_app.config['TRACTION_TENANT_ID'] + self.api_key = current_app.config['TRACTION_API_KEY'] + self.headers = { + "Authorization": f"Bearer {token}" + } + + def make_post(self, endpoint, body): + r = requests.post( + endpoint, + headers=self.headers, + json=body + ) + try: + return r.json() + except: + pass + + def request_token(self): + r = requests.post( + f'{self.endpoint}/multitenancy/tenant/{self.tenant_id}/token', + json={"api_key": self.api_key} + ) + return r.json()['token'] + + def new_presentation_request(self): + attributes = ["permissions", "email", "id", "credentialType"] + schema_id = 'Mo2T76ZKcQvdYNPdgGbMFi:2:Delegated Issuing Entity:0.1' + cred_def_id = "Mo2T76ZKcQvdYNPdgGbMFi:3:CL:2074989:Delegated Issuing Entity" + presentation_request={ + "name": "Orgbook Publisher Authorization", + "version": "1.0", + "nonce": str(random.randrange(10000000, 99999999, 8)), + "requested_attributes": { + "issuerInfo": { + "names": attributes, + "restrictions":[ + { + "cred_def_id": cred_def_id + } + ] + } + }, + "requested_predicates": {} + } + print(json.dumps(presentation_request, indent=2)) + presentation_request = { + "presentation_request": { + "indy": presentation_request + } + } + # timestamp = (datetime.utcnow() - datetime(1970, 1, 1)).total_seconds() + # presentation_request['non_revoked'] = { + # "from": 123, + # "to": 123 + # } + r = self.make_post( + f'{self.endpoint}/present-proof-2.0/create-request', + presentation_request + ) + + pres_ex_id = r['pres_ex_id'] + oob_invitation = { + "attachments": [ + { + "id": pres_ex_id, + "type": "present-proof" + } + ] + } + r = self.make_post( + f'{self.endpoint}/out-of-band/create-invitation', + oob_invitation + ) + return r['invitation'] \ No newline at end of file diff --git a/frontend/app/routes/auth/routes.py b/frontend/app/routes/auth/routes.py index f9343e3..a4fd81b 100644 --- a/frontend/app/routes/auth/routes.py +++ b/frontend/app/routes/auth/routes.py @@ -4,22 +4,31 @@ url_for, redirect, session, + jsonify ) from app.routes.auth import bp +from app.plugins.traction import TractionController +from app.plugins.db import SQLite from .forms import IssuerAccessForm, AdminAccessForm -# @bp.before_request -# def before_request_callback(): -# if "token" not in session: -# return redirect(url_for("auth.logout")) +@bp.before_request +def before_request_callback(): + if not session.get('traction_token'): + session['traction_token'] = TractionController().request_token() @bp.route("/login", methods=["GET", "POST"]) def login(): admin_access_form = AdminAccessForm() issuer_access_form = IssuerAccessForm() - session['invitation'] = 'my_qr_code' + + invitation = TractionController(session['traction_token']).new_presentation_request() + invitation_id = invitation['pres']['@id'] + # SQLite().new_invitation(invitation_id, invitation) + + session['invitation'] = url_for('auth.invitation', invitation_id) + if admin_access_form.submit.data and admin_access_form.validate(): session['token'] = True return redirect(url_for('main.index')) @@ -28,10 +37,16 @@ def login(): return render_template( "pages/auth/index.jinja", title='Login', + form=issuer_access_form, admin_access_form=admin_access_form, issuer_access_form=issuer_access_form, ) +@bp.route("/invitation/{invitation_id}", methods=["GET"]) +def invitation(invitation_id: str): + invitation = SQLite().get_invitation(invitation_id) + return jsonify(invitation) + @bp.route("/logout", methods=["GET"]) def logout(): diff --git a/frontend/config.py b/frontend/config.py index 8f26fc9..d24aef4 100644 --- a/frontend/config.py +++ b/frontend/config.py @@ -13,8 +13,9 @@ class Config(object): # # Backend API # ORGBOOK_PUBLISHER = os.environ["ORGBOOK_PUBLISHER"] - ADMIN_ID = os.environ["TRACTION_TENANT_ID"] - ADMIN_KEY = os.environ["TRACTION_API_KEY"] + TRACTION_API_URL = os.environ["TRACTION_API_URL"] + TRACTION_API_KEY = os.environ["TRACTION_API_KEY"] + TRACTION_TENANT_ID = os.environ["TRACTION_TENANT_ID"] # # Flask-session with redis # SESSION_TYPE = "redis" From b43275f9b4a7997d9ee7dcb4fd004664388af10e Mon Sep 17 00:00:00 2001 From: PatStLouis Date: Wed, 16 Oct 2024 18:57:15 +0000 Subject: [PATCH 2/2] pre dev provisioning Signed-off-by: PatStLouis --- .github/dependabot.yaml | 34 + .github/workflows/chart-releaser.yaml | 36 + .github/workflows/image-publisher.yaml | 94 +++ .gitignore | 2 +- backend/app/__init__.py | 4 +- .../contexts/credentials_examples_v2.jsonld | 6 + backend/app/contexts/credentials_v2.jsonld | 340 +++++++++ backend/app/contexts/untp_dcc_0.4.2.jsonld | 687 ++++++++++++++++++ .../credentials/business-registration.json | 14 + .../app/data/registrations/issuers/cpo.json | 5 + .../app/data/registrations/issuers/dpl.json | 5 + .../app/data/registrations/issuers/roc.json | 5 + backend/app/dependencies.py | 4 - backend/app/models/credential.py | 162 ++++- backend/app/models/linked_data.py | 35 + backend/app/models/registrations.py | 6 + backend/app/models/untp.py | 4 +- backend/app/plugins/__init__.py | 4 - backend/app/plugins/agent.py | 100 --- backend/app/plugins/did_web.py | 71 -- backend/app/plugins/github.py | 22 - backend/app/plugins/orgbook.py | 3 +- backend/app/plugins/registrar.py | 304 +++++--- backend/app/plugins/soup.py | 2 +- backend/app/plugins/traction.py | 12 +- backend/app/plugins/untp.py | 26 +- backend/app/routers/credentials.py | 31 +- backend/app/routers/issuers.py | 149 ---- backend/app/routers/registrations.py | 79 +- backend/app/routers/related_resources.py | 31 - backend/app/routers/utilities.py | 14 - backend/app/utilities.py | 2 - backend/app/utils.py | 42 ++ backend/poetry.lock | 650 +++++++++++++++++ backend/pyproject.toml | 23 + backend/requirements.txt | 3 + charts/orgbook-publisher/Chart.yaml | 22 + .../orgbook-publisher/templates/_helpers.tpl | 86 +++ .../templates/backend/deployment.yaml | 74 ++ .../templates/backend/ingress.yaml | 32 + .../templates/backend/networkpolicy.yaml | 21 + .../templates/backend/secret.yaml | 13 + .../templates/backend/service.yaml | 16 + .../templates/postgresql/networkpolicy.yaml | 21 + charts/orgbook-publisher/values.yaml | 93 +++ 45 files changed, 2777 insertions(+), 612 deletions(-) create mode 100644 .github/dependabot.yaml create mode 100644 .github/workflows/chart-releaser.yaml create mode 100644 .github/workflows/image-publisher.yaml create mode 100644 backend/app/contexts/credentials_examples_v2.jsonld create mode 100644 backend/app/contexts/credentials_v2.jsonld create mode 100644 backend/app/contexts/untp_dcc_0.4.2.jsonld create mode 100644 backend/app/data/registrations/credentials/business-registration.json create mode 100644 backend/app/data/registrations/issuers/cpo.json create mode 100644 backend/app/data/registrations/issuers/dpl.json create mode 100644 backend/app/data/registrations/issuers/roc.json delete mode 100644 backend/app/dependencies.py create mode 100644 backend/app/models/linked_data.py delete mode 100644 backend/app/plugins/agent.py delete mode 100644 backend/app/plugins/did_web.py delete mode 100644 backend/app/plugins/github.py delete mode 100644 backend/app/routers/issuers.py delete mode 100644 backend/app/routers/related_resources.py delete mode 100644 backend/app/routers/utilities.py create mode 100644 backend/app/utils.py create mode 100644 backend/poetry.lock create mode 100644 backend/pyproject.toml create mode 100644 charts/orgbook-publisher/Chart.yaml create mode 100644 charts/orgbook-publisher/templates/_helpers.tpl create mode 100644 charts/orgbook-publisher/templates/backend/deployment.yaml create mode 100644 charts/orgbook-publisher/templates/backend/ingress.yaml create mode 100644 charts/orgbook-publisher/templates/backend/networkpolicy.yaml create mode 100644 charts/orgbook-publisher/templates/backend/secret.yaml create mode 100644 charts/orgbook-publisher/templates/backend/service.yaml create mode 100644 charts/orgbook-publisher/templates/postgresql/networkpolicy.yaml create mode 100644 charts/orgbook-publisher/values.yaml diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml new file mode 100644 index 0000000..93826c7 --- /dev/null +++ b/.github/dependabot.yaml @@ -0,0 +1,34 @@ + # For details on how this file works refer to: + # - https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file +version: 2 +updates: + # Maintain dependencies for GitHub Actions + # - Check for updates once a week + # - Group all updates into a single PR + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + groups: + all-actions: + patterns: [ "*" ] + + # Maintain dependencies for Python Packages + - package-ecosystem: "pip" + directory: "/server" + schedule: + interval: "weekly" + day: "monday" + time: "04:00" + timezone: "Canada/Pacific" + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-major"] + + - package-ecosystem: "docker" + directory: "/server" + schedule: + interval: "weekly" + day: "monday" + time: "04:00" + timezone: "Canada/Pacific" diff --git a/.github/workflows/chart-releaser.yaml b/.github/workflows/chart-releaser.yaml new file mode 100644 index 0000000..3583d1d --- /dev/null +++ b/.github/workflows/chart-releaser.yaml @@ -0,0 +1,36 @@ +name: Release Charts + +on: + release: + types: [published] + +jobs: + release-charts: + permissions: + contents: write + packages: write + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Configure Git + run: | + git config user.name "$GITHUB_ACTOR" + git config user.email "$GITHUB_ACTOR@users.noreply.github.com" + + - name: Install Helm + uses: azure/setup-helm@v4 + env: + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + + - name: Add dependency chart repos + run: | + helm repo add bitnami https://charts.bitnami.com/bitnami + + - name: Run chart-releaser + uses: helm/chart-releaser-action@v1.6.0 + env: + CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}" \ No newline at end of file diff --git a/.github/workflows/image-publisher.yaml b/.github/workflows/image-publisher.yaml new file mode 100644 index 0000000..cc8c14f --- /dev/null +++ b/.github/workflows/image-publisher.yaml @@ -0,0 +1,94 @@ +name: Publish Orgbook Publisher Image +run-name: Publish Orgbook Publisher ${{ inputs.tag || github.event.release.tag_name }} Image +on: + release: + types: [published] + + workflow_dispatch: + inputs: + tag: + description: "Image tag" + required: true + type: string + platforms: + description: "Platforms - Comma separated list of the platforms to support." + required: true + default: linux/amd64 + type: string + ref: + description: "Optional - The branch, tag or SHA to checkout." + required: false + type: string + +permissions: + contents: read + packages: write + +env: + PLATFORMS: ${{ inputs.platforms || 'linux/amd64,linux/arm64' }} + +jobs: + publish-image: + if: github.repository_owner == 'OpSecId' + strategy: + fail-fast: false + + name: Publish Orgbook Publisher Image + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@v4 + with: + ref: ${{ inputs.ref || '' }} + + - name: Gather image info + id: info + run: | + echo "repo-owner=${GITHUB_REPOSITORY_OWNER,,}" >> $GITHUB_OUTPUT + + - name: Cache Docker layers + uses: actions/cache@v4 + with: + path: /tmp/.buildx-cache + key: ${{ runner.os }}-buildx-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-buildx- + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to the GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup Image Metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: | + ghcr.io/${{ steps.info.outputs.repo-owner }}/orgbook-publisher + tags: | + type=raw,value=${{ inputs.tag || github.event.release.tag_name }} + + - name: Build and Push Image to ghcr.io + uses: docker/build-push-action@v6 + with: + push: true + context: server/ + file: server/Dockerfile + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=local,src=/tmp/.buildx-cache + cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max + platforms: ${{ env.PLATFORMS }} + + # Temp fix + # https://github.com/docker/build-push-action/issues/252 + # https://github.com/moby/buildkit/issues/1896 + - name: Move cache + run: | + rm -rf /tmp/.buildx-cache + mv /tmp/.buildx-cache-new /tmp/.buildx-cache diff --git a/.gitignore b/.gitignore index d41a49d..a9fbc95 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ # Linting stuff -.ruff_cache +.ruff_cache/ # Databases app.db diff --git a/backend/app/__init__.py b/backend/app/__init__.py index 57e314b..f5151ea 100644 --- a/backend/app/__init__.py +++ b/backend/app/__init__.py @@ -21,8 +21,8 @@ async def server_status(): return JSONResponse(status_code=200, content={"status": "ok"}) -api_router.include_router(credentials.router, tags=["Credentials"]) -api_router.include_router(registrations.router, tags=["Registrations"]) +api_router.include_router(credentials.router) +api_router.include_router(registrations.router) app.include_router(api_router) diff --git a/backend/app/contexts/credentials_examples_v2.jsonld b/backend/app/contexts/credentials_examples_v2.jsonld new file mode 100644 index 0000000..f3685c2 --- /dev/null +++ b/backend/app/contexts/credentials_examples_v2.jsonld @@ -0,0 +1,6 @@ +{ + "@context": { + "@vocab": "https://www.w3.org/ns/credentials/examples#" + } + } + \ No newline at end of file diff --git a/backend/app/contexts/credentials_v2.jsonld b/backend/app/contexts/credentials_v2.jsonld new file mode 100644 index 0000000..d04cfcc --- /dev/null +++ b/backend/app/contexts/credentials_v2.jsonld @@ -0,0 +1,340 @@ +{ + "@context": { + "@protected": true, + + "id": "@id", + "type": "@type", + + "description": "https://schema.org/description", + "digestMultibase": { + "@id": "https://w3id.org/security#digestMultibase", + "@type": "https://w3id.org/security#multibase" + }, + "digestSRI": { + "@id": "https://www.w3.org/2018/credentials#digestSRI", + "@type": "https://www.w3.org/2018/credentials#sriString" + }, + "mediaType": { + "@id": "https://schema.org/encodingFormat" + }, + "name": "https://schema.org/name", + + "VerifiableCredential": { + "@id": "https://www.w3.org/2018/credentials#VerifiableCredential", + "@context": { + "@protected": true, + + "id": "@id", + "type": "@type", + + "confidenceMethod": { + "@id": "https://www.w3.org/2018/credentials#confidenceMethod", + "@type": "@id" + }, + "credentialSchema": { + "@id": "https://www.w3.org/2018/credentials#credentialSchema", + "@type": "@id" + }, + "credentialStatus": { + "@id": "https://www.w3.org/2018/credentials#credentialStatus", + "@type": "@id" + }, + "credentialSubject": { + "@id": "https://www.w3.org/2018/credentials#credentialSubject", + "@type": "@id" + }, + "description": "https://schema.org/description", + "evidence": { + "@id": "https://www.w3.org/2018/credentials#evidence", + "@type": "@id" + }, + "issuer": { + "@id": "https://www.w3.org/2018/credentials#issuer", + "@type": "@id" + }, + "name": "https://schema.org/name", + "proof": { + "@id": "https://w3id.org/security#proof", + "@type": "@id", + "@container": "@graph" + }, + "refreshService": { + "@id": "https://www.w3.org/2018/credentials#refreshService", + "@type": "@id" + }, + "relatedResource": { + "@id": "https://www.w3.org/2018/credentials#relatedResource", + "@type": "@id" + }, + "renderMethod": { + "@id": "https://www.w3.org/2018/credentials#renderMethod", + "@type": "@id" + }, + "termsOfUse": { + "@id": "https://www.w3.org/2018/credentials#termsOfUse", + "@type": "@id" + }, + "validFrom": { + "@id": "https://www.w3.org/2018/credentials#validFrom", + "@type": "http://www.w3.org/2001/XMLSchema#dateTime" + }, + "validUntil": { + "@id": "https://www.w3.org/2018/credentials#validUntil", + "@type": "http://www.w3.org/2001/XMLSchema#dateTime" + } + } + }, + + "EnvelopedVerifiableCredential": + "https://www.w3.org/2018/credentials#EnvelopedVerifiableCredential", + + "VerifiablePresentation": { + "@id": "https://www.w3.org/2018/credentials#VerifiablePresentation", + "@context": { + "@protected": true, + + "id": "@id", + "type": "@type", + + "holder": { + "@id": "https://www.w3.org/2018/credentials#holder", + "@type": "@id" + }, + "proof": { + "@id": "https://w3id.org/security#proof", + "@type": "@id", + "@container": "@graph" + }, + "termsOfUse": { + "@id": "https://www.w3.org/2018/credentials#termsOfUse", + "@type": "@id" + }, + "verifiableCredential": { + "@id": "https://www.w3.org/2018/credentials#verifiableCredential", + "@type": "@id", + "@container": "@graph", + "@context": null + } + } + }, + + "EnvelopedVerifiablePresentation": + "https://www.w3.org/2018/credentials#EnvelopedVerifiablePresentation", + + "JsonSchemaCredential": + "https://www.w3.org/2018/credentials#JsonSchemaCredential", + + "JsonSchema": { + "@id": "https://www.w3.org/2018/credentials#JsonSchema", + "@context": { + "@protected": true, + + "id": "@id", + "type": "@type", + + "jsonSchema": { + "@id": "https://www.w3.org/2018/credentials#jsonSchema", + "@type": "@json" + } + } + }, + + "BitstringStatusListCredential": + "https://www.w3.org/ns/credentials/status#BitstringStatusListCredential", + + "BitstringStatusList": { + "@id": "https://www.w3.org/ns/credentials/status#BitstringStatusList", + "@context": { + "@protected": true, + + "id": "@id", + "type": "@type", + + "encodedList": { + "@id": "https://www.w3.org/ns/credentials/status#encodedList", + "@type": "https://w3id.org/security#multibase" + }, + "statusMessage": { + "@id": "https://www.w3.org/ns/credentials/status#statusMessage", + "@context": { + "@protected": true, + + "id": "@id", + "type": "@type", + + "message": "https://www.w3.org/ns/credentials/status#message", + "status": "https://www.w3.org/ns/credentials/status#status" + } + }, + "statusPurpose": + "https://www.w3.org/ns/credentials/status#statusPurpose", + "statusReference": { + "@id": "https://www.w3.org/ns/credentials/status#statusReference", + "@type": "@id" + }, + "statusSize": { + "@id": "https://www.w3.org/ns/credentials/status#statusSize", + "@type": "https://www.w3.org/2001/XMLSchema#positiveInteger" + }, + "ttl": "https://www.w3.org/ns/credentials/status#ttl" + } + }, + + "BitstringStatusListEntry": { + "@id": + "https://www.w3.org/ns/credentials/status#BitstringStatusListEntry", + "@context": { + "@protected": true, + + "id": "@id", + "type": "@type", + + "statusListCredential": { + "@id": + "https://www.w3.org/ns/credentials/status#statusListCredential", + "@type": "@id" + }, + "statusListIndex": + "https://www.w3.org/ns/credentials/status#statusListIndex", + "statusPurpose": + "https://www.w3.org/ns/credentials/status#statusPurpose" + } + }, + + "DataIntegrityProof": { + "@id": "https://w3id.org/security#DataIntegrityProof", + "@context": { + "@protected": true, + + "id": "@id", + "type": "@type", + + "challenge": "https://w3id.org/security#challenge", + "created": { + "@id": "http://purl.org/dc/terms/created", + "@type": "http://www.w3.org/2001/XMLSchema#dateTime" + }, + "cryptosuite": { + "@id": "https://w3id.org/security#cryptosuite", + "@type": "https://w3id.org/security#cryptosuiteString" + }, + "domain": "https://w3id.org/security#domain", + "expires": { + "@id": "https://w3id.org/security#expiration", + "@type": "http://www.w3.org/2001/XMLSchema#dateTime" + }, + "nonce": "https://w3id.org/security#nonce", + "previousProof": { + "@id": "https://w3id.org/security#previousProof", + "@type": "@id" + }, + "proofPurpose": { + "@id": "https://w3id.org/security#proofPurpose", + "@type": "@vocab", + "@context": { + "@protected": true, + + "id": "@id", + "type": "@type", + + "assertionMethod": { + "@id": "https://w3id.org/security#assertionMethod", + "@type": "@id", + "@container": "@set" + }, + "authentication": { + "@id": "https://w3id.org/security#authenticationMethod", + "@type": "@id", + "@container": "@set" + }, + "capabilityDelegation": { + "@id": "https://w3id.org/security#capabilityDelegationMethod", + "@type": "@id", + "@container": "@set" + }, + "capabilityInvocation": { + "@id": "https://w3id.org/security#capabilityInvocationMethod", + "@type": "@id", + "@container": "@set" + }, + "keyAgreement": { + "@id": "https://w3id.org/security#keyAgreementMethod", + "@type": "@id", + "@container": "@set" + } + } + }, + "proofValue": { + "@id": "https://w3id.org/security#proofValue", + "@type": "https://w3id.org/security#multibase" + }, + "verificationMethod": { + "@id": "https://w3id.org/security#verificationMethod", + "@type": "@id" + } + } + }, + + "...": { + "@id": "https://www.iana.org/assignments/jwt#..." + }, + "_sd": { + "@id": "https://www.iana.org/assignments/jwt#_sd", + "@type": "@json" + }, + "_sd_alg": { + "@id": "https://www.iana.org/assignments/jwt#_sd_alg" + }, + "aud": { + "@id": "https://www.iana.org/assignments/jwt#aud", + "@type": "@id" + }, + "cnf": { + "@id": "https://www.iana.org/assignments/jwt#cnf", + "@context": { + "@protected": true, + + "kid": { + "@id": "https://www.iana.org/assignments/jwt#kid", + "@type": "@id" + }, + "jwk": { + "@id": "https://www.iana.org/assignments/jwt#jwk", + "@type": "@json" + } + } + }, + "exp": { + "@id": "https://www.iana.org/assignments/jwt#exp", + "@type": "https://www.w3.org/2001/XMLSchema#nonNegativeInteger" + }, + "iat": { + "@id": "https://www.iana.org/assignments/jwt#iat", + "@type": "https://www.w3.org/2001/XMLSchema#nonNegativeInteger" + }, + "iss": { + "@id": "https://www.iana.org/assignments/jose#iss", + "@type": "@id" + }, + "jku": { + "@id": "https://www.iana.org/assignments/jose#jku", + "@type": "@id" + }, + "kid": { + "@id": "https://www.iana.org/assignments/jose#kid", + "@type": "@id" + }, + "nbf": { + "@id": "https://www.iana.org/assignments/jwt#nbf", + "@type": "https://www.w3.org/2001/XMLSchema#nonNegativeInteger" + }, + "sub": { + "@id": "https://www.iana.org/assignments/jose#sub", + "@type": "@id" + }, + "x5u": { + "@id": "https://www.iana.org/assignments/jose#x5u", + "@type": "@id" + } + } + } \ No newline at end of file diff --git a/backend/app/contexts/untp_dcc_0.4.2.jsonld b/backend/app/contexts/untp_dcc_0.4.2.jsonld new file mode 100644 index 0000000..042eaac --- /dev/null +++ b/backend/app/contexts/untp_dcc_0.4.2.jsonld @@ -0,0 +1,687 @@ +{ + "@context": { + "untp-dcc": "https://test.uncefact.org/vocabulary/untp/dcc/0/", + "untp-core": "https://test.uncefact.org/vocabulary/untp/core/0/", + "geojson": "https://datatracker.ietf.org/doc/html/rfc7946#", + "xsd": "http://www.w3.org/2001/XMLSchema#", + "@protected": true, + "@version": 1.1, + "Party": { + "@protected": true, + "@id": "untp-core:Party", + "@context": { + "registeredId": { + "@id": "untp-core:registeredId", + "@type": "xsd:string" + }, + "idScheme": { + "@id": "untp-core:idScheme", + "@type": "@id" + }, + "registrationCountry": { + "@id": "untp-core:registrationCountry", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/CountryId" + } + }, + "organisationWebsite": { + "@id": "untp-core:organisationWebsite", + "@type": "xsd:string" + }, + "industryCategory": { + "@id": "untp-core:industryCategory", + "@type": "@id" + }, + "otherIdentifier": { + "@id": "untp-core:otherIdentifier", + "@type": "@id" + } + } + }, + "IdentifierScheme": { + "@protected": true, + "@id": "untp-core:IdentifierScheme" + }, + "Identifier": { + "@protected": true, + "@id": "untp-core:Identifier", + "@context": { + "registeredId": { + "@id": "untp-core:registeredId", + "@type": "xsd:string" + }, + "idScheme": { + "@id": "untp-core:idScheme", + "@type": "@id" + } + } + }, + "ConformityAssessmentScheme": { + "@protected": true, + "@id": "untp-dcc:ConformityAssessmentScheme", + "@context": { + "issuingParty": { + "@id": "untp-dcc:issuingParty", + "@type": "@id" + }, + "issueDate": { + "@id": "untp-dcc:issueDate", + "@type": "xsd:string" + }, + "trustmark": "untp-core:trustmark" + } + }, + "Product": { + "@protected": true, + "@id": "untp-core:Product", + "@context": { + "registeredId": "untp-core:registeredId", + "idScheme": "untp-core:idScheme", + "serialNumber": "untp-core:serialNumber", + "batchNumber": "untp-core:batchNumber", + "productImage": { + "@protected": true, + "@id": "untp-core:productImage", + "@context": { + "linkURL": { + "@id": "untp-core:linkURL", + "@type": "xsd:string" + }, + "linkName": { + "@id": "untp-core:linkName", + "@type": "xsd:string" + }, + "linkType": { + "@id": "untp-core:linkType", + "@type": "xsd:string" + } + } + }, + "productCategory": { + "@id": "untp-core:productCategory", + "@type": "@id" + }, + "furtherInformation": { + "@protected": true, + "@id": "untp-core:furtherInformation", + "@context": { + "linkURL": { + "@id": "untp-core:linkURL", + "@type": "xsd:string" + }, + "linkName": { + "@id": "untp-core:linkName", + "@type": "xsd:string" + }, + "linkType": { + "@id": "untp-core:linkType", + "@type": "xsd:string" + } + } + }, + "producedByParty": { + "@id": "untp-core:producedByParty", + "@type": "@id" + }, + "producedAtFacility": { + "@id": "untp-core:producedAtFacility", + "@type": "@id" + }, + "dimensions": { + "@protected": true, + "@id": "untp-core:dimensions", + "@context": { + "weight": { + "@protected": true, + "@id": "untp-core:weight", + "@context": { + "value": { + "@id": "untp-core:value", + "@type": "xsd:decimal" + }, + "unit": { + "@id": "untp-core:unit", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/UnitMeasureCode" + } + } + } + }, + "length": { + "@protected": true, + "@id": "untp-core:length", + "@context": { + "value": { + "@id": "untp-core:value", + "@type": "xsd:decimal" + }, + "unit": { + "@id": "untp-core:unit", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/UnitMeasureCode" + } + } + } + }, + "width": { + "@protected": true, + "@id": "untp-core:width", + "@context": { + "value": { + "@id": "untp-core:value", + "@type": "xsd:decimal" + }, + "unit": { + "@id": "untp-core:unit", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/UnitMeasureCode" + } + } + } + }, + "height": { + "@protected": true, + "@id": "untp-core:height", + "@context": { + "value": { + "@id": "untp-core:value", + "@type": "xsd:decimal" + }, + "unit": { + "@id": "untp-core:unit", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/UnitMeasureCode" + } + } + } + }, + "volume": { + "@protected": true, + "@id": "untp-core:volume", + "@context": { + "value": { + "@id": "untp-core:value", + "@type": "xsd:decimal" + }, + "unit": { + "@id": "untp-core:unit", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/UnitMeasureCode" + } + } + } + } + } + }, + "productionDate": { + "@id": "untp-core:productionDate", + "@type": "xsd:string" + }, + "countryOfProduction": { + "@id": "untp-core:countryOfProduction", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/CountryId" + } + }, + "IDverifiedByCAB": { + "@id": "untp-dcc:IDverifiedByCAB", + "@type": "xsd:boolean" + } + } + }, + "Facility": { + "@protected": true, + "@id": "untp-core:Facility", + "@context": { + "registeredId": "untp-core:registeredId", + "idScheme": "untp-core:idScheme", + "countryOfOperation": { + "@id": "untp-core:countryOfOperation", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/CountryId" + } + }, + "processCategory": { + "@id": "untp-core:processCategory", + "@type": "@id" + }, + "operatedByParty": { + "@id": "untp-core:operatedByParty", + "@type": "@id" + }, + "otherIdentifier": { + "@id": "untp-core:otherIdentifier", + "@type": "@id" + }, + "locationInformation": "untp-core:locationInformation", + "address": "untp-core:address", + "IDverifiedByCAB": { + "@id": "untp-dcc:IDverifiedByCAB", + "@type": "xsd:boolean" + } + } + }, + "ConformityAssessment": { + "@protected": true, + "@id": "untp-dcc:ConformityAssessment", + "@context": { + "assessmentDate": { + "@id": "untp-dcc:assessmentDate", + "@type": "xsd:string" + }, + "referenceStandard": "untp-core:referenceStandard", + "referenceRegulation": "untp-core:referenceRegulation", + "assessmentCriteria": "untp-core:assessmentCriteria", + "declaredValue": "untp-core:declaredValue", + "compliance": "untp-core:compliance", + "conformityTopic": "untp-core:conformityTopic", + "assessedProduct": { + "@id": "untp-dcc:assessedProduct", + "@type": "@id" + }, + "assessedFacility": { + "@id": "untp-dcc:assessedFacility", + "@type": "@id" + }, + "assessedOrganisation": { + "@id": "untp-dcc:assessedOrganisation", + "@type": "@id" + }, + "auditor": { + "@id": "untp-dcc:auditor", + "@type": "@id" + } + } + }, + "ConformityAttestation": { + "@protected": true, + "@id": "untp-dcc:ConformityAttestation", + "@context": { + "assessorLevel": "untp-core:assessorLevel", + "assessmentLevel": "untp-core:assessmentLevel", + "attestationType": "untp-core:attestationType", + "issuedToParty": { + "@id": "untp-dcc:issuedToParty", + "@type": "@id" + }, + "authorisation": "untp-core:authorisation", + "conformityCertificate": "untp-core:conformityCertificate", + "auditableEvidence": "untp-core:auditableEvidence", + "scope": { + "@id": "untp-dcc:scope", + "@type": "@id" + }, + "assessment": { + "@id": "untp-dcc:assessment", + "@type": "@id" + } + } + }, + "DigitalConformityCredential": { + "@protected": true, + "@id": "untp-dcc:DigitalConformityCredential", + "@context": { + "credentialSubject": { + "@id": "untp-dcc:credentialSubject", + "@type": "@id" + } + } + }, + "CredentialIssuer": { + "@protected": true, + "@id": "untp-core:CredentialIssuer", + "@context": { + "otherIdentifier": { + "@id": "untp-core:otherIdentifier", + "@type": "@id" + } + } + }, + "Classification": { + "@protected": true, + "@id": "untp-core:Classification", + "@context": { + "code": { + "@id": "untp-core:code", + "@type": "xsd:string" + }, + "schemeID": { + "@id": "untp-core:schemeID", + "@type": "xsd:string" + }, + "schemeName": { + "@id": "untp-core:schemeName", + "@type": "xsd:string" + } + } + }, + "Endorsement": { + "@protected": true, + "@id": "untp-core:Endorsement", + "@context": { + "trustmark": { + "@protected": true, + "@id": "untp-core:trustmark", + "@context": { + "fileName": { + "@id": "untp-core:fileName", + "@type": "xsd:string" + }, + "fileType": { + "@id": "untp-core:fileType", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://mimetype.io/all-types" + } + }, + "file": { + "@id": "untp-core:file", + "@type": "xsd:string" + } + } + }, + "issuingAuthority": { + "@id": "untp-core:issuingAuthority", + "@type": "@id" + }, + "accreditationCertificate": { + "@protected": true, + "@id": "untp-core:accreditationCertificate", + "@context": { + "linkURL": { + "@id": "untp-core:linkURL", + "@type": "xsd:string" + }, + "linkName": { + "@id": "untp-core:linkName", + "@type": "xsd:string" + }, + "linkType": { + "@id": "untp-core:linkType", + "@type": "xsd:string" + } + } + } + } + }, + "Attestation": { + "@protected": true, + "@id": "untp-core:Attestation", + "@context": { + "assessorLevel": { + "@id": "untp-core:assessorLevel", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://test.uncefact.org/vocabulary/untp/core/0/assessorLevelCode#" + } + }, + "assessmentLevel": { + "@id": "untp-core:assessmentLevel", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://test.uncefact.org/vocabulary/untp/core/0/assessmentLevelCode#" + } + }, + "attestationType": { + "@id": "untp-core:attestationType", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://test.uncefact.org/vocabulary/untp/core/0/attestationTypeCode#" + } + }, + "issuedToParty": { + "@id": "untp-core:issuedToParty", + "@type": "@id" + }, + "authorisation": { + "@id": "untp-core:authorisation", + "@type": "@id" + }, + "conformityCertificate": { + "@protected": true, + "@id": "untp-core:conformityCertificate", + "@context": { + "linkURL": { + "@id": "untp-core:linkURL", + "@type": "xsd:string" + }, + "linkName": { + "@id": "untp-core:linkName", + "@type": "xsd:string" + }, + "linkType": { + "@id": "untp-core:linkType", + "@type": "xsd:string" + }, + "hashDigest": { + "@id": "untp-core:hashDigest", + "@type": "xsd:string" + }, + "hashMethod": { + "@id": "untp-core:hashMethod", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://test.uncefact.org/vocabulary/untp/core/0/hashMethodCode#" + } + }, + "encryptionMethod": { + "@id": "untp-core:encryptionMethod", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://test.uncefact.org/vocabulary/untp/core/0/encryptionMethodCode#" + } + } + } + }, + "auditableEvidence": { + "@protected": true, + "@id": "untp-core:auditableEvidence", + "@context": { + "linkURL": { + "@id": "untp-core:linkURL", + "@type": "xsd:string" + }, + "linkName": { + "@id": "untp-core:linkName", + "@type": "xsd:string" + }, + "linkType": { + "@id": "untp-core:linkType", + "@type": "xsd:string" + }, + "hashDigest": { + "@id": "untp-core:hashDigest", + "@type": "xsd:string" + }, + "hashMethod": { + "@id": "untp-core:hashMethod", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://test.uncefact.org/vocabulary/untp/core/0/hashMethodCode#" + } + }, + "encryptionMethod": { + "@id": "untp-core:encryptionMethod", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://test.uncefact.org/vocabulary/untp/core/0/encryptionMethodCode#" + } + } + } + } + } + }, + "Standard": { + "@protected": true, + "@id": "untp-core:Standard", + "@context": { + "issuingParty": { + "@id": "untp-core:issuingParty", + "@type": "@id" + }, + "issueDate": { + "@id": "untp-core:issueDate", + "@type": "xsd:string" + } + } + }, + "Regulation": { + "@protected": true, + "@id": "untp-core:Regulation", + "@context": { + "jurisdictionCountry": { + "@id": "untp-core:jurisdictionCountry", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/CountryId" + } + }, + "administeredBy": { + "@id": "untp-core:administeredBy", + "@type": "@id" + }, + "effectiveDate": { + "@id": "untp-core:effectiveDate", + "@type": "xsd:string" + } + } + }, + "Criterion": { + "@protected": true, + "@id": "untp-core:Criterion", + "@context": { + "thresholdValues": { + "@protected": true, + "@id": "untp-core:thresholdValues", + "@context": { + "metricName": { + "@id": "untp-core:metricName", + "@type": "xsd:string" + }, + "metricValue": { + "@protected": true, + "@id": "untp-core:metricValue", + "@context": { + "value": { + "@id": "untp-core:value", + "@type": "xsd:decimal" + }, + "unit": { + "@id": "untp-core:unit", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/UnitMeasureCode" + } + } + } + }, + "score": { + "@id": "untp-core:score", + "@type": "xsd:string" + }, + "accuracy": { + "@id": "untp-core:accuracy", + "@type": "xsd:decimal" + } + } + } + } + }, + "Declaration": { + "@protected": true, + "@id": "untp-core:Declaration", + "@context": { + "referenceStandard": { + "@id": "untp-core:referenceStandard", + "@type": "@id" + }, + "referenceRegulation": { + "@id": "untp-core:referenceRegulation", + "@type": "@id" + }, + "assessmentCriteria": { + "@id": "untp-core:assessmentCriteria", + "@type": "@id" + }, + "assessmentDate": { + "@id": "untp-core:assessmentDate", + "@type": "xsd:string" + }, + "declaredValue": { + "@protected": true, + "@id": "untp-core:declaredValue", + "@context": { + "metricName": { + "@id": "untp-core:metricName", + "@type": "xsd:string" + }, + "metricValue": { + "@protected": true, + "@id": "untp-core:metricValue", + "@context": { + "value": { + "@id": "untp-core:value", + "@type": "xsd:decimal" + }, + "unit": { + "@id": "untp-core:unit", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/UnitMeasureCode" + } + } + } + }, + "score": { + "@id": "untp-core:score", + "@type": "xsd:string" + }, + "accuracy": { + "@id": "untp-core:accuracy", + "@type": "xsd:decimal" + } + } + }, + "compliance": { + "@id": "untp-core:compliance", + "@type": "xsd:boolean" + }, + "conformityTopic": { + "@id": "untp-core:conformityTopic", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://test.uncefact.org/vocabulary/untp/core/0/conformityTopicCode#" + } + } + } + } + } + } \ No newline at end of file diff --git a/backend/app/data/registrations/credentials/business-registration.json b/backend/app/data/registrations/credentials/business-registration.json new file mode 100644 index 0000000..c266afc --- /dev/null +++ b/backend/app/data/registrations/credentials/business-registration.json @@ -0,0 +1,14 @@ +{ + "type": "BusinessRegistration", + "version": "v1.0", + "issuer": "did:web:{{}}:business-corporations-act:registrar-of-companies", + "mappings": { + "legalName": "$.credentialSubject.legalName", + "registrationId": "$.credentialSubject.registrationId" + }, + "relatedResources": { + "context": "https://www.w3.org/ns/credentials/examples/v2", + "legalAct": "https://www.bclaws.gov.bc.ca/civix/document/id/complete/statreg/02057_00" + } +} + diff --git a/backend/app/data/registrations/issuers/cpo.json b/backend/app/data/registrations/issuers/cpo.json new file mode 100644 index 0000000..02d2864 --- /dev/null +++ b/backend/app/data/registrations/issuers/cpo.json @@ -0,0 +1,5 @@ +{ + "name": "Chief Permitting Officer", + "description": "The person designated as the chief permitting officer.", + "scope": "Mines Act" +} \ No newline at end of file diff --git a/backend/app/data/registrations/issuers/dpl.json b/backend/app/data/registrations/issuers/dpl.json new file mode 100644 index 0000000..4db040b --- /dev/null +++ b/backend/app/data/registrations/issuers/dpl.json @@ -0,0 +1,5 @@ +{ + "name": "Director of Petroleum Lands", + "description": "An officer or employee of the ministry who is designated as the Director of Petroleum Lands by the minister.", + "scope": "Petroleum and Natural Gas Act" +} \ No newline at end of file diff --git a/backend/app/data/registrations/issuers/roc.json b/backend/app/data/registrations/issuers/roc.json new file mode 100644 index 0000000..2d27050 --- /dev/null +++ b/backend/app/data/registrations/issuers/roc.json @@ -0,0 +1,5 @@ +{ + "name": "Registrar of Companies", + "description": "The person appointed as the Registrar of Companies.", + "scope": "Business Corporations Act" +} \ No newline at end of file diff --git a/backend/app/dependencies.py b/backend/app/dependencies.py deleted file mode 100644 index 2c1559c..0000000 --- a/backend/app/dependencies.py +++ /dev/null @@ -1,4 +0,0 @@ -# from fastapi import HTTPException -# from config import settings -# from app.models.web_requests import RegisterDID -# from app.plugins import AskarVerifier, AskarStorage diff --git a/backend/app/models/credential.py b/backend/app/models/credential.py index 802e405..06c5894 100644 --- a/backend/app/models/credential.py +++ b/backend/app/models/credential.py @@ -1,40 +1,152 @@ from typing import Union, List, Dict, Any -from pydantic import BaseModel, Field +from pydantic import Field, BaseModel, field_validator from pydantic.json_schema import SkipJsonSchema -from .proof import DataIntegrityProof from config import settings -import uuid +from app.utils import valid_datetime_string, valid_uri -BASE_CONTEXT = "https://www.w3.org/ns/credentials/v2" -BASE_VC_TYPE = "VerifiableCredential" -EXAMPLE_ID = f"https://{settings.DOMAIN}/credentials/{uuid.uuid4()}" -EXAMPLE_ISSUER = "did:web:example.com:issuer" -EXAMPLE_SUBJECT = {"id": "did:web:example.com:subject"} +class NameField(BaseModel, extra='forbid'): + value: str = Field(None, alias="@value") + language: str = Field(None, alias="@language") + direction: str = Field(None, alias="@direction") -class BaseModel(BaseModel): + +class DescriptionField(BaseModel, extra='forbid'): + value: str = Field(None, alias="@value") + language: str = Field(None, alias="@language") + direction: str = Field(None, alias="@direction") + +class BaseModel(BaseModel, extra='allow'): + id: SkipJsonSchema[str] = Field(None) + type: Union[str, List[str]] = Field(None) + name: SkipJsonSchema[Union[str, NameField, List[NameField]]] = Field(None) + description: SkipJsonSchema[Union[str, DescriptionField, List[DescriptionField]]] = Field(None) def model_dump(self, **kwargs) -> Dict[str, Any]: return super().model_dump(by_alias=True, exclude_none=True, **kwargs) - class Issuer(BaseModel): - id: str = Field(example=EXAMPLE_ISSUER) - name: SkipJsonSchema[str] = Field(None) - description: SkipJsonSchema[str] = Field(None) + pass + + +class RelatedResource(BaseModel): + id: SkipJsonSchema[str] = Field() + digestSRI: str = Field(None) + digestMultibase: str = Field(None) + + +class CredentialSubject(BaseModel): + pass + + +class CredentialSchema(BaseModel): + id: Union[str, List[str]] = Field() + type: Union[str, List[str]] = Field() + + @field_validator("id") + @classmethod + def validate_credential_schema_id(cls, value): + assert valid_uri(value) + + +class CredentialStatus(BaseModel): + id: str = Field(None) + type: Union[str, List[str]] = Field() + statusPurpose: str = Field(None) + statusListIndex: str = Field(None) + statusListCredential: str = Field(None) + + @field_validator("id") + @classmethod + def validate_credential_status_id(cls, value): + if value: + assert valid_uri(value) + + return value + + +class TermsOfUse(BaseModel): + type: Union[str, List[str]] = Field() + + +class RefreshService(BaseModel): + type: Union[str, List[str]] = Field() + + +class Evidence(BaseModel): + type: Union[str, List[str]] = Field() + + +class RenderMethod(BaseModel): + pass class Credential(BaseModel): - context: List[str] = Field(None, alias="@context") - type: List[str] = Field(None) - id: SkipJsonSchema[str] = Field(None) - issuer: Issuer = Field(None) - name: SkipJsonSchema[str] = Field(None) - description: SkipJsonSchema[str] = Field(None) + type: Union[str, List[str]] = Field() validFrom: SkipJsonSchema[str] = Field(None) validUntil: SkipJsonSchema[str] = Field(None) - credentialSubject: Union[dict, List[dict]] = Field(example=EXAMPLE_SUBJECT) - credentialStatus: SkipJsonSchema[Union[dict, List[dict]]] = Field(None) - credentialSchema: SkipJsonSchema[Union[dict, List[dict]]] = Field(None) - termsOfUse: SkipJsonSchema[Union[dict, List[dict]]] = Field(None) - renderMethod: SkipJsonSchema[Union[dict, List[dict]]] = Field(None) - # proof: Union[DataIntegrityProof, List[DataIntegrityProof]] = Field(None) + credentialSubject: Union[List[CredentialSubject], CredentialSubject] = Field() + credentialStatus: SkipJsonSchema[Union[List[CredentialStatus], CredentialStatus]] = Field(None) + credentialSchema: SkipJsonSchema[Union[List[CredentialSchema], CredentialSchema]] = Field(None) + termsOfUse: SkipJsonSchema[Union[List[TermsOfUse], TermsOfUse]] = Field(None) + refreshService: SkipJsonSchema[Union[List[RefreshService], RefreshService]] = Field(None) + evidence: SkipJsonSchema[Union[List[Evidence], Evidence]] = Field(None) + renderMethod: SkipJsonSchema[Union[List[RenderMethod], RenderMethod]] = Field(None) + relatedResource: SkipJsonSchema[Union[List[RelatedResource], RelatedResource]] = Field(None) + + @field_validator("id") + @classmethod + def validate_credential_id(cls, value): + assert valid_uri(value) + return value + + @field_validator("type") + @classmethod + def validate_credential_type(cls, value): + asserted_value = value if isinstance(value, list) else [value] + assert "VerifiableCredential" in asserted_value + return value + + @field_validator("validFrom") + @classmethod + def validate_valid_from_date(cls, value): + assert valid_datetime_string(value) + return value + + @field_validator("validUntil") + @classmethod + def validate_valid_until_date(cls, value): + assert valid_datetime_string(value) + return value + + @field_validator("credentialSubject") + @classmethod + def validate_credential_subject(cls, value): + asserted_value = value if isinstance(value, list) else [value] + for subject in asserted_value: + assert bool(subject.model_dump()) + return value + + @field_validator("relatedResource") + @classmethod + def validate_related_ressource(cls, value): + asserted_value = value if isinstance(value, list) else [value] + for ressource in asserted_value: + assert valid_uri(ressource.id) + assert ressource.digestSRI or ressource.digestMultibase + return value + + # @field_validator("credentialStatus") + # @classmethod + # def validate_credential_status(cls, value): + # assert isinstance(value, dict) or isinstance(value, list) + # assert ( + # all(isinstance(item, dict) for item in value) + # if isinstance(value, list) + # else True + # ) + # assert "type" in value or all("type" in item for item in value) + # return value + + # def add_validity_period(): + # validFrom = str(datetime.now().isoformat("T", "seconds")) + # validUntil = str(datetime.now().isoformat("T", "seconds")) diff --git a/backend/app/models/linked_data.py b/backend/app/models/linked_data.py new file mode 100644 index 0000000..f15c607 --- /dev/null +++ b/backend/app/models/linked_data.py @@ -0,0 +1,35 @@ +from pyld import jsonld +import json + +CONTEXT_DIR = 'app/contexts/' + +CACHED_CONTEXTS = { + 'https://test.uncefact.org/vocabulary/untp/dcc/0.4.2/': 'untp_dcc_0.4.2', + 'https://www.w3.org/ns/credentials/v2': 'credentials_v2', + 'https://www.w3.org/ns/credentials/examples/v2': 'credentials_examples_v2' +} + +class LinkedData: + def __init__(self): + pass + + def load_cached_ctx(self, context_url): + with open(f'{CONTEXT_DIR}{CACHED_CONTEXTS[context_url]}.jsonld', 'r') as f: + context = json.loads(f.read()) + return context + + def is_valid_context(self, context): + if isinstance(context, list): + for idx, ctx_entry in enumerate(context): + if isinstance(ctx_entry, str): + if ctx_entry in CACHED_CONTEXTS: + context[idx] = self.load_cached_ctx(ctx_entry) + elif isinstance(context, str): + if context in CACHED_CONTEXTS: + context = self.load_cached_ctx(context) + jsonld.compact({}, context) + try: + jsonld.compact({}, context) + return True + except: + return False \ No newline at end of file diff --git a/backend/app/models/registrations.py b/backend/app/models/registrations.py index 15e6166..8e74897 100644 --- a/backend/app/models/registrations.py +++ b/backend/app/models/registrations.py @@ -16,6 +16,11 @@ class IssuerRegistration(BaseModel): ) url: str = Field(None, example="https://www2.gov.bc.ca/gov/content/governments/organizational-structure/ministries-organizations/ministries/energy-mines-and-petroleum-resources") # image: str = Field(None, example="https://") + multikey: str = Field(None) + +class RelatedResource(BaseModel): + id: str = Field() + type: str = Field() class RelatedResources(BaseModel): context: str = Field(example="https://bcgov.github.io/digital-trust-toolkit/contexts/BCPetroleumAndNaturalGasTitle/v1.jsonld") @@ -26,6 +31,7 @@ class RelatedResources(BaseModel): class CredentialRegistration(BaseModel): type: str = Field('BCPetroleumAndNaturalGasTitleCredential') + subjectType: str = Field('PetroleumAndNaturalGasTitle') untpType: str = Field(None, example='DigitalConformityCredential') version: str = Field(example='v1.0') issuer: str = Field(example=f'did:web:{settings.TDW_SERVER_URL.split("//")[-1]}:petroleum-and-natural-gas-act:director-of-petroleum-lands') diff --git a/backend/app/models/untp.py b/backend/app/models/untp.py index 695c836..0ce2303 100644 --- a/backend/app/models/untp.py +++ b/backend/app/models/untp.py @@ -169,7 +169,7 @@ class ConformityAssessmentScheme(BaseModel): class ConformityAttestation(BaseModel): type: List[str] = ["ConformityAttestation"] - # id: str + id: str = None assessorLevel: Optional[str] = None assessmentLevel: str = None attestationType: str = None @@ -234,4 +234,4 @@ class ConformityTopicCode(str, Enum): Social_Community = "Social.Community" Governance_Ethics = "Governance.Ethics" Governance_Compliance = "Governance.Compliance" - Governance_Transparency = "Governance.Transparency" + Governance_Transparency = "Governance.Transparency" \ No newline at end of file diff --git a/backend/app/plugins/__init__.py b/backend/app/plugins/__init__.py index 061b7ed..aeaf234 100644 --- a/backend/app/plugins/__init__.py +++ b/backend/app/plugins/__init__.py @@ -1,6 +1,4 @@ from .askar import AskarStorage, AskarVerifier, AskarWallet -from .did_web import DidWebEndorser -from .agent import AgentController from .orgbook import OrgbookPublisher from .untp import DigitalConformityCredential from .status_list import BitstringStatusList @@ -9,11 +7,9 @@ from .soup import Soup __all__ = [ - "AgentController", "AskarVerifier", "AskarStorage", "AskarWallet", - "DidWebEndorser", "OrgbookPublisher", "BitstringStatusList", "PublisherRegistrar", diff --git a/backend/app/plugins/agent.py b/backend/app/plugins/agent.py deleted file mode 100644 index a1e5c32..0000000 --- a/backend/app/plugins/agent.py +++ /dev/null @@ -1,100 +0,0 @@ -from fastapi import HTTPException -from config import settings -from datetime import datetime, timezone, timedelta -import requests -import secrets - - -class AgentController: - def __init__(self): - self.endpoint = settings.AGENT_ADMIN_URL - self.headers = {"X-API-KEY": settings.AGENT_ADMIN_API_KEY} - self.endorser = settings.ENDORSER_DID - self.endorser_vm = settings.ENDORSER_VM - - def create_key_pair(self, kid, seed=None): - r = requests.post( - f"{self.endpoint}/wallet/keys", - headers=self.headers, - json={ - "kid": kid, - "key_type": "ed25519", - }, - ) - try: - return r.json()["multikey"] - except: - raise HTTPException( - status_code=r.status_code, detail="Couldn't create did." - ) - - def register_did(self, did, seed=None): - # TODO remove this section once seed is optional in acapy - if not seed: - seed = secrets.token_hex(16) - r = requests.post( - f"{self.endpoint}/did/web", - headers=self.headers, - json={ - "id": f"{did}#key-01", - "key_type": "ed25519", - "seed": seed, - "type": "MultiKey", - }, - ) - try: - return r.json()["verificationMethod"] - except: - raise HTTPException( - status_code=r.status_code, detail="Couldn't create did." - ) - - def issuer_proof_options(self, verification_method): - return { - "type": "DataIntegrityProof", - "cryptosuite": "eddsa-jcs-2022", - "proofPurpose": "assertionMethod", - "verificationMethod": verification_method, - "created": str(datetime.now(timezone.utc).isoformat("T", "seconds")), - } - - def endorser_proof_options(self): - return { - "type": "DataIntegrityProof", - "cryptosuite": "eddsa-jcs-2022", - "verificationMethod": self.endorser_vm, - "proofPurpose": "authentication", - "created": str(datetime.now(timezone.utc).isoformat("T", "seconds")), - "expires": str( - (datetime.now(timezone.utc) + timedelta(minutes=10)).isoformat( - "T", "seconds" - ) - ), - } - - def sign_document(self, document, options): - r = requests.post( - f"{self.endpoint}/wallet/di/add-proof", - headers=self.headers, - json={"document": document, "options": options}, - ) - try: - return r.json()["securedDocument"] - except: - raise HTTPException( - status_code=r.status_code, detail="Couldn't sign document." - ) - - def endorse_document(self, document, options): - options["verificationMethod"] = self.endorser_vm - r = requests.post( - f"{self.endpoint}/wallet/di/add-proof", - headers=self.headers, - json={"document": document, "options": options}, - ) - try: - return r.json()["securedDocument"] - except: - raise HTTPException( - status_code=r.status_code, detail="Couldn't endorser document." - ) diff --git a/backend/app/plugins/did_web.py b/backend/app/plugins/did_web.py deleted file mode 100644 index 14ced90..0000000 --- a/backend/app/plugins/did_web.py +++ /dev/null @@ -1,71 +0,0 @@ -from fastapi import HTTPException -from config import settings -from app.plugins.agent import AgentController -from app.plugins import AskarWallet -from app.models import DidDocument, VerificationMethod, Service -import requests - - -class DidWebEndorser: - def __init__(self): - self.did = settings.TDW_ENDORSER_DID - self.server = settings.TDW_SERVER_URL - - async def did_registration(self, namespace, identifier, url=None): - did_request = self.request_did( - namespace=namespace, - identifier=identifier, - ) - did_document = did_request["didDocument"] - did = did_document["id"] - multikey = await AskarWallet().create_key(f"{did}#key-01") - # multikey = AgentController().create_key_pair(kid=f'{did}#key-01') - did_document = DidDocument( - id=did, - authentication=[f"{did}#key-01"], - assertionMethod=[f"{did}#key-01"], - verificationMethod=[ - VerificationMethod( - id=f"{did}#key-01", controller=did, publicKeyMultibase=multikey - ) - ], - ).model_dump() - # if url: - # did_doc['service'] = [{ - # 'id': did_doc['id']+'#ministry', - # 'type': 'LinkedDomain', - # 'serviceEndpoint': url, - # }] - - issuer_options = did_request["proofOptions"].copy() - issuer_options["verificationMethod"] = f"{did}#key-01" - signed_did_doc = await AskarWallet().add_proof(did_document, issuer_options) - # signed_did_doc = AgentController().sign_document(did_doc, proof_options) - endorser_options = did_request["proofOptions"].copy() - endorsed_did_doc = await AskarWallet().add_proof( - signed_did_doc, endorser_options - ) - # endorsed_did_doc = AgentController().endorse_document(signed_did_doc, proof_options) - - did_registration = self.register_did(endorsed_did_doc) - # print(did_registration) - # return did_registration - return endorsed_did_doc - - def request_did(self, namespace, identifier): - r = requests.get(f"{self.server}?namespace={namespace}&identifier={identifier}") - try: - return r.json() - except: - raise HTTPException( - status_code=r.status_code, detail="Couldn't request did." - ) - - def register_did(self, endorsed_did_doc): - r = requests.post(f"{self.server}", json={"didDocument": endorsed_did_doc}) - try: - return r.json()["didDocument"] - except: - raise HTTPException( - status_code=r.status_code, detail="Couldn't register did." - ) diff --git a/backend/app/plugins/github.py b/backend/app/plugins/github.py deleted file mode 100644 index dce07ec..0000000 --- a/backend/app/plugins/github.py +++ /dev/null @@ -1,22 +0,0 @@ -from github import Github - -# Authentication is defined via github.Auth -from github import Auth - -# using an access token -auth = Auth.Token("access_token") - -# First create a Github instance: - -# Public Web Github -g = Github(auth=auth) - -# Github Enterprise with custom hostname -g = Github(base_url="https://{hostname}/api/v3", auth=auth) - -# Then play with your Github objects: -for repo in g.get_user().get_repos(): - print(repo.name) - -# To close connections after use -g.close() diff --git a/backend/app/plugins/orgbook.py b/backend/app/plugins/orgbook.py index a75d977..9d13ddd 100644 --- a/backend/app/plugins/orgbook.py +++ b/backend/app/plugins/orgbook.py @@ -2,8 +2,9 @@ from config import settings from app.models import Credential from app.utilities import freeze_ressource_digest -from app.plugins import AgentController, AskarStorage, AskarWallet +from app.plugins import AskarStorage, AskarWallet from app.plugins.untp import DigitalConformityCredential +from app.plugins.traction import TractionController # from .ips import IPSView import requests diff --git a/backend/app/plugins/registrar.py b/backend/app/plugins/registrar.py index 29e330d..28c781b 100644 --- a/backend/app/plugins/registrar.py +++ b/backend/app/plugins/registrar.py @@ -1,8 +1,13 @@ from config import settings from fastapi import HTTPException import requests -from app.models import DidDocument, VerificationMethod, Service +from app.plugins import AskarStorage +from app.plugins.soup import Soup +from app.models import DidDocument, VerificationMethod, Service, Credential +import app.models.untp as untp +from app.models.credential import Issuer from app.plugins.traction import TractionController +from app.plugins.untp import DigitalConformityCredential from app.utilities import multikey_to_jwk @@ -12,137 +17,218 @@ def __init__(self): self.tdw_server = settings.TDW_SERVER_URL self.endorser_multikey = settings.TDW_ENDORSER_MULTIKEY - def register_issuer(self, name, scope, url, description): + def register_issuer(self, name, scope, url, description, multikey=None): namespace = scope.replace(" ", "-").lower() identifier = name.replace(" ", "-").lower() + + # Request identifier from TDW server r = requests.get(f'{self.tdw_server}?namespace={namespace}&identifier={identifier}') try: did = r.json()['didDocument']['id'] - except: - raise HTTPException(status_code=r.status_code, detail=r.json()) - - - multikey_kid = f'{did}#multikey-01' - jwk_kid = f'{did}#jwk-01' - + except (ValueError, KeyError): + raise HTTPException(status_code=r.status_code, detail=r.text) + + # Register Authorized key in traction + multikey_kid = f'{did}#key-01-multikey' + jwk_kid = f'{did}#key-01-jwk' + traction = TractionController() # traction.authorize() - multikey = traction.create_did_key() - # traction.bind_key(multikey, multikey_kid) - - verification_method_multikey = VerificationMethod( - id=multikey_kid, - type='Multikey', - controller=did, - publicKeyMultibase=multikey - ) - - verification_method_jwk = VerificationMethod( - id=jwk_kid, - type='JsonWebKey', - controller=did, - publicKeyJwk=multikey_to_jwk(multikey) - ) - - service = Service( - id=f'{did}#bcgov-website', - type='LinkedDomains', - serviceEndpoint=url, - ) if url else None + authorized_key = traction.create_did_key() + + # Bind an issuing multikey if not value is provided + if not multikey: + multikey = authorized_key + try: + traction.bind_key(multikey, multikey_kid) + except KeyError: + pass did_document = DidDocument( id=did, name=name, description=description, - authentication=[verification_method_multikey.id], - assertionMethod=[verification_method_multikey.id], + authentication=[multikey_kid], + assertionMethod=[multikey_kid], verificationMethod=[ - verification_method_multikey, - verification_method_jwk + VerificationMethod( + id=multikey_kid, + type='Multikey', + controller=did, + publicKeyMultibase=multikey + ), + VerificationMethod( + id=jwk_kid, + type='JsonWebKey', + controller=did, + publicKeyJwk=multikey_to_jwk(multikey) + ) ], - service=[service] if service else None + service=[Service( + id=f'{did}#bcgov-website', + type='LinkedDomains', + serviceEndpoint=url, + )] if url else None ).model_dump() - + client_proof_options = r.json()['proofOptions'].copy() - client_proof_options['verificationMethod'] = f'did:key:{multikey}#{multikey}' + client_proof_options['verificationMethod'] = f'did:key:{authorized_key}#{authorized_key}' signed_did_document = traction.add_di_proof(did_document, client_proof_options) - + endorser_proof_options = r.json()['proofOptions'].copy() endorser_proof_options['verificationMethod'] = f'did:key:{self.endorser_multikey}#{self.endorser_multikey}' endorsed_did_document = traction.add_di_proof(signed_did_document, endorser_proof_options) - - r = requests.post(f'{self.tdw_server}/{namespace}/{identifier}', json={ + + r = requests.post(self.tdw_server, json={ 'didDocument': endorsed_did_document }) try: - return r.json()['didDocument'] - except: - raise HTTPException(status_code=r.status_code, detail=r.json()) + log_entry = r.json()['logEntry'] + except (ValueError, KeyError): + raise HTTPException(status_code=r.status_code, detail=r.text) + proof_options = { + 'type': 'DataIntegrityProof', + 'cryptosuite': 'eddsa-jcs-2022', + 'proofPurpose': 'assertionMethod', + 'verificationMethod': f'did:key:{authorized_key}#{authorized_key}', + } + signed_log_entry = traction.add_di_proof(log_entry, proof_options) + r = requests.post(f'{self.tdw_server}/{namespace}/{identifier}', json={ + 'logEntry': signed_log_entry + }) + try: + log_entry = r.json() + except (ValueError, KeyError): + raise HTTPException(status_code=r.status_code, detail=r.text) + return did_document + + async def register_credential(self, credential_registration): + issuer = await AskarStorage().fetch('issuer', credential_registration['issuer']) + issuer = Issuer( + id=issuer['id'], + name=issuer['name'], + description=issuer['description'], + ) - def register_credential(self): - pass + # W3C type and context + contexts = ["https://www.w3.org/ns/credentials/v2"] + types = ["VerifiableCredential"] + # credential_subject = {'type': []} + + # UNTP type and context + if "untpType" in credential_registration: + credential_subject = {} + + # DigitalConformityCredential template + if credential_registration["untpType"] == "DigitalConformityCredential": + contexts.append(DigitalConformityCredential().context) + types.append(credential_registration["untpType"]) + + legal_act_info = Soup(credential_registration["relatedResources"]["legalAct"]).legal_act_info() + + credential_subject = credential_subject | untp.ConformityAttestation( + assessmentLevel='GovtApproval', + attestationType='Certification', + scope = untp.ConformityAssessmentScheme( + id=credential_registration["relatedResources"]["governance"], + name=f'Governance document for {credential_registration["type"]}' + ), + issuedToParty = untp.Party( + idScheme=untp.IdentifierScheme( + id="https://www.bcregistry.gov.bc.ca/", + name="BC Registry" + ) + ), + assessment=[untp.ConformityAssessment( + compliance=True, + conformityTopic="Governance.Compliance", + referenceRegulation=untp.Regulation( + id=legal_act_info["id"], + name=legal_act_info["name"], + effectiveDate=legal_act_info["effectiveDate"], + jurisdictionCountry="CA", + administeredBy=untp.Party( + id="https://gov.bc.ca", + name="Government of British Columbia" + ) + ) + )] + ).model_dump() + + # BCGov type and context + contexts.append(credential_registration["relatedResources"]["context"]) + types.append(credential_registration["type"]) + credential_subject['type'].append(credential_registration['subjectType']) + + credential_template = Credential( + context=contexts, + type=types, + issuer=issuer, + credentialSubject=credential_subject + ).model_dump() + return credential_template - def publish_credential(self, credential_data): - credential_registration = {} - credential = { - 'credentialSubject': {} - } - if 'untpType' in credential_registration: - if credential_registration['untpType'] == 'DigitalConformityCredential': - type = ['ConformityAttestation'] - attestationType = "Certification" - assessmentLevel = "GovtApproval" - scope = { - "id": "https://bcgov.github.io/digital-trust-toolkit/docs/governance/pilots/bc-petroleum-and-natural-gas-title/governance", - "name": "B.C. Petroleum & Natural Gas Title - DRAFT" - } - issuedToParty = { - "id": "https://orgbook.gov.bc.ca/entity/A0131571", - "idScheme": { - "id": "https://www.bcregistry.gov.bc.ca/", - "name": "BC Registry", - "type": "IdentifierScheme" - }, - "name": "PACIFIC CANBRIAM ENERGY LIMITED", - "registeredId": credential_data['entityId'], - "type": [ - "Entity", - ] - } - assessment = { - "type": ["ConformityAssessment"], - "conformityTopic": "Governance.Compliance", - "compliance": True, - "referenceRegulation": { - "administeredBy": { - "id": "https://www2.gov.bc.ca/gov/content/home", - "idScheme": { - "id": "https://www2.gov.bc.ca/gov/content/home", - "name": "BC-GOV", - "type": "IdentifierScheme" - }, - "name": "Government of British Columbia", - "registeredId": "BC-GOV", - "type": [ - "Entity" - ] - }, - "effectiveDate": act_soup['date'], - "id": credential_registration['legalAct'], - "jurisdictionCountry": "CA", - "name": act_soup['title'], - "type": [ - "Regulation" - ] - } - } - assessedFacility = [] - assessedProduct = [] - facility = { - "type": ["Facility"] - } - product = { - "type": ["Product"] - } + # def publish_credential(self, credential_data): + # credential_registration = {} + # credential = { + # 'credentialSubject': {} + # } + # if 'untpType' in credential_registration: + # if credential_registration['untpType'] == 'DigitalConformityCredential': + # type = ['ConformityAttestation'] + # attestationType = "Certification" + # assessmentLevel = "GovtApproval" + # scope = { + # "id": "https://bcgov.github.io/digital-trust-toolkit/docs/governance/pilots/bc-petroleum-and-natural-gas-title/governance", + # "name": "B.C. Petroleum & Natural Gas Title - DRAFT" + # } + # issuedToParty = { + # "id": "https://orgbook.gov.bc.ca/entity/A0131571", + # "idScheme": { + # "id": "https://www.bcregistry.gov.bc.ca/", + # "name": "BC Registry", + # "type": "IdentifierScheme" + # }, + # "name": "PACIFIC CANBRIAM ENERGY LIMITED", + # "registeredId": credential_data['entityId'], + # "type": [ + # "Entity", + # ] + # } + # assessment = { + # "type": ["ConformityAssessment"], + # "conformityTopic": "Governance.Compliance", + # "compliance": True, + # "referenceRegulation": { + # "administeredBy": { + # "id": "https://www2.gov.bc.ca/gov/content/home", + # "idScheme": { + # "id": "https://www2.gov.bc.ca/gov/content/home", + # "name": "BC-GOV", + # "type": "IdentifierScheme" + # }, + # "name": "Government of British Columbia", + # "registeredId": "BC-GOV", + # "type": [ + # "Entity" + # ] + # }, + # "effectiveDate": act_soup['date'], + # "id": credential_registration['legalAct'], + # "jurisdictionCountry": "CA", + # "name": act_soup['title'], + # "type": [ + # "Regulation" + # ] + # } + # } + # assessedFacility = [] + # assessedProduct = [] + # facility = { + # "type": ["Facility"] + # } + # product = { + # "type": ["Product"] + # } - act_soup = {} \ No newline at end of file + # act_soup = {} \ No newline at end of file diff --git a/backend/app/plugins/soup.py b/backend/app/plugins/soup.py index dce67f7..f66558c 100644 --- a/backend/app/plugins/soup.py +++ b/backend/app/plugins/soup.py @@ -20,5 +20,5 @@ def legal_act_info(self): return { 'id': self.url, 'name': title, - 'date': date.split('current to')[-1].strip() + 'effectiveDate': date.split('current to')[-1].strip() } \ No newline at end of file diff --git a/backend/app/plugins/traction.py b/backend/app/plugins/traction.py index 3462ec7..6b059fd 100644 --- a/backend/app/plugins/traction.py +++ b/backend/app/plugins/traction.py @@ -6,6 +6,7 @@ class TractionController: def __init__(self): + self.endorser_key = settings.TDW_ENDORSER_MULTIKEY self.endpoint = settings.TRACTION_API_URL self.tenant_id = settings.TRACTION_TENANT_ID self.api_key = settings.TRACTION_API_KEY @@ -14,7 +15,8 @@ def __init__(self): def _try_response(self, response, response_key=None): try: return response.json()[response_key] - except: + except ValueError: + print(response.json()) raise HTTPException(status_code=response.status_code, detail=response.json()) def authorize(self): @@ -62,6 +64,14 @@ def add_di_proof(self, document, options): }) return self._try_response(r, 'securedDocument') + def endorse(self, document, options): + options['verificationMethod'] = f'did:key:{self.endorser_key}#{self.endorser_key}' + r = requests.post(f'{self.endpoint}/vc/di/add-proof', headers=self.headers, json={ + 'document': document, + 'options': options, + }) + return self._try_response(r, 'securedDocument') + def verify_di_proof(self, secured_document): r = requests.post(f'{self.endpoint}/vc/di/verify', headers=self.headers, json={ 'securedDocument': secured_document, diff --git a/backend/app/plugins/untp.py b/backend/app/plugins/untp.py index 16c827c..8f22740 100644 --- a/backend/app/plugins/untp.py +++ b/backend/app/plugins/untp.py @@ -10,11 +10,13 @@ def __init__(self): self.context = "https://test.uncefact.org/vocabulary/untp/dcc/0.4.2/" self.type = "DigitalConformityCredential" - def attestation(self, credential_registration=None, products=None, facilities=None): + def attestation(self, scope, regulation, products=None, facilities=None): conformity_attestation = untp.ConformityAttestation( + assessmentLevel='GovtApproval', + attestationType='Certification', scope = untp.ConformityAssessmentScheme( - id=credential_registration["relatedResources"]["governance"], - name=credential_registration["relatedResources"]["governance"], + id=scope['id'], + name=scope['name'], ), issuedToParty = untp.Party( idScheme=untp.IdentifierScheme( @@ -23,11 +25,11 @@ def attestation(self, credential_registration=None, products=None, facilities=No ) ) ) - conformity_attestation.assessment = [self.add_assessment( - # credential_registration["relatedResources"]["legalAct"], - # products, - # facilities, - )] + # conformity_attestation.assessment = [self.add_assessment( + # regulation, + # # products, + # # facilities, + # )] return conformity_attestation # def add_subject_party(self, entity_id): @@ -37,12 +39,10 @@ def add_assessment(self, regulation=None, products=[], facilities=[]): assessment = untp.ConformityAssessment( compliance=True, conformityTopic="Governance.Compliance", - assessmentLevel='GovtApproval', - attestationType='Certification', referenceRegulation=untp.Regulation( - # id=regulation["id"], - # name=regulation["name"], - # effectiveDate=regulation["effectiveDate"], + id=regulation["id"], + name=regulation["name"], + effectiveDate=regulation["effectiveDate"], jurisdictionCountry="CA", administeredBy=untp.Party( id="https://gov.bc.ca", diff --git a/backend/app/routers/credentials.py b/backend/app/routers/credentials.py index 0934c0d..3994285 100644 --- a/backend/app/routers/credentials.py +++ b/backend/app/routers/credentials.py @@ -1,4 +1,4 @@ -from fastapi import APIRouter, Depends, HTTPException, Request +from fastapi import APIRouter, Depends, HTTPException, Request, Header from fastapi.responses import JSONResponse from app.models.web_schemas import ( IssueCredential, @@ -18,9 +18,13 @@ from datetime import datetime, timezone, timedelta import json -router = APIRouter() +router = APIRouter(prefix='/credentials', tags=["Credentials"]) -@router.post("/credentials") +@router.post("/forward") +async def forward_credential(request_body: PublishCredential): + pass + +@router.post("/publish") async def publish_credential(request_body: PublishCredential): # valid_from = request_body.model_dump()['validFrom'] # valid_until = request_body.model_dump()['validUntil'] @@ -94,7 +98,7 @@ async def publish_credential(request_body: PublishCredential): 'options': proof_options }) -@router.post("/credentials/issue") +@router.post("/issue") async def issue_credential(request_body: IssueCredential): credential = request_body.model_dump()["credential"] options = request_body.model_dump()["options"] @@ -144,8 +148,7 @@ async def issue_credential(request_body: IssueCredential): proof_options = { "type": "DataIntegrityProof", "cryptosuite": "eddsa-jcs-2022", - "proofPurpose": "authentication", - # 'proofPurpose': 'assertionMethod', + 'proofPurpose': 'assertionMethod', "verificationMethod": verification_method, "created": str(datetime.now().isoformat("T", "seconds")), } @@ -157,11 +160,13 @@ async def issue_credential(request_body: IssueCredential): @router.get("/credentials/{credential_id}") -async def get_credential(credential_id: str, envelope: bool = False): - headers = {"Content-Type": "application/ld+json"} +async def get_credential(credential_id: str, request: Request): vc = await AskarStorage().fetch('credential', credential_id) - if envelope: + if 'application/vc+jwt' in request.headers: + headers = {"Content-Type": "application/vc+jwt"} vc = await AskarWallet().sign_vc_jose(vc) + else: + headers = {"Content-Type": "application/vc"} return JSONResponse( status_code=200, content=vc, @@ -175,7 +180,7 @@ async def get_credential(credential_id: str, envelope: bool = False): @router.get("/credentials/status/{status_credential_id}") -async def get_status_list_credential(status_credential_id: str): +async def get_status_list_credential(status_credential_id: str, request: Request): status_list_credential = await AskarStorage().fetch( "statusListCredential", status_credential_id ) @@ -192,8 +197,14 @@ async def get_status_list_credential(status_credential_id: str): "proofValue": None, } status_list_credential["proof"] = proof + if 'application/vc+jwt' in request.headers: + headers = {"Content-Type": "application/vc+jwt"} + status_list_credential = await AskarWallet().sign_vc_jose(status_list_credential) + else: + headers = {"Content-Type": "application/vc"} return JSONResponse( status_code=200, content=status_list_credential, + headers=headers ) diff --git a/backend/app/routers/issuers.py b/backend/app/routers/issuers.py deleted file mode 100644 index 784719d..0000000 --- a/backend/app/routers/issuers.py +++ /dev/null @@ -1,149 +0,0 @@ -from fastapi import APIRouter, Depends, HTTPException, Request -from fastapi.responses import JSONResponse -from app.models.registrations import IssuerRegistration -from config import settings -from app.plugins import AskarStorage, DidWebEndorser, AskarWallet -from app.models import DidDocument, VerificationMethod, Service - -router = APIRouter() - - -@router.post("/issuers", summary="Register issuer.") -async def register_issuer(request_body: IssuerRegistration): - url = vars(request_body)["url"] - name = vars(request_body)["name"] - scope = vars(request_body)["scope"] - description = vars(request_body)["description"] - namespace = scope.replace(" ", "-").lower() - identifier = name.replace(" ", "-").lower() - did = f"did:web:{settings.DOMAIN}:{namespace}:{identifier}" - # did_document = await DidWebEndorser().did_registration(namespace, identifier) - multikey = await AskarWallet().create_key() - - verification_method_multikey = VerificationMethod( - id=f'{did}#multikey-01', - type='Multikey', - controller=did, - publicKeyMultibase=multikey - ) - - verification_method_jwk = VerificationMethod( - id=f'{did}#jwk-01', - type='JsonWebKey', - controller=did, - publicKeyJwk={ - "kty": "OKP", - "crv": "Ed25519", - "x": multikey - } - ) - - service = Service( - id=f'{did}#bcgov-website', - type='LinkedDomains', - serviceEndpoint=url, - ) if url else None - - did_document = DidDocument( - id=did, - name=name, - description=description, - authentication=[verification_method_multikey.id], - assertionMethod=[verification_method_multikey.id], - verificationMethod=[ - verification_method_multikey, - verification_method_jwk - ], - service=[service] if service else None - ).model_dump() - options = { - 'type': 'DataIntegrityProof', - 'cryptosuite': 'eddsa-jcs-2022', - 'proofPurpose': 'authentication', - 'verificationMethod': f'did:key:{multikey}#{multikey}', - } - signed_did_doc = await AskarWallet().add_proof(did_document, options) - - # await AskarStorage().store('didRegistration', did_document['id'], did_document) - return JSONResponse(status_code=201, content=signed_did_doc) - - -# @router.get("/issuers") -# async def get_pending_issuer_registrations(did: str): -# if did: -# registrations = [ -# await AskarStorage().fetch('didRegistration', did) -# ] -# else: -# did_registrations = [] -# registrations = [ -# await AskarStorage().fetch('didRegistration', did) -# for did in did_registrations -# ] -# return JSONResponse( -# status_code=200, -# content={'registrations': registrations}, -# ) - -# @router.post("/issuers/{did}") -# async def approve_pending_issuer_registration(did: str): -# did_document = await AskarStorage().fetch('didRegistration', did) -# await AskarStorage().store('didDocument', did, did_document) -# # await AskarStorage().remove('didRegistration', did) -# return JSONResponse( -# status_code=200, -# content=did_document, -# ) - -# @router.delete("/issuers/{did}") -# async def cancel_pending_issuer_registration(did: str): -# # await AskarStorage().remove('didRegistration', did) -# return JSONResponse( -# status_code=200, -# content={}, -# ) - - -# @router.get("/{namespace}/{identifier}/did.json") -# async def get_issuer_did_document(namespace: str, identifier: str): -# headers = {"Content-Type": "application/ld+json"} -# did = f'did:web:{settings.DOMAIN}:{namespace}:{identifier}' -# issuer = next((issuer for issuer in settings.ISSUERS if issuer['id'] == did), None) -# # did_document = await AskarStorage().fetch('didDocument', did) -# multikey = await AskarWallet().get_multikey(did) -# jwk = multikey -# did_document = DidDocument( -# id=did, -# name=issuer['name'], -# description=issuer['description'], -# authentication=[f'{did}#multikey-01'], -# assertionMethod=[f'{did}#multikey-01'], -# verificationMethod=[ -# VerificationMethod( -# type='Multikey', -# id=f'{did}#multikey-01', -# controller=did, -# publicKeyMultibase=multikey, -# ), -# VerificationMethod( -# type='JsonWebKey', -# id=f'{did}#jwk-01', -# controller=did, -# publicKeyJwk={ -# "kty": "OKP", -# "crv": "Ed25519", -# "x": AskarWallet().multikey_to_jwk(multikey) -# }, -# ), -# ], -# service=[Service( -# id=f'{did}#bcgov-url', -# type='LinkedDomains', -# serviceEndpoint=issuer['url'], -# )] -# ).model_dump() -# return JSONResponse( -# status_code=200, -# content=did_document, -# headers=headers -# ) diff --git a/backend/app/routers/registrations.py b/backend/app/routers/registrations.py index a47ee03..03f64a0 100644 --- a/backend/app/routers/registrations.py +++ b/backend/app/routers/registrations.py @@ -6,74 +6,53 @@ from app.plugins import AskarStorage, BitstringStatusList, PublisherRegistrar, Soup from app.security import check_api_key_header -router = APIRouter(prefix='/registrar') +router = APIRouter(prefix='/registrations', tags=["Registrations"]) @router.post("/issuers") async def register_issuer(request_body: IssuerRegistration, authorized = Depends(check_api_key_header)): - namespace = vars(request_body)["scope"].replace(" ", "-").lower() - identifier = vars(request_body)["name"].replace(" ", "-").lower() - issuer = { - 'id': f'did:web:{settings.TDW_SERVER_URL.split("//")[-1]}:{namespace}:{identifier}', - 'name': vars(request_body)["name"], - 'description': vars(request_body)["description"] - } - await AskarStorage().store('didRegistration', issuer['id'], issuer) - return JSONResponse(status_code=201, content=issuer) - + registration = vars(request_body) did_document = PublisherRegistrar().register_issuer( - vars(request_body)["name"], - vars(request_body)["scope"], - vars(request_body)["url"], - vars(request_body)["description"] + registration["name"], + registration["scope"], + registration["url"], + registration["description"], + registration["multikey"], ) - - await AskarStorage().store('didRegistration', did_document['id'], did_document) - return JSONResponse(status_code=201, content=did_document) -# @router.post("/issuers/{did}") -# async def approve_pending_issuer_registration(did: str): -# did_document = await AskarStorage().fetch('didRegistration', did) -# await AskarStorage().store('didDocument', did, did_document) -# # await AskarStorage().remove('didRegistration', did) -# return JSONResponse( -# status_code=200, -# content=did_document, -# ) + await AskarStorage().store('issuer', did_document['id'], did_document) + issuer = { + 'id': did_document['id'], + 'name': registration["name"], + 'scope': registration["scope"], + } + return JSONResponse(status_code=201, content=issuer) @router.delete("/issuers/{did}") -async def register_issuer(did: str, authorized = Depends(check_api_key_header)): +async def delete_issuer(did: str, authorized = Depends(check_api_key_header)): - await AskarStorage().remove('didRegistration', did) + await AskarStorage().remove('issuer', did) return JSONResponse(status_code=200, content={}) @router.post("/credentials") async def register_credential_type(request_body: CredentialRegistration, authorized = Depends(check_api_key_header)): credential_registration = request_body.model_dump() - - untp_type = credential_registration.get('untpType') - if untp_type == 'DigitalConformityCredential': - if not credential_registration['relatedResources'].get('governance') \ - or not credential_registration['relatedResources'].get('legalAct'): - pass - governance = { - 'id': credential_registration['relatedResources'].get('governance').lstrip('/'), - 'name': credential_registration['relatedResources'].get('governance').lstrip('/').split('/')[-1].replace('-', ' ').title() - } - legal_act = Soup( - credential_registration['relatedResources'].get('legalAct') - ).legal_act_info() # Create a new status list for this type of credential # status_list = await BitstringStatusList().create(credential_registration) # credential_registration["statusList"] = [status_list] - - await AskarStorage().replace( - "credentialRegistration", - credential_registration["type"], - credential_registration, - ) - - return JSONResponse(status_code=201, content=credential_registration) + # await AskarStorage().replace( + # "credentialRegistration", + # credential_registration["type"], + # credential_registration, + # ) + # return JSONResponse(status_code=201, content=credential_registration) + credential_template = await PublisherRegistrar().register_credential(credential_registration) + # await AskarStorage().replace( + # "credentialTemplate", + # credential_registration["type"], + # credential_template, + # ) + return JSONResponse(status_code=201, content=credential_template) diff --git a/backend/app/routers/related_resources.py b/backend/app/routers/related_resources.py deleted file mode 100644 index 28f36df..0000000 --- a/backend/app/routers/related_resources.py +++ /dev/null @@ -1,31 +0,0 @@ -from fastapi import APIRouter, HTTPException -from fastapi.responses import JSONResponse -from app.models.web_schemas import RegisterIssuer -from config import settings -from app.plugins import AskarStorage -from app.models import DidDocument -import json - -router = APIRouter() - - -@router.get("/contexts/{filename}/v1") -async def get_context_file(filename: str): - try: - headers = {"Content-Type": "application/ld+json"} - with open(f"app/related_resources/contexts/{filename}.jsonld", "r") as f: - context = json.loads(f.read()) - return JSONResponse(status_code=200, content=context, headers=headers) - except: - raise HTTPException(status_code=404, detail="Ressource not found.") - - -@router.get("/oca-bundles/{filename}/v1") -async def get_oca_bundle(filename: str): - try: - headers = {"Content-Type": "application/json"} - with open(f"app/related_resources/oca_bundles/{filename}.json", "r") as f: - context = json.loads(f.read()) - return JSONResponse(status_code=200, content=context, headers=headers) - except: - raise HTTPException(status_code=404, detail="Ressource not found.") diff --git a/backend/app/routers/utilities.py b/backend/app/routers/utilities.py deleted file mode 100644 index 0de5653..0000000 --- a/backend/app/routers/utilities.py +++ /dev/null @@ -1,14 +0,0 @@ -from fastapi import APIRouter, HTTPException -from fastapi.responses import JSONResponse -from app.models.web_schemas import RegisterIssuer -from config import settings -from app.plugins import AskarStorage -from app.models import DidDocument -import json - -router = APIRouter() - - -@router.post("/utilities/oca-bundle-generator") -async def generate_oca_bundle(request_body: CredentialRegistration): - pass diff --git a/backend/app/utilities.py b/backend/app/utilities.py index 7dfc997..eb80aa8 100644 --- a/backend/app/utilities.py +++ b/backend/app/utilities.py @@ -1,7 +1,5 @@ import requests -import hashlib import base64 -import base58 from multiformats import multibase MULTIKEY = [ diff --git a/backend/app/utils.py b/backend/app/utils.py new file mode 100644 index 0000000..df978ae --- /dev/null +++ b/backend/app/utils.py @@ -0,0 +1,42 @@ +import uuid +import base64 +from datetime import datetime, timezone, timedelta +import validators +import re +from fastapi import HTTPException + +def valid_datetime_string(datetime_string): + try: + datetime.fromisoformat(datetime_string) + return True + except: + return False + +def valid_uri(value): + DID_REGEX = re.compile("did:([a-z0-9]+):((?:[a-zA-Z0-9._%-]*:)*[a-zA-Z0-9._%-]+)") + if DID_REGEX.match(value) or validators.url(value): + return True + return False + +def check_validity_period(credential): + if 'validFrom' in credential and 'validUntil' in credential: + start = datetime.fromisoformat(credential['validFrom']) + end = datetime.fromisoformat(credential['validUntil']) + if start > end: + raise HTTPException(status_code=400, detail="Bad validity period.") + return False + + +def id_from_string(string): + return f'urn:uuid:{str(uuid.uuid5(uuid.NAMESPACE_DNS, string))}' + +def b64_encode(message): + return base64.urlsafe_b64encode(message).decode().rstrip("=") + +def datetime_range(days=None, minutes=None): + start = datetime.now(timezone.utc).isoformat('T', 'seconds') + if days: + end = (datetime.now(timezone.utc)+ timedelta(days=days)).isoformat('T', 'seconds') + elif minutes: + end = (datetime.now(timezone.utc)+ timedelta(minutes=minutes)).isoformat('T', 'seconds') + return start, end \ No newline at end of file diff --git a/backend/poetry.lock b/backend/poetry.lock new file mode 100644 index 0000000..be6e512 --- /dev/null +++ b/backend/poetry.lock @@ -0,0 +1,650 @@ +# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. + +[[package]] +name = "annotated-types" +version = "0.7.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +files = [ + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, +] + +[[package]] +name = "anyio" +version = "4.6.2.post1" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +optional = false +python-versions = ">=3.9" +files = [ + {file = "anyio-4.6.2.post1-py3-none-any.whl", hash = "sha256:6d170c36fba3bdd840c73d3868c1e777e33676a69c3a72cf0a0d5d6d8009b61d"}, + {file = "anyio-4.6.2.post1.tar.gz", hash = "sha256:4c8bc31ccdb51c7f7bd251f51c609e038d63e34219b44aa86e47576389880b4c"}, +] + +[package.dependencies] +idna = ">=2.8" +sniffio = ">=1.1" + +[package.extras] +doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21.0b1)"] +trio = ["trio (>=0.26.1)"] + +[[package]] +name = "aries-askar" +version = "0.3.2" +description = "" +optional = false +python-versions = ">=3.6.3" +files = [ + {file = "aries_askar-0.3.2-py3-none-macosx_10_9_universal2.whl", hash = "sha256:02ddbe1773ce72c57edafff5777a1337d4a678da7484596712949170fb3ca1dc"}, + {file = "aries_askar-0.3.2-py3-none-manylinux2014_aarch64.whl", hash = "sha256:176eebcf833bb9974a162fd931c8d67669e4f0145b351ce9cb1289fd2d5a345c"}, + {file = "aries_askar-0.3.2-py3-none-manylinux2014_x86_64.whl", hash = "sha256:63f9ab97db4778ced830a6d1135e1f8bd1ca564de27218bd114f1cffbd31b04c"}, + {file = "aries_askar-0.3.2-py3-none-win_amd64.whl", hash = "sha256:6b4253377d5ed167ed94790e49c58584b68f897d2541ac4bb18fd37e9264164b"}, +] + +[package.dependencies] +cached-property = ">=1.5.2,<1.6.0" + +[[package]] +name = "bases" +version = "0.3.0" +description = "Python library for general Base-N encodings." +optional = false +python-versions = ">=3.7" +files = [ + {file = "bases-0.3.0-py3-none-any.whl", hash = "sha256:a2fef3366f3e522ff473d2e95c21523fe8e44251038d5c6150c01481585ebf5b"}, + {file = "bases-0.3.0.tar.gz", hash = "sha256:70f04a4a45d63245787f9e89095ca11042685b6b64b542ad916575ba3ccd1570"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0" +typing-validation = ">=1.1.0" + +[package.extras] +dev = ["base58", "mypy", "pylint", "pytest", "pytest-cov"] + +[[package]] +name = "beautifulsoup4" +version = "4.12.3" +description = "Screen-scraping library" +optional = false +python-versions = ">=3.6.0" +files = [ + {file = "beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed"}, + {file = "beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051"}, +] + +[package.dependencies] +soupsieve = ">1.2" + +[package.extras] +cchardet = ["cchardet"] +chardet = ["chardet"] +charset-normalizer = ["charset-normalizer"] +html5lib = ["html5lib"] +lxml = ["lxml"] + +[[package]] +name = "bitarray" +version = "2.9.3" +description = "efficient arrays of booleans -- C extension" +optional = false +python-versions = "*" +files = [ + {file = "bitarray-2.9.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2cf5f5400636c7dda797fd681795ce63932458620fe8c40955890380acba9f62"}, + {file = "bitarray-2.9.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3487b4718ffa5942fab777835ee36085f8dda7ec4bd0b28433efb117f84852b6"}, + {file = "bitarray-2.9.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:10f44b1e4994035408bea54d7bf0aec79744cad709706bedf28091a48bb7f1a4"}, + {file = "bitarray-2.9.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb5c16f97c65add6535748a9c98c70e7ca79759c38a2eb990127fef72f76111a"}, + {file = "bitarray-2.9.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:13dbfc42971ba84e9c4ba070f720df6570285a3f89187f07ef422efcb611c19f"}, + {file = "bitarray-2.9.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c28076acfbe7f9a5494d7ae98094a6e209c390c340938845f294818ebf5e4d3"}, + {file = "bitarray-2.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b7cdd21835936d9a66477836ca23b2cb63295142cb9d9158883e2c0f1f8f6bd"}, + {file = "bitarray-2.9.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9f60887ab3a46e507fa6f8544d8d4b0748da48718591dfe3fe80c62bdea60f10"}, + {file = "bitarray-2.9.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f75e1abd4a37cba3002521d3f5e2b50ef4f4a74342207cad3f52468411d5d8ba"}, + {file = "bitarray-2.9.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:dc63da9695383c048b83f5ab77eab35a55bbb2e77c7b6e762eba219929b45b84"}, + {file = "bitarray-2.9.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:6fe5a57b859d9bc9c2fd27c78c4b7b83158faf984202de6fb44618caeebfff10"}, + {file = "bitarray-2.9.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:1fe5a37bd9441a5ecc2f6e71b43df7176fa376a542ef97484310b8b46a45649a"}, + {file = "bitarray-2.9.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8a16e42c169ca818d6a15b5dd5acd5d2a26af0fa0588e1036e0e58d01f8387d4"}, + {file = "bitarray-2.9.3-cp310-cp310-win32.whl", hash = "sha256:5e6b5e7940af3474ffaa930cd1ce8215181cbe864d6b5ddb67a15d3c15e935cd"}, + {file = "bitarray-2.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:c63dbb99ef2ab1281871678624f9c9a5f1682b826e668ce559275ec488b3fa8b"}, + {file = "bitarray-2.9.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:49fb93b488d180f5c84b79fe687c585a84bf0295ff035d63e09ee24ce1da0558"}, + {file = "bitarray-2.9.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c2944fb83bbc2aa7f29a713bc4f8c1318e54fa0d06a72bedd350a3fb4a4b91d8"}, + {file = "bitarray-2.9.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3612d9d3788dc62f1922c917b1539f1cdf02cecc9faef8ae213a8b36093136ca"}, + {file = "bitarray-2.9.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90a9300cdb7c99b1e692bb790cba8acecee1a345a83e58e28c94a0d87c522237"}, + {file = "bitarray-2.9.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1211ed66acbbb221fd7554abf4206a384d79e6192d5cb95325c5c361bbb52a74"}, + {file = "bitarray-2.9.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:67757279386accf93eba76b8f97b5acf1664a3e350cbea5f300f53490f8764fd"}, + {file = "bitarray-2.9.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64e19c6a99c32f460c2613f797f77aa37d8e298891d00ea5355158cce80e11ec"}, + {file = "bitarray-2.9.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:72734bd3775f43c5a75385730abb9f84fee6c627eb14f579de4be478f1615c8c"}, + {file = "bitarray-2.9.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a92703471b5d3316c7481bc1852f620f42f7a1b62be27f39d13694827635786f"}, + {file = "bitarray-2.9.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:d5d77c81300ca430d4b195ccfbb629d6858258f541b6e96c6b11ec1563cd2681"}, + {file = "bitarray-2.9.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:3ba8a29c0d091c952ced1607ce715f5e0524899f24333a493807d00f5938463d"}, + {file = "bitarray-2.9.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:418171d035b191dbe5e86cd2bfb5c3e1ae7d947edc22857a897d1c7251674ae5"}, + {file = "bitarray-2.9.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1e0bd272eba256183be2a17488f9cb096d2e6d3435ecf2e28c1e0857c6d20749"}, + {file = "bitarray-2.9.3-cp311-cp311-win32.whl", hash = "sha256:cc3fd2b0637a619cf13e122bbcf4729ae214d5f25623675597e67c25f9edfe61"}, + {file = "bitarray-2.9.3-cp311-cp311-win_amd64.whl", hash = "sha256:e1fc2a81a585dbe5e367682156e6350d908a56e2ffd6ca651b0af01994db596f"}, + {file = "bitarray-2.9.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:dc47be026f76f1728af00dc7140cec8483fe2f0c476bbf2a59ef47865e00ff96"}, + {file = "bitarray-2.9.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:82b091742ff511cdb06f90af0d2c22e7af3dbff9b8212e2e0d88dfef6a8570b3"}, + {file = "bitarray-2.9.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2d5edb4302a0e3a3d1d0eeb891de3c615d4cb7a446fb41c21eecdcfb29400a6f"}, + {file = "bitarray-2.9.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb4786c5525069c19820549dd2f42d33632bc42959ad167138bd8ee5024b922b"}, + {file = "bitarray-2.9.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9bfe2de2b4df61ccb9244871a0fdf1fff83be0c1bd7187048c3cf7f81c5fe631"}, + {file = "bitarray-2.9.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:31e4f69538f95d2934587d957eea0d283162322dd1af29e57122b20b8cd60f92"}, + {file = "bitarray-2.9.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ca44908b2bc08d8995770018638d62626706864f9c599b7818225a12f3dbc2c"}, + {file = "bitarray-2.9.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:279f8de5d251ee521e365df29c927d9b5732f1ed4f373d2dbbd278fcbad94ff5"}, + {file = "bitarray-2.9.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c49bb631b38431c09ecd534d56ef04264397d24d18c4ee6653c84e14ae09d92d"}, + {file = "bitarray-2.9.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:192bffc93ee9a5b6c833c98d1dcc81f5633ddd726b85e18341387d0c1d51f691"}, + {file = "bitarray-2.9.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c516cec28c6511df51d87033f40ec420324a2247469b0c989d344f4d27ea37d2"}, + {file = "bitarray-2.9.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:66241cb9a1c1db294f46cd440141e57e8242874e38f3f61877f72d92ae14768a"}, + {file = "bitarray-2.9.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ab1f0e7631110c89bea7b605c0c35832333eb9cc97e5de05d71c76d42a1858c9"}, + {file = "bitarray-2.9.3-cp312-cp312-win32.whl", hash = "sha256:42aa5bee6fe8ad3385eaf5c6585016bbc38a7b75efb52ce5c6f8e00e05237dfa"}, + {file = "bitarray-2.9.3-cp312-cp312-win_amd64.whl", hash = "sha256:dc3fd647d845b94fac3652390866f921f914a17f3807a031c826f68dae3f43e3"}, + {file = "bitarray-2.9.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:fcfcc1989e3e021a282624017b7fb754210f5332e933b1c3ebc79643727b6551"}, + {file = "bitarray-2.9.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:71b1e229a706798a9e106ca7b03d4c63455deb40b18c92950ec073a05a8f8285"}, + {file = "bitarray-2.9.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4bb49556d3d505d24c942a4206ad4d0d40e89fa3016a7ea6edc994d5c08d4a8e"}, + {file = "bitarray-2.9.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4466aa1e533a59d5f7fd37219d154ec3f2ba73fce3d8a2e11080ec475bc15fb"}, + {file = "bitarray-2.9.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a9b75adc0fd0bf278bea89dc3d679d74e10d2df98d3d074b7f3d36f323138818"}, + {file = "bitarray-2.9.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:701582bbbeac372b1cd8a3c9daf6c2336dc2d22e14373a6271d788bc4f2b6edc"}, + {file = "bitarray-2.9.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ea1f119668bbdbd68008031491515e84441e505163918819994b28f295f762c"}, + {file = "bitarray-2.9.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9f400bc18a70bfdb073532c3054ecd78a0e64f96ff7b6140adde5b122580ec2b"}, + {file = "bitarray-2.9.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:aacff5656fb3e15cede7d02903da2634d376aa928d7a81ec8df19b0724d7972a"}, + {file = "bitarray-2.9.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8a2ae42a14cbf766d4478d7101da6359b0648dd813e60eb3486ac56ad2f5add3"}, + {file = "bitarray-2.9.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:616698edb547d10f0b960cb9f2e8629c55a420dd4c2b1ab46706f49a1815621d"}, + {file = "bitarray-2.9.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f277c50ba184929dfeed39b6cf9468e3446093521b0aeb52bd54a21ca08f5473"}, + {file = "bitarray-2.9.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:661237739b385c90d8837d5e96b06de093cc6e610236977e198f88f5a979686e"}, + {file = "bitarray-2.9.3-cp313-cp313-win32.whl", hash = "sha256:68acec6c19d798051f178a1197b76f891985f683f95a4b12811b68e58b080f5a"}, + {file = "bitarray-2.9.3-cp313-cp313-win_amd64.whl", hash = "sha256:3055720afdcfd7e8f630fa16db7bed7e55c9d0a1f4756195e3b250e203f3b436"}, + {file = "bitarray-2.9.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:72bf17d0e7d8a4f645655a07999d23e42472cbf2100b8dad7ce26586075241d7"}, + {file = "bitarray-2.9.3-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cfd332b5f1ad8c4dc3cc79ecef33c19b42d8d8e6a39fd5c9ecb5855be0b9723"}, + {file = "bitarray-2.9.3-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d5b466ef1e48f25621c9d27e95deb5e33b8656827ed8aa530b972de73870bd1f"}, + {file = "bitarray-2.9.3-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:938cf26fdaf4d0adfac82d830c025523c5d36ddead0470b735286028231c1784"}, + {file = "bitarray-2.9.3-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0f766669e768ef9a2b23ecfa710b38b6a48da3f91755113c79320b207ae255d"}, + {file = "bitarray-2.9.3-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b6337c0c64044f35ddfb241143244aac707a68f34ae31a71dad115f773ccc8b"}, + {file = "bitarray-2.9.3-cp36-cp36m-musllinux_1_2_aarch64.whl", hash = "sha256:731b59540167f8b2b20f69f487ecee2339fc4657059906a16cb51acac17f89c3"}, + {file = "bitarray-2.9.3-cp36-cp36m-musllinux_1_2_i686.whl", hash = "sha256:4feed0539a9d6432361fc4d3820eea3a81fa631d542f166cf8430aad81a971da"}, + {file = "bitarray-2.9.3-cp36-cp36m-musllinux_1_2_ppc64le.whl", hash = "sha256:eb65c96a42e73f35175ec738d67992ffdf054c20abee3933cfcfa2343fa1187d"}, + {file = "bitarray-2.9.3-cp36-cp36m-musllinux_1_2_s390x.whl", hash = "sha256:4f40ceac94d182de6135759d81289683ff3e4cf0da709bc5826a7fe00d754114"}, + {file = "bitarray-2.9.3-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:5b29f7844080a281635a231a37e99f0bd6f567af6cf19f4f6d212137f99a9cdf"}, + {file = "bitarray-2.9.3-cp36-cp36m-win32.whl", hash = "sha256:947cf522a3b339b73114d12417fd848fa01303dbaa7883ced4c87688dba5637c"}, + {file = "bitarray-2.9.3-cp36-cp36m-win_amd64.whl", hash = "sha256:ea794ea60d514d68777a87a74106110db7a4bbc2c46720e67010e3071afefb95"}, + {file = "bitarray-2.9.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c7bc7cb79dcac8bdce23b305e671c06eaeffb012fa065b8c33bc51df7e1733f0"}, + {file = "bitarray-2.9.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d6380ad0f929ad9220abadd1c9b7234271c4b6ea9c753a88611d489e93a8f2e"}, + {file = "bitarray-2.9.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05f4e2451e2ad450b41ede8440e52c1fd798e81027e1dc2256292ec0787d3bf1"}, + {file = "bitarray-2.9.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7267885c98138f3707c710d5b08eedef150a3e5112c760cfe1200f3366fd7064"}, + {file = "bitarray-2.9.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:976957423cb41df8fe0eb811dbb53d8c5ab1ca3beec7a3ca7ff679be44a72714"}, + {file = "bitarray-2.9.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c0ec5141a69f73ed6ff17ea7344d5cc166e087095bfe3661dbb42b519e76aa16"}, + {file = "bitarray-2.9.3-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:218a1b7c0652a3c1020f903ded0f9768c3719fb6d43a6e9d346e985292992d35"}, + {file = "bitarray-2.9.3-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:cf0c9ebf2df280794244e1e12ed626357506ddaa2f0d6f69efe493ae7bbf4bf7"}, + {file = "bitarray-2.9.3-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:c450a04a7e091b57d4c0bd1531648522cd0ef26913ad0e5dea0432ea29b0e5c1"}, + {file = "bitarray-2.9.3-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:a212eb89a50e32ef4969387e44a7410447dc59587615e3966d090edc338a1b85"}, + {file = "bitarray-2.9.3-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:4269232026212ee6b73379b88a578107a6b36a6182307a49d5509686c7495261"}, + {file = "bitarray-2.9.3-cp37-cp37m-win32.whl", hash = "sha256:8a0fb358e6a43f216c3fb0871e2ac14c16563aec363c23bc2fbbb18f6201285d"}, + {file = "bitarray-2.9.3-cp37-cp37m-win_amd64.whl", hash = "sha256:a8368774cdc737eec8fce6f28d0abc095fbc0edccf8fab8d29fddc264b68def9"}, + {file = "bitarray-2.9.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:7d0724a4fef6ded914075a3385ea2d05afdeed567902f83490ed4e7e7e75d9bf"}, + {file = "bitarray-2.9.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0e11b37c6dff6f41ebc49914628824ceb8c8d6ebd0fda2ebe3c0fe0c63e8621e"}, + {file = "bitarray-2.9.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:085f4081d72c7468f82f722a9f113e03a1f7a4c132ef4c2a4e680c5d78b7db00"}, + {file = "bitarray-2.9.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b530b5fbed2900634fbc43f546e384abd72ad9c49795ff5bd6a93cac1aa9c4d8"}, + {file = "bitarray-2.9.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09ff88e4385967571146fb0d270442de39393d44198f4d108f3350cfd6486f0b"}, + {file = "bitarray-2.9.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a344bb212ddf87db4976a6711d274660a5d887da4fd3faafcdaa092152f85a6d"}, + {file = "bitarray-2.9.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc569c96b990f92fd5946d5b50501fee48b01a116a286d1de7961ebd9c6f06f3"}, + {file = "bitarray-2.9.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2fbbe7938ef8a7abe3e8519fa0578b51d2787f7171d3144e7d373551b5851fd"}, + {file = "bitarray-2.9.3-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:0b5912fab904507b47217509b01aa903d7f98b6e725e490a7f01661f4d9a4fa7"}, + {file = "bitarray-2.9.3-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:0c836ccfca9cf60927256738ef234dfe500565492eff269610cdd1bca56801d0"}, + {file = "bitarray-2.9.3-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:af0e4441ebf51c18fc450962f1e201c96f444d63b17cc8dcf7c0b05111bd4486"}, + {file = "bitarray-2.9.3-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:9e9b57175fb6fe76d7ddd0647e06a25f6e23f4b54b5febf337c5a840ab37dc3b"}, + {file = "bitarray-2.9.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:7f7de81721ae9492926bd067007ac974692182bb83fc8f0ba330a67f37a018bd"}, + {file = "bitarray-2.9.3-cp38-cp38-win32.whl", hash = "sha256:4beafb6b6e344385480df6611fdebfcb3579bbb40636ce1ddf5e72fb744e095f"}, + {file = "bitarray-2.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:d8eaeca98900bd6f06a29cdef57999813a67d314f661d14901d71e04f4cf9f00"}, + {file = "bitarray-2.9.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:413965d9d384aef90e58b959f4a39f1d5060b145c26080297b7b4cf23cf38faa"}, + {file = "bitarray-2.9.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2fbb56f2bb89c3a15304a6c0ea56013dc340a98337d9bbd7fc5c21451dc05f8c"}, + {file = "bitarray-2.9.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b8a84f39f7885627711473872d8fc58fc7a0a1e4ecd9ddf42daf9a3643432742"}, + {file = "bitarray-2.9.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45147a9c8580e857c1344d15bd49d2b4387777bd582a2ede11be2ba740653f28"}, + {file = "bitarray-2.9.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed255423dc60c6b2d5c0d90c13dea2962a31929767fdf1c525ab3210269e75c5"}, + {file = "bitarray-2.9.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4f5bd02671ea5c4ad52bbfe0e8e8197b6e8fa85dec1e93a4a05448c19354cc65"}, + {file = "bitarray-2.9.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1c99c58f044549c93fb6d4cda22678deccaed19845eaa2e6917b5b7ca058f2d"}, + {file = "bitarray-2.9.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:921ee87681e32e17d1849e11c96eb6a8a7edaa1269dd26831013daf8546bde05"}, + {file = "bitarray-2.9.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2ed97d8ec40c4658d9f9aa8f26cb473f44fa1dbccba3fa3fbe4a102e38c6a8d7"}, + {file = "bitarray-2.9.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:9d7f7db37edb9c50c9aad6a18f2e87dd7dc5ff2a33406821804a03263fedb2ca"}, + {file = "bitarray-2.9.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:292f726cdb9efc744ed0a1d7453c44151526648148a28d9a2495cc7c7b2c62a8"}, + {file = "bitarray-2.9.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2cc94784238782a9376f307b1aa9a85ce77b6eded9f82d2fe062db7fdb02c645"}, + {file = "bitarray-2.9.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5051436b1d318f6ce0df3b2f8a60bfa66a54c1d9e8719d6cb6b448140e7061f2"}, + {file = "bitarray-2.9.3-cp39-cp39-win32.whl", hash = "sha256:a3d436c686ce59fd0b93438ed2c0e1d3e1716e56bce64b874d05b9f49f1ca5d1"}, + {file = "bitarray-2.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:f168fc45664266a560f2cb28a327041b7f69d4a7faad8ab89e0a1dd7c270a70d"}, + {file = "bitarray-2.9.3-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ae36787299cff41f212aee33cfe1defee13979a41552665a412b6ca3fa8f7eb8"}, + {file = "bitarray-2.9.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42afe48abb8eeb386d93e7f1165ace1dd027f136a8a31edd2b20bc57a0c071d7"}, + {file = "bitarray-2.9.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:451ceecdb86bb95ae101b0d65c8c4524d692ae3666662fef8c89877ce17748c5"}, + {file = "bitarray-2.9.3-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f4d67d3e3de2aede737b12cd75a84963700c941b77b579c14bd05517e05d7a9f"}, + {file = "bitarray-2.9.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:2406d13ded84049b4238815a5821e44d6f58ba00fbb6b705b6ef8ccd88be8f03"}, + {file = "bitarray-2.9.3-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0db944fc2a048020fc940841ef46c0295b045d45a5a582cba69f78962a49a384"}, + {file = "bitarray-2.9.3-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25c603f141171a7d108773d5136d14e572c473e4cdb3fb464c39c8a138522eb2"}, + {file = "bitarray-2.9.3-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86c06b02705305cab0914d209caa24effda81316e2f2555a71a9aa399b75c5a5"}, + {file = "bitarray-2.9.3-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ddda45b24a802eaaca8f794e6267ff2b62de5fe7b900b76d6f662d95192bebf"}, + {file = "bitarray-2.9.3-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:81490623950d04870c6dd4d7e6df2eb68dd04eca8bec327895ebee8bbe0cc3c7"}, + {file = "bitarray-2.9.3-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a9e69ac6a514cc574891c24a50847022dac2fef8c3f4df530f92820a07337755"}, + {file = "bitarray-2.9.3-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:545c695ee69d26b41351ced4c76244d8b6225669fc0af3652ff8ed5a6b28325d"}, + {file = "bitarray-2.9.3-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fbb2e6daabd2a64d091ac7460b0c5c5f9268199ae9a8ce32737cf5273987f1fa"}, + {file = "bitarray-2.9.3-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a969e5cf63144b944ee8d0a0739f53ef1ae54725b5e01258d690a8995d880526"}, + {file = "bitarray-2.9.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:73bbb9301ac9000f869c51db2cc5fcc6541985d3fcdcfe6e02f90c9e672a00be"}, + {file = "bitarray-2.9.3-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7c07e346926488a85a48542d898f4168f3587ec42379fef0d18be301e08a3f27"}, + {file = "bitarray-2.9.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a26d8a14cd8ee496306f2afac34833502dd1ae826355af309333b6f252b23fe"}, + {file = "bitarray-2.9.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cef148ed37c892395ca182d6a235524165a9f765f4283d0a1ced891e7c43c67a"}, + {file = "bitarray-2.9.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:94f35a8f0c8a50ee98a8bef9a070d0b68ecf623f20a2148cc039aba5557346a6"}, + {file = "bitarray-2.9.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:b03207460daae828e2743874c84264e8d96a8c6156490279092b624cd5d2de08"}, + {file = "bitarray-2.9.3.tar.gz", hash = "sha256:9eff55cf189b0c37ba97156a00d640eb7392db58a8049be6f26ff2712b93fa89"}, +] + +[[package]] +name = "bitstring" +version = "4.2.3" +description = "Simple construction, analysis and modification of binary data." +optional = false +python-versions = ">=3.8" +files = [ + {file = "bitstring-4.2.3-py3-none-any.whl", hash = "sha256:20ed0036e2fcf0323acb0f92f0b7b178516a080f3e91061470aa019ac4ede404"}, + {file = "bitstring-4.2.3.tar.gz", hash = "sha256:e0c447af3fda0d114f77b88c2d199f02f97ee7e957e6d719f40f41cf15fbb897"}, +] + +[package.dependencies] +bitarray = ">=2.9.0,<3.0.0" + +[[package]] +name = "bs4" +version = "0.0.2" +description = "Dummy package for Beautiful Soup (beautifulsoup4)" +optional = false +python-versions = "*" +files = [ + {file = "bs4-0.0.2-py2.py3-none-any.whl", hash = "sha256:abf8742c0805ef7f662dce4b51cca104cffe52b835238afc169142ab9b3fbccc"}, + {file = "bs4-0.0.2.tar.gz", hash = "sha256:a48685c58f50fe127722417bae83fe6badf500d54b55f7e39ffe43b798653925"}, +] + +[package.dependencies] +beautifulsoup4 = "*" + +[[package]] +name = "cached-property" +version = "1.5.2" +description = "A decorator for caching properties in classes." +optional = false +python-versions = "*" +files = [ + {file = "cached-property-1.5.2.tar.gz", hash = "sha256:9fa5755838eecbb2d234c3aa390bd80fbd3ac6b6869109bfc1b499f7bd89a130"}, + {file = "cached_property-1.5.2-py2.py3-none-any.whl", hash = "sha256:df4f613cf7ad9a588cc381aaf4a512d26265ecebd5eb9e1ba12f1319eb85a6a0"}, +] + +[[package]] +name = "canonicaljson" +version = "2.0.0" +description = "Canonical JSON" +optional = false +python-versions = ">=3.7" +files = [ + {file = "canonicaljson-2.0.0-py3-none-any.whl", hash = "sha256:c38a315de3b5a0532f1ec1f9153cd3d716abfc565a558d00a4835428a34fca5b"}, + {file = "canonicaljson-2.0.0.tar.gz", hash = "sha256:e2fdaef1d7fadc5d9cb59bd3d0d41b064ddda697809ac4325dced721d12f113f"}, +] + +[[package]] +name = "click" +version = "8.1.7" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "fastapi" +version = "0.115.2" +description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" +optional = false +python-versions = ">=3.8" +files = [ + {file = "fastapi-0.115.2-py3-none-any.whl", hash = "sha256:61704c71286579cc5a598763905928f24ee98bfcc07aabe84cfefb98812bbc86"}, + {file = "fastapi-0.115.2.tar.gz", hash = "sha256:3995739e0b09fa12f984bce8fa9ae197b35d433750d3d312422d846e283697ee"}, +] + +[package.dependencies] +pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" +starlette = ">=0.37.2,<0.41.0" +typing-extensions = ">=4.8.0" + +[package.extras] +all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.7)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] +standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "jinja2 (>=2.11.2)", "python-multipart (>=0.0.7)", "uvicorn[standard] (>=0.12.0)"] + +[[package]] +name = "h11" +version = "0.14.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +optional = false +python-versions = ">=3.7" +files = [ + {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, + {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, +] + +[[package]] +name = "idna" +version = "3.10" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.6" +files = [ + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, +] + +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + +[[package]] +name = "multiformats" +version = "0.3.1.post4" +description = "Python implementation of multiformats protocols." +optional = false +python-versions = ">=3.7" +files = [ + {file = "multiformats-0.3.1.post4-py3-none-any.whl", hash = "sha256:5b1d61bd8275c9e817bdbee38dbd501b26629011962ee3c86c46e7ccd0b14129"}, + {file = "multiformats-0.3.1.post4.tar.gz", hash = "sha256:d00074fdbc7d603c2084b4c38fa17bbc28173cf2750f51f46fbbc5c4d5605fbb"}, +] + +[package.dependencies] +bases = ">=0.3.0" +multiformats-config = ">=0.3.0" +typing-extensions = ">=4.6.0" +typing-validation = ">=1.1.0" + +[package.extras] +dev = ["blake3", "mmh3", "mypy", "pycryptodomex", "pylint", "pyskein", "pytest", "pytest-cov", "rich"] +full = ["blake3", "mmh3", "pycryptodomex", "pyskein", "rich"] + +[[package]] +name = "multiformats-config" +version = "0.3.1" +description = "Pre-loading configuration module for the 'multiformats' package." +optional = false +python-versions = ">=3.7" +files = [ + {file = "multiformats-config-0.3.1.tar.gz", hash = "sha256:7eaa80ef5d9c5ee9b86612d21f93a087c4a655cbcb68960457e61adbc62b47a7"}, + {file = "multiformats_config-0.3.1-py3-none-any.whl", hash = "sha256:dec4c9d42ed0d9305889b67440f72e8e8d74b82b80abd7219667764b5b0a8e1d"}, +] + +[package.dependencies] +multiformats = "*" + +[package.extras] +dev = ["mypy", "pylint", "pytest", "pytest-cov"] + +[[package]] +name = "pydantic" +version = "2.9.2" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic-2.9.2-py3-none-any.whl", hash = "sha256:f048cec7b26778210e28a0459867920654d48e5e62db0958433636cde4254f12"}, + {file = "pydantic-2.9.2.tar.gz", hash = "sha256:d155cef71265d1e9807ed1c32b4c8deec042a44a50a4188b25ac67ecd81a9c0f"}, +] + +[package.dependencies] +annotated-types = ">=0.6.0" +pydantic-core = "2.23.4" +typing-extensions = [ + {version = ">=4.12.2", markers = "python_version >= \"3.13\""}, + {version = ">=4.6.1", markers = "python_version < \"3.13\""}, +] + +[package.extras] +email = ["email-validator (>=2.0.0)"] +timezone = ["tzdata"] + +[[package]] +name = "pydantic-core" +version = "2.23.4" +description = "Core functionality for Pydantic validation and serialization" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic_core-2.23.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:b10bd51f823d891193d4717448fab065733958bdb6a6b351967bd349d48d5c9b"}, + {file = "pydantic_core-2.23.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4fc714bdbfb534f94034efaa6eadd74e5b93c8fa6315565a222f7b6f42ca1166"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63e46b3169866bd62849936de036f901a9356e36376079b05efa83caeaa02ceb"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed1a53de42fbe34853ba90513cea21673481cd81ed1be739f7f2efb931b24916"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cfdd16ab5e59fc31b5e906d1a3f666571abc367598e3e02c83403acabc092e07"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255a8ef062cbf6674450e668482456abac99a5583bbafb73f9ad469540a3a232"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a7cd62e831afe623fbb7aabbb4fe583212115b3ef38a9f6b71869ba644624a2"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f09e2ff1f17c2b51f2bc76d1cc33da96298f0a036a137f5440ab3ec5360b624f"}, + {file = "pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e38e63e6f3d1cec5a27e0afe90a085af8b6806ee208b33030e65b6516353f1a3"}, + {file = "pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0dbd8dbed2085ed23b5c04afa29d8fd2771674223135dc9bc937f3c09284d071"}, + {file = "pydantic_core-2.23.4-cp310-none-win32.whl", hash = "sha256:6531b7ca5f951d663c339002e91aaebda765ec7d61b7d1e3991051906ddde119"}, + {file = "pydantic_core-2.23.4-cp310-none-win_amd64.whl", hash = "sha256:7c9129eb40958b3d4500fa2467e6a83356b3b61bfff1b414c7361d9220f9ae8f"}, + {file = "pydantic_core-2.23.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:77733e3892bb0a7fa797826361ce8a9184d25c8dffaec60b7ffe928153680ba8"}, + {file = "pydantic_core-2.23.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b84d168f6c48fabd1f2027a3d1bdfe62f92cade1fb273a5d68e621da0e44e6d"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df49e7a0861a8c36d089c1ed57d308623d60416dab2647a4a17fe050ba85de0e"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ff02b6d461a6de369f07ec15e465a88895f3223eb75073ffea56b84d9331f607"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:996a38a83508c54c78a5f41456b0103c30508fed9abcad0a59b876d7398f25fd"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d97683ddee4723ae8c95d1eddac7c192e8c552da0c73a925a89fa8649bf13eea"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:216f9b2d7713eb98cb83c80b9c794de1f6b7e3145eef40400c62e86cee5f4e1e"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6f783e0ec4803c787bcea93e13e9932edab72068f68ecffdf86a99fd5918878b"}, + {file = "pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d0776dea117cf5272382634bd2a5c1b6eb16767c223c6a5317cd3e2a757c61a0"}, + {file = "pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d5f7a395a8cf1621939692dba2a6b6a830efa6b3cee787d82c7de1ad2930de64"}, + {file = "pydantic_core-2.23.4-cp311-none-win32.whl", hash = "sha256:74b9127ffea03643e998e0c5ad9bd3811d3dac8c676e47db17b0ee7c3c3bf35f"}, + {file = "pydantic_core-2.23.4-cp311-none-win_amd64.whl", hash = "sha256:98d134c954828488b153d88ba1f34e14259284f256180ce659e8d83e9c05eaa3"}, + {file = "pydantic_core-2.23.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f3e0da4ebaef65158d4dfd7d3678aad692f7666877df0002b8a522cdf088f231"}, + {file = "pydantic_core-2.23.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f69a8e0b033b747bb3e36a44e7732f0c99f7edd5cea723d45bc0d6e95377ffee"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:723314c1d51722ab28bfcd5240d858512ffd3116449c557a1336cbe3919beb87"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb2802e667b7051a1bebbfe93684841cc9351004e2badbd6411bf357ab8d5ac8"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d18ca8148bebe1b0a382a27a8ee60350091a6ddaf475fa05ef50dc35b5df6327"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33e3d65a85a2a4a0dc3b092b938a4062b1a05f3a9abde65ea93b233bca0e03f2"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:128585782e5bfa515c590ccee4b727fb76925dd04a98864182b22e89a4e6ed36"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:68665f4c17edcceecc112dfed5dbe6f92261fb9d6054b47d01bf6371a6196126"}, + {file = "pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:20152074317d9bed6b7a95ade3b7d6054845d70584216160860425f4fbd5ee9e"}, + {file = "pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9261d3ce84fa1d38ed649c3638feefeae23d32ba9182963e465d58d62203bd24"}, + {file = "pydantic_core-2.23.4-cp312-none-win32.whl", hash = "sha256:4ba762ed58e8d68657fc1281e9bb72e1c3e79cc5d464be146e260c541ec12d84"}, + {file = "pydantic_core-2.23.4-cp312-none-win_amd64.whl", hash = "sha256:97df63000f4fea395b2824da80e169731088656d1818a11b95f3b173747b6cd9"}, + {file = "pydantic_core-2.23.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7530e201d10d7d14abce4fb54cfe5b94a0aefc87da539d0346a484ead376c3cc"}, + {file = "pydantic_core-2.23.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df933278128ea1cd77772673c73954e53a1c95a4fdf41eef97c2b779271bd0bd"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cb3da3fd1b6a5d0279a01877713dbda118a2a4fc6f0d821a57da2e464793f05"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c6dcb030aefb668a2b7009c85b27f90e51e6a3b4d5c9bc4c57631292015b0d"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:696dd8d674d6ce621ab9d45b205df149399e4bb9aa34102c970b721554828510"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2971bb5ffe72cc0f555c13e19b23c85b654dd2a8f7ab493c262071377bfce9f6"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8394d940e5d400d04cad4f75c0598665cbb81aecefaca82ca85bd28264af7f9b"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0dff76e0602ca7d4cdaacc1ac4c005e0ce0dcfe095d5b5259163a80d3a10d327"}, + {file = "pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7d32706badfe136888bdea71c0def994644e09fff0bfe47441deaed8e96fdbc6"}, + {file = "pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed541d70698978a20eb63d8c5d72f2cc6d7079d9d90f6b50bad07826f1320f5f"}, + {file = "pydantic_core-2.23.4-cp313-none-win32.whl", hash = "sha256:3d5639516376dce1940ea36edf408c554475369f5da2abd45d44621cb616f769"}, + {file = "pydantic_core-2.23.4-cp313-none-win_amd64.whl", hash = "sha256:5a1504ad17ba4210df3a045132a7baeeba5a200e930f57512ee02909fc5c4cb5"}, + {file = "pydantic_core-2.23.4-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d4488a93b071c04dc20f5cecc3631fc78b9789dd72483ba15d423b5b3689b555"}, + {file = "pydantic_core-2.23.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:81965a16b675b35e1d09dd14df53f190f9129c0202356ed44ab2728b1c905658"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ffa2ebd4c8530079140dd2d7f794a9d9a73cbb8e9d59ffe24c63436efa8f271"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:61817945f2fe7d166e75fbfb28004034b48e44878177fc54d81688e7b85a3665"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:29d2c342c4bc01b88402d60189f3df065fb0dda3654744d5a165a5288a657368"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5e11661ce0fd30a6790e8bcdf263b9ec5988e95e63cf901972107efc49218b13"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d18368b137c6295db49ce7218b1a9ba15c5bc254c96d7c9f9e924a9bc7825ad"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ec4e55f79b1c4ffb2eecd8a0cfba9955a2588497d96851f4c8f99aa4a1d39b12"}, + {file = "pydantic_core-2.23.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:374a5e5049eda9e0a44c696c7ade3ff355f06b1fe0bb945ea3cac2bc336478a2"}, + {file = "pydantic_core-2.23.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5c364564d17da23db1106787675fc7af45f2f7b58b4173bfdd105564e132e6fb"}, + {file = "pydantic_core-2.23.4-cp38-none-win32.whl", hash = "sha256:d7a80d21d613eec45e3d41eb22f8f94ddc758a6c4720842dc74c0581f54993d6"}, + {file = "pydantic_core-2.23.4-cp38-none-win_amd64.whl", hash = "sha256:5f5ff8d839f4566a474a969508fe1c5e59c31c80d9e140566f9a37bba7b8d556"}, + {file = "pydantic_core-2.23.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a4fa4fc04dff799089689f4fd502ce7d59de529fc2f40a2c8836886c03e0175a"}, + {file = "pydantic_core-2.23.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0a7df63886be5e270da67e0966cf4afbae86069501d35c8c1b3b6c168f42cb36"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dcedcd19a557e182628afa1d553c3895a9f825b936415d0dbd3cd0bbcfd29b4b"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f54b118ce5de9ac21c363d9b3caa6c800341e8c47a508787e5868c6b79c9323"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86d2f57d3e1379a9525c5ab067b27dbb8a0642fb5d454e17a9ac434f9ce523e3"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:de6d1d1b9e5101508cb37ab0d972357cac5235f5c6533d1071964c47139257df"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1278e0d324f6908e872730c9102b0112477a7f7cf88b308e4fc36ce1bdb6d58c"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9a6b5099eeec78827553827f4c6b8615978bb4b6a88e5d9b93eddf8bb6790f55"}, + {file = "pydantic_core-2.23.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e55541f756f9b3ee346b840103f32779c695a19826a4c442b7954550a0972040"}, + {file = "pydantic_core-2.23.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a5c7ba8ffb6d6f8f2ab08743be203654bb1aaa8c9dcb09f82ddd34eadb695605"}, + {file = "pydantic_core-2.23.4-cp39-none-win32.whl", hash = "sha256:37b0fe330e4a58d3c58b24d91d1eb102aeec675a3db4c292ec3928ecd892a9a6"}, + {file = "pydantic_core-2.23.4-cp39-none-win_amd64.whl", hash = "sha256:1498bec4c05c9c787bde9125cfdcc63a41004ff167f495063191b863399b1a29"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f455ee30a9d61d3e1a15abd5068827773d6e4dc513e795f380cdd59932c782d5"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1e90d2e3bd2c3863d48525d297cd143fe541be8bbf6f579504b9712cb6b643ec"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e203fdf807ac7e12ab59ca2bfcabb38c7cf0b33c41efeb00f8e5da1d86af480"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e08277a400de01bc72436a0ccd02bdf596631411f592ad985dcee21445bd0068"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f220b0eea5965dec25480b6333c788fb72ce5f9129e8759ef876a1d805d00801"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d06b0c8da4f16d1d1e352134427cb194a0a6e19ad5db9161bf32b2113409e728"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ba1a0996f6c2773bd83e63f18914c1de3c9dd26d55f4ac302a7efe93fb8e7433"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:9a5bce9d23aac8f0cf0836ecfc033896aa8443b501c58d0602dbfd5bd5b37753"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:78ddaaa81421a29574a682b3179d4cf9e6d405a09b99d93ddcf7e5239c742e21"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:883a91b5dd7d26492ff2f04f40fbb652de40fcc0afe07e8129e8ae779c2110eb"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88ad334a15b32a791ea935af224b9de1bf99bcd62fabf745d5f3442199d86d59"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:233710f069d251feb12a56da21e14cca67994eab08362207785cf8c598e74577"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:19442362866a753485ba5e4be408964644dd6a09123d9416c54cd49171f50744"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:624e278a7d29b6445e4e813af92af37820fafb6dcc55c012c834f9e26f9aaaef"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f5ef8f42bec47f21d07668a043f077d507e5bf4e668d5c6dfe6aaba89de1a5b8"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:aea443fffa9fbe3af1a9ba721a87f926fe548d32cab71d188a6ede77d0ff244e"}, + {file = "pydantic_core-2.23.4.tar.gz", hash = "sha256:2584f7cf844ac4d970fba483a717dbe10c1c1c96a969bf65d61ffe94df1b2863"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" + +[[package]] +name = "pydantic-settings" +version = "2.5.2" +description = "Settings management using Pydantic" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic_settings-2.5.2-py3-none-any.whl", hash = "sha256:2c912e55fd5794a59bf8c832b9de832dcfdf4778d79ff79b708744eed499a907"}, + {file = "pydantic_settings-2.5.2.tar.gz", hash = "sha256:f90b139682bee4d2065273d5185d71d37ea46cfe57e1b5ae184fc6a0b2484ca0"}, +] + +[package.dependencies] +pydantic = ">=2.7.0" +python-dotenv = ">=0.21.0" + +[package.extras] +azure-key-vault = ["azure-identity (>=1.16.0)", "azure-keyvault-secrets (>=4.8.0)"] +toml = ["tomli (>=2.0.1)"] +yaml = ["pyyaml (>=6.0.1)"] + +[[package]] +name = "python-dotenv" +version = "1.0.1" +description = "Read key-value pairs from a .env file and set them as environment variables" +optional = false +python-versions = ">=3.8" +files = [ + {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, + {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, +] + +[package.extras] +cli = ["click (>=5.0)"] + +[[package]] +name = "sniffio" +version = "1.3.1" +description = "Sniff out which async library your code is running under" +optional = false +python-versions = ">=3.7" +files = [ + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, +] + +[[package]] +name = "soupsieve" +version = "2.6" +description = "A modern CSS selector implementation for Beautiful Soup." +optional = false +python-versions = ">=3.8" +files = [ + {file = "soupsieve-2.6-py3-none-any.whl", hash = "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9"}, + {file = "soupsieve-2.6.tar.gz", hash = "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb"}, +] + +[[package]] +name = "starlette" +version = "0.40.0" +description = "The little ASGI library that shines." +optional = false +python-versions = ">=3.8" +files = [ + {file = "starlette-0.40.0-py3-none-any.whl", hash = "sha256:c494a22fae73805376ea6bf88439783ecfba9aac88a43911b48c653437e784c4"}, + {file = "starlette-0.40.0.tar.gz", hash = "sha256:1a3139688fb298ce5e2d661d37046a66ad996ce94be4d4983be019a23a04ea35"}, +] + +[package.dependencies] +anyio = ">=3.4.0,<5" + +[package.extras] +full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7)", "pyyaml"] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, +] + +[[package]] +name = "typing-validation" +version = "1.2.11.post4" +description = "A simple library for runtime type-checking." +optional = false +python-versions = ">=3.7" +files = [ + {file = "typing_validation-1.2.11.post4-py3-none-any.whl", hash = "sha256:73dd504ddebf5210e80d5f65ba9b30efbd0fa42f266728fda7c4f0ba335c699c"}, + {file = "typing_validation-1.2.11.post4.tar.gz", hash = "sha256:7aed04ecfbda07e63b7266f90e5d096f96344f7facfe04bb081b21e4a9781670"}, +] + +[package.extras] +dev = ["mypy", "pylint", "pytest", "pytest-cov", "rich"] + +[[package]] +name = "uvicorn" +version = "0.32.0" +description = "The lightning-fast ASGI server." +optional = false +python-versions = ">=3.8" +files = [ + {file = "uvicorn-0.32.0-py3-none-any.whl", hash = "sha256:60b8f3a5ac027dcd31448f411ced12b5ef452c646f76f02f8cc3f25d8d26fd82"}, + {file = "uvicorn-0.32.0.tar.gz", hash = "sha256:f78b36b143c16f54ccdb8190d0a26b5f1901fe5a3c777e1ab29f26391af8551e"}, +] + +[package.dependencies] +click = ">=7.0" +h11 = ">=0.8" + +[package.extras] +standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] + +[[package]] +name = "validators" +version = "0.34.0" +description = "Python Data Validation for Humans™" +optional = false +python-versions = ">=3.8" +files = [ + {file = "validators-0.34.0-py3-none-any.whl", hash = "sha256:c804b476e3e6d3786fa07a30073a4ef694e617805eb1946ceee3fe5a9b8b1321"}, + {file = "validators-0.34.0.tar.gz", hash = "sha256:647fe407b45af9a74d245b943b18e6a816acf4926974278f6dd617778e1e781f"}, +] + +[package.extras] +crypto-eth-addresses = ["eth-hash[pycryptodome] (>=0.7.0)"] + +[metadata] +lock-version = "2.0" +python-versions = "^3.12" +content-hash = "14540c59bc6f85874310da0d474a68ff99039c886ed9641d926aebc305de4799" diff --git a/backend/pyproject.toml b/backend/pyproject.toml new file mode 100644 index 0000000..ae28dbe --- /dev/null +++ b/backend/pyproject.toml @@ -0,0 +1,23 @@ +[tool.poetry] +name = "orgbook-publisher" +version = "0.1.0" +description = "" +authors = ["PatStLouis "] +readme = "README.md" + +[tool.poetry.dependencies] +python = "^3.12" +fastapi = "^0.115.2" +uvicorn = "^0.32.0" +pydantic-settings = "^2.5.2" +validators = "^0.34.0" +aries-askar = "^0.3.2" +multiformats = "^0.3.1.post4" +canonicaljson = "^2.0.0" +bitstring = "^4.2.3" +bs4 = "^0.0.2" + + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/backend/requirements.txt b/backend/requirements.txt index d79ffd1..c39b06c 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -3,6 +3,7 @@ anyio==4.4.0 aries-askar==0.3.2 base58==2.1.1 bases==0.3.0 +beautifulsoup4==4.12.3 bitarray==2.9.2 bitstring==4.2.3 cached-property==1.5.2 @@ -30,10 +31,12 @@ python-dotenv==1.0.1 requests==2.32.3 ruff==0.6.8 sniffio==1.3.1 +soupsieve==2.6 starlette==0.38.4 typing-validation==1.2.11.post4 typing_extensions==4.12.2 untp_models==0.0.2 urllib3==2.2.2 uvicorn==0.30.6 +validators==0.34.0 wrapt==1.16.0 diff --git a/charts/orgbook-publisher/Chart.yaml b/charts/orgbook-publisher/Chart.yaml new file mode 100644 index 0000000..5cfeb3c --- /dev/null +++ b/charts/orgbook-publisher/Chart.yaml @@ -0,0 +1,22 @@ +apiVersion: v2 +name: orgbook-publisher +description: An api server to register and manage credentials. +type: application +version: 0.0.1 +appVersion: "0.0.1" + +maintainers: + - name: PatStLouis + email: patrick.st-louis@opsecid.ca + url: https://github.com/PatStLouis + +dependencies: + - name: postgresql + version: 11.9.13 + repository: https://charts.bitnami.com/bitnami/ + condition: postgresql.enabled + - name: common + repository: https://charts.bitnami.com/bitnami/ + tags: + - bitnami-common + version: 2.x.x diff --git a/charts/orgbook-publisher/templates/_helpers.tpl b/charts/orgbook-publisher/templates/_helpers.tpl new file mode 100644 index 0000000..995c361 --- /dev/null +++ b/charts/orgbook-publisher/templates/_helpers.tpl @@ -0,0 +1,86 @@ +{{- define "global.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{- define "global.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{- define "global.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{- define "common.labels" -}} +app: {{ include "global.name" . }} +helm.sh/chart: {{ include "global.chart" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +{{- end }} + +{{- define "common.selectorLabels" -}} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + + +{{/* +Returns a secret if it already in Kubernetes, otherwise it creates +it randomly. +*/}} +{{- define "getOrGeneratePass" }} +{{- $len := (default 16 .Length) | int -}} +{{- $obj := (lookup "v1" .Kind .Namespace .Name).data -}} +{{- if $obj }} +{{- index $obj .Key -}} +{{- else if (eq (lower .Kind) "secret") -}} +{{- randAlphaNum $len | b64enc -}} +{{- else -}} +{{- randAlphaNum $len -}} +{{- end -}} +{{- end }} + + +{{/* BACKEND */}} + +{{- define "backend.fullname" -}} +{{ template "global.fullname" . }} +{{- end -}} + +{{- define "backend.selectorLabels" -}} +app.kubernetes.io/name: {{ include "backend.fullname" . }} +{{ include "common.selectorLabels" . }} +{{- end -}} + +{{- define "backend.labels" -}} +{{ include "common.labels" . }} +{{ include "backend.selectorLabels" . }} +{{- end -}} + +{{/* POSTGRESQL */}} +{{- define "global.postgresql.fullname" -}} +{{- if .Values.postgresql.fullnameOverride }} +{{- .Values.postgresql.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $postgresContext := dict "Values" .Values.postgresql "Release" .Release "Chart" (dict "Name" "postgresql") -}} +{{ template "postgresql.primary.fullname" $postgresContext }} +{{- end -}} +{{- end -}} + +{{- define "postgresql.selectorLabels" -}} +app.kubernetes.io/name: {{ include "global.postgresql.fullname" . }} +{{ include "common.selectorLabels" . }} +{{- end -}} + +{{- define "postgresql.labels" -}} +{{ include "common.labels" . }} +{{ include "postgresql.selectorLabels" . }} +{{- end -}} \ No newline at end of file diff --git a/charts/orgbook-publisher/templates/backend/deployment.yaml b/charts/orgbook-publisher/templates/backend/deployment.yaml new file mode 100644 index 0000000..cd2d7de --- /dev/null +++ b/charts/orgbook-publisher/templates/backend/deployment.yaml @@ -0,0 +1,74 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "backend.fullname" . }} + labels: + {{- include "backend.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.backend.replicaCount }} + selector: + matchLabels: + {{- include "backend.selectorLabels" . | nindent 6 }} + template: + metadata: + annotations: + {{- toYaml .Values.backend.podAnnotations | nindent 8 }} + labels: + {{- include "backend.selectorLabels" . | nindent 8 }} + spec: + imagePullSecrets: + {{- toYaml .Values.backend.image.pullSecrets | nindent 8 }} + securityContext: + {{- toYaml .Values.backend.podSecurityContext | nindent 8 }} + containers: + - name: {{ include "backend.fullname" . }} + securityContext: + {{- toYaml .Values.backend.containerSecurityContext | nindent 12 }} + image: "{{ .Values.backend.image.repository }}:{{ .Values.backend.image.tag }}" + imagePullPolicy: {{ .Values.backend.image.pullPolicy }} + env: + - name: SECRET_KEY + valueFrom: + secretKeyRef: + name: {{ include "backend.fullname" . }} + key: secret-key + - name: DOMAIN + value: {{ .Values.backend.host }} + - name: ENDORSER_MULTIKEY + value: {{ .Values.backend.environment.ENDORSER_MULTIKEY }} + - name: POSTGRES_USER + value: {{ .Values.postgresql.auth.username }} + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .Values.postgresql.nameOverride }} + key: password + - name: POSTGRES_SERVER_NAME + value: {{ include "global.postgresql.fullname" . }} + - name: POSTGRES_SERVER_PORT + value: {{ .Values.postgresql.primary.service.ports.postgresql | quote }} + ports: + - name: api + containerPort: {{ .Values.backend.service.apiPort }} + protocol: TCP + livenessProbe: + httpGet: + path: /server/status + port: {{ .Values.backend.service.apiPort }} + failureThreshold: 2 + initialDelaySeconds: 60 + periodSeconds: 5 + readinessProbe: + httpGet: + path: /server/status + port: {{ .Values.backend.service.apiPort }} + initialDelaySeconds: 60 + resources: + {{- toYaml .Values.backend.resources | nindent 12 }} + nodeSelector: + {{- toYaml .Values.backend.nodeSelector | nindent 8 }} + affinity: + {{- toYaml .Values.backend.affinity | nindent 8 }} + tolerations: + {{- toYaml .Values.backend.tolerations | nindent 8 }} \ No newline at end of file diff --git a/charts/orgbook-publisher/templates/backend/ingress.yaml b/charts/orgbook-publisher/templates/backend/ingress.yaml new file mode 100644 index 0000000..1b95be0 --- /dev/null +++ b/charts/orgbook-publisher/templates/backend/ingress.yaml @@ -0,0 +1,32 @@ +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "backend.fullname" . }} + labels: + {{- if .Values.ingress.labels }} + {{- toYaml .Values.ingress.labels | nindent 4 }} + {{- end }} + {{- include "backend.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if .Values.ingress.tls }} + tls: + - hosts: + - {{ .Values.backend.host | quote }} + secretName: {{ .Values.fullnameOverride }}-tls + {{- end }} + rules: + - host: {{ .Values.backend.host | quote }} + http: + paths: + - backend: + service: + name: {{ include "backend.fullname" . }} + port: + number: {{ .Values.backend.service.servicePort }} + path: / + pathType: ImplementationSpecific \ No newline at end of file diff --git a/charts/orgbook-publisher/templates/backend/networkpolicy.yaml b/charts/orgbook-publisher/templates/backend/networkpolicy.yaml new file mode 100644 index 0000000..6134b77 --- /dev/null +++ b/charts/orgbook-publisher/templates/backend/networkpolicy.yaml @@ -0,0 +1,21 @@ +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: {{ include "backend.fullname" . }}-ingress + labels: + {{- include "backend.labels" . | nindent 4 }} +spec: + podSelector: + matchLabels: + {{- include "backend.selectorLabels" . | nindent 6 }} + ingress: + - from: + - namespaceSelector: + matchLabels: + {{- toYaml .Values.networkPolicy.ingress.namespaceSelector | nindent 14 }} + - podSelector: + matchLabels: + {{- toYaml .Values.backend.networkPolicy.ingress.podSelector | nindent 14 }} + policyTypes: + - Ingress \ No newline at end of file diff --git a/charts/orgbook-publisher/templates/backend/secret.yaml b/charts/orgbook-publisher/templates/backend/secret.yaml new file mode 100644 index 0000000..d8b90f9 --- /dev/null +++ b/charts/orgbook-publisher/templates/backend/secret.yaml @@ -0,0 +1,13 @@ +--- +apiVersion: v1 +kind: Secret +metadata: + annotations: + "helm.sh/resource-policy": keep + name: {{ include "backend.fullname" . }} + labels: + {{- include "backend.labels" . | nindent 4 }} + namespace: {{ .Release.Namespace }} +type: Opaque +data: + secret-key: {{ include "getOrGeneratePass" (dict "Namespace" .Release.Namespace "Kind" "Secret" "Name" (include "backend.fullname" .) "Key" "secret-key" "Length" 32) }} diff --git a/charts/orgbook-publisher/templates/backend/service.yaml b/charts/orgbook-publisher/templates/backend/service.yaml new file mode 100644 index 0000000..a4ca7cc --- /dev/null +++ b/charts/orgbook-publisher/templates/backend/service.yaml @@ -0,0 +1,16 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ include "backend.fullname" . }} + labels: + {{- include "backend.labels" . | nindent 4 }} +spec: + type: {{ .Values.backend.service.type }} + selector: + {{- include "backend.selectorLabels" . | nindent 4 }} + ports: + - port: {{ .Values.backend.service.servicePort }} + targetPort: {{ .Values.backend.service.apiPort }} + protocol: TCP + name: api \ No newline at end of file diff --git a/charts/orgbook-publisher/templates/postgresql/networkpolicy.yaml b/charts/orgbook-publisher/templates/postgresql/networkpolicy.yaml new file mode 100644 index 0000000..89d7ea0 --- /dev/null +++ b/charts/orgbook-publisher/templates/postgresql/networkpolicy.yaml @@ -0,0 +1,21 @@ +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: {{ include "global.postgresql.fullname" . }} + labels: + {{- include "postgresql.labels" . | nindent 4 }} +spec: + podSelector: + matchLabels: + {{- include "postgresql.selectorLabels" . | nindent 6 }} + ingress: + - ports: + - protocol: TCP + port: {{ .Values.postgresql.primary.service.ports.postgresql }} + from: + - podSelector: + matchLabels: + {{- include "backend.selectorLabels" . | nindent 14 }} + policyTypes: + - Ingress \ No newline at end of file diff --git a/charts/orgbook-publisher/values.yaml b/charts/orgbook-publisher/values.yaml new file mode 100644 index 0000000..1039215 --- /dev/null +++ b/charts/orgbook-publisher/values.yaml @@ -0,0 +1,93 @@ +--- +nameOverride: "orgbook-publisher" +fullnameOverride: "orgbook-publisher" + +selectorLabels: {} + +ingress: + tls: false + labels: [] + annotations: [] + +networkPolicy: + ingress: + namespaceSelector: [] + +backend: + image: + repository: ghcr.io/OpSecId/orgbook-publisher + tag: 0.0.1 + pullPolicy: IfNotPresent + pullSecrets: [] + # host is required when enabling TLS in the ingress + # host: publisher.myapp.example + + environment: + TRACTION_API_URL: "" + TRACTION_API_KEY: "" + TRACTION_TENANT_ID: "" + ORGBOOK_URL: "" + TDW_SERVER_URL: "" + TDW_ENDORSER_MULTIKEY: "" + + replicaCount: 1 + + podAnnotations: {} + podSecurityContext: {} + containerSecurityContext: {} + + service: + type: ClusterIP + apiPort: 8000 + servicePort: 8000 + + resources: + limits: + cpu: 100m + memory: 512Mi + requests: + cpu: 10m + memory: 128Mi + + networkPolicy: + ingress: + podSelector: {} + +postgresql: + enabled: true + fullnameOverride: "orgbook-publisher-postgresql" + nameOverride: "orgbook-publisher-postgresql" + architecture: standalone + auth: + enablePostgresUser: true + existingSecret: "" + secretKeys: + adminPasswordKey: admin-password + userPasswordKey: database-password + username: "orgbook-publisher" + + ## PostgreSQL Primary parameters + primary: + persistence: + enabled: true + size: 1Gi + containerSecurityContext: + enabled: false + podSecurityContext: + enabled: false + resources: + limits: + cpu: 800m + memory: 500Mi + requests: + cpu: 100m + memory: 100Mi + service: + ports: + postgresql: 5432 + extendedConfiguration: | + max_connections = 500 + + networkPolicy: + ingress: + podSelector: {}