diff --git a/.env.ci b/.env.ci index fe6e10f8..f448185b 100644 --- a/.env.ci +++ b/.env.ci @@ -8,6 +8,7 @@ HF_TOKEN="Hugging Face API Token" ENVIRONMENT = "mainnet" NILAI_GUNICORN_WORKERS = 10 +AUTH_STRATEGY = "nuc" # The domain name of the server # - It must be written as "localhost" or "test.nilai.nillion" @@ -18,7 +19,8 @@ NILAI_SERVER_DOMAIN = "localhost" ATTESTATION_HOST = "attestation" ATTESTATION_PORT = 8080 - +# nilAuth Trusted URLs +NILAUTH_TRUSTED_ROOT_ISSUERS = "http://nilauth:30921" # Postgres Docker Compose Config POSTGRES_HOST = "postgres" diff --git a/.env.sample b/.env.sample index 5c523ef4..a9c428fd 100644 --- a/.env.sample +++ b/.env.sample @@ -8,6 +8,8 @@ HF_TOKEN="Hugging Face API Token" ENVIRONMENT = "mainnet" NILAI_GUNICORN_WORKERS = 10 +AUTH_STRATEGY = "api_key" + # The domain name of the server # - It must be written as "localhost" or "test.nilai.nillion" # - Do not put "https://" or "http://" in the domain name or / at the end @@ -17,6 +19,9 @@ NILAI_SERVER_DOMAIN = "localhost" ATTESTATION_HOST = "attestation" ATTESTATION_PORT = 8080 +# nilAuth Trusted URLs +NILAUTH_TRUSTED_ROOT_ISSUERS = "http://localhost:30921" + # Postgres Docker Compose Config POSTGRES_HOST = "postgres" POSTGRES_USER = "user" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 815d0bc0..42a51ad1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -92,9 +92,10 @@ jobs: with: enable-cache: true cache-dependency-glob: "**/pyproject.toml" - - name: Install dependencies - run: uv sync + run: | + apt-get update && apt-get install curl git pkg-config automake file -y + uv sync - name: Build vllm run: docker build -t nillion/nilai-vllm:latest -f docker/vllm.Dockerfile . @@ -103,7 +104,7 @@ jobs: run: docker build -t nillion/nilai-attestation:latest -f docker/attestation.Dockerfile . - name: Build nilal API - run: docker build -t nillion/nilai-api:latest -f docker/api.Dockerfile --target nilai . + run: docker build -t nillion/nilai-api:latest -f docker/api.Dockerfile --target nilai --platform linux/amd64 . - name: Create .env run: | @@ -126,7 +127,6 @@ jobs: - name: Run E2E tests run: | set -e - export AUTH_TOKEN=$(docker exec nilai-api uv run src/nilai_api/commands/add_user.py --name test1 --email test1@test.com --ratelimit-minute 1000 --ratelimit-hour 1000 --ratelimit-day 1000 | jq ".apikey" -r) export ENVIRONMENT=ci uv run pytest -v tests/e2e diff --git a/.gitignore b/.gitignore index e53cf46b..f1592719 100644 --- a/.gitignore +++ b/.gitignore @@ -170,3 +170,6 @@ grafana/runtime-data/* prometheus/data/* !prometheus/data/.gitkeep + +private_key.key +private_key.key.lock diff --git a/README.md b/README.md index 99eae5c5..271f8213 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ docker build -t nillion/nilai-attestation:latest -f docker/attestation.Dockerfil # Build vLLM docker container docker build -t nillion/nilai-vllm:latest -f docker/vllm.Dockerfile . # Build nilai_api container -docker build -t nillion/nilai-api:latest -f docker/api.Dockerfile --target nilai . +docker build -t nillion/nilai-api:latest -f docker/api.Dockerfile --target nilai --platform linux/amd64 . ``` To deploy: ```shell @@ -195,6 +195,8 @@ git checkout v0.7.3 # We use v0.7.3 # Build vLLM OpenAI (vllm folder) cd vllm docker build -f Dockerfile.arm -t vllm/vllm-openai . --shm-size=4g +# Build nilai attestation container +docker build -t nillion/nilai-attestation:latest -f docker/attestation.Dockerfile . # Build vLLM docker container (root folder) docker build -t nillion/nilai-vllm:latest -f docker/vllm.Dockerfile . # Build nilai_api container diff --git a/docker-compose.dev.macos.yml b/docker-compose.dev.macos.yml deleted file mode 100644 index 63dabcfe..00000000 --- a/docker-compose.dev.macos.yml +++ /dev/null @@ -1,20 +0,0 @@ -services: - api: - platform: linux/amd64 - ports: - - "8080:8080" - volumes: - - ./nilai-api/:/app/nilai-api/ - - ./packages/:/app/packages/ - attestation: - ports: - - "8081:8080" - volumes: - - ./nilai-attestation/:/app/nilai-attestation/ - - ./packages/:/app/packages/ - redis: - ports: - - "6379:6379" - postgres: - ports: - - "5432:5432" diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index da88e4f5..efd40057 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -1,10 +1,13 @@ services: api: + platform: linux/amd64 # for macOS to force running on Rosetta 2 ports: - "8080:8080" volumes: - ./nilai-api/:/app/nilai-api/ - ./packages/:/app/packages/ + networks: + - nilauth attestation: ports: - "8081:8080" @@ -20,3 +23,49 @@ services: grafana: ports: - "3000:3000" + + nilauth-postgres: + image: postgres:17 + environment: + POSTGRES_PASSWORD: postgres + ports: + - "30432:5432" + networks: + - nilauth + + nilchain: + image: ghcr.io/nillionnetwork/nilchain-devnet:v0.1.0 + restart: unless-stopped + shm_size: 128mb + ports: + - "26648:26648" # JSON RPC + - "26649:26649" # gRPC + - "26650:26650" # REST + networks: + - nilauth + + nilauth: + image: public.ecr.aws/k5d9x2g2/nilauth:latest + depends_on: + - nilauth-postgres + - nilchain + volumes: + - ./docker/nilauth/config.yaml:/opt/config.yaml + command: ["--config-file", "/opt/config.yaml"] + ports: + - "30921:30921" # main server + - "30922:30022" # metrics server + networks: + - nilauth + + token-price-api: + image: caddy:2 + ports: + - "30923:80" + command: | + caddy respond --listen :80 --body '{"nillion":{"usd":1}}' --header "Content-Type: application/json" + networks: + - nilauth + +networks: + nilauth: diff --git a/docker/api.Dockerfile b/docker/api.Dockerfile index 1e39e595..69df609c 100644 --- a/docker/api.Dockerfile +++ b/docker/api.Dockerfile @@ -5,7 +5,7 @@ COPY --link . /app/ WORKDIR /app/nilai-api/ RUN apt-get update && \ -apt-get install build-essential curl -y && \ +apt-get install build-essential curl git pkg-config automake file -y && \ apt-get clean && \ apt-get autoremove && \ rm -rf /var/lib/apt/lists/* && \ diff --git a/docker/nilauth/config.yaml b/docker/nilauth/config.yaml new file mode 100644 index 00000000..fe42c669 --- /dev/null +++ b/docker/nilauth/config.yaml @@ -0,0 +1,23 @@ +server: + bind_endpoint: 0.0.0.0:30921 + +private_key: + hex: 7c5c8d3114ac2410249ca2baae7dec86ac2950a389ac44a5fdca8941b92b6c86 + +metrics: + bind_endpoint: 0.0.0.0:30022 + +payments: + nilchain_url: http://nilchain:26648 + + subscriptions: + renewal_threshold_seconds: 1000 + length_seconds: 120 + dollar_cost: 1 + + token_price: + base_url: http://token-price-api/api/v3/simple/price + api_key: abc + +postgres: + url: postgres://postgres:postgres@nilauth-postgres:5432/postgres diff --git a/nilai-api/alembic/versions/ca76e3ebe6ee_fix_remove_mail_and_adjust_field_lengths.py b/nilai-api/alembic/versions/ca76e3ebe6ee_fix_remove_mail_and_adjust_field_lengths.py new file mode 100644 index 00000000..4d176609 --- /dev/null +++ b/nilai-api/alembic/versions/ca76e3ebe6ee_fix_remove_mail_and_adjust_field_lengths.py @@ -0,0 +1,82 @@ +"""fix: remove mail and adjust field lengths + +Revision ID: ca76e3ebe6ee +Revises: da89d3230653 +Create Date: 2025-04-25 09:08:37.775973 + +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = "ca76e3ebe6ee" +down_revision: Union[str, None] = "da89d3230653" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column( + "query_logs", + "userid", + existing_type=sa.VARCHAR(length=50), + type_=sa.String(length=75), + existing_nullable=False, + ) + op.alter_column( + "users", + "userid", + existing_type=sa.VARCHAR(length=50), + type_=sa.String(length=75), + existing_nullable=False, + ) + op.alter_column( + "users", + "apikey", + existing_type=sa.VARCHAR(length=50), + type_=sa.String(length=75), + existing_nullable=False, + ) + op.drop_index("ix_users_email", table_name="users") + op.drop_index("ix_users_apikey", table_name="users") + op.create_index(op.f("ix_users_apikey"), "users", ["apikey"], unique=False) + op.drop_column("users", "email") + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column( + "users", + sa.Column("email", sa.VARCHAR(length=255), autoincrement=False, nullable=False), + ) + op.drop_index(op.f("ix_users_apikey"), table_name="users") + op.create_index("ix_users_apikey", "users", ["apikey"], unique=True) + op.create_index("ix_users_email", "users", ["email"], unique=True) + op.alter_column( + "users", + "apikey", + existing_type=sa.String(length=75), + type_=sa.VARCHAR(length=50), + existing_nullable=False, + ) + op.alter_column( + "users", + "userid", + existing_type=sa.String(length=75), + type_=sa.VARCHAR(length=50), + existing_nullable=False, + ) + op.alter_column( + "query_logs", + "userid", + existing_type=sa.String(length=75), + type_=sa.VARCHAR(length=50), + existing_nullable=False, + ) + # ### end Alembic commands ### diff --git a/nilai-api/pyproject.toml b/nilai-api/pyproject.toml index bc44d7be..7dbae40e 100644 --- a/nilai-api/pyproject.toml +++ b/nilai-api/pyproject.toml @@ -31,6 +31,7 @@ dependencies = [ "verifier", "web3>=7.8.0", "click>=8.1.8", + "nuc", ] @@ -40,3 +41,4 @@ build-backend = "hatchling.build" [tool.uv.sources] nilai-common = { workspace = true } +nuc = { git = "https://github.com/NillionNetwork/nuc-py.git" } diff --git a/nilai-api/src/nilai_api/attestation/__init__.py b/nilai-api/src/nilai_api/attestation/__init__.py index b0fb8224..795be454 100644 --- a/nilai-api/src/nilai_api/attestation/__init__.py +++ b/nilai-api/src/nilai_api/attestation/__init__.py @@ -1,6 +1,6 @@ from fastapi import HTTPException import httpx -from nilai_common import SETTINGS, Nonce, AttestationReport +from nilai_common import Nonce, AttestationReport, SETTINGS from nilai_common.logger import setup_logger logger = setup_logger(__name__) diff --git a/nilai-api/src/nilai_api/auth/__init__.py b/nilai-api/src/nilai_api/auth/__init__.py index 2287feb1..17fa9146 100644 --- a/nilai-api/src/nilai_api/auth/__init__.py +++ b/nilai-api/src/nilai_api/auth/__init__.py @@ -8,6 +8,8 @@ from nilai_api.db.users import UserManager, UserModel from nilai_api.auth.strategies import STRATEGIES +from nuc.validate import ValidationException + logger = getLogger(__name__) bearer_scheme = HTTPBearer() @@ -38,6 +40,8 @@ async def get_user( raise e except ValueError as e: raise AuthenticationError(detail="Authentication failed: " + str(e)) + except ValidationException as e: + raise AuthenticationError(detail="NUC validation failed: " + str(e)) except Exception as e: raise AuthenticationError(detail="Unexpected authentication error: " + str(e)) diff --git a/nilai-api/src/nilai_api/auth/nuc.py b/nilai-api/src/nilai_api/auth/nuc.py new file mode 100644 index 00000000..0b9ab92a --- /dev/null +++ b/nilai-api/src/nilai_api/auth/nuc.py @@ -0,0 +1,74 @@ +from typing import Tuple +from nuc.validate import NucTokenValidator, ValidationParameters, InvocationRequirement +from nuc.envelope import NucTokenEnvelope +from nuc.nilauth import NilauthClient +from nuc.token import Did, NucToken +from functools import lru_cache +from nilai_api.config import NILAUTH_TRUSTED_ROOT_ISSUERS +from nilai_api.state import state + +from nilai_common.logger import setup_logger + +logger = setup_logger(__name__) + + +@lru_cache(maxsize=1) +def get_validator() -> NucTokenValidator: + """ + Get the public key of the Nilauth service + + Returns: + A NucTokenValidator that can be used to validate NUC tokens + The validator is cached to avoid re-initializing the validator for each request + """ + try: + nilauth_public_keys = [ + Did(NilauthClient(key).about().public_key.serialize()) + for key in NILAUTH_TRUSTED_ROOT_ISSUERS + ] + except Exception as e: + logger.error(f"Error getting validator: {e}") + raise e + + return NucTokenValidator(nilauth_public_keys) + + +@lru_cache(maxsize=1) +def get_validation_parameters() -> ValidationParameters: + """ + Get the validation parameters for the NUC token + + Returns: + The validation parameters for the NUC token + """ + default_parameters = ValidationParameters.default() + default_parameters.token_requirements = InvocationRequirement( + audience=Did(state.public_key.serialize()) + ) + return default_parameters + + +def validate_nuc(nuc_token: str) -> Tuple[str, str]: + """ + Validate a NUC token + + Args: + nuc_token: The NUC token to validate + + Returns: + The subscription holder and the user that the NUC token is for in hex format (str, str) + """ + nuc_token_envelope = NucTokenEnvelope.parse(nuc_token) + logger.info(f"Validating NUC token: {nuc_token_envelope.token.token}") + logger.info(f"Validation parameters: {get_validation_parameters()}") + logger.info(f"Public key: {state.public_key.serialize()}") + get_validator().validate(nuc_token_envelope, get_validation_parameters()) + token: NucToken = nuc_token_envelope.token.token + + # Validate the + # Return the subject of the token, the subscription holder + subscription_holder = token.subject.public_key.hex() + user = token.issuer.public_key.hex() + logger.info(f"Subscription holder: {subscription_holder}") + logger.info(f"User: {user}") + return subscription_holder, user diff --git a/nilai-api/src/nilai_api/auth/strategies.py b/nilai-api/src/nilai_api/auth/strategies.py index f343b212..871ad2ce 100644 --- a/nilai-api/src/nilai_api/auth/strategies.py +++ b/nilai-api/src/nilai_api/auth/strategies.py @@ -1,12 +1,17 @@ from nilai_api.db.users import UserManager, UserModel from nilai_api.auth.jwt import validate_jwt +from nilai_api.auth.nuc import validate_nuc +# All strategies must return a UserModel +# The strategies can raise any exception, which will be caught and converted to an AuthenticationError +# The exception detail will be passed to the client -async def api_key_strategy(api_key): + +async def api_key_strategy(api_key) -> UserModel: return await UserManager.check_api_key(api_key) -async def jwt_strategy(jwt_creds): +async def jwt_strategy(jwt_creds) -> UserModel: result = validate_jwt(jwt_creds) user = await UserManager.check_api_key(result.user_address) if user: @@ -14,16 +19,35 @@ async def jwt_strategy(jwt_creds): user = UserModel( userid=result.user_address, name=result.pub_key, - email=result.pub_key, apikey=result.user_address, ) await UserManager.insert_user_model(user) return user +async def nuc_strategy(nuc_token) -> UserModel: + """ + Validate a NUC token and return the user model + """ + subscription_holder, user = validate_nuc(nuc_token) + + user_model = await UserManager.check_user(user) + if user_model: + return user_model + + user_model = UserModel( + userid=user, + name=user, + apikey=subscription_holder, + ) + await UserManager.insert_user_model(user_model) + return user_model + + STRATEGIES = { "api_key": api_key_strategy, "jwt": jwt_strategy, + "nuc": nuc_strategy, } __all__ = ["STRATEGIES"] diff --git a/nilai-api/src/nilai_api/commands/add_user.py b/nilai-api/src/nilai_api/commands/add_user.py index 172ae961..521c546d 100644 --- a/nilai-api/src/nilai_api/commands/add_user.py +++ b/nilai-api/src/nilai_api/commands/add_user.py @@ -7,7 +7,6 @@ @click.command() @click.option("--name", type=str, required=True, help="User Name") -@click.option("--email", type=str, required=True, help="User Email") @click.option("--apikey", type=str, help="API Key") @click.option("--userid", type=str, help="User Id") @click.option("--ratelimit-day", type=int, help="number of request per day") @@ -15,7 +14,6 @@ @click.option("--ratelimit-minute", type=int, help="number of request per minute") def main( name, - email, apikey: str | None, userid: str | None, ratelimit_day: int | None, @@ -25,7 +23,6 @@ def main( async def add_user(): user = await UserManager.insert_user( name, - email, apikey, userid, ratelimit_day, @@ -36,7 +33,6 @@ async def add_user(): { "userid": user.userid, "name": user.name, - "email": user.email, "apikey": user.apikey, "ratelimit_day": user.ratelimit_day, "ratelimit_hour": user.ratelimit_hour, diff --git a/nilai-api/src/nilai_api/config/__init__.py b/nilai-api/src/nilai_api/config/__init__.py index 7b1ec498..bcd7cd79 100644 --- a/nilai-api/src/nilai_api/config/__init__.py +++ b/nilai-api/src/nilai_api/config/__init__.py @@ -21,7 +21,9 @@ DB_NAME = os.getenv("POSTGRES_DB", "nilai_users") -AUTH_STRATEGY = "api_key" +NILAUTH_TRUSTED_ROOT_ISSUERS = os.getenv("NILAUTH_TRUSTED_ROOT_ISSUERS", "").split(",") + +AUTH_STRATEGY = os.getenv("AUTH_STRATEGY", "api_key") if ENVIRONMENT == "mainnet": @@ -29,5 +31,5 @@ elif ENVIRONMENT == "testnet": from .testnet import * # noqa else: - # default to testnet - from .testnet import * # noqa + # default to mainnet with no limits + from .mainnet import * # noqa diff --git a/nilai-api/src/nilai_api/config/mainnet.py b/nilai-api/src/nilai_api/config/mainnet.py index 2a27af1a..3c1552f6 100644 --- a/nilai-api/src/nilai_api/config/mainnet.py +++ b/nilai-api/src/nilai_api/config/mainnet.py @@ -1,5 +1,3 @@ -AUTH_STRATEGY = "api_key" - # It defines the number of concurrent requests allowed for each model. # At a same point of time, if all models are available, # there can be 45 + 30 + 15 + 5 = 85 concurrent requests in the system diff --git a/nilai-api/src/nilai_api/config/testnet.py b/nilai-api/src/nilai_api/config/testnet.py index 8efcb7df..7e6bdf1b 100644 --- a/nilai-api/src/nilai_api/config/testnet.py +++ b/nilai-api/src/nilai_api/config/testnet.py @@ -1,5 +1,3 @@ -AUTH_STRATEGY = "jwt" - # It defines the number of concurrent requests allowed for each model. # At a same point of time, if all models are available, # there can be 10 + 10 + 5 + 5 = 30 concurrent requests in the system diff --git a/nilai-api/src/nilai_api/crypto.py b/nilai-api/src/nilai_api/crypto.py index a58d172a..cab6ce1b 100644 --- a/nilai-api/src/nilai_api/crypto.py +++ b/nilai-api/src/nilai_api/crypto.py @@ -1,25 +1,80 @@ +import os +import fcntl from base64 import b64encode -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives import hashes, serialization -from cryptography.hazmat.primitives.asymmetric import ec +from secp256k1 import PrivateKey, PublicKey +PRIVATE_KEY_PATH = "private_key.key" -def generate_key_pair(): - private_key = ec.generate_private_key(ec.SECP256R1(), default_backend()) - public_key = private_key.public_key() - verifying_key = b64encode( - public_key.public_bytes( - encoding=serialization.Encoding.PEM, - format=serialization.PublicFormat.SubjectPublicKeyInfo, - ) - ).decode() - return private_key, public_key, verifying_key +def generate_key_pair() -> tuple[PrivateKey, PublicKey, str]: + """ + Generate or load a key pair safely, preventing concurrent access using fcntl. -def sign_message(private_key, message): - return private_key.sign(message.encode(), ec.ECDSA(hashes.SHA256())) + Returns: + tuple[PrivateKey, PublicKey, str]: Private key, public key, and base64-encoded public key. + """ + private_key: PrivateKey + # Use a separate lock file to avoid corrupting the key file itself + lock_path = PRIVATE_KEY_PATH + ".lock" -def verify_signature(public_key, message, signature): - public_key.verify(signature, message.encode(), ec.ECDSA(hashes.SHA256())) + # Ensure the lock file exists + open(lock_path, "a").close() + + with open(lock_path, "r+") as lock_file: + fcntl.flock(lock_file, fcntl.LOCK_EX) + + if os.path.exists(PRIVATE_KEY_PATH): + with open(PRIVATE_KEY_PATH, "rb") as f: + private_key_bytes: bytes = f.read() + if not private_key_bytes: + raise ValueError("Private key file is empty or corrupted.") + private_key = PrivateKey(private_key_bytes) + else: + private_key = PrivateKey() + with open(PRIVATE_KEY_PATH, "wb") as f: + private_key_bytes: bytes = private_key.private_key # type: ignore + f.write(private_key_bytes) + + # Release the lock + fcntl.flock(lock_file, fcntl.LOCK_UN) + + public_key = private_key.pubkey + if public_key is None: + raise ValueError("Keypair generation failed: Public key is None") + + b64_public_key: str = b64encode(public_key.serialize()).decode() + return private_key, public_key, b64_public_key + + +def sign_message(private_key: PrivateKey, message: str) -> bytes: + """ + Sign a message using the private key. + + Args: + private_key (PrivateKey): The private key to sign the message with. + message (str): The message to sign. + + Returns: + bytes: The signature of the message. + """ + signature = private_key.ecdsa_sign(message.encode()) + serialized_signature: bytes = private_key.ecdsa_serialize(signature) + return serialized_signature + + +def verify_signature(public_key: PublicKey, message: str, signature: bytes) -> bool: + """ + Verify a signature using the public key. + + Args: + public_key (PublicKey): The public key to verify the signature with. + message (str): The message to verify the signature with. + signature (bytes): The signature to verify. + + Returns: + bool: True if the signature is valid, False otherwise. + """ + sig = public_key.ecdsa_deserialize(signature) + return public_key.ecdsa_verify(message.encode(), sig) diff --git a/nilai-api/src/nilai_api/db/logs.py b/nilai-api/src/nilai_api/db/logs.py index c4e4b854..36ab6e7c 100644 --- a/nilai-api/src/nilai_api/db/logs.py +++ b/nilai-api/src/nilai_api/db/logs.py @@ -17,7 +17,7 @@ class QueryLog(Base): id: int = Column(Integer, primary_key=True, autoincrement=True) # type: ignore userid: str = Column( - String(36), ForeignKey(UserModel.userid), nullable=False, index=True + String(75), ForeignKey(UserModel.userid), nullable=False, index=True ) # type: ignore query_timestamp: datetime = Column( DateTime, server_default=sqlalchemy.func.now(), nullable=False diff --git a/nilai-api/src/nilai_api/db/users.py b/nilai-api/src/nilai_api/db/users.py index 048ac97e..b94e796d 100644 --- a/nilai-api/src/nilai_api/db/users.py +++ b/nilai-api/src/nilai_api/db/users.py @@ -23,10 +23,9 @@ class UserModel(Base): __tablename__ = "users" - userid: str = Column(String(50), primary_key=True, index=True) # type: ignore + userid: str = Column(String(75), primary_key=True, index=True) # type: ignore name: str = Column(String(100), nullable=False) # type: ignore - email: str = Column(String(255), unique=True, nullable=False, index=True) # type: ignore - apikey: str = Column(String(50), unique=True, nullable=False, index=True) # type: ignore + apikey: str = Column(String(75), unique=False, nullable=False, index=True) # type: ignore prompt_tokens: int = Column(Integer, default=0, nullable=False) # type: ignore completion_tokens: int = Column(Integer, default=0, nullable=False) # type: ignore queries: int = Column(Integer, default=0, nullable=False) # type: ignore @@ -34,12 +33,14 @@ class UserModel(Base): DateTime, server_default=sqlalchemy.func.now(), nullable=False ) # type: ignore last_activity: datetime = Column(DateTime, nullable=True) # type: ignore - ratelimit_day: int = Column(Integer, default=1000, nullable=True) # type: ignore - ratelimit_hour: int = Column(Integer, default=100, nullable=True) # type: ignore - ratelimit_minute: int = Column(Integer, default=10, nullable=True) # type: ignore + ratelimit_day: int = Column(Integer, default=USER_RATE_LIMIT_DAY, nullable=True) # type: ignore + ratelimit_hour: int = Column(Integer, default=USER_RATE_LIMIT_HOUR, nullable=True) # type: ignore + ratelimit_minute: int = Column( + Integer, default=USER_RATE_LIMIT_MINUTE, nullable=True + ) # type: ignore def __repr__(self): - return f"" + return f"" @dataclass @@ -86,7 +87,6 @@ async def update_last_activity(userid: str): @staticmethod async def insert_user( name: str, - email: str, apikey: str | None = None, userid: str | None = None, ratelimit_day: int | None = USER_RATE_LIMIT_DAY, @@ -98,7 +98,6 @@ async def insert_user( Args: name (str): Name of the user - email (str): Email of the user apikey (str): API key for the user userid (str): Unique ID for the user ratelimit_day (int): Daily rate limit @@ -118,7 +117,6 @@ async def insert_user( user = UserModel( userid=userid, name=name, - email=email, apikey=apikey, ratelimit_day=ratelimit_day, ratelimit_hour=ratelimit_hour, @@ -144,6 +142,27 @@ async def insert_user_model(user: UserModel) -> UserModel: logger.error(f"Error inserting user: {e}") raise + @staticmethod + async def check_user(userid: str) -> Optional[UserModel]: + """ + Validate a user. + + Args: + userid (str): User ID to validate + + Returns: + User's name if user is valid, None otherwise + """ + try: + async with get_db_session() as session: + query = sqlalchemy.select(UserModel).filter(UserModel.userid == userid) # type: ignore + user = await session.execute(query) + user = user.scalar_one_or_none() + return user + except SQLAlchemyError as e: + logger.error(f"Error checking API key: {e}") + return None + @staticmethod async def check_api_key(api_key: str) -> Optional[UserModel]: """ diff --git a/nilai-api/src/nilai_api/rate_limiting.py b/nilai-api/src/nilai_api/rate_limiting.py index 47d31eec..e888ba4c 100644 --- a/nilai-api/src/nilai_api/rate_limiting.py +++ b/nilai-api/src/nilai_api/rate_limiting.py @@ -48,8 +48,14 @@ class UserRateLimits(BaseModel): def get_user_limits(user: Annotated[UserModel, Depends(get_user)]) -> UserRateLimits: + # TODO: When the only allowed strategy is NUC, we can change the apikey name to subscription_holder + # In apikey mode, the apikey is unique as the userid. + # In nuc mode, the apikey is associated with a subscription holder and the userid is the user + # For NUCs we want the rate limit to be per subscription holder, not per user + # In JWT mode, the apikey is the userid too + # So we use the apikey as the id return UserRateLimits( - id=user.userid, + id=user.apikey, day_limit=user.ratelimit_day, hour_limit=user.ratelimit_hour, minute_limit=user.ratelimit_minute, diff --git a/nilai-api/src/nilai_api/routers/private.py b/nilai-api/src/nilai_api/routers/private.py index 13134818..b579e0b6 100644 --- a/nilai-api/src/nilai_api/routers/private.py +++ b/nilai-api/src/nilai_api/routers/private.py @@ -77,7 +77,7 @@ async def get_attestation( """ attestation_report = await get_attestation_report(nonce) - attestation_report.verifying_key = state.verifying_key + attestation_report.verifying_key = state.b64_public_key return attestation_report @@ -215,24 +215,36 @@ async def chat_completion_stream_generator() -> AsyncGenerator[str, None]: }, ) # type: ignore + prompt_token_usage: int = 0 + completion_token_usage: int = 0 async for chunk in response: - if chunk.usage is not None: - await UserManager.update_token_usage( - user.userid, - prompt_tokens=chunk.usage.prompt_tokens, - completion_tokens=chunk.usage.completion_tokens, - ) - await QueryLogManager.log_query( - user.userid, - model=req.model, - prompt_tokens=chunk.usage.prompt_tokens, - completion_tokens=chunk.usage.completion_tokens, - ) - + if ( + chunk.usage is not None + and chunk.usage.prompt_tokens is not None + and chunk.usage.completion_tokens is not None + ): + prompt_token_usage = chunk.usage.prompt_tokens + completion_token_usage += chunk.usage.completion_tokens + + logger.info( + f"Prompt token usage: {chunk.usage.prompt_tokens}/{prompt_token_usage}, Completion token usage: {chunk.usage.completion_tokens}/{completion_token_usage}" + ) data = chunk.model_dump_json(exclude_unset=True) yield f"data: {data}\n\n" await asyncio.sleep(0) + await UserManager.update_token_usage( + user.userid, + prompt_tokens=prompt_token_usage, + completion_tokens=completion_token_usage, + ) + await QueryLogManager.log_query( + user.userid, + model=req.model, + prompt_tokens=prompt_token_usage, + completion_tokens=completion_token_usage, + ) + except Exception as e: logger.error(f"Error streaming response: {e}") return diff --git a/nilai-api/src/nilai_api/routers/public.py b/nilai-api/src/nilai_api/routers/public.py index 3066fbfa..2a6b09bc 100644 --- a/nilai-api/src/nilai_api/routers/public.py +++ b/nilai-api/src/nilai_api/routers/public.py @@ -9,6 +9,14 @@ router = APIRouter() +@router.get("/v1/public_key", tags=["Public"]) +async def get_public_key() -> str: + """ + Get the public key of the API. + """ + return state.b64_public_key + + # Health Check Endpoint @router.get("/v1/health", tags=["Health"]) async def health_check() -> HealthCheckResponse: diff --git a/nilai-api/src/nilai_api/state.py b/nilai-api/src/nilai_api/state.py index f3f8bd1e..21349fb0 100644 --- a/nilai-api/src/nilai_api/state.py +++ b/nilai-api/src/nilai_api/state.py @@ -13,7 +13,7 @@ class AppState: def __init__(self): - self.private_key, self.public_key, self.verifying_key = generate_key_pair() + self.private_key, self.public_key, self.b64_public_key = generate_key_pair() self.sem = Semaphore(2) self.discovery_service = ModelServiceDiscovery( diff --git a/nilai-attestation/gunicorn.conf.py b/nilai-attestation/gunicorn.conf.py index 6e2e452e..fd58ad78 100644 --- a/nilai-attestation/gunicorn.conf.py +++ b/nilai-attestation/gunicorn.conf.py @@ -5,7 +5,8 @@ bind = [f"0.0.0.0:{SETTINGS.attestation_port}"] # Set the number of workers (2) -workers = SETTINGS.gunicorn_workers +workers = 1 + # Set the number of threads per worker (16) threads = 1 diff --git a/nilai-auth/README.md b/nilai-auth/README.md new file mode 100644 index 00000000..296f3b76 --- /dev/null +++ b/nilai-auth/README.md @@ -0,0 +1,13 @@ +# Example: nilAuth services. + +# nilAuth Services + +This repository contains two main services: + +## nilai-auth-server + +This server acts as a delegation authority for Nillion User Compute (NUC) tokens, specifically for interacting with the Nilai API. It handles obtaining a root NUC token from a configured NilAuth instance, managing subscriptions on the Nillion Chain, and delegating compute capabilities to end-user public keys. See `nilai-auth/nilai-auth-server/README.md` for more details. + +## nilai-auth-client + +This client demonstrates the end-to-end process of authenticating with the Nilai API using Nillion User Compute (NUC) tokens obtained via the Nilai Auth Server. See `nilai-auth/nilai-auth-client/README.md` for more details. diff --git a/nilai-auth/nilai-auth-client/README.md b/nilai-auth/nilai-auth-client/README.md new file mode 100644 index 00000000..157ba8aa --- /dev/null +++ b/nilai-auth/nilai-auth-client/README.md @@ -0,0 +1,43 @@ +# Nilai Auth Client + +This client demonstrates the end-to-end process of authenticating with the Nilai API using Nillion User Compute (NUC) tokens obtained via the Nilai Auth Server. + +## Functionality + +1. **Key Generation:** Generates a new secp256k1 private/public key pair for the user. +2. **Request Delegation:** Sends the user's public key (base64 encoded) to the Nilai Auth Server (`/v1/delegate/` endpoint) to request a delegated NUC token. +3. **Token Validation:** Validates the received delegated token against the public key of the NilAuth instance (acting as the root issuer). +4. **Nilai Public Key Retrieval:** Fetches the public key of the target Nilai API instance (`/v1/public_key` endpoint). +5. **Invocation Token Creation:** Creates an invocation NUC token by: + * Extending the previously obtained delegated token. + * Setting the audience to the Nilai API's public key. + * Signing the invocation token with the user's private key. +6. **Invocation Token Validation:** Validates the created invocation token, ensuring it's correctly targeted at the Nilai API. +7. **API Call:** Uses the `openai` library (configured with the Nilai API base URL) to make a chat completion request. + * The invocation token is passed as the `api_key` in the request header. + * The Nilai API verifies this token before processing the request. +8. **Prints Response:** Outputs the response received from the Nilai API. + +## Prerequisites + +* A running **Nilai Auth Server** (default: `localhost:8100`). +* A running **Nilai API** instance (default: `localhost:8080`). +* A running **NilAuth** node (default: `localhost:30921`). + +## Running the Client + +```bash +cd nilai-auth/nilai-auth-client +# Make sure dependencies are installed (e.g., using uv or pip) +python src/nilai_auth_client/main.py +``` + +## Configuration + +Endpoints for the dependent services are currently hardcoded in `main.py`: + +* `SERVICE_ENDPOINT`: Nilai Auth Server (`localhost:8100`) +* `NILAI_ENDPOINT`: Nilai API (`localhost:8080`) +* `NILAUTH_ENDPOINT`: NilAuth Node (`localhost:30921`) + +These could be made configurable via environment variables or command-line arguments if needed. diff --git a/nilai-auth/nilai-auth-client/examples/tutorial.ipynb b/nilai-auth/nilai-auth-client/examples/tutorial.ipynb new file mode 100644 index 00000000..05a24818 --- /dev/null +++ b/nilai-auth/nilai-auth-client/examples/tutorial.ipynb @@ -0,0 +1,473 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## NUC Token Delegation and Subscription Management Tutorial\n", + "\n", + "This notebook demonstrates the process of interacting with the NilAuth service to manage subscriptions and prepare for NUC token operations.\n", + "\n", + "**Steps:**\n", + "\n", + "1. **Import Libraries:** Import necessary classes and functions from `nuc`, `cosmpy`, `secp256k1`, and standard Python libraries.\n", + "2. **Load Keys:** Define functions to load or generate the Nilchain wallet (`cosmpy`) private key and the NilAuth (`secp256k1`) private key. These keys are currently hardcoded for demonstration purposes. **Note:** Hardcoding keys is insecure for production environments.\n", + "3. **Initialize Keys and Wallet:** Call the functions to get the `NilAuthPrivateKey` (`builder_private_key`) and the `cosmpy` wallet and keypair. Print the wallet address.\n", + "4. **Configure Nilchain Connection:** Set up the `NetworkConfig` for connecting to the Nillion devnet.\n", + "5. **Connect to Ledger:** Create a `LedgerClient` instance using the network configuration.\n", + "6. **Query Balance:** Check and print the `unil` balance of the initialized wallet on the Nillion Chain.\n", + "7. **Initialize NilAuth Client:** Create a `NilauthClient` instance, connecting to the local NilAuth service endpoint.\n", + "8. **Initialize Payer:** Create a `Payer` object, configuring it with the Nilchain wallet, chain details, and gRPC endpoint. This object will be used to pay for transactions like subscriptions.\n", + "9. **Check Subscription Status:** Use the `NilauthClient` and the builder's private key to check the current subscription status associated with the key.\n", + "10. **Pay for Subscription (if necessary):**\n", + " * If the key is not currently subscribed, use the `nilauth_client.pay_subscription` method along with the `Payer` object to pay for a new subscription on the Nillion Chain.\n", + " * If already subscribed, print the time remaining until expiration and renewal availability." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "l/SYifzu2Iqc3dsWoWHRP2oSMHwrORY/PDw5fDwtJDQ=\n", + "\n", + "Paying for wallet: nillion1mqukqr7d4s3eqhcxwctu7yypm560etp2dghpy6\n", + "Wallet balance: 999999989000000 unil\n", + "[>] Creating nilauth client\n", + "[>] Creating payer\n", + "IS SUBSCRIBED: True\n", + "[>] Subscription is already paid for\n", + "EXPIRES IN: 0:01:01.197447\n", + "CAN BE RENEWED IN: -1 day, 23:44:21.197434\n" + ] + } + ], + "source": [ + "# %% Import necessary libraries\n", + "from nuc.payer import Payer\n", + "from nuc.builder import NucTokenBuilder\n", + "from nuc.nilauth import NilauthClient\n", + "from nuc.envelope import NucTokenEnvelope\n", + "from nuc.token import Command, Did, InvocationBody, DelegationBody\n", + "from nuc.validate import (\n", + " NucTokenValidator,\n", + " ValidationParameters,\n", + " InvocationRequirement,\n", + " ValidationException,\n", + ")\n", + "from cosmpy.crypto.keypairs import PrivateKey as NilchainPrivateKey\n", + "from cosmpy.aerial.wallet import LocalWallet\n", + "from cosmpy.aerial.client import LedgerClient, NetworkConfig\n", + "from secp256k1 import PrivateKey as NilAuthPrivateKey\n", + "import base64\n", + "import datetime\n", + "\n", + "\n", + "# %% Define functions to load keys (replace with secure loading in production)\n", + "def get_wallet():\n", + " \"\"\"Loads the Nilchain wallet private key and creates a wallet object.\"\"\"\n", + " # IMPORTANT: Hardcoding private keys is insecure. Use environment variables or a secrets manager.\n", + " keypair = NilchainPrivateKey(\"l/SYifzu2Iqc3dsWoWHRP2oSMHwrORY/PDw5fDwtJDQ=\")\n", + " print(f\"Nilchain Private Key (bytes): {keypair.private_key}\")\n", + " print(f\"Nilchain Public Key (bytes): {keypair.public_key}\")\n", + " wallet = LocalWallet(\n", + " keypair, prefix=\"nillion\"\n", + " ) # Nillion uses the 'nillion' address prefix\n", + " return wallet, keypair\n", + "\n", + "\n", + "def get_private_key():\n", + " \"\"\"Loads the NilAuth private key used for signing NUC tokens.\"\"\"\n", + " # IMPORTANT: Hardcoding private keys is insecure. Use environment variables or a secrets manager.\n", + " private_key = NilAuthPrivateKey(\n", + " base64.b64decode(\"l/SYifzu2Iqc3dsWoWHRP2oSMHwrORY/PDw5fDwtJDQ=\")\n", + " )\n", + " return private_key\n", + "\n", + "\n", + "# %% Initialize keys and wallet\n", + "# This key will be used to sign NUC tokens later (e.g., the root token from NilAuth or delegated tokens)\n", + "builder_private_key = get_private_key()\n", + "\n", + "# This wallet is used for interacting with the Nillion Chain (e.g., paying subscriptions)\n", + "wallet, keypair = get_wallet()\n", + "address = wallet.address()\n", + "print(f\"Paying for wallet: {address}\")\n", + "\n", + "# %% Configure and connect to Nillion Chain\n", + "cfg = NetworkConfig(\n", + " chain_id=\"nillion-chain-devnet\",\n", + " url=\"grpc+http://localhost:26649\", # Nillion Chain gRPC endpoint\n", + " fee_minimum_gas_price=1,\n", + " fee_denomination=\"unil\", # The currency used for fees\n", + " staking_denomination=\"unil\", # The currency used for staking\n", + ")\n", + "ledger_client = LedgerClient(cfg)\n", + "\n", + "# %% Query wallet balance\n", + "balances = ledger_client.query_bank_balance(address, \"unil\")\n", + "print(f\"Wallet balance: {balances} unil\")\n", + "\n", + "# %% Initialize NilAuth Client and Payer\n", + "print(\"[>] Creating nilauth client\")\n", + "# Connect to the NilAuth service which issues root NUC tokens\n", + "nilauth_client = NilauthClient(\"http://localhost:30921\") # NilAuth service endpoint\n", + "\n", + "print(\"[>] Creating payer\")\n", + "# The Payer object bundles wallet details needed to pay for chain transactions\n", + "payer = Payer(\n", + " wallet_private_key=keypair,\n", + " chain_id=\"nillion-chain-devnet\",\n", + " grpc_endpoint=\"http://localhost:26649\", # Nillion Chain gRPC endpoint for the payer\n", + " gas_limit=1000000000000, # Gas limit for transactions\n", + ")\n", + "\n", + "# %% Check and manage NilAuth subscription\n", + "# Check if the builder_private_key is associated with an active subscription\n", + "subscription_details = nilauth_client.subscription_status(builder_private_key)\n", + "print(f\"IS SUBSCRIBED: {subscription_details.subscribed}\")\n", + "\n", + "# If not subscribed, pay for one\n", + "if not subscription_details.subscribed:\n", + " print(\"[>] Paying for subscription\")\n", + " nilauth_client.pay_subscription(\n", + " key=builder_private_key, # The key to associate the subscription with\n", + " payer=payer, # The payer object to execute the transaction\n", + " )\n", + "else:\n", + " # If already subscribed, print details\n", + " print(\"[>] Subscription is already paid for\")\n", + " now = datetime.datetime.now(datetime.timezone.utc)\n", + " print(f\"EXPIRES IN: {subscription_details.details.expires_at - now}\")\n", + " # Note: Renewal might only be possible within a certain window before expiry\n", + " print(f\"CAN BE RENEWED IN: {subscription_details.details.renewable_at - now}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Requesting and Preparing NUC Tokens\n", + "\n", + "This section focuses on obtaining the initial NUC token (the \"root\" token) from NilAuth and preparing for delegation by generating a new key pair.\n", + "\n", + "**Steps:**\n", + "\n", + "1. **Request Root Token:** Use the `nilauth_client` (initialized earlier) and the `builder_private_key` (which has an active subscription) to request a root NUC token from the NilAuth service. This token grants the initial set of permissions associated with the `builder_private_key`.\n", + "2. **Print Root Token:** Display the raw, encoded string representation of the obtained root token.\n", + "3. **Display Builder Keys:** Print the raw private key bytes and the hex-encoded public key of the `builder_private_key` for reference. This is the key that *owns* the root token.\n", + "4. **Generate Delegated Key Pair:** Create a completely *new* `NilAuthPrivateKey` instance. This key pair (`delegated_key` and its corresponding public key) will represent the entity *receiving* delegated permissions from the root token.\n", + "5. **Display Delegated Keys:** Print the raw private key bytes and the hex-encoded public key of the newly generated `delegated_key`.\n", + "6. **Parse Root Token:** Convert the raw `root_token` string into a structured `NucTokenEnvelope` object using `NucTokenEnvelope.parse()`. This allows programmatic access to the token's claims and structure.\n", + "7. **Print Parsed Envelope:** Display the `NucTokenEnvelope` object, showing its parsed structure." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Root Token: eyJhbGciOiJFUzI1NksifQ.eyJpc3MiOiJkaWQ6bmlsOjAzNTIwZTcwYmQ5N2E1ZmE2ZDcwYzYxNGQ1MGVlNDdiZjQ0NWFlMGIwOTQxYTFkNjFkZGQ1YWZhMDIyYjk3YWIxNCIsImF1ZCI6ImRpZDpuaWw6MDMwOTIzZjJlNzEyMGM1MGU0MjkwNWI4NTdkZGQyOTQ3ZjZlY2NlZDZiYjAyYWFiNjRlNjNiMjhlOWUyZTA2ZDEwIiwic3ViIjoiZGlkOm5pbDowMzA5MjNmMmU3MTIwYzUwZTQyOTA1Yjg1N2RkZDI5NDdmNmVjY2VkNmJiMDJhYWI2NGU2M2IyOGU5ZTJlMDZkMTAiLCJleHAiOjE3NDU1MDg2OTcsImNtZCI6Ii9uaWwiLCJwb2wiOltdLCJub25jZSI6ImMxNDE0MTIwYjg5ODk3ZTdlM2YyN2ZiZGE0OWEyZDAwIiwicHJmIjpbXX0.3csz4DdLahcYmc6vbwE-HdweGF6TCFUiatoO6AAssSJNUR0VGiRHmCSU3rlmdYAHPHEPcLWNBBLo-e9bMOQXUg\n", + "Builder Private Key: 97f49889fceed88a9cdddb16a161d13f6a12307c2b39163f3c3c397c3c2d2434\n", + "Builder Public Key: 030923f2e7120c50e42905b857ddd2947f6ecced6bb02aab64e63b28e9e2e06d10\n", + "Delegated Private Key: 5586c1abd910c869517a5c1a733c19b3513fef5f700e2c8624e867b620e981ca\n", + "Delegated Public Key: 03f65bb3be4bd7752e9d680ac315025c5252e9e36836217fe9bdb4b8514f04e8d9\n", + "Root Token Envelope: \n" + ] + } + ], + "source": [ + "# %% Request Root Token from NilAuth\n", + "# Use the key associated with the subscription to request the base NUC token\n", + "root_token = nilauth_client.request_token(key=builder_private_key)\n", + "print(f\"Root Token (raw string): {root_token}\")\n", + "\n", + "# %% Display Builder Key Details (Owner of Root Token)\n", + "print(f\"Builder Private Key (bytes): {builder_private_key.serialize()}\")\n", + "print(f\"Builder Public Key (hex): {builder_private_key.pubkey.serialize().hex()}\")\n", + "\n", + "# %% Generate a New Key Pair for Delegation Target\n", + "# This key pair will be the recipient of the delegated permissions\n", + "delegated_key = NilAuthPrivateKey()\n", + "print(f\"Delegated Private Key (bytes): {delegated_key.serialize()}\")\n", + "print(f\"Delegated Public Key (hex): {delegated_key.pubkey.serialize().hex()}\")\n", + "\n", + "# %% Parse the Root Token String into an Object\n", + "# Parsing allows easier access to token attributes and structure\n", + "root_token_envelope = NucTokenEnvelope.parse(root_token)\n", + "print(f\"Root Token Envelope (parsed object): {root_token_envelope}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Creating a Delegated NUC Token\n", + "\n", + "Now that we have the root token and a key pair for the intended recipient, we create a new NUC token that delegates specific permissions from the root token holder (`builder_private_key`) to the recipient (`delegated_key`).\n", + "\n", + "**Steps:**\n", + "\n", + "1. **Initialize Builder:** Start building a new token using `NucTokenBuilder.extending()`, passing the previously parsed `root_token_envelope`. This signifies that the new token derives its authority from the root token.\n", + "2. **Set Body (Delegation):** Specify the token's body using `.body()`. Here, `DelegationBody(policies=[])` indicates this is a delegation token. Policies could further restrict the delegation, but none are added in this example.\n", + "3. **Set Audience:** Define the recipient of this delegated token using `.audience()`. We pass a `Did` (Decentralized Identifier) object created from the *public key* of the `delegated_key` generated in the previous step. This means only the holder of `delegated_key`'s private key can use this token effectively for further actions (like creating an invocation).\n", + "4. **Specify Command:** Grant permission to execute a specific command using `.command()`. Here, `Command([\"nil\", \"ai\", \"generate\"])` authorizes the audience (the holder of `delegated_key`) to perform the `nil ai generate` action. Multiple commands could be listed.\n", + "5. **Build and Sign:** Finalize the token creation and sign it using `.build()`. Crucially, the signing key here is `builder_private_key` – the private key corresponding to the issuer of the *root* token, proving its authority to delegate.\n", + "6. **Print Delegated Token:** Display the raw, encoded string representation of the newly created delegated token.\n", + "7. **Parse Delegated Token:** Convert the raw `delegated_token` string into a structured `NucTokenEnvelope` object.\n", + "8. **Print Parsed Envelope:** Display the `delegated_token_envelope` object to show its structure, including the specified audience and command." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Delegation Token: eyJhbGciOiJFUzI1NksifQ.eyJpc3MiOiAiZGlkOm5pbDowMzA5MjNmMmU3MTIwYzUwZTQyOTA1Yjg1N2RkZDI5NDdmNmVjY2VkNmJiMDJhYWI2NGU2M2IyOGU5ZTJlMDZkMTAiLCAiYXVkIjogImRpZDpuaWw6MDNmNjViYjNiZTRiZDc3NTJlOWQ2ODBhYzMxNTAyNWM1MjUyZTllMzY4MzYyMTdmZTliZGI0Yjg1MTRmMDRlOGQ5IiwgInN1YiI6ICJkaWQ6bmlsOjAzMDkyM2YyZTcxMjBjNTBlNDI5MDViODU3ZGRkMjk0N2Y2ZWNjZWQ2YmIwMmFhYjY0ZTYzYjI4ZTllMmUwNmQxMCIsICJjbWQiOiAiL25pbC9haS9nZW5lcmF0ZSIsICJwb2wiOiBbXSwgIm5vbmNlIjogImZlNTE1ZDA0MGY3MWEyYmY0NzdlYjc2MzBjOWE5YjViIiwgInByZiI6IFsiYWU3ZmY3NTkxYjcxMjQ1MjY3Nzg0NTBmMDZlZDlkYjlkYTU2YmIwZjRmOTIzZmI1YTUzMzMyNWViYWU5ZmQ5MSJdfQ.CguMqBWX0YX2rErcpiHX4PvExo6kiEmnE3QOJMPZ1KU_iiQD1p6kzjY5YRHHT_mWVjgQVNsVR2B9swr7Zk63mw/eyJhbGciOiJFUzI1NksifQ.eyJpc3MiOiJkaWQ6bmlsOjAzNTIwZTcwYmQ5N2E1ZmE2ZDcwYzYxNGQ1MGVlNDdiZjQ0NWFlMGIwOTQxYTFkNjFkZGQ1YWZhMDIyYjk3YWIxNCIsImF1ZCI6ImRpZDpuaWw6MDMwOTIzZjJlNzEyMGM1MGU0MjkwNWI4NTdkZGQyOTQ3ZjZlY2NlZDZiYjAyYWFiNjRlNjNiMjhlOWUyZTA2ZDEwIiwic3ViIjoiZGlkOm5pbDowMzA5MjNmMmU3MTIwYzUwZTQyOTA1Yjg1N2RkZDI5NDdmNmVjY2VkNmJiMDJhYWI2NGU2M2IyOGU5ZTJlMDZkMTAiLCJleHAiOjE3NDU1MDg2OTcsImNtZCI6Ii9uaWwiLCJwb2wiOltdLCJub25jZSI6ImMxNDE0MTIwYjg5ODk3ZTdlM2YyN2ZiZGE0OWEyZDAwIiwicHJmIjpbXX0.3csz4DdLahcYmc6vbwE-HdweGF6TCFUiatoO6AAssSJNUR0VGiRHmCSU3rlmdYAHPHEPcLWNBBLo-e9bMOQXUg\n", + "Delegated Token Envelope: \n" + ] + } + ], + "source": [ + "# %% Create the Delegated Token\n", + "# Use the NucTokenBuilder to create a new token based on the root token\n", + "delegated_token = (\n", + " NucTokenBuilder.extending(\n", + " root_token_envelope\n", + " ) # Start from the root token's authority\n", + " .body(\n", + " DelegationBody(policies=[])\n", + " ) # Mark as a delegation token (no specific policies here)\n", + " .audience(\n", + " Did(delegated_key.pubkey.serialize())\n", + " ) # Set the recipient to the delegated public key\n", + " .command(\n", + " Command([\"nil\", \"ai\", \"generate\"])\n", + " ) # Authorize the 'nil ai generate' command\n", + " .build(\n", + " builder_private_key\n", + " ) # Sign the delegation using the *root* token's private key\n", + ")\n", + "\n", + "# Print the resulting delegated token string\n", + "print(f\"Delegation Token (raw string): {delegated_token}\")\n", + "\n", + "# %% Parse the Delegated Token String into an Object\n", + "delegated_token_envelope = NucTokenEnvelope.parse(delegated_token)\n", + "\n", + "# Print the parsed object to see its structure\n", + "print(f\"Delegated Token Envelope (parsed object): {delegated_token_envelope}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Creating an Invocation NUC Token\n", + "\n", + "This final step creates the token that will actually be sent to the target service (e.g., Nilai API) to authorize a specific action. This is called an \"invocation\" token. It uses the permissions granted by the `delegated_token` and targets a specific service endpoint.\n", + "\n", + "**Steps:**\n", + "\n", + "1. **Generate Placeholder Target Key:** Create a *new* `NilAuthPrivateKey` instance (`nilai_public_key`). **WARNING:** In a real application, you would **fetch the actual public key of the service you want to call** (e.g., from a discovery endpoint like `/v1/public_key` on the Nilai API) instead of generating a new one here. This generated key acts as a placeholder for the target service's identity in this example.\n", + "2. **Display Placeholder Keys:** Print the details of this placeholder key pair.\n", + "3. **Display Delegated Token:** Re-print the parsed `delegated_token_envelope` for context.\n", + "4. **Initialize Invocation Builder:** Start building the invocation token using `NucTokenBuilder.extending()`, passing the `delegated_token_envelope`. This signifies the invocation derives its authority from the permissions granted in the delegation token.\n", + "5. **Set Body (Invocation):** Specify the token's body using `.body()`. `InvocationBody(args={})` marks this as an invocation token. The `args` dictionary could contain specific parameters for the command being invoked, but it's empty here.\n", + "6. **Set Audience (Target Service):** Define the intended recipient service using `.audience()`. We pass a `Did` created from the public key of the **placeholder** `nilai_public_key`. **Critically, in a real scenario, this MUST be the actual public key of the target service.** This ensures the token is only valid for that specific service instance.\n", + "7. **Build and Sign:** Finalize the token creation and sign it using `.build()`. The signing key is `delegated_key` – the private key that *received* the permissions in the previous delegation step. This proves the caller is authorized by the delegation.\n", + "8. **Print Invocation Token:** Display the raw, encoded string representation of the invocation token. This is the token you would typically send as an API key or Bearer token to the target service." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "New Private Key: 6288b98e03a595f54788137b0a9353d334b363af30c71d469ae8368a2257ab0e\n", + "Delegated Key: 02e24769ba1fc9aa1dd819ca12cc8179b5417e3bebf9c9c9e5880342d48246f420\n", + "Delegated Token Envelope: \n", + "Invocation: eyJhbGciOiJFUzI1NksifQ.eyJpc3MiOiAiZGlkOm5pbDowM2Y2NWJiM2JlNGJkNzc1MmU5ZDY4MGFjMzE1MDI1YzUyNTJlOWUzNjgzNjIxN2ZlOWJkYjRiODUxNGYwNGU4ZDkiLCAiYXVkIjogImRpZDpuaWw6MDJlMjQ3NjliYTFmYzlhYTFkZDgxOWNhMTJjYzgxNzliNTQxN2UzYmViZjljOWM5ZTU4ODAzNDJkNDgyNDZmNDIwIiwgInN1YiI6ICJkaWQ6bmlsOjAzMDkyM2YyZTcxMjBjNTBlNDI5MDViODU3ZGRkMjk0N2Y2ZWNjZWQ2YmIwMmFhYjY0ZTYzYjI4ZTllMmUwNmQxMCIsICJjbWQiOiAiL25pbC9haS9nZW5lcmF0ZSIsICJhcmdzIjoge30sICJub25jZSI6ICI1ZmRkODk1NjE4MWM1YTZiZjRlNDVhMzBlYzc1YmE0NSIsICJwcmYiOiBbIjBjYWY3MWU3ZGVjMWEwNjc3Y2M0ZWNhYjJhYzY5Nzk1MWRjZDBjZDc4MTM4MDA4MzVkZjY2MmE2ODQzNTNjYzAiXX0.2D5Z7JcodjWyPcRqPx2OIn2Marnk-46XCJAlYIBzdTZCPQSs99gfWslQdoaC84pWYqiaoTvYuogqTDTdrtXaeA/eyJhbGciOiJFUzI1NksifQ.eyJpc3MiOiAiZGlkOm5pbDowMzA5MjNmMmU3MTIwYzUwZTQyOTA1Yjg1N2RkZDI5NDdmNmVjY2VkNmJiMDJhYWI2NGU2M2IyOGU5ZTJlMDZkMTAiLCAiYXVkIjogImRpZDpuaWw6MDNmNjViYjNiZTRiZDc3NTJlOWQ2ODBhYzMxNTAyNWM1MjUyZTllMzY4MzYyMTdmZTliZGI0Yjg1MTRmMDRlOGQ5IiwgInN1YiI6ICJkaWQ6bmlsOjAzMDkyM2YyZTcxMjBjNTBlNDI5MDViODU3ZGRkMjk0N2Y2ZWNjZWQ2YmIwMmFhYjY0ZTYzYjI4ZTllMmUwNmQxMCIsICJjbWQiOiAiL25pbC9haS9nZW5lcmF0ZSIsICJwb2wiOiBbXSwgIm5vbmNlIjogImZlNTE1ZDA0MGY3MWEyYmY0NzdlYjc2MzBjOWE5YjViIiwgInByZiI6IFsiYWU3ZmY3NTkxYjcxMjQ1MjY3Nzg0NTBmMDZlZDlkYjlkYTU2YmIwZjRmOTIzZmI1YTUzMzMyNWViYWU5ZmQ5MSJdfQ.CguMqBWX0YX2rErcpiHX4PvExo6kiEmnE3QOJMPZ1KU_iiQD1p6kzjY5YRHHT_mWVjgQVNsVR2B9swr7Zk63mw/eyJhbGciOiJFUzI1NksifQ.eyJpc3MiOiJkaWQ6bmlsOjAzNTIwZTcwYmQ5N2E1ZmE2ZDcwYzYxNGQ1MGVlNDdiZjQ0NWFlMGIwOTQxYTFkNjFkZGQ1YWZhMDIyYjk3YWIxNCIsImF1ZCI6ImRpZDpuaWw6MDMwOTIzZjJlNzEyMGM1MGU0MjkwNWI4NTdkZGQyOTQ3ZjZlY2NlZDZiYjAyYWFiNjRlNjNiMjhlOWUyZTA2ZDEwIiwic3ViIjoiZGlkOm5pbDowMzA5MjNmMmU3MTIwYzUwZTQyOTA1Yjg1N2RkZDI5NDdmNmVjY2VkNmJiMDJhYWI2NGU2M2IyOGU5ZTJlMDZkMTAiLCJleHAiOjE3NDU1MDg2OTcsImNtZCI6Ii9uaWwiLCJwb2wiOltdLCJub25jZSI6ImMxNDE0MTIwYjg5ODk3ZTdlM2YyN2ZiZGE0OWEyZDAwIiwicHJmIjpbXX0.3csz4DdLahcYmc6vbwE-HdweGF6TCFUiatoO6AAssSJNUR0VGiRHmCSU3rlmdYAHPHEPcLWNBBLo-e9bMOQXUg\n", + "--------------------------------\n" + ] + } + ], + "source": [ + "# %% Generate Placeholder Target Key (Replace with actual service key retrieval in practice)\n", + "# WARNING: This creates a random key. In a real scenario, fetch the target service's public key.\n", + "nilai_public_key = NilAuthPrivateKey() # Placeholder for the target service's key\n", + "\n", + "# Display the placeholder key details\n", + "print(f\"Placeholder Target Private Key (bytes): {nilai_public_key.serialize()}\")\n", + "print(\n", + " f\"Placeholder Target Public Key (hex): {nilai_public_key.pubkey.serialize().hex()}\"\n", + ")\n", + "\n", + "# Display the delegation token again for context\n", + "print(f\"Delegated Token Envelope (used for invocation): {delegated_token_envelope}\")\n", + "\n", + "# %% Create the Invocation Token\n", + "# Use the NucTokenBuilder to create the token that calls the service\n", + "invocation = (\n", + " NucTokenBuilder.extending(\n", + " delegated_token_envelope\n", + " ) # Start from the delegated token's authority\n", + " .body(\n", + " InvocationBody(args={})\n", + " ) # Mark as an invocation token (no specific args here)\n", + " .audience(\n", + " Did(nilai_public_key.pubkey.serialize())\n", + " ) # Set the target service (using placeholder key here)\n", + " .build(delegated_key) # Sign with the *delegated* private key\n", + ")\n", + "\n", + "# Print the resulting invocation token string (this would be sent to the service)\n", + "print(f\"Invocation Token (raw string): {invocation}\")\n", + "print(\"--------------------------------\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Validating the NUC Token Chain\n", + "\n", + "After creating the root, delegated, and invocation tokens, it's crucial to validate them to ensure they are correctly formed, properly signed, and that the chain of delegation is intact. Validation typically checks signatures, expiration times (if set), audience restrictions, and command permissions against a trusted root issuer (in this case, the NilAuth service).\n", + "\n", + "**Steps:**\n", + "\n", + "1. **Get NilAuth Public Key:** Retrieve the public key of the NilAuth service itself using `nilauth_client.about().public_key.serialize()`. This key acts as the ultimate trust anchor for validating the token chain, as NilAuth issued the root token. Wrap it in a `Did` object.\n", + "2. **Print NilAuth Public Key:** Display the retrieved NilAuth public key `Did`.\n", + "3. **Parse Invocation Token:** Convert the raw `invocation` token string into a structured `NucTokenEnvelope` object.\n", + "4. **Print Parsed Invocation Envelope:** Display the parsed invocation token object.\n", + "5. **Print Proof Count:** Show the number of proofs (signatures) attached to the invocation envelope. An invocation token derived from a delegated token, which itself derived from a root token, should have multiple proofs forming a chain back to the root issuer.\n", + "6. **Initialize Validator:** Create instances of `NucTokenValidator`. The validator needs a list of trusted root public keys. Here, we only trust the `nilauth_public_key`.\n", + "7. **Validate Delegated Token:** Call `validator.validate()` on the `delegated_token_envelope`. This checks if it's correctly signed by the `builder_private_key` (whose authority ultimately comes from NilAuth) and if its structure is valid. (Note: The root token validation is commented out but would follow the same principle).\n", + "8. **Prepare Invocation Validation Parameters:** Create `ValidationParameters` specifically for the invocation token.\n", + " * Set `token_requirements` to an `InvocationRequirement`.\n", + " * Crucially, set the `audience` within the `InvocationRequirement` to the **expected audience** (the placeholder `nilai_public_key.pubkey`'s `Did` in this example, but should be the actual target service's `Did` in practice). This tells the validator to specifically check if the token was intended for this recipient.\n", + "9. **Validate Invocation Token:** Call `validator.validate()` on the `invocation_envelope` using the specific `validation_parameters`. This checks:\n", + " * The signature (must be signed by `delegated_key`).\n", + " * The chain of proofs back to the trusted `nilauth_public_key`.\n", + " * The audience matches the one specified in `validation_parameters`.\n", + " * Expiration, command permissions (implicitly checked based on delegation chain)." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Nilauth Public Key: did:nil:03520e70bd97a5fa6d70c614d50ee47bf445ae0b0941a1d61ddd5afa022b97ab14\n", + "Invocation Envelope: \n", + "Invocation Envelope Token Proofs: 2\n", + "Validating Root Token Envelope\n", + "Validating Delegated Token Envelope\n", + "None\n", + "Validating Invocation Envelope\n", + "InvocationRequirement(audience=Did(public_key=b'\\x02\\xe2Gi\\xba\\x1f\\xc9\\xaa\\x1d\\xd8\\x19\\xca\\x12\\xcc\\x81y\\xb5A~;\\xeb\\xf9\\xc9\\xc9\\xe5\\x88\\x03B\\xd4\\x82F\\xf4 '))\n", + "{\"iss\": \"did:nil:03f65bb3be4bd7752e9d680ac315025c5252e9e36836217fe9bdb4b8514f04e8d9\", \"aud\": \"did:nil:02e24769ba1fc9aa1dd819ca12cc8179b5417e3bebf9c9c9e5880342d48246f420\", \"sub\": \"did:nil:030923f2e7120c50e42905b857ddd2947f6ecced6bb02aab64e63b28e9e2e06d10\", \"cmd\": \"/nil/ai/generate\", \"args\": {}, \"nonce\": \"5fdd8956181c5a6bf4e45a30ec75ba45\", \"prf\": [\"0caf71e7dec1a0677cc4ecab2ac697951dcd0cd7813800835df662a684353cc0\"]}\n", + "Expected audience: did:nil:02e24769ba1fc9aa1dd819ca12cc8179b5417e3bebf9c9c9e5880342d48246f420\n", + "Token audience: did:nil:02e24769ba1fc9aa1dd819ca12cc8179b5417e3bebf9c9c9e5880342d48246f420\n" + ] + } + ], + "source": [ + "# %% Get the Public Key of the Root Issuer (NilAuth)\n", + "# The NilAuth service's public key is the ultimate trust anchor\n", + "nilauth_public_key = Did(nilauth_client.about().public_key.serialize())\n", + "print(f\"Nilauth Public Key (Trust Anchor): {nilauth_public_key}\")\n", + "\n", + "# %% Parse the Invocation Token String into an Object\n", + "invocation_envelope = NucTokenEnvelope.parse(invocation)\n", + "print(f\"Invocation Envelope (parsed object): {invocation_envelope}\")\n", + "\n", + "# An invocation token derived from a delegated token should have multiple proofs (signatures)\n", + "print(f\"Invocation Envelope Token Proofs Count: {len(invocation_envelope.proofs)}\")\n", + "\n", + "# %% Validate the Tokens\n", + "# Initialize the validator with the trusted root public key(s)\n", + "validator = NucTokenValidator([nilauth_public_key])\n", + "\n", + "# --- Root Token Validation (Optional - Commented Out) ---\n", + "# print(\"Validating Root Token Envelope...\")\n", + "# try:\n", + "# validator.validate(root_token_envelope)\n", + "# print(\"Root Token is Valid.\")\n", + "# except ValidationException as e:\n", + "# print(f\"Root Token Validation Failed: {e}\")\n", + "\n", + "# --- Delegated Token Validation ---\n", + "print(\"Validating Delegated Token Envelope...\")\n", + "try:\n", + " # Basic validation checks structure and signature relative to the root\n", + " validator.validate(delegated_token_envelope)\n", + " print(\"Delegated Token is Valid.\")\n", + "except ValidationException as e:\n", + " print(f\"Delegated Token Validation Failed: {e}\")\n", + "\n", + "# --- Invocation Token Validation ---\n", + "print(\"Validating Invocation Envelope...\")\n", + "try:\n", + " # Prepare specific parameters for invocation validation\n", + " default_parameters = ValidationParameters.default()\n", + " # Tell the validator to check if the audience matches our (placeholder) target service key\n", + " default_parameters.token_requirements = InvocationRequirement(\n", + " audience=Did(\n", + " nilai_public_key.pubkey.serialize()\n", + " ) # Use actual service key DID here\n", + " )\n", + " validation_parameters = default_parameters\n", + "\n", + " # Validate the invocation token against the root and check specific requirements\n", + " validator.validate(invocation_envelope, validation_parameters)\n", + " print(\"Invocation Token is Valid (including audience check).\")\n", + "except ValidationException as e:\n", + " print(f\"Invocation Token Validation Failed: {e}\")\n", + "except Exception as e:\n", + " print(f\"An unexpected error occurred during invocation validation: {e}\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.10" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/nilai-auth/nilai-auth-client/pyproject.toml b/nilai-auth/nilai-auth-client/pyproject.toml new file mode 100644 index 00000000..e22369ba --- /dev/null +++ b/nilai-auth/nilai-auth-client/pyproject.toml @@ -0,0 +1,20 @@ +[project] +name = "nilai-auth-client" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +authors = [ + { name = "José Cabrero-Holgueras", email = "jose.cabrero@nillion.com" } +] +requires-python = ">=3.12" +dependencies = [ + "nuc-helpers", + "openai>=1.70.0", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.uv.sources] +nuc-helpers = { workspace = true } diff --git a/nilai-auth/nilai-auth-client/src/nilai_auth_client/__init__.py b/nilai-auth/nilai-auth-client/src/nilai_auth_client/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/nilai-auth/nilai-auth-client/src/nilai_auth_client/main.py b/nilai-auth/nilai-auth-client/src/nilai_auth_client/main.py new file mode 100644 index 00000000..705a493a --- /dev/null +++ b/nilai-auth/nilai-auth-client/src/nilai_auth_client/main.py @@ -0,0 +1,102 @@ +# Do an HTTP request to the nilai-auth-server +import httpx +from secp256k1 import PrivateKey as NilAuthPrivateKey +from nuc.validate import ValidationParameters, InvocationRequirement + +import base64 + +from nuc.token import Did + +import openai + +from nuc_helpers import ( + DelegationToken, + InvocationToken, + get_nilai_public_key, + get_invocation_token, + validate_token, +) + +SERVICE_ENDPOINT = "localhost:8100" +NILAI_ENDPOINT = "localhost:8080" +NILAUTH_ENDPOINT = "localhost:30921" + + +def retrieve_delegation_token(b64_public_key: str) -> DelegationToken: + """ + Get a delegation token for the given public key + + Args: + b64_public_key: The base64 encoded public key + + Returns: + delegation_token: The delegation token + """ + response = httpx.post( + f"http://{SERVICE_ENDPOINT}/v1/delegate/", + json={"user_public_key": b64_public_key}, + ) + return DelegationToken(**response.json()) + + +def main(): + """ + Main function + """ + # Create a user private key and public key + user_private_key = NilAuthPrivateKey() + user_public_key = user_private_key.pubkey + + if user_public_key is None: + raise Exception("Failed to get public key") + + b64_public_key = base64.b64encode(user_public_key.serialize()).decode("utf-8") + + delegation_token = retrieve_delegation_token(b64_public_key) + + validate_token( + f"http://{NILAUTH_ENDPOINT}", + delegation_token.token, + ValidationParameters.default(), + ) + nilai_public_key = get_nilai_public_key(f"http://{NILAI_ENDPOINT}") + if nilai_public_key is None: + raise Exception("Failed to get nilai public key") + + invocation_token: InvocationToken = get_invocation_token( + delegation_token, + nilai_public_key, + user_private_key, + ) + + default_validation_parameters = ValidationParameters.default() + default_validation_parameters.token_requirements = InvocationRequirement( + audience=Did(nilai_public_key.serialize()) + ) + + validate_token( + f"http://{NILAUTH_ENDPOINT}", + invocation_token.token, + default_validation_parameters, + ) + client = openai.OpenAI( + base_url=f"http://{NILAI_ENDPOINT}/v1", api_key=invocation_token.token + ) + + response = client.chat.completions.create( + model="meta-llama/Llama-3.2-1B-Instruct", + messages=[ + { + "role": "system", + "content": "You are a helpful assistant that provides accurate and concise information.", + }, + {"role": "user", "content": "What is the capital of France?"}, + ], + temperature=0.2, + max_tokens=100, + ) + print(response) + + +if __name__ == "__main__": + main() diff --git a/nilai-auth/nilai-auth-client/src/nilai_auth_client/py.typed b/nilai-auth/nilai-auth-client/src/nilai_auth_client/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/nilai-auth/nilai-auth-server/README.md b/nilai-auth/nilai-auth-server/README.md new file mode 100644 index 00000000..4ef0bd72 --- /dev/null +++ b/nilai-auth/nilai-auth-server/README.md @@ -0,0 +1,56 @@ +# Nilai Auth Server + +This server acts as a delegation authority for Nillion User Compute (NUC) tokens, specifically for interacting with the Nilai API. It handles obtaining a root NUC token from a configured NilAuth instance, managing subscriptions on the Nillion Chain, and delegating compute capabilities to end-user public keys. + +## Functionality + +1. **Wallet Initialization:** On startup (or first request), it initializes a Nilchain wallet using a hardcoded private key (for development purposes). +2. **NilAuth Client:** Connects to a NilAuth instance specified in `NILAUTH_TRUSTED_ROOT_ISSUERS`. +3. **Subscription Management:** Checks the Nilchain subscription status associated with its wallet. If not subscribed, it pays for the subscription using its wallet. +4. **Root Token Retrieval:** Obtains a root NUC token from the NilAuth instance. +5. **Delegation Endpoint (`/v1/delegate/`):** + * Accepts a POST request containing the end-user's public key (`user_public_key`). + * Validates the subscription and root token. + * Creates a new NUC token, extending the root token's capabilities. + * Sets the audience of the new token to the provided user public key. + * Authorizes the `nil ai generate` command. + * Signs the new token with its private key. + * Returns the delegated NUC token to the user. + +## Prerequisites + +* A running NilAuth instance accessible at the URL(s) defined in the `NILAUTH_TRUSTED_ROOT_ISSUERS` environment variable (or configured within `nilai_auth_server/config.py`). +* A running Nillion Chain node accessible via gRPC (currently hardcoded to `http://localhost:26649`). +* The server's wallet must have sufficient `unil` tokens to pay for NilAuth subscriptions if needed. + +## Running the Server + +Use a ASGI server like Uvicorn: + +```bash +cd nilai-auth/nilai-auth-server +uv run python3 src/nilai_auth_server/app.py +``` + +## Configuration + +* **Private Key:** Currently hardcoded within `app.py`. **This should be replaced with a secure key management solution for production.** +* **Nilchain gRPC Endpoint:** Hardcoded to `http://localhost:26649` in `app.py`. Consider making this configurable. +* **NilAuth Trusted Issuers:** Configured via `NILAUTH_TRUSTED_ROOT_ISSUERS` in `config.py`. + +## API + +### POST `/v1/delegate/` + +* **Request Body:** + ```json + { + "user_public_key": "string (base64 encoded secp256k1 public key)" + } + ``` +* **Response Body:** + ```json + { + "token": "string (NUC token envelope)" + } + ``` diff --git a/nilai-auth/nilai-auth-server/gunicorn.conf.py b/nilai-auth/nilai-auth-server/gunicorn.conf.py new file mode 100644 index 00000000..494c3c5d --- /dev/null +++ b/nilai-auth/nilai-auth-server/gunicorn.conf.py @@ -0,0 +1,16 @@ +# gunicorn.config.py + +# Bind to address and port +bind = ["0.0.0.0:8080"] + +# Set the number of workers (2) +workers = 1 + +# Set the number of threads per worker (16) +threads = 1 + +# Set the timeout (120 seconds) +timeout = 120 + +# Set the worker class to UvicornWorker for async handling +worker_class = "uvicorn.workers.UvicornWorker" diff --git a/nilai-auth/nilai-auth-server/pyproject.toml b/nilai-auth/nilai-auth-server/pyproject.toml new file mode 100644 index 00000000..95a622cd --- /dev/null +++ b/nilai-auth/nilai-auth-server/pyproject.toml @@ -0,0 +1,22 @@ +[project] +name = "nilai-auth-server" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +authors = [ + { name = "José Cabrero-Holgueras", email = "jose.cabrero@nillion.com" } +] +requires-python = ">=3.12" +dependencies = [ + "fastapi[standard]>=0.115.5", + "gunicorn>=23.0.0", + "nuc-helpers", + "uvicorn>=0.34.0", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.uv.sources] +nuc-helpers = { workspace = true } diff --git a/nilai-auth/nilai-auth-server/src/nilai_auth_server/__init__.py b/nilai-auth/nilai-auth-server/src/nilai_auth_server/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/nilai-auth/nilai-auth-server/src/nilai_auth_server/app.py b/nilai-auth/nilai-auth-server/src/nilai_auth_server/app.py new file mode 100644 index 00000000..419cb1cc --- /dev/null +++ b/nilai-auth/nilai-auth-server/src/nilai_auth_server/app.py @@ -0,0 +1,68 @@ +from fastapi import FastAPI +from nuc.nilauth import NilauthClient +from pydantic import BaseModel +from secp256k1 import PublicKey as NilAuthPublicKey +import base64 +from nilai_auth_server.config import NILAUTH_TRUSTED_ROOT_ISSUER + +from nuc_helpers import ( + RootToken, + DelegationToken, + pay_for_subscription, + get_wallet_and_private_key, + get_root_token, + get_delegation_token, +) + +app = FastAPI() + +PRIVATE_KEY = "l/SYifzu2Iqc3dsWoWHRP2oSMHwrORY/PDw5fDwtJDQ=" # This is an example private key with funds for testing devnet, and should not be used in production +NILCHAIN_GRPC = "localhost:26649" + + +class DelegateRequest(BaseModel): + user_public_key: str + + +@app.post("/v1/delegate/") +def delegate(request: DelegateRequest) -> DelegationToken: + """ + Delegate the root token to the delegated key + + Args: + request: The request body + """ + + server_wallet, server_keypair, server_private_key = get_wallet_and_private_key( + PRIVATE_KEY + ) + nilauth_client = NilauthClient(f"http://{NILAUTH_TRUSTED_ROOT_ISSUER}") + + # Pay for the subscription + pay_for_subscription( + nilauth_client, + server_wallet, + server_keypair, + server_private_key, + f"http://{NILCHAIN_GRPC}", + ) + + # Create a root token + root_token: RootToken = get_root_token(nilauth_client, server_private_key) + + user_public_key = NilAuthPublicKey( + base64.b64decode(request.user_public_key), raw=True + ) + + delegation_token: DelegationToken = get_delegation_token( + root_token, + server_private_key, + user_public_key, + ) + return delegation_token + + +if __name__ == "__main__": + import uvicorn + + uvicorn.run(app, host="0.0.0.0", port=8100) diff --git a/nilai-auth/nilai-auth-server/src/nilai_auth_server/config.py b/nilai-auth/nilai-auth-server/src/nilai_auth_server/config.py new file mode 100644 index 00000000..4109136a --- /dev/null +++ b/nilai-auth/nilai-auth-server/src/nilai_auth_server/config.py @@ -0,0 +1,4 @@ +from dotenv import load_dotenv + +load_dotenv() +NILAUTH_TRUSTED_ROOT_ISSUER = "localhost:30921" diff --git a/nilai-auth/nilai-auth-server/src/nilai_auth_server/py.typed b/nilai-auth/nilai-auth-server/src/nilai_auth_server/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/nilai-auth/nuc-helpers/README.md b/nilai-auth/nuc-helpers/README.md new file mode 100644 index 00000000..e69de29b diff --git a/nilai-auth/nuc-helpers/pyproject.toml b/nilai-auth/nuc-helpers/pyproject.toml new file mode 100644 index 00000000..347e9272 --- /dev/null +++ b/nilai-auth/nuc-helpers/pyproject.toml @@ -0,0 +1,23 @@ +[project] +name = "nuc-helpers" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +authors = [ + { name = "José Cabrero-Holgueras", email = "jose.cabrero@nillion.com" } +] +requires-python = ">=3.12" +dependencies = [ + "cosmpy==0.9.2", + "nuc", + "pydantic>=2.11.2", + "secp256k1>=0.14.0", + "httpx>=0.28.1", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.uv.sources] +nuc = { git = "https://github.com/NillionNetwork/nuc-py.git" } diff --git a/nilai-auth/nuc-helpers/src/nuc_helpers/__init__.py b/nilai-auth/nuc-helpers/src/nuc_helpers/__init__.py new file mode 100644 index 00000000..88c75cea --- /dev/null +++ b/nilai-auth/nuc-helpers/src/nuc_helpers/__init__.py @@ -0,0 +1,34 @@ +from nuc_helpers.helpers import ( + RootToken, + DelegationToken, + InvocationToken, + get_wallet_and_private_key, + pay_for_subscription, + get_root_token, + get_delegation_token, + get_invocation_token, + get_nilai_public_key, + get_nilauth_public_key, + validate_token, + NilAuthPublicKey, + NilAuthPrivateKey, + NilchainPrivateKey, +) + + +__all__ = [ + "RootToken", + "DelegationToken", + "InvocationToken", + "get_wallet_and_private_key", + "pay_for_subscription", + "get_root_token", + "get_delegation_token", + "get_invocation_token", + "get_nilai_public_key", + "get_nilauth_public_key", + "validate_token", + "NilAuthPublicKey", + "NilAuthPrivateKey", + "NilchainPrivateKey", +] diff --git a/nilai-auth/nuc-helpers/src/nuc_helpers/helpers.py b/nilai-auth/nuc-helpers/src/nuc_helpers/helpers.py new file mode 100644 index 00000000..8dc7b937 --- /dev/null +++ b/nilai-auth/nuc-helpers/src/nuc_helpers/helpers.py @@ -0,0 +1,266 @@ +import base64 +import datetime +import logging +from functools import lru_cache +from typing import Tuple +import httpx + +# Importing the pydantic library dependencies +from pydantic import BaseModel + +# Importing the secp256k1 library dependencies +from secp256k1 import PrivateKey as NilAuthPrivateKey, PublicKey as NilAuthPublicKey + +# Importing the nuc library dependencies +from nuc.payer import Payer +from nuc.builder import NucTokenBuilder +from nuc.nilauth import NilauthClient +from nuc.envelope import NucTokenEnvelope +from nuc.token import Command, Did, InvocationBody +from nuc.validate import NucTokenValidator, ValidationParameters + +# Importing the cosmpy library dependencies +from cosmpy.crypto.keypairs import PrivateKey as NilchainPrivateKey +from cosmpy.aerial.wallet import LocalWallet, Address +from cosmpy.aerial.client import LedgerClient, NetworkConfig + +logger = logging.getLogger(__name__) + +## Pydantic models + + +class RootToken(BaseModel): + token: str + + +class DelegationToken(BaseModel): + token: str + + +class InvocationToken(BaseModel): + token: str + + +## Helpers +@lru_cache(maxsize=1) +def get_wallet_and_private_key( + private_key_bytes: str | bytes | None = None, +) -> Tuple[LocalWallet, NilchainPrivateKey, NilAuthPrivateKey]: + """ + Get the wallet and private key + + Args: + private_key_bytes: The private key bytes to use for the wallet + + Returns: + wallet: The wallet + keypair: The keypair + private_key: The private key + """ + keypair = NilchainPrivateKey(private_key_bytes) + wallet = LocalWallet(keypair, prefix="nillion") + private_key = NilAuthPrivateKey(base64.b64decode(keypair.private_key)) + return wallet, keypair, private_key + + +def get_root_token( + nilauth_client: NilauthClient, private_key: NilAuthPrivateKey +) -> RootToken: + """ + Get the root token from nilauth + + Args: + nilauth_client: The nilauth client + private_key: The private key of the user + + Returns: + The root token + """ + ## Getting the root token from nilauth + root_token: str = nilauth_client.request_token(key=private_key) + + return RootToken(token=root_token) + + +def get_unil_balance(address: Address, grpc_endpoint: str) -> int: + """ + Get the UNIL balance of the user + + Args: + address: The address of the user + grpc_endpoint: The endpoint of the grpc server + + Returns: + The balance of the user in UNIL + """ + logger.info("grpc_endpoint", grpc_endpoint) + + cfg = NetworkConfig( + chain_id="nillion-chain-devnet", + url="grpc+" + grpc_endpoint, + fee_minimum_gas_price=1, + fee_denomination="unil", + staking_denomination="unil", + ) + ledger_client = LedgerClient(cfg) + balance = ledger_client.query_bank_balance(address, "unil") # type: ignore + return balance + + +def pay_for_subscription( + nilauth_client: NilauthClient, + wallet: LocalWallet, + keypair: NilchainPrivateKey, + private_key: NilAuthPrivateKey, + grpc_endpoint: str, +) -> None: + """ + Pay for the subscription using the Nilchain keypair if the user is not subscribed + + Args: + nilauth_client: The nilauth client + keypair: The Nilchain keypair + private_key: The NilAuth private key of the user + grpc_endpoint: The endpoint of the grpc server + """ + + if ( + get_unil_balance(wallet.address(), grpc_endpoint=grpc_endpoint) + < nilauth_client.subscription_cost() + ): + raise RuntimeError("User does not have enough UNIL to pay for the subscription") + + payer = Payer( + wallet_private_key=keypair, + chain_id="nillion-chain-devnet", + grpc_endpoint=grpc_endpoint, + gas_limit=1000000000000, + ) + + # Pretty print the subscription details + subscription_details = nilauth_client.subscription_status(private_key) + logger.info(f"IS SUBSCRIBED: {subscription_details.subscribed}") + if not subscription_details or subscription_details.subscribed is None: + raise RuntimeError( + f"User subscription details could not be retrieved: {subscription_details}, {subscription_details.subscribed}, {subscription_details.details}" + ) + + if not subscription_details.subscribed: + logger.info("[>] Paying for subscription") + nilauth_client.pay_subscription( + key=private_key, + payer=payer, + ) + else: + logger.info("[>] Subscription is already paid for") + + if subscription_details.details is None: + raise RuntimeError( + f"Subscription details could not be retrieved: {subscription_details}" + ) + + logger.info( + f"EXPIRES IN: {subscription_details.details.expires_at - datetime.datetime.now(datetime.timezone.utc)}" + ) + logger.info( + f"CAN BE RENEWED IN: {subscription_details.details.renewable_at - datetime.datetime.now(datetime.timezone.utc)}" + ) + + +def get_delegation_token( + root_token: RootToken, + private_key: NilAuthPrivateKey, + user_public_key: NilAuthPublicKey, +) -> DelegationToken: + """ + Delegate the root token to the delegated key + + Args: + user_public_key_b64: The base64 encoded public key of the user + nilauth_url: The URL of the nilauth server + grpc_endpoint: The endpoint of the grpc server + Returns: + The delegation token + """ + + root_token_envelope = NucTokenEnvelope.parse(root_token.token) + delegated_token = ( + NucTokenBuilder.extending(root_token_envelope) + .audience(Did(user_public_key.serialize())) + .command(Command(["nil", "ai", "generate"])) + .build(private_key) + ) + return DelegationToken(token=delegated_token) + + +def get_nilai_public_key(nilai_url: str) -> NilAuthPublicKey: + """ + Get the nilai public key from the nilai server + + Args: + nilai_url: The URL of the nilai server + + Returns: + The nilai public key + """ + response = httpx.get(f"{nilai_url}/v1/public_key") + public_key = NilAuthPublicKey(base64.b64decode(response.text), raw=True) + logger.info(f"Nilai public key: {public_key.serialize().hex()}") + return public_key + + +def get_invocation_token( + delegation_token: RootToken | DelegationToken, + nilai_public_key: NilAuthPublicKey, + delegated_key: NilAuthPrivateKey, +) -> InvocationToken: + """ + Make an invocation token for the given delegated token and nilai public key + + Args: + delegated_token: The delegated token + nilai_public_key: The nilai public key + delegated_key: The private key + """ + logger.info(f"Delegation token: {delegation_token}") + delegated_token_envelope = NucTokenEnvelope.parse(delegation_token.token) + + invocation = ( + NucTokenBuilder.extending(delegated_token_envelope) + .body(InvocationBody(args={})) + .audience(Did(nilai_public_key.serialize())) + .build(delegated_key) + ) + return InvocationToken(token=invocation) + + +def get_nilauth_public_key(nilauth_url: str) -> Did: + """ + Get the nilauth public key from the nilauth server + + Args: + nilauth_url: The URL of the nilauth server + + Returns: + The nilauth public key as a Did + """ + nilauth_client = NilauthClient(nilauth_url) + nilauth_public_key = Did(nilauth_client.about().public_key.serialize()) + return nilauth_public_key + + +def validate_token( + nilauth_url: str, token: str, validation_parameters: ValidationParameters +): + """ + Validate a token + + Args: + token: The token to validate + validation_parameters: The validation parameters + """ + validator = NucTokenValidator([get_nilauth_public_key(nilauth_url)]) + + validator.validate(NucTokenEnvelope.parse(token), validation_parameters) + + logger.info("[>] Token validated") diff --git a/nilai-auth/nuc-helpers/src/nuc_helpers/main.py b/nilai-auth/nuc-helpers/src/nuc_helpers/main.py new file mode 100644 index 00000000..c106f5d0 --- /dev/null +++ b/nilai-auth/nuc-helpers/src/nuc_helpers/main.py @@ -0,0 +1,138 @@ +from nuc_helpers import ( + get_wallet_and_private_key, + pay_for_subscription, + get_root_token, + get_delegation_token, + get_nilai_public_key, + get_invocation_token, + validate_token, + InvocationToken, + RootToken, + DelegationToken, + NilAuthPublicKey, + NilAuthPrivateKey, +) +from nuc.nilauth import NilauthClient +from nuc.token import Did +from nuc.validate import ValidationParameters, InvocationRequirement + + +def b2b2c_test(): + # Services must be running for this to work + PRIVATE_KEY = "l/SYifzu2Iqc3dsWoWHRP2oSMHwrORY/PDw5fDwtJDQ=" # This is an example private key with funds for testing devnet, and should not be used in production + NILAI_ENDPOINT = "localhost:8080" + NILAUTH_ENDPOINT = "localhost:30921" + NILCHAIN_GRPC = "localhost:26649" + + # Server private key + server_wallet, server_keypair, server_private_key = get_wallet_and_private_key( + PRIVATE_KEY + ) + nilauth_client = NilauthClient(f"http://{NILAUTH_ENDPOINT}") + + # Pay for the subscription + pay_for_subscription( + nilauth_client, + server_wallet, + server_keypair, + server_private_key, + f"http://{NILCHAIN_GRPC}", + ) + + # Create a root token + root_token: RootToken = get_root_token(nilauth_client, server_private_key) + + # Create a user private key and public key + user_private_key = NilAuthPrivateKey() + user_public_key = user_private_key.pubkey + + if user_public_key is None: + raise Exception("Failed to get public key") + # b64_public_key = base64.b64encode(public_key.serialize()).decode("utf-8") + + delegation_token: DelegationToken = get_delegation_token( + root_token, + server_private_key, + user_public_key, + ) + + print("Delegation token: ", delegation_token, type(delegation_token)) + nilai_public_key: NilAuthPublicKey = get_nilai_public_key( + f"http://{NILAI_ENDPOINT}" + ) + invocation_token: InvocationToken = get_invocation_token( + delegation_token, + nilai_public_key, + user_private_key, + ) + + print("Root token type: ", type(root_token)) + default_validation_parameters = ValidationParameters.default() + default_validation_parameters.token_requirements = InvocationRequirement( + audience=Did(nilai_public_key.serialize()) + ) + + validate_token( + f"http://{NILAUTH_ENDPOINT}", + invocation_token.token, + default_validation_parameters, + ) + + +def b2c_test(): + # Services must be running for this to work + PRIVATE_KEY = "l/SYifzu2Iqc3dsWoWHRP2oSMHwrORY/PDw5fDwtJDQ=" # This is an example private key with funds for testing devnet, and should not be used in production + NILAI_ENDPOINT = "localhost:8080" + NILAUTH_ENDPOINT = "localhost:30921" + NILCHAIN_GRPC = "localhost:26649" + + # Server private key + server_wallet, server_keypair, server_private_key = get_wallet_and_private_key( + PRIVATE_KEY + ) + nilauth_client = NilauthClient(f"http://{NILAUTH_ENDPOINT}") + + # Pay for the subscription + pay_for_subscription( + nilauth_client, + server_wallet, + server_keypair, + server_private_key, + f"http://{NILCHAIN_GRPC}", + ) + + # Create a root token + root_token: RootToken = get_root_token(nilauth_client, server_private_key) + + nilai_public_key: NilAuthPublicKey = get_nilai_public_key( + f"http://{NILAI_ENDPOINT}" + ) + invocation_token: InvocationToken = get_invocation_token( + root_token, + nilai_public_key, + server_private_key, + ) + + print("Root token type: ", type(root_token)) + default_validation_parameters = ValidationParameters.default() + default_validation_parameters.token_requirements = InvocationRequirement( + audience=Did(nilai_public_key.serialize()) + ) + + validate_token( + f"http://{NILAUTH_ENDPOINT}", + invocation_token.token, + default_validation_parameters, + ) + + +def main(): + """ + Main function to test the helpers + """ + b2b2c_test() + b2c_test() + + +if __name__ == "__main__": + main() diff --git a/nilai-auth/nuc-helpers/src/nuc_helpers/py.typed b/nilai-auth/nuc-helpers/src/nuc_helpers/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/pyproject.toml b/pyproject.toml index 8e7ba0df..8a7aa14e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,7 @@ dependencies = [ "nilai-api", "nilai-common", "nilai-models", + "nuc-helpers", ] [dependency-groups] @@ -21,11 +22,11 @@ dev = [ "isort>=5.13.2", "pytest-mock>=3.14.0", "pytest>=8.3.3", - "ruff>=0.11.4", + "ruff>=0.11.7", "uvicorn>=0.32.1", "pytest-asyncio>=0.25.0", "testcontainers>=4.9.1", - "pyright>=1.1.399", + "pyright>=1.1.400", "pre-commit>=4.1.0", "httpx>=0.28.1", ] @@ -38,14 +39,15 @@ build-backend = "setuptools.build_meta" find = { include = ["nilai"] } [tool.uv.workspace] -members = ["nilai-models", "nilai-api", "packages/nilai-common", "packages/verifier"] +members = ["nilai-models", "nilai-api", "packages/nilai-common", "nilai-auth/nilai-auth-server", "nilai-auth/nilai-auth-client", "nilai-auth/nuc-helpers"] [tool.uv.sources] nilai-common = { workspace = true } nilai-api = { workspace = true } nilai-models = { workspace = true } +nuc-helpers = { workspace = true } [tool.pyright] -exclude = [".venv", "packages/verifier/"] +exclude = [".venv"] [tool.ruff] -exclude = [".venv", "packages/verifier"] +exclude = [".venv"] diff --git a/tests/e2e/config.py b/tests/e2e/config.py index 138b7cea..14d90078 100644 --- a/tests/e2e/config.py +++ b/tests/e2e/config.py @@ -1,7 +1,8 @@ import os ENVIRONMENT = os.getenv("ENVIRONMENT", "dev") -AUTH_TOKEN = os.environ["AUTH_TOKEN"] +# Left for API key for backwards compatibility +AUTH_TOKEN = os.getenv("AUTH_TOKEN", "") if ENVIRONMENT == "dev": BASE_URL = "http://localhost:8080/v1" diff --git a/tests/e2e/nuc.py b/tests/e2e/nuc.py new file mode 100644 index 00000000..c7c27adb --- /dev/null +++ b/tests/e2e/nuc.py @@ -0,0 +1,62 @@ +from nuc_helpers import ( + get_wallet_and_private_key, + pay_for_subscription, + get_root_token, + get_nilai_public_key, + get_invocation_token, + validate_token, + InvocationToken, + RootToken, + NilAuthPublicKey, +) +from nuc.nilauth import NilauthClient +from nuc.token import Did +from nuc.validate import ValidationParameters, InvocationRequirement + + +def get_nuc_token() -> InvocationToken: + # Services must be running for this to work + PRIVATE_KEY = "l/SYifzu2Iqc3dsWoWHRP2oSMHwrORY/PDw5fDwtJDQ=" # This is an example private key with funds for testing devnet, and should not be used in production + NILAI_ENDPOINT = "localhost:8080" + NILAUTH_ENDPOINT = "localhost:30921" + NILCHAIN_GRPC = "localhost:26649" + + # Server private key + server_wallet, server_keypair, server_private_key = get_wallet_and_private_key( + PRIVATE_KEY + ) + nilauth_client = NilauthClient(f"http://{NILAUTH_ENDPOINT}") + + # Pay for the subscription + pay_for_subscription( + nilauth_client, + server_wallet, + server_keypair, + server_private_key, + f"http://{NILCHAIN_GRPC}", + ) + + # Create a root token + root_token: RootToken = get_root_token(nilauth_client, server_private_key) + + nilai_public_key: NilAuthPublicKey = get_nilai_public_key( + f"http://{NILAI_ENDPOINT}" + ) + invocation_token: InvocationToken = get_invocation_token( + root_token, + nilai_public_key, + server_private_key, + ) + + default_validation_parameters = ValidationParameters.default() + default_validation_parameters.token_requirements = InvocationRequirement( + audience=Did(nilai_public_key.serialize()) + ) + + validate_token( + f"http://{NILAUTH_ENDPOINT}", + invocation_token.token, + default_validation_parameters, + ) + + return invocation_token diff --git a/tests/e2e/test_http.py b/tests/e2e/test_http.py index a020ae1b..704b3169 100644 --- a/tests/e2e/test_http.py +++ b/tests/e2e/test_http.py @@ -10,7 +10,8 @@ import json -from .config import BASE_URL, AUTH_TOKEN, test_models +from .config import BASE_URL, test_models +from .nuc import get_nuc_token import httpx import pytest @@ -18,12 +19,13 @@ @pytest.fixture def client(): """Create an HTTPX client with default headers""" + invocation_token = get_nuc_token() return httpx.Client( base_url=BASE_URL, headers={ "accept": "application/json", "Content-Type": "application/json", - "Authorization": f"Bearer {AUTH_TOKEN}", + "Authorization": f"Bearer {invocation_token.token}", }, timeout=None, ) diff --git a/tests/e2e/test_openai.py b/tests/e2e/test_openai.py index d5540b47..374d8142 100644 --- a/tests/e2e/test_openai.py +++ b/tests/e2e/test_openai.py @@ -13,13 +13,15 @@ import pytest from openai import OpenAI from openai.types.chat import ChatCompletion -from .config import BASE_URL, AUTH_TOKEN, test_models +from .config import BASE_URL, test_models +from .nuc import get_nuc_token @pytest.fixture def client(): """Create an OpenAI client configured to use the Nilai API""" - return OpenAI(base_url=BASE_URL, api_key=AUTH_TOKEN) + invocation_token = get_nuc_token() + return OpenAI(base_url=BASE_URL, api_key=invocation_token.token) @pytest.mark.parametrize( @@ -335,11 +337,13 @@ def test_usage_endpoint(client): # The OpenAI client doesn't have a built-in method for this import requests + invocation_token = get_nuc_token() + url = BASE_URL + "/usage" response = requests.get( url, headers={ - "Authorization": f"Bearer {AUTH_TOKEN}", + "Authorization": f"Bearer {invocation_token.token}", "Content-Type": "application/json", }, ) @@ -371,10 +375,11 @@ def test_attestation_endpoint(client): import requests url = BASE_URL + "/attestation/report" + invocation_token = get_nuc_token() response = requests.get( url, headers={ - "Authorization": f"Bearer {AUTH_TOKEN}", + "Authorization": f"Bearer {invocation_token.token}", "Content-Type": "application/json", }, params={"nonce": "0" * 64}, diff --git a/tests/unit/nilai_api/routers/test_private.py b/tests/unit/nilai_api/routers/test_private.py index 910d6d72..f3138302 100644 --- a/tests/unit/nilai_api/routers/test_private.py +++ b/tests/unit/nilai_api/routers/test_private.py @@ -21,6 +21,7 @@ def mock_user(): mock = MagicMock(spec=UserModel) mock.userid = "test-user-id" mock.name = "Test User" + mock.apikey = "test-api-key" mock.prompt_tokens = 100 mock.completion_tokens = 50 mock.total_tokens = 150 @@ -95,7 +96,7 @@ def mock_state(mocker, event_loop): mocker.patch.object(state, "discovery_service", mock_discovery_service) # Patch other attributes - mocker.patch.object(state, "verifying_key", "test-verifying-key") + mocker.patch.object(state, "b64_public_key", "test-verifying-key") # Patch get_model method mocker.patch.object(state, "get_model", return_value=model_endpoint) diff --git a/tests/unit/nilai_api/test_cryptography.py b/tests/unit/nilai_api/test_cryptography.py index 3719feb5..3811db11 100644 --- a/tests/unit/nilai_api/test_cryptography.py +++ b/tests/unit/nilai_api/test_cryptography.py @@ -1,23 +1,23 @@ from base64 import b64decode import pytest -from cryptography.exceptions import InvalidSignature -from cryptography.hazmat.primitives.asymmetric import ec +from secp256k1 import PrivateKey, PublicKey from nilai_api.crypto import generate_key_pair, sign_message, verify_signature def test_generate_key_pair(): # Generate keys - private_key, public_key, verifying_key = generate_key_pair() + private_key, public_key, b64_public_key = generate_key_pair() # Check private_key and public_key are instances of the expected types - assert isinstance(private_key, ec.EllipticCurvePrivateKey) - assert isinstance(public_key, ec.EllipticCurvePublicKey) + assert isinstance(private_key, PrivateKey) + assert isinstance(public_key, PublicKey) # Ensure the verifying_key is a valid PEM-encoded public key - decoded_key = b64decode(verifying_key) - assert b"BEGIN PUBLIC KEY" in decoded_key - assert b"END PUBLIC KEY" in decoded_key + decoded_key = b64decode(b64_public_key) + assert decoded_key == public_key.serialize(), ( + "Public key should be valid and match the generated key" + ) def test_sign_message(): @@ -31,8 +31,10 @@ def test_sign_message(): signature = sign_message(private_key, message) # Ensure the signature is a byte string - assert isinstance(signature, bytes) - assert len(signature) > 0 + assert isinstance(signature, bytes), ( + f"Signature should be a byte string but is: {type(signature)}" + ) + assert len(signature) > 0, f"Signature should not be empty but is: {signature}" def test_verify_signature_valid(): @@ -64,8 +66,9 @@ def test_verify_signature_invalid_message(): signature = sign_message(private_key, message) # Verify the tampered message (should raise InvalidSignature) - with pytest.raises(InvalidSignature): - verify_signature(public_key, tampered_message, signature) + assert not verify_signature(public_key, tampered_message, signature), ( + "Signature should be invalid" + ) def test_verify_signature_invalid_signature(): @@ -82,5 +85,6 @@ def test_verify_signature_invalid_signature(): tampered_signature = signature[:-1] + b"\x00" # Verify the tampered signature (should raise InvalidSignature) - with pytest.raises(InvalidSignature): - verify_signature(public_key, message, tampered_signature) + assert not verify_signature(public_key, message, tampered_signature), ( + "Signature should be invalid" + ) diff --git a/tests/unit/nilai_api/test_state.py b/tests/unit/nilai_api/test_state.py index 7430d5e1..bd5ba299 100644 --- a/tests/unit/nilai_api/test_state.py +++ b/tests/unit/nilai_api/test_state.py @@ -12,7 +12,7 @@ def app_state(mocker): def test_generate_key_pair(app_state): assert app_state.private_key is not None assert app_state.public_key is not None - assert app_state.verifying_key is not None + assert app_state.b64_public_key is not None def test_semaphore_initialization(app_state): diff --git a/uv.lock b/uv.lock index 7d80dbb1..8f0e1b75 100644 --- a/uv.lock +++ b/uv.lock @@ -6,8 +6,11 @@ requires-python = ">=3.12" members = [ "nilai", "nilai-api", + "nilai-auth-client", + "nilai-auth-server", "nilai-common", "nilai-models", + "nuc-helpers", ] [[package]] @@ -217,6 +220,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9e/45/302d6712a8ff733a259446a7d24ff3c868715103032f50eef0d93ba70221/bcl-2.3.1-cp39-abi3-win_amd64.whl", hash = "sha256:52cf26c4ecd76e806c6576c4848633ff44ebfff528fca63ad0e52085b6ba5aa9", size = 96394 }, ] +[[package]] +name = "bech32" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ab/fe/b67ac9b123e25a3c1b8fc3f3c92648804516ab44215adb165284e024c43f/bech32-1.2.0.tar.gz", hash = "sha256:7d6db8214603bd7871fcfa6c0826ef68b85b0abd90fa21c285a9c5e21d2bd899", size = 3695 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/41/7022a226e5a6ac7091a95ba36bad057012ab7330b9894ad4e14e31d0b858/bech32-1.2.0-py3-none-any.whl", hash = "sha256:990dc8e5a5e4feabbdf55207b5315fdd9b73db40be294a19b3752cde9e79d981", size = 4587 }, +] + [[package]] name = "bitarray" version = "3.3.1" @@ -410,6 +422,26 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, ] +[[package]] +name = "cosmpy" +version = "0.9.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bech32" }, + { name = "ecdsa" }, + { name = "googleapis-common-protos" }, + { name = "grpcio" }, + { name = "jsonschema" }, + { name = "protobuf" }, + { name = "pycryptodome" }, + { name = "python-dateutil" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/12/5f/39167cf97a03813911e518d1b615c4ef5fc3e4eb26454b8cb3b557a03fba/cosmpy-0.9.2.tar.gz", hash = "sha256:0f0eb80152f28ef5ee4d846d581d2e34ba2d952900f0e3570cacb84bb376f664", size = 205720 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/bf/2b5e594858b0d41e372c9e4f975b3e5b2b655af1670f3a600d684d5c68d4/cosmpy-0.9.2-py3-none-any.whl", hash = "sha256:3591311198b08a0aa75340851ca166669974f17ffaa207a8d2cb26504fb0fa19", size = 413103 }, +] + [[package]] name = "cryptography" version = "43.0.1" @@ -820,6 +852,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/48/39/49b4cbcecd17290974b38244f983a0f3e7cfde6b3f710f30958ca7312b27/futurist-3.1.0-py3-none-any.whl", hash = "sha256:a0733cd6f8ba8e173913336527a82843118980c19959645252eed1d8682e9dc1", size = 37119 }, ] +[[package]] +name = "googleapis-common-protos" +version = "1.70.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/39/24/33db22342cf4a2ea27c9955e6713140fedd51e8b141b5ce5260897020f1a/googleapis_common_protos-1.70.0.tar.gz", hash = "sha256:0e1b44e0ea153e6594f9f394fef15193a68aaaea2d843f83e2742717ca753257", size = 145903 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/f1/62a193f0227cf15a920390abe675f386dec35f7ae3ffe6da582d3ade42c7/googleapis_common_protos-1.70.0-py3-none-any.whl", hash = "sha256:b8bfcca8c25a2bb253e0e0b0adaf8c00773e5e6af6fd92397576680b807e0fd8", size = 294530 }, +] + [[package]] name = "greenlet" version = "3.1.1" @@ -853,6 +897,34 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ac/38/08cc303ddddc4b3d7c628c3039a61a3aae36c241ed01393d00c2fd663473/greenlet-3.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:411f015496fec93c1c8cd4e5238da364e1da7a124bcb293f085bf2860c32c6f6", size = 1142112 }, ] +[[package]] +name = "grpcio" +version = "1.71.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/95/aa11fc09a85d91fbc7dd405dcb2a1e0256989d67bf89fa65ae24b3ba105a/grpcio-1.71.0.tar.gz", hash = "sha256:2b85f7820475ad3edec209d3d89a7909ada16caab05d3f2e08a7e8ae3200a55c", size = 12549828 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/83/bd4b6a9ba07825bd19c711d8b25874cd5de72c2a3fbf635c3c344ae65bd2/grpcio-1.71.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:0ff35c8d807c1c7531d3002be03221ff9ae15712b53ab46e2a0b4bb271f38537", size = 5184101 }, + { url = "https://files.pythonhosted.org/packages/31/ea/2e0d90c0853568bf714693447f5c73272ea95ee8dad107807fde740e595d/grpcio-1.71.0-cp312-cp312-macosx_10_14_universal2.whl", hash = "sha256:b78a99cd1ece4be92ab7c07765a0b038194ded2e0a26fd654591ee136088d8d7", size = 11310927 }, + { url = "https://files.pythonhosted.org/packages/ac/bc/07a3fd8af80467390af491d7dc66882db43884128cdb3cc8524915e0023c/grpcio-1.71.0-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:dc1a1231ed23caac1de9f943d031f1bc38d0f69d2a3b243ea0d664fc1fbd7fec", size = 5654280 }, + { url = "https://files.pythonhosted.org/packages/16/af/21f22ea3eed3d0538b6ef7889fce1878a8ba4164497f9e07385733391e2b/grpcio-1.71.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e6beeea5566092c5e3c4896c6d1d307fb46b1d4bdf3e70c8340b190a69198594", size = 6312051 }, + { url = "https://files.pythonhosted.org/packages/49/9d/e12ddc726dc8bd1aa6cba67c85ce42a12ba5b9dd75d5042214a59ccf28ce/grpcio-1.71.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5170929109450a2c031cfe87d6716f2fae39695ad5335d9106ae88cc32dc84c", size = 5910666 }, + { url = "https://files.pythonhosted.org/packages/d9/e9/38713d6d67aedef738b815763c25f092e0454dc58e77b1d2a51c9d5b3325/grpcio-1.71.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:5b08d03ace7aca7b2fadd4baf291139b4a5f058805a8327bfe9aece7253b6d67", size = 6012019 }, + { url = "https://files.pythonhosted.org/packages/80/da/4813cd7adbae6467724fa46c952d7aeac5e82e550b1c62ed2aeb78d444ae/grpcio-1.71.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:f903017db76bf9cc2b2d8bdd37bf04b505bbccad6be8a81e1542206875d0e9db", size = 6637043 }, + { url = "https://files.pythonhosted.org/packages/52/ca/c0d767082e39dccb7985c73ab4cf1d23ce8613387149e9978c70c3bf3b07/grpcio-1.71.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:469f42a0b410883185eab4689060a20488a1a0a00f8bbb3cbc1061197b4c5a79", size = 6186143 }, + { url = "https://files.pythonhosted.org/packages/00/61/7b2c8ec13303f8fe36832c13d91ad4d4ba57204b1c723ada709c346b2271/grpcio-1.71.0-cp312-cp312-win32.whl", hash = "sha256:ad9f30838550695b5eb302add33f21f7301b882937460dd24f24b3cc5a95067a", size = 3604083 }, + { url = "https://files.pythonhosted.org/packages/fd/7c/1e429c5fb26122055d10ff9a1d754790fb067d83c633ff69eddcf8e3614b/grpcio-1.71.0-cp312-cp312-win_amd64.whl", hash = "sha256:652350609332de6dac4ece254e5d7e1ff834e203d6afb769601f286886f6f3a8", size = 4272191 }, + { url = "https://files.pythonhosted.org/packages/04/dd/b00cbb45400d06b26126dcfdbdb34bb6c4f28c3ebbd7aea8228679103ef6/grpcio-1.71.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:cebc1b34ba40a312ab480ccdb396ff3c529377a2fce72c45a741f7215bfe8379", size = 5184138 }, + { url = "https://files.pythonhosted.org/packages/ed/0a/4651215983d590ef53aac40ba0e29dda941a02b097892c44fa3357e706e5/grpcio-1.71.0-cp313-cp313-macosx_10_14_universal2.whl", hash = "sha256:85da336e3649a3d2171e82f696b5cad2c6231fdd5bad52616476235681bee5b3", size = 11310747 }, + { url = "https://files.pythonhosted.org/packages/57/a3/149615b247f321e13f60aa512d3509d4215173bdb982c9098d78484de216/grpcio-1.71.0-cp313-cp313-manylinux_2_17_aarch64.whl", hash = "sha256:f9a412f55bb6e8f3bb000e020dbc1e709627dcb3a56f6431fa7076b4c1aab0db", size = 5653991 }, + { url = "https://files.pythonhosted.org/packages/ca/56/29432a3e8d951b5e4e520a40cd93bebaa824a14033ea8e65b0ece1da6167/grpcio-1.71.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:47be9584729534660416f6d2a3108aaeac1122f6b5bdbf9fd823e11fe6fbaa29", size = 6312781 }, + { url = "https://files.pythonhosted.org/packages/a3/f8/286e81a62964ceb6ac10b10925261d4871a762d2a763fbf354115f9afc98/grpcio-1.71.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c9c80ac6091c916db81131d50926a93ab162a7e97e4428ffc186b6e80d6dda4", size = 5910479 }, + { url = "https://files.pythonhosted.org/packages/35/67/d1febb49ec0f599b9e6d4d0d44c2d4afdbed9c3e80deb7587ec788fcf252/grpcio-1.71.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:789d5e2a3a15419374b7b45cd680b1e83bbc1e52b9086e49308e2c0b5bbae6e3", size = 6013262 }, + { url = "https://files.pythonhosted.org/packages/a1/04/f9ceda11755f0104a075ad7163fc0d96e2e3a9fe25ef38adfc74c5790daf/grpcio-1.71.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:1be857615e26a86d7363e8a163fade914595c81fec962b3d514a4b1e8760467b", size = 6643356 }, + { url = "https://files.pythonhosted.org/packages/fb/ce/236dbc3dc77cf9a9242adcf1f62538734ad64727fabf39e1346ad4bd5c75/grpcio-1.71.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:a76d39b5fafd79ed604c4be0a869ec3581a172a707e2a8d7a4858cb05a5a7637", size = 6186564 }, + { url = "https://files.pythonhosted.org/packages/10/fd/b3348fce9dd4280e221f513dd54024e765b21c348bc475516672da4218e9/grpcio-1.71.0-cp313-cp313-win32.whl", hash = "sha256:74258dce215cb1995083daa17b379a1a5a87d275387b7ffe137f1d5131e2cfbb", size = 3601890 }, + { url = "https://files.pythonhosted.org/packages/be/f8/db5d5f3fc7e296166286c2a397836b8b042f7ad1e11028d82b061701f0f7/grpcio-1.71.0-cp313-cp313-win_amd64.whl", hash = "sha256:22c3bc8d488c039a199f7a003a38cb7635db6656fa96437a8accde8322ce2366", size = 4273308 }, +] + [[package]] name = "gunicorn" version = "23.0.0" @@ -1043,6 +1115,33 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/91/29/df4b9b42f2be0b623cbd5e2140cafcaa2bef0759a00b7b70104dcfe2fb51/joblib-1.4.2-py3-none-any.whl", hash = "sha256:06d478d5674cbc267e7496a410ee875abd68e4340feff4490bcb7afb88060ae6", size = 301817 }, ] +[[package]] +name = "jsonschema" +version = "4.23.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/2e/03362ee4034a4c917f697890ccd4aec0800ccf9ded7f511971c75451deec/jsonschema-4.23.0.tar.gz", hash = "sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4", size = 325778 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/4a/4f9dbeb84e8850557c02365a0eee0649abe5eb1d84af92a25731c6c0f922/jsonschema-4.23.0-py3-none-any.whl", hash = "sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566", size = 88462 }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2024.10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/10/db/58f950c996c793472e336ff3655b13fbcf1e3b359dcf52dcf3ed3b52c352/jsonschema_specifications-2024.10.1.tar.gz", hash = "sha256:0f38b83639958ce1152d02a7f062902c41c8fd20d558b0c34344292d417ae272", size = 15561 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/0f/8910b19ac0670a0f80ce1008e5e751c4a57e14d2c4c13a482aa6079fa9d6/jsonschema_specifications-2024.10.1-py3-none-any.whl", hash = "sha256:a09a0680616357d9a0ecf05c12ad234479f549239d0f5b55f3deea67475da9bf", size = 18459 }, +] + [[package]] name = "mako" version = "1.3.9" @@ -1203,6 +1302,7 @@ dependencies = [ { name = "nilai-api" }, { name = "nilai-common" }, { name = "nilai-models" }, + { name = "nuc-helpers" }, ] [package.dev-dependencies] @@ -1225,6 +1325,7 @@ requires-dist = [ { name = "nilai-api", editable = "nilai-api" }, { name = "nilai-common", editable = "packages/nilai-common" }, { name = "nilai-models", editable = "nilai-models" }, + { name = "nuc-helpers", editable = "nilai-auth/nuc-helpers" }, ] [package.metadata.requires-dev] @@ -1233,11 +1334,11 @@ dev = [ { name = "httpx", specifier = ">=0.28.1" }, { name = "isort", specifier = ">=5.13.2" }, { name = "pre-commit", specifier = ">=4.1.0" }, - { name = "pyright", specifier = ">=1.1.399" }, + { name = "pyright", specifier = ">=1.1.400" }, { name = "pytest", specifier = ">=8.3.3" }, { name = "pytest-asyncio", specifier = ">=0.25.0" }, { name = "pytest-mock", specifier = ">=3.14.0" }, - { name = "ruff", specifier = ">=0.11.4" }, + { name = "ruff", specifier = ">=0.11.7" }, { name = "testcontainers", specifier = ">=4.9.1" }, { name = "uvicorn", specifier = ">=0.32.1" }, ] @@ -1259,6 +1360,7 @@ dependencies = [ { name = "httpx" }, { name = "nilai-common" }, { name = "nilrag" }, + { name = "nuc" }, { name = "openai" }, { name = "pg8000" }, { name = "prometheus-fastapi-instrumentator" }, @@ -1284,6 +1386,7 @@ requires-dist = [ { name = "httpx", specifier = ">=0.27.2" }, { name = "nilai-common", editable = "packages/nilai-common" }, { name = "nilrag", specifier = ">=0.1.11" }, + { name = "nuc", git = "https://github.com/NillionNetwork/nuc-py.git" }, { name = "openai", specifier = ">=1.59.9" }, { name = "pg8000", specifier = ">=1.31.2" }, { name = "prometheus-fastapi-instrumentator", specifier = ">=7.0.2" }, @@ -1295,6 +1398,40 @@ requires-dist = [ { name = "web3", specifier = ">=7.8.0" }, ] +[[package]] +name = "nilai-auth-client" +version = "0.1.0" +source = { editable = "nilai-auth/nilai-auth-client" } +dependencies = [ + { name = "nuc-helpers" }, + { name = "openai" }, +] + +[package.metadata] +requires-dist = [ + { name = "nuc-helpers", editable = "nilai-auth/nuc-helpers" }, + { name = "openai", specifier = ">=1.70.0" }, +] + +[[package]] +name = "nilai-auth-server" +version = "0.1.0" +source = { editable = "nilai-auth/nilai-auth-server" } +dependencies = [ + { name = "fastapi", extra = ["standard"] }, + { name = "gunicorn" }, + { name = "nuc-helpers" }, + { name = "uvicorn" }, +] + +[package.metadata] +requires-dist = [ + { name = "fastapi", extras = ["standard"], specifier = ">=0.115.5" }, + { name = "gunicorn", specifier = ">=23.0.0" }, + { name = "nuc-helpers", editable = "nilai-auth/nuc-helpers" }, + { name = "uvicorn", specifier = ">=0.34.0" }, +] + [[package]] name = "nilai-common" version = "0.1.0" @@ -1369,6 +1506,37 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 }, ] +[[package]] +name = "nuc" +version = "0.0.0a0" +source = { git = "https://github.com/NillionNetwork/nuc-py.git#ff950979d3ebe0c0808453e578fc76c927575687" } +dependencies = [ + { name = "cosmpy" }, + { name = "requests" }, + { name = "secp256k1" }, +] + +[[package]] +name = "nuc-helpers" +version = "0.1.0" +source = { editable = "nilai-auth/nuc-helpers" } +dependencies = [ + { name = "cosmpy" }, + { name = "httpx" }, + { name = "nuc" }, + { name = "pydantic" }, + { name = "secp256k1" }, +] + +[package.metadata] +requires-dist = [ + { name = "cosmpy", specifier = "==0.9.2" }, + { name = "httpx", specifier = ">=0.28.1" }, + { name = "nuc", git = "https://github.com/NillionNetwork/nuc-py.git" }, + { name = "pydantic", specifier = ">=2.11.2" }, + { name = "secp256k1", specifier = ">=0.14.0" }, +] + [[package]] name = "numpy" version = "1.26.4" @@ -1741,6 +1909,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b8/d3/c3cb8f1d6ae3b37f83e1de806713a9b3642c5895f0215a62e1a4bd6e5e34/propcache-0.3.1-py3-none-any.whl", hash = "sha256:9a8ecf38de50a7f518c21568c80f985e776397b902f1ce0b01f799aba1608b40", size = 12376 }, ] +[[package]] +name = "protobuf" +version = "4.25.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/48/d5/cccc7e82bbda9909ced3e7a441a24205ea07fea4ce23a772743c0c7611fa/protobuf-4.25.6.tar.gz", hash = "sha256:f8cfbae7c5afd0d0eaccbe73267339bff605a2315860bb1ba08eb66670a9a91f", size = 380631 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/41/0ff3559d9a0fbdb37c9452f2b84e61f7784d8d7b9850182c7ef493f523ee/protobuf-4.25.6-cp310-abi3-win32.whl", hash = "sha256:61df6b5786e2b49fc0055f636c1e8f0aff263808bb724b95b164685ac1bcc13a", size = 392454 }, + { url = "https://files.pythonhosted.org/packages/79/84/c700d6c3f3be770495b08a1c035e330497a31420e4a39a24c22c02cefc6c/protobuf-4.25.6-cp310-abi3-win_amd64.whl", hash = "sha256:b8f837bfb77513fe0e2f263250f423217a173b6d85135be4d81e96a4653bcd3c", size = 413443 }, + { url = "https://files.pythonhosted.org/packages/b7/03/361e87cc824452376c2abcef0eabd18da78a7439479ec6541cf29076a4dc/protobuf-4.25.6-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:6d4381f2417606d7e01750e2729fe6fbcda3f9883aa0c32b51d23012bded6c91", size = 394246 }, + { url = "https://files.pythonhosted.org/packages/64/d5/7dbeb69b74fa88f297c6d8f11b7c9cef0c2e2fb1fdf155c2ca5775cfa998/protobuf-4.25.6-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:5dd800da412ba7f6f26d2c08868a5023ce624e1fdb28bccca2dc957191e81fb5", size = 293714 }, + { url = "https://files.pythonhosted.org/packages/d4/f0/6d5c100f6b18d973e86646aa5fc09bc12ee88a28684a56fd95511bceee68/protobuf-4.25.6-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:4434ff8bb5576f9e0c78f47c41cdf3a152c0b44de475784cd3fd170aef16205a", size = 294634 }, + { url = "https://files.pythonhosted.org/packages/71/eb/be11a1244d0e58ee04c17a1f939b100199063e26ecca8262c04827fe0bf5/protobuf-4.25.6-py3-none-any.whl", hash = "sha256:07972021c8e30b870cfc0863409d033af940213e0e7f64e27fe017b929d2c9f7", size = 156466 }, +] + [[package]] name = "psutil" version = "7.0.0" @@ -1865,15 +2047,15 @@ crypto = [ [[package]] name = "pyright" -version = "1.1.399" +version = "1.1.400" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "nodeenv" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/db/9d/d91d5f6d26b2db95476fefc772e2b9a16d54c6bd0ea6bb5c1b6d635ab8b4/pyright-1.1.399.tar.gz", hash = "sha256:439035d707a36c3d1b443aec980bc37053fbda88158eded24b8eedcf1c7b7a1b", size = 3856954 } +sdist = { url = "https://files.pythonhosted.org/packages/6c/cb/c306618a02d0ee8aed5fb8d0fe0ecfed0dbf075f71468f03a30b5f4e1fe0/pyright-1.1.400.tar.gz", hash = "sha256:b8a3ba40481aa47ba08ffb3228e821d22f7d391f83609211335858bf05686bdb", size = 3846546 } wheels = [ - { url = "https://files.pythonhosted.org/packages/2f/b5/380380c9e7a534cb1783c70c3e8ac6d1193c599650a55838d0557586796e/pyright-1.1.399-py3-none-any.whl", hash = "sha256:55f9a875ddf23c9698f24208c764465ffdfd38be6265f7faf9a176e1dc549f3b", size = 5592584 }, + { url = "https://files.pythonhosted.org/packages/c8/a5/5d285e4932cf149c90e3c425610c5efaea005475d5f96f1bfdb452956c62/pyright-1.1.400-py3-none-any.whl", hash = "sha256:c80d04f98b5a4358ad3a35e241dbf2a408eee33a40779df365644f8054d2517e", size = 5563460 }, ] [[package]] @@ -2011,6 +2193,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3c/5f/fa26b9b2672cbe30e07d9a5bdf39cf16e3b80b42916757c5f92bca88e4ba/redis-5.2.1-py3-none-any.whl", hash = "sha256:ee7e1056b9aea0f04c6c2ed59452947f34c4940ee025f5dd83e6a6418b6989e4", size = 261502 }, ] +[[package]] +name = "referencing" +version = "0.36.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775 }, +] + [[package]] name = "regex" version = "2024.11.6" @@ -2103,29 +2299,76 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/99/fb/e4c0ced9893b84ac95b7181d69a9786ce5879aeb3bbbcbba80a164f85d6a/rlp-4.1.0-py3-none-any.whl", hash = "sha256:8eca394c579bad34ee0b937aecb96a57052ff3716e19c7a578883e767bc5da6f", size = 19973 }, ] +[[package]] +name = "rpds-py" +version = "0.24.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/b3/52b213298a0ba7097c7ea96bee95e1947aa84cc816d48cebb539770cdf41/rpds_py-0.24.0.tar.gz", hash = "sha256:772cc1b2cd963e7e17e6cc55fe0371fb9c704d63e44cacec7b9b7f523b78919e", size = 26863 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/e0/1c55f4a3be5f1ca1a4fd1f3ff1504a1478c1ed48d84de24574c4fa87e921/rpds_py-0.24.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:d8551e733626afec514b5d15befabea0dd70a343a9f23322860c4f16a9430205", size = 366945 }, + { url = "https://files.pythonhosted.org/packages/39/1b/a3501574fbf29118164314dbc800d568b8c1c7b3258b505360e8abb3902c/rpds_py-0.24.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e374c0ce0ca82e5b67cd61fb964077d40ec177dd2c4eda67dba130de09085c7", size = 351935 }, + { url = "https://files.pythonhosted.org/packages/dc/47/77d3d71c55f6a374edde29f1aca0b2e547325ed00a9da820cabbc9497d2b/rpds_py-0.24.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d69d003296df4840bd445a5d15fa5b6ff6ac40496f956a221c4d1f6f7b4bc4d9", size = 390817 }, + { url = "https://files.pythonhosted.org/packages/4e/ec/1e336ee27484379e19c7f9cc170f4217c608aee406d3ae3a2e45336bff36/rpds_py-0.24.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8212ff58ac6dfde49946bea57474a386cca3f7706fc72c25b772b9ca4af6b79e", size = 401983 }, + { url = "https://files.pythonhosted.org/packages/07/f8/39b65cbc272c635eaea6d393c2ad1ccc81c39eca2db6723a0ca4b2108fce/rpds_py-0.24.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:528927e63a70b4d5f3f5ccc1fa988a35456eb5d15f804d276709c33fc2f19bda", size = 451719 }, + { url = "https://files.pythonhosted.org/packages/32/05/05c2b27dd9c30432f31738afed0300659cb9415db0ff7429b05dfb09bbde/rpds_py-0.24.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a824d2c7a703ba6daaca848f9c3d5cb93af0505be505de70e7e66829affd676e", size = 442546 }, + { url = "https://files.pythonhosted.org/packages/7d/e0/19383c8b5d509bd741532a47821c3e96acf4543d0832beba41b4434bcc49/rpds_py-0.24.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44d51febb7a114293ffd56c6cf4736cb31cd68c0fddd6aa303ed09ea5a48e029", size = 393695 }, + { url = "https://files.pythonhosted.org/packages/9d/15/39f14e96d94981d0275715ae8ea564772237f3fa89bc3c21e24de934f2c7/rpds_py-0.24.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3fab5f4a2c64a8fb64fc13b3d139848817a64d467dd6ed60dcdd6b479e7febc9", size = 427218 }, + { url = "https://files.pythonhosted.org/packages/22/b9/12da7124905a680f690da7a9de6f11de770b5e359f5649972f7181c8bf51/rpds_py-0.24.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9be4f99bee42ac107870c61dfdb294d912bf81c3c6d45538aad7aecab468b6b7", size = 568062 }, + { url = "https://files.pythonhosted.org/packages/88/17/75229017a2143d915f6f803721a6d721eca24f2659c5718a538afa276b4f/rpds_py-0.24.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:564c96b6076a98215af52f55efa90d8419cc2ef45d99e314fddefe816bc24f91", size = 596262 }, + { url = "https://files.pythonhosted.org/packages/aa/64/8e8a1d8bd1b6b638d6acb6d41ab2cec7f2067a5b8b4c9175703875159a7c/rpds_py-0.24.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:75a810b7664c17f24bf2ffd7f92416c00ec84b49bb68e6a0d93e542406336b56", size = 564306 }, + { url = "https://files.pythonhosted.org/packages/68/1c/a7eac8d8ed8cb234a9b1064647824c387753343c3fab6ed7c83481ed0be7/rpds_py-0.24.0-cp312-cp312-win32.whl", hash = "sha256:f6016bd950be4dcd047b7475fdf55fb1e1f59fc7403f387be0e8123e4a576d30", size = 224281 }, + { url = "https://files.pythonhosted.org/packages/bb/46/b8b5424d1d21f2f2f3f2d468660085318d4f74a8df8289e3dd6ad224d488/rpds_py-0.24.0-cp312-cp312-win_amd64.whl", hash = "sha256:998c01b8e71cf051c28f5d6f1187abbdf5cf45fc0efce5da6c06447cba997034", size = 239719 }, + { url = "https://files.pythonhosted.org/packages/9d/c3/3607abc770395bc6d5a00cb66385a5479fb8cd7416ddef90393b17ef4340/rpds_py-0.24.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3d2d8e4508e15fc05b31285c4b00ddf2e0eb94259c2dc896771966a163122a0c", size = 367072 }, + { url = "https://files.pythonhosted.org/packages/d8/35/8c7ee0fe465793e3af3298dc5a9f3013bd63e7a69df04ccfded8293a4982/rpds_py-0.24.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0f00c16e089282ad68a3820fd0c831c35d3194b7cdc31d6e469511d9bffc535c", size = 351919 }, + { url = "https://files.pythonhosted.org/packages/91/d3/7e1b972501eb5466b9aca46a9c31bcbbdc3ea5a076e9ab33f4438c1d069d/rpds_py-0.24.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:951cc481c0c395c4a08639a469d53b7d4afa252529a085418b82a6b43c45c240", size = 390360 }, + { url = "https://files.pythonhosted.org/packages/a2/a8/ccabb50d3c91c26ad01f9b09a6a3b03e4502ce51a33867c38446df9f896b/rpds_py-0.24.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c9ca89938dff18828a328af41ffdf3902405a19f4131c88e22e776a8e228c5a8", size = 400704 }, + { url = "https://files.pythonhosted.org/packages/53/ae/5fa5bf0f3bc6ce21b5ea88fc0ecd3a439e7cb09dd5f9ffb3dbe1b6894fc5/rpds_py-0.24.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed0ef550042a8dbcd657dfb284a8ee00f0ba269d3f2286b0493b15a5694f9fe8", size = 450839 }, + { url = "https://files.pythonhosted.org/packages/e3/ac/c4e18b36d9938247e2b54f6a03746f3183ca20e1edd7d3654796867f5100/rpds_py-0.24.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b2356688e5d958c4d5cb964af865bea84db29971d3e563fb78e46e20fe1848b", size = 441494 }, + { url = "https://files.pythonhosted.org/packages/bf/08/b543969c12a8f44db6c0f08ced009abf8f519191ca6985509e7c44102e3c/rpds_py-0.24.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78884d155fd15d9f64f5d6124b486f3d3f7fd7cd71a78e9670a0f6f6ca06fb2d", size = 393185 }, + { url = "https://files.pythonhosted.org/packages/da/7e/f6eb6a7042ce708f9dfc781832a86063cea8a125bbe451d663697b51944f/rpds_py-0.24.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6a4a535013aeeef13c5532f802708cecae8d66c282babb5cd916379b72110cf7", size = 426168 }, + { url = "https://files.pythonhosted.org/packages/38/b0/6cd2bb0509ac0b51af4bb138e145b7c4c902bb4b724d6fd143689d6e0383/rpds_py-0.24.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:84e0566f15cf4d769dade9b366b7b87c959be472c92dffb70462dd0844d7cbad", size = 567622 }, + { url = "https://files.pythonhosted.org/packages/64/b0/c401f4f077547d98e8b4c2ec6526a80e7cb04f519d416430ec1421ee9e0b/rpds_py-0.24.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:823e74ab6fbaa028ec89615ff6acb409e90ff45580c45920d4dfdddb069f2120", size = 595435 }, + { url = "https://files.pythonhosted.org/packages/9f/ec/7993b6e803294c87b61c85bd63e11142ccfb2373cf88a61ec602abcbf9d6/rpds_py-0.24.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c61a2cb0085c8783906b2f8b1f16a7e65777823c7f4d0a6aaffe26dc0d358dd9", size = 563762 }, + { url = "https://files.pythonhosted.org/packages/1f/29/4508003204cb2f461dc2b83dd85f8aa2b915bc98fe6046b9d50d4aa05401/rpds_py-0.24.0-cp313-cp313-win32.whl", hash = "sha256:60d9b630c8025b9458a9d114e3af579a2c54bd32df601c4581bd054e85258143", size = 223510 }, + { url = "https://files.pythonhosted.org/packages/f9/12/09e048d1814195e01f354155fb772fb0854bd3450b5f5a82224b3a319f0e/rpds_py-0.24.0-cp313-cp313-win_amd64.whl", hash = "sha256:6eea559077d29486c68218178ea946263b87f1c41ae7f996b1f30a983c476a5a", size = 239075 }, + { url = "https://files.pythonhosted.org/packages/d2/03/5027cde39bb2408d61e4dd0cf81f815949bb629932a6c8df1701d0257fc4/rpds_py-0.24.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:d09dc82af2d3c17e7dd17120b202a79b578d79f2b5424bda209d9966efeed114", size = 362974 }, + { url = "https://files.pythonhosted.org/packages/bf/10/24d374a2131b1ffafb783e436e770e42dfdb74b69a2cd25eba8c8b29d861/rpds_py-0.24.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5fc13b44de6419d1e7a7e592a4885b323fbc2f46e1f22151e3a8ed3b8b920405", size = 348730 }, + { url = "https://files.pythonhosted.org/packages/7a/d1/1ef88d0516d46cd8df12e5916966dbf716d5ec79b265eda56ba1b173398c/rpds_py-0.24.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c347a20d79cedc0a7bd51c4d4b7dbc613ca4e65a756b5c3e57ec84bd43505b47", size = 387627 }, + { url = "https://files.pythonhosted.org/packages/4e/35/07339051b8b901ecefd449ebf8e5522e92bcb95e1078818cbfd9db8e573c/rpds_py-0.24.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:20f2712bd1cc26a3cc16c5a1bfee9ed1abc33d4cdf1aabd297fe0eb724df4272", size = 394094 }, + { url = "https://files.pythonhosted.org/packages/dc/62/ee89ece19e0ba322b08734e95441952062391065c157bbd4f8802316b4f1/rpds_py-0.24.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aad911555286884be1e427ef0dc0ba3929e6821cbeca2194b13dc415a462c7fd", size = 449639 }, + { url = "https://files.pythonhosted.org/packages/15/24/b30e9f9e71baa0b9dada3a4ab43d567c6b04a36d1cb531045f7a8a0a7439/rpds_py-0.24.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0aeb3329c1721c43c58cae274d7d2ca85c1690d89485d9c63a006cb79a85771a", size = 438584 }, + { url = "https://files.pythonhosted.org/packages/28/d9/49f7b8f3b4147db13961e19d5e30077cd0854ccc08487026d2cb2142aa4a/rpds_py-0.24.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a0f156e9509cee987283abd2296ec816225145a13ed0391df8f71bf1d789e2d", size = 391047 }, + { url = "https://files.pythonhosted.org/packages/49/b0/e66918d0972c33a259ba3cd7b7ff10ed8bd91dbcfcbec6367b21f026db75/rpds_py-0.24.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:aa6800adc8204ce898c8a424303969b7aa6a5e4ad2789c13f8648739830323b7", size = 418085 }, + { url = "https://files.pythonhosted.org/packages/e1/6b/99ed7ea0a94c7ae5520a21be77a82306aac9e4e715d4435076ead07d05c6/rpds_py-0.24.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a18fc371e900a21d7392517c6f60fe859e802547309e94313cd8181ad9db004d", size = 564498 }, + { url = "https://files.pythonhosted.org/packages/28/26/1cacfee6b800e6fb5f91acecc2e52f17dbf8b0796a7c984b4568b6d70e38/rpds_py-0.24.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:9168764133fd919f8dcca2ead66de0105f4ef5659cbb4fa044f7014bed9a1797", size = 590202 }, + { url = "https://files.pythonhosted.org/packages/a9/9e/57bd2f9fba04a37cef673f9a66b11ca8c43ccdd50d386c455cd4380fe461/rpds_py-0.24.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5f6e3cec44ba05ee5cbdebe92d052f69b63ae792e7d05f1020ac5e964394080c", size = 561771 }, + { url = "https://files.pythonhosted.org/packages/9f/cf/b719120f375ab970d1c297dbf8de1e3c9edd26fe92c0ed7178dd94b45992/rpds_py-0.24.0-cp313-cp313t-win32.whl", hash = "sha256:8ebc7e65ca4b111d928b669713865f021b7773350eeac4a31d3e70144297baba", size = 221195 }, + { url = "https://files.pythonhosted.org/packages/2d/e5/22865285789f3412ad0c3d7ec4dc0a3e86483b794be8a5d9ed5a19390900/rpds_py-0.24.0-cp313-cp313t-win_amd64.whl", hash = "sha256:675269d407a257b8c00a6b58205b72eec8231656506c56fd429d924ca00bb350", size = 237354 }, +] + [[package]] name = "ruff" -version = "0.11.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e8/5b/3ae20f89777115944e89c2d8c2e795dcc5b9e04052f76d5347e35e0da66e/ruff-0.11.4.tar.gz", hash = "sha256:f45bd2fb1a56a5a85fae3b95add03fb185a0b30cf47f5edc92aa0355ca1d7407", size = 3933063 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9c/db/baee59ac88f57527fcbaad3a7b309994e42329c6bc4d4d2b681a3d7b5426/ruff-0.11.4-py3-none-linux_armv6l.whl", hash = "sha256:d9f4a761ecbde448a2d3e12fb398647c7f0bf526dbc354a643ec505965824ed2", size = 10106493 }, - { url = "https://files.pythonhosted.org/packages/c1/d6/9a0962cbb347f4ff98b33d699bf1193ff04ca93bed4b4222fd881b502154/ruff-0.11.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:8c1747d903447d45ca3d40c794d1a56458c51e5cc1bc77b7b64bd2cf0b1626cc", size = 10876382 }, - { url = "https://files.pythonhosted.org/packages/3a/8f/62bab0c7d7e1ae3707b69b157701b41c1ccab8f83e8501734d12ea8a839f/ruff-0.11.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:51a6494209cacca79e121e9b244dc30d3414dac8cc5afb93f852173a2ecfc906", size = 10237050 }, - { url = "https://files.pythonhosted.org/packages/09/96/e296965ae9705af19c265d4d441958ed65c0c58fc4ec340c27cc9d2a1f5b/ruff-0.11.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f171605f65f4fc49c87f41b456e882cd0c89e4ac9d58e149a2b07930e1d466f", size = 10424984 }, - { url = "https://files.pythonhosted.org/packages/e5/56/644595eb57d855afed6e54b852e2df8cd5ca94c78043b2f29bdfb29882d5/ruff-0.11.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ebf99ea9af918878e6ce42098981fc8c1db3850fef2f1ada69fb1dcdb0f8e79e", size = 9957438 }, - { url = "https://files.pythonhosted.org/packages/86/83/9d3f3bed0118aef3e871ded9e5687fb8c5776bde233427fd9ce0a45db2d4/ruff-0.11.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edad2eac42279df12e176564a23fc6f4aaeeb09abba840627780b1bb11a9d223", size = 11547282 }, - { url = "https://files.pythonhosted.org/packages/40/e6/0c6e4f5ae72fac5ccb44d72c0111f294a5c2c8cc5024afcb38e6bda5f4b3/ruff-0.11.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f103a848be9ff379fc19b5d656c1f911d0a0b4e3e0424f9532ececf319a4296e", size = 12182020 }, - { url = "https://files.pythonhosted.org/packages/b5/92/4aed0e460aeb1df5ea0c2fbe8d04f9725cccdb25d8da09a0d3f5b8764bf8/ruff-0.11.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:193e6fac6eb60cc97b9f728e953c21cc38a20077ed64f912e9d62b97487f3f2d", size = 11679154 }, - { url = "https://files.pythonhosted.org/packages/1b/d3/7316aa2609f2c592038e2543483eafbc62a0e1a6a6965178e284808c095c/ruff-0.11.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7af4e5f69b7c138be8dcffa5b4a061bf6ba6a3301f632a6bce25d45daff9bc99", size = 13905985 }, - { url = "https://files.pythonhosted.org/packages/63/80/734d3d17546e47ff99871f44ea7540ad2bbd7a480ed197fe8a1c8a261075/ruff-0.11.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:126b1bf13154aa18ae2d6c3c5efe144ec14b97c60844cfa6eb960c2a05188222", size = 11348343 }, - { url = "https://files.pythonhosted.org/packages/04/7b/70fc7f09a0161dce9613a4671d198f609e653d6f4ff9eee14d64c4c240fb/ruff-0.11.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e8806daaf9dfa881a0ed603f8a0e364e4f11b6ed461b56cae2b1c0cab0645304", size = 10308487 }, - { url = "https://files.pythonhosted.org/packages/1a/22/1cdd62dabd678d75842bf4944fd889cf794dc9e58c18cc547f9eb28f95ed/ruff-0.11.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5d94bb1cc2fc94a769b0eb975344f1b1f3d294da1da9ddbb5a77665feb3a3019", size = 9929091 }, - { url = "https://files.pythonhosted.org/packages/9f/20/40e0563506332313148e783bbc1e4276d657962cc370657b2fff20e6e058/ruff-0.11.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:995071203d0fe2183fc7a268766fd7603afb9996785f086b0d76edee8755c896", size = 10924659 }, - { url = "https://files.pythonhosted.org/packages/b5/41/eef9b7aac8819d9e942f617f9db296f13d2c4576806d604aba8db5a753f1/ruff-0.11.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7a37ca937e307ea18156e775a6ac6e02f34b99e8c23fe63c1996185a4efe0751", size = 11428160 }, - { url = "https://files.pythonhosted.org/packages/ff/61/c488943414fb2b8754c02f3879de003e26efdd20f38167ded3fb3fc1cda3/ruff-0.11.4-py3-none-win32.whl", hash = "sha256:0e9365a7dff9b93af933dab8aebce53b72d8f815e131796268709890b4a83270", size = 10311496 }, - { url = "https://files.pythonhosted.org/packages/b6/2b/2a1c8deb5f5dfa3871eb7daa41492c4d2b2824a74d2b38e788617612a66d/ruff-0.11.4-py3-none-win_amd64.whl", hash = "sha256:5a9fa1c69c7815e39fcfb3646bbfd7f528fa8e2d4bebdcf4c2bd0fa037a255fb", size = 11399146 }, - { url = "https://files.pythonhosted.org/packages/4f/03/3aec4846226d54a37822e4c7ea39489e4abd6f88388fba74e3d4abe77300/ruff-0.11.4-py3-none-win_arm64.whl", hash = "sha256:d435db6b9b93d02934cf61ef332e66af82da6d8c69aefdea5994c89997c7a0fc", size = 10450306 }, +version = "0.11.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5b/89/6f9c9674818ac2e9cc2f2b35b704b7768656e6b7c139064fc7ba8fbc99f1/ruff-0.11.7.tar.gz", hash = "sha256:655089ad3224070736dc32844fde783454f8558e71f501cb207485fe4eee23d4", size = 4054861 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/ec/21927cb906c5614b786d1621dba405e3d44f6e473872e6df5d1a6bca0455/ruff-0.11.7-py3-none-linux_armv6l.whl", hash = "sha256:d29e909d9a8d02f928d72ab7837b5cbc450a5bdf578ab9ebee3263d0a525091c", size = 10245403 }, + { url = "https://files.pythonhosted.org/packages/e2/af/fec85b6c2c725bcb062a354dd7cbc1eed53c33ff3aa665165871c9c16ddf/ruff-0.11.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:dd1fb86b168ae349fb01dd497d83537b2c5541fe0626e70c786427dd8363aaee", size = 11007166 }, + { url = "https://files.pythonhosted.org/packages/31/9a/2d0d260a58e81f388800343a45898fd8df73c608b8261c370058b675319a/ruff-0.11.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d3d7d2e140a6fbbc09033bce65bd7ea29d6a0adeb90b8430262fbacd58c38ada", size = 10378076 }, + { url = "https://files.pythonhosted.org/packages/c2/c4/9b09b45051404d2e7dd6d9dbcbabaa5ab0093f9febcae664876a77b9ad53/ruff-0.11.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4809df77de390a1c2077d9b7945d82f44b95d19ceccf0c287c56e4dc9b91ca64", size = 10557138 }, + { url = "https://files.pythonhosted.org/packages/5e/5e/f62a1b6669870a591ed7db771c332fabb30f83c967f376b05e7c91bccd14/ruff-0.11.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f3a0c2e169e6b545f8e2dba185eabbd9db4f08880032e75aa0e285a6d3f48201", size = 10095726 }, + { url = "https://files.pythonhosted.org/packages/45/59/a7aa8e716f4cbe07c3500a391e58c52caf665bb242bf8be42c62adef649c/ruff-0.11.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49b888200a320dd96a68e86736cf531d6afba03e4f6cf098401406a257fcf3d6", size = 11672265 }, + { url = "https://files.pythonhosted.org/packages/dd/e3/101a8b707481f37aca5f0fcc3e42932fa38b51add87bfbd8e41ab14adb24/ruff-0.11.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:2b19cdb9cf7dae00d5ee2e7c013540cdc3b31c4f281f1dacb5a799d610e90db4", size = 12331418 }, + { url = "https://files.pythonhosted.org/packages/dd/71/037f76cbe712f5cbc7b852e4916cd3cf32301a30351818d32ab71580d1c0/ruff-0.11.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:64e0ee994c9e326b43539d133a36a455dbaab477bc84fe7bfbd528abe2f05c1e", size = 11794506 }, + { url = "https://files.pythonhosted.org/packages/ca/de/e450b6bab1fc60ef263ef8fcda077fb4977601184877dce1c59109356084/ruff-0.11.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bad82052311479a5865f52c76ecee5d468a58ba44fb23ee15079f17dd4c8fd63", size = 13939084 }, + { url = "https://files.pythonhosted.org/packages/0e/2c/1e364cc92970075d7d04c69c928430b23e43a433f044474f57e425cbed37/ruff-0.11.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7940665e74e7b65d427b82bffc1e46710ec7f30d58b4b2d5016e3f0321436502", size = 11450441 }, + { url = "https://files.pythonhosted.org/packages/9d/7d/1b048eb460517ff9accd78bca0fa6ae61df2b276010538e586f834f5e402/ruff-0.11.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:169027e31c52c0e36c44ae9a9c7db35e505fee0b39f8d9fca7274a6305295a92", size = 10441060 }, + { url = "https://files.pythonhosted.org/packages/3a/57/8dc6ccfd8380e5ca3d13ff7591e8ba46a3b330323515a4996b991b10bd5d/ruff-0.11.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:305b93f9798aee582e91e34437810439acb28b5fc1fee6b8205c78c806845a94", size = 10058689 }, + { url = "https://files.pythonhosted.org/packages/23/bf/20487561ed72654147817885559ba2aa705272d8b5dee7654d3ef2dbf912/ruff-0.11.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a681db041ef55550c371f9cd52a3cf17a0da4c75d6bd691092dfc38170ebc4b6", size = 11073703 }, + { url = "https://files.pythonhosted.org/packages/9d/27/04f2db95f4ef73dccedd0c21daf9991cc3b7f29901a4362057b132075aa4/ruff-0.11.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:07f1496ad00a4a139f4de220b0c97da6d4c85e0e4aa9b2624167b7d4d44fd6b6", size = 11532822 }, + { url = "https://files.pythonhosted.org/packages/e1/72/43b123e4db52144c8add336581de52185097545981ff6e9e58a21861c250/ruff-0.11.7-py3-none-win32.whl", hash = "sha256:f25dfb853ad217e6e5f1924ae8a5b3f6709051a13e9dad18690de6c8ff299e26", size = 10362436 }, + { url = "https://files.pythonhosted.org/packages/c5/a0/3e58cd76fdee53d5c8ce7a56d84540833f924ccdf2c7d657cb009e604d82/ruff-0.11.7-py3-none-win_amd64.whl", hash = "sha256:0a931d85959ceb77e92aea4bbedfded0a31534ce191252721128f77e5ae1f98a", size = 11566676 }, + { url = "https://files.pythonhosted.org/packages/68/ca/69d7c7752bce162d1516e5592b1cc6b6668e9328c0d270609ddbeeadd7cf/ruff-0.11.7-py3-none-win_arm64.whl", hash = "sha256:778c1e5d6f9e91034142dfd06110534ca13220bfaad5c3735f6cb844654f6177", size = 10677936 }, ] [[package]] @@ -2228,6 +2471,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d9/9f/8b2f2749ccfbe4fcef08650896ac47ed919ff25b7ac57b7a1ae7da16c8c3/scramp-1.4.5-py3-none-any.whl", hash = "sha256:50e37c464fc67f37994e35bee4151e3d8f9320e9c204fca83a5d313c121bbbe7", size = 12781 }, ] +[[package]] +name = "secp256k1" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9b/41/bb668a6e4192303542d2d90c3b38d564af3c17c61bd7d4039af4f29405fe/secp256k1-0.14.0.tar.gz", hash = "sha256:82c06712d69ef945220c8b53c1a0d424c2ff6a1f64aee609030df79ad8383397", size = 2420607 } + [[package]] name = "sentence-transformers" version = "4.0.2"