From 155545b84b60eebf9fc2e22be8f808e2066a5170 Mon Sep 17 00:00:00 2001 From: AndyTitu Date: Mon, 8 Sep 2025 21:29:34 +0200 Subject: [PATCH 1/6] Refactor code to add support for desktop integration --- example/example.py | 2 +- src/onepassword/__init__.py | 3 +- src/onepassword/client.py | 22 ++++--- src/onepassword/core.py | 113 ++++++++++++++++++-------------- src/onepassword/defaults.py | 17 ++++- src/onepassword/desktop_core.py | 98 +++++++++++++++++++++++++++ src/onepassword/items.py | 17 +++-- src/onepassword/items_files.py | 13 ++-- src/onepassword/items_shares.py | 7 +- src/onepassword/secrets.py | 13 ++-- src/onepassword/vaults.py | 7 +- 11 files changed, 222 insertions(+), 90 deletions(-) create mode 100644 src/onepassword/desktop_core.py diff --git a/example/example.py b/example/example.py index aa7a83b3..85ad1636 100644 --- a/example/example.py +++ b/example/example.py @@ -17,7 +17,7 @@ async def main(): # Connects to 1Password. client = await Client.authenticate( - auth=token, + auth=DesktopAuth("AndiTituTest"), # Set the following to your own integration name and version. integration_name="My 1Password Integration", integration_version="v1.0.0", diff --git a/src/onepassword/__init__.py b/src/onepassword/__init__.py index 040ae01c..26d4fcba 100644 --- a/src/onepassword/__init__.py +++ b/src/onepassword/__init__.py @@ -1,6 +1,6 @@ # Code generated by op-codegen - DO NO EDIT MANUALLY -from .client import Client +from .client import Client, DesktopAuth from .defaults import DEFAULT_INTEGRATION_NAME, DEFAULT_INTEGRATION_VERSION from .types import * # noqa F403 from .errors import * # noqa F403 @@ -20,6 +20,7 @@ "Vaults", "DEFAULT_INTEGRATION_NAME", "DEFAULT_INTEGRATION_VERSION", + "DesktopAuth", ] for name, obj in inspect.getmembers(sys.modules["onepassword.types"]): diff --git a/src/onepassword/client.py b/src/onepassword/client.py index ae5377f5..c1de1095 100644 --- a/src/onepassword/client.py +++ b/src/onepassword/client.py @@ -2,8 +2,9 @@ from __future__ import annotations import weakref -from .core import _init_client, _release_client -from .defaults import new_default_config +from .core import Core, UniffiCore +from .desktop_core import DesktopCore +from .defaults import new_default_config, DesktopAuth from .secrets import Secrets from .items import Items from .vaults import Vaults @@ -16,21 +17,26 @@ class Client: @classmethod async def authenticate( - cls, auth: str, integration_name: str, integration_version: str + cls, auth: str | DesktopAuth, integration_name: str, integration_version: str ) -> Client: config = new_default_config( - auth=auth or "", + auth=auth, integration_name=integration_name, integration_version=integration_version, ) - client_id = int(await _init_client(config)) + if isinstance(auth, str): + core = UniffiCore() + elif isinstance(auth, DesktopAuth): + core = DesktopCore() + + client_id = int(await core.init_client(config)) authenticated_client = cls() - authenticated_client.secrets = Secrets(client_id) - authenticated_client.items = Items(client_id) - authenticated_client.vaults = Vaults(client_id) + authenticated_client.secrets = Secrets(client_id, core) + authenticated_client.items = Items(client_id, core) + authenticated_client.vaults = Vaults(client_id, core) authenticated_client._finalizer = weakref.finalize( cls, _release_client, client_id ) diff --git a/src/onepassword/core.py b/src/onepassword/core.py index b1592e15..d47068a6 100644 --- a/src/onepassword/core.py +++ b/src/onepassword/core.py @@ -1,6 +1,6 @@ import json import platform - +from typing import Protocol from onepassword.errors import raise_typed_exception # In empirical tests, we determined that maximum message size that can cross the FFI boundary @@ -8,52 +8,65 @@ # We set the limit to 50MB to be safe and consistent with the other SDKs (where this limit is 64MB), to be reconsidered upon further testing MESSAGE_LIMIT = 50 * 1024 * 1024 -machine_arch = platform.machine().lower() - -if machine_arch in ["x86_64", "amd64"]: - import onepassword.lib.x86_64.op_uniffi_core as core -elif machine_arch in ["aarch64", "arm64"]: - import onepassword.lib.aarch64.op_uniffi_core as core -else: - raise ImportError( - f"Your machine's architecture is not currently supported: {machine_arch}" - ) - - -# InitClient creates a client instance in the current core module and returns its unique ID. -async def _init_client(client_config): - try: - return await core.init_client(json.dumps(client_config)) - except Exception as e: - raise_typed_exception(e) - - -# Invoke calls specified business logic from the SDK core. -async def _invoke(invoke_config): - serialized_config = json.dumps(invoke_config) - if len(serialized_config.encode()) > MESSAGE_LIMIT: - raise ValueError( - f"message size exceeds the limit of {MESSAGE_LIMIT} bytes, please contact 1Password at support@1password.com or https://developer.1password.com/joinslack if you need help." - ) - try: - return await core.invoke(serialized_config) - except Exception as e: - raise_typed_exception(e) - - -# Invoke calls specified business logic from the SDK core. -def _invoke_sync(invoke_config): - serialized_config = json.dumps(invoke_config) - if len(serialized_config.encode()) > MESSAGE_LIMIT: - raise ValueError( - f"message size exceeds the limit of {MESSAGE_LIMIT} bytes, please contact 1Password at support@1password.com or https://developer.1password.com/joinslack if you need help." - ) - try: - return core.invoke_sync(serialized_config) - except Exception as e: - raise_typed_exception(e) - - -# ReleaseClient releases memory in the SDK core associated with the given client ID. -def _release_client(client_id): - return core.release_client(json.dumps(client_id)) +class Core(Protocol): + async def init_client(self, client_config: dict) -> str: ... + async def invoke(self, invoke_config: dict) -> str: ... + def invoke_sync(self, invoke_config: dict) -> str: ... + def release_client(self, client_id: int) -> None: ... + +class UniffiCore: + def __init__(self): + machine_arch = platform.machine().lower() + + if machine_arch in ["x86_64", "amd64"]: + import onepassword.lib.x86_64.op_uniffi_core as core + elif machine_arch in ["aarch64", "arm64"]: + import onepassword.lib.aarch64.op_uniffi_core as core + else: + raise ImportError( + f"Your machine's architecture is not currently supported: {machine_arch}" + ) + + self.core = core + + async def init_client(self, client_config: dict): + """Creates a client instance in the current core module and returns its unique ID.""" + try: + return await self.core.init_client(json.dumps(client_config)) + except Exception as e: + raise_typed_exception(e) + + async def invoke(self, invoke_config: dict): + """Invoke business logic asynchronously.""" + serialized_config = json.dumps(invoke_config) + if len(serialized_config.encode()) > MESSAGE_LIMIT: + raise ValueError( + f"message size exceeds the limit of {MESSAGE_LIMIT} bytes, " + "please contact 1Password at support@1password.com or " + "https://developer.1password.com/joinslack if you need help." + ) + try: + return await self.core.invoke(serialized_config) + except Exception as e: + raise_typed_exception(e) + + def invoke_sync(self, invoke_config: dict): + """Invoke business logic synchronously.""" + serialized_config = json.dumps(invoke_config) + if len(serialized_config.encode()) > MESSAGE_LIMIT: + raise ValueError( + f"message size exceeds the limit of {MESSAGE_LIMIT} bytes, " + "please contact 1Password at support@1password.com or " + "https://developer.1password.com/joinslack if you need help." + ) + try: + return self.core.invoke_sync(serialized_config) + except Exception as e: + raise_typed_exception(e) + + def release_client(self, client_id: int): + """Releases memory in the SDK core associated with the given client ID.""" + try: + return self.core.release_client(json.dumps(client_id)) + except Exception as e: + raise_typed_exception(e) diff --git a/src/onepassword/defaults.py b/src/onepassword/defaults.py index cfa4048c..350f61ec 100644 --- a/src/onepassword/defaults.py +++ b/src/onepassword/defaults.py @@ -9,11 +9,19 @@ DEFAULT_REQUEST_LIBRARY_VERSION = "0.11.24" DEFAULT_OS_VERSION = "0.0.0" +class DesktopAuth: + def __init__(self, account_name: str): + """ + Initialize a DesktopAuth instance. + + Args: + account_name (str): The name of the account. + """ + self.account_name = account_name # Generates a configuration dictionary with the user's parameters -def new_default_config(auth, integration_name, integration_version): +def new_default_config(auth: DesktopAuth | str, integration_name, integration_version): client_config_dict = { - "serviceAccountToken": auth, "programmingLanguage": SDK_LANGUAGE, "sdkVersion": SDK_VERSION, "integrationName": integration_name, @@ -24,4 +32,9 @@ def new_default_config(auth, integration_name, integration_version): "osVersion": DEFAULT_OS_VERSION, "architecture": platform.machine(), } + if isinstance(auth, str): + client_config_dict["serviceAccountToken"] = auth + elif isinstance(auth, DesktopAuth): + client_config_dict["accountName"] = auth.account_name + return client_config_dict diff --git a/src/onepassword/desktop_core.py b/src/onepassword/desktop_core.py new file mode 100644 index 00000000..8af173c3 --- /dev/null +++ b/src/onepassword/desktop_core.py @@ -0,0 +1,98 @@ +import ctypes +import json +import os +import platform +import sys +from ctypes import c_uint8, c_size_t, c_int32, POINTER, byref, c_void_p +from .core import UniffiCore + + +def find_1password_lib_path(): + host_os = platform.system().lower() # "darwin", "linux", "windows" + + core = UniffiCore() + if core is None: + raise RuntimeError("failed to get ExtismCore") + + locations_raw = core.invoke({ + "invocation": { + "parameters": { + "methodName": "GetDesktopAppIPCClientLocations", + "serializedParams": {"host_os": host_os}, + } + } + }) + + try: + locations = json.loads(locations_raw) + except Exception as e: + raise RuntimeError(f"failed to parse core response: {e}") + + for lib_path in locations: + if os.path.exists(lib_path): + return lib_path + + raise FileNotFoundError("1Password desktop application not found") + +class DesktopCore: + def __init__(self): + # Determine the path to the desktop app. + path = find_1password_lib_path() + + self.lib = ctypes.CDLL(path) + + # Bind the Rust-exported functions + self.send_message = self.lib.op_sdk_ipc_send_message + self.send_message.argtypes = [ + POINTER(c_uint8), # msg_ptr + c_size_t, # msg_len + POINTER(POINTER(c_uint8)), # out_buf + POINTER(c_size_t), # out_len + POINTER(c_size_t), # out_cap + ] + self.send_message.restype = c_int32 + + self.free_message = self.lib.op_sdk_ipc_free_response + self.free_message.argtypes = [POINTER(c_uint8), c_size_t, c_size_t] + self.free_message.restype = None + + def call_shared_library(self, payload: bytes) -> bytes: + out_buf = POINTER(c_uint8)() + out_len = c_size_t() + out_cap = c_size_t() + + ret = self.send_message( + (ctypes.cast(payload, POINTER(c_uint8))), + len(payload), + byref(out_buf), + byref(out_len), + byref(out_cap), + ) + + if ret != 0: + raise RuntimeError(f"send_message failed with code {ret}") + + # Copy bytes into Python + data = ctypes.string_at(out_buf, out_len.value) + + # Free memory via Rust's exported function + self.free_message(out_buf, out_len, out_cap) + + return data + + def init_client(self, config: dict) -> int: + payload = json.dumps(config).encode("utf-8") + resp = self.call_shared_library(payload) + return json.loads(resp) + + def invoke(self, invoke_config: dict) -> str: + payload = json.dumps(invoke_config).encode("utf-8") + resp = self.call_shared_library(payload) + return resp.decode("utf-8") + + def release_client(self, client_id: int): + payload = json.dumps(client_id).encode("utf-8") + try: + self.call_shared_library(payload) + except Exception as e: + print(f"failed to release client: {e}") diff --git a/src/onepassword/items.py b/src/onepassword/items.py index 0093e00d..4207ac26 100644 --- a/src/onepassword/items.py +++ b/src/onepassword/items.py @@ -1,6 +1,5 @@ # Code generated by op-codegen - DO NO EDIT MANUALLY -from .core import _invoke, _invoke_sync from typing import Optional, List from pydantic import TypeAdapter from .items_shares import ItemsShares @@ -13,17 +12,17 @@ class Items: The Items API holds all operations the SDK client can perform on 1Password items. """ - def __init__(self, client_id): + def __init__(self, client_id, core): self.client_id = client_id + self.core = core self.shares = ItemsShares(client_id) - self.files = ItemsFiles(client_id) async def create(self, params: ItemCreateParams) -> Item: """ Create a new item. """ - response = await _invoke( + response = await self.core.invoke( { "invocation": { "clientId": self.client_id, @@ -42,7 +41,7 @@ async def get(self, vault_id: str, item_id: str) -> Item: """ Get an item by vault and item ID """ - response = await _invoke( + response = await self.core.invoke( { "invocation": { "clientId": self.client_id, @@ -61,7 +60,7 @@ async def put(self, item: Item) -> Item: """ Update an existing item. """ - response = await _invoke( + response = await self.core.invoke( { "invocation": { "clientId": self.client_id, @@ -80,7 +79,7 @@ async def delete(self, vault_id: str, item_id: str) -> None: """ Delete an item. """ - response = await _invoke( + response = await self.core.invoke( { "invocation": { "clientId": self.client_id, @@ -98,7 +97,7 @@ async def archive(self, vault_id: str, item_id: str) -> None: """ Archive an item. """ - response = await _invoke( + response = await self.core.invoke( { "invocation": { "clientId": self.client_id, @@ -116,7 +115,7 @@ async def list(self, vault_id: str, *filters: ItemListFilter) -> List[ItemOvervi """ List items based on filters. """ - response = await _invoke( + response = await self.core.invoke( { "invocation": { "clientId": self.client_id, diff --git a/src/onepassword/items_files.py b/src/onepassword/items_files.py index 28213c99..68d83ff6 100644 --- a/src/onepassword/items_files.py +++ b/src/onepassword/items_files.py @@ -1,20 +1,21 @@ # Code generated by op-codegen - DO NO EDIT MANUALLY -from .core import _invoke, _invoke_sync +from .core import Core from typing import Optional, List from pydantic import TypeAdapter from .types import DocumentCreateParams, FileAttributes, FileCreateParams, Item class ItemsFiles: - def __init__(self, client_id): + def __init__(self, client_id, core): self.client_id = client_id + self.core = core async def attach(self, item: Item, file_params: FileCreateParams) -> Item: """ Attach files to Items """ - response = await _invoke( + response = await self.core.invoke( { "invocation": { "clientId": self.client_id, @@ -36,7 +37,7 @@ async def read(self, vault_id: str, item_id: str, attr: FileAttributes) -> bytes """ Read file content from the Item """ - response = await _invoke( + response = await self.core.invoke( { "invocation": { "clientId": self.client_id, @@ -59,7 +60,7 @@ async def delete(self, item: Item, section_id: str, field_id: str) -> Item: """ Delete a field file from Item using the section and field IDs """ - response = await _invoke( + response = await self.core.invoke( { "invocation": { "clientId": self.client_id, @@ -84,7 +85,7 @@ async def replace_document( """ Replace the document file within a document item """ - response = await _invoke( + response = await self.core.invoke( { "invocation": { "clientId": self.client_id, diff --git a/src/onepassword/items_shares.py b/src/onepassword/items_shares.py index 473292de..e71cadda 100644 --- a/src/onepassword/items_shares.py +++ b/src/onepassword/items_shares.py @@ -1,6 +1,5 @@ # Code generated by op-codegen - DO NO EDIT MANUALLY -from .core import _invoke, _invoke_sync from typing import Optional, List from pydantic import TypeAdapter from .types import Item, ItemShareAccountPolicy, ItemShareParams, ValidRecipient @@ -16,7 +15,7 @@ async def get_account_policy( """ Get the item sharing policy of your account. """ - response = await _invoke( + response = await self.core.invoke( { "invocation": { "clientId": self.client_id, @@ -37,7 +36,7 @@ async def validate_recipients( """ Validate the recipients of an item sharing link. """ - response = await _invoke( + response = await self.core.invoke( { "invocation": { "clientId": self.client_id, @@ -61,7 +60,7 @@ async def create( """ Create a new item sharing link. """ - response = await _invoke( + response = await self.core.invoke( { "invocation": { "clientId": self.client_id, diff --git a/src/onepassword/secrets.py b/src/onepassword/secrets.py index c5f77a2d..758a967d 100644 --- a/src/onepassword/secrets.py +++ b/src/onepassword/secrets.py @@ -1,6 +1,6 @@ # Code generated by op-codegen - DO NO EDIT MANUALLY -from .core import _invoke, _invoke_sync +from .core import Core from typing import Optional, List from pydantic import TypeAdapter from .types import GeneratePasswordResponse, PasswordRecipe, ResolveAllResponse @@ -12,14 +12,15 @@ class Secrets: Use secret reference URIs to securely load secrets from 1Password: op:///[/]/ """ - def __init__(self, client_id): + def __init__(self, client_id, core): self.client_id = client_id + self.core = core async def resolve(self, secret_reference: str) -> str: """ Resolve returns the secret the provided secret reference points to. """ - response = await _invoke( + response = await self.core.invoke( { "invocation": { "clientId": self.client_id, @@ -38,7 +39,7 @@ async def resolve_all(self, secret_references: List[str]) -> ResolveAllResponse: """ Resolve takes in a list of secret references and returns the secrets they point to or errors if any. """ - response = await _invoke( + response = await self.core.invoke( { "invocation": { "clientId": self.client_id, @@ -58,7 +59,7 @@ def validate_secret_reference(secret_reference: str) -> None: """ Validate the secret reference to ensure there are no syntax errors. """ - response = _invoke_sync( + response = self.core.invoke_sync( { "invocation": { "parameters": { @@ -73,7 +74,7 @@ def validate_secret_reference(secret_reference: str) -> None: @staticmethod def generate_password(recipe: PasswordRecipe) -> GeneratePasswordResponse: - response = _invoke_sync( + response = self.core.invoke_sync( { "invocation": { "parameters": { diff --git a/src/onepassword/vaults.py b/src/onepassword/vaults.py index 2b066d5c..7bfa46d4 100644 --- a/src/onepassword/vaults.py +++ b/src/onepassword/vaults.py @@ -1,6 +1,6 @@ # Code generated by op-codegen - DO NO EDIT MANUALLY -from .core import _invoke, _invoke_sync +from .core import Core from typing import Optional, List from pydantic import TypeAdapter from .types import VaultOverview @@ -11,14 +11,15 @@ class Vaults: The Vaults API holds all the operations the SDK client can perform on 1Password vaults. """ - def __init__(self, client_id): + def __init__(self, client_id, core): self.client_id = client_id + self.core = core async def list(self) -> List[VaultOverview]: """ List all vaults """ - response = await _invoke( + response = await self.core.invoke( { "invocation": { "clientId": self.client_id, From c8589e0dd3192f9fee8b68fcf302fa71ac9d46d5 Mon Sep 17 00:00:00 2001 From: AndyTitu Date: Mon, 8 Sep 2025 21:34:37 +0200 Subject: [PATCH 2/6] Inject core in all places needed --- example/example.py | 2 +- src/onepassword/items.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/example/example.py b/example/example.py index 85ad1636..aa7a83b3 100644 --- a/example/example.py +++ b/example/example.py @@ -17,7 +17,7 @@ async def main(): # Connects to 1Password. client = await Client.authenticate( - auth=DesktopAuth("AndiTituTest"), + auth=token, # Set the following to your own integration name and version. integration_name="My 1Password Integration", integration_version="v1.0.0", diff --git a/src/onepassword/items.py b/src/onepassword/items.py index 4207ac26..5c1f190d 100644 --- a/src/onepassword/items.py +++ b/src/onepassword/items.py @@ -15,8 +15,8 @@ class Items: def __init__(self, client_id, core): self.client_id = client_id self.core = core - self.shares = ItemsShares(client_id) - self.files = ItemsFiles(client_id) + self.shares = ItemsShares(client_id, core) + self.files = ItemsFiles(client_id, core) async def create(self, params: ItemCreateParams) -> Item: """ From 099f7d2b4fc8d718a6c569e55bd636470eb06da9 Mon Sep 17 00:00:00 2001 From: AndyTitu Date: Mon, 8 Sep 2025 21:36:02 +0200 Subject: [PATCH 3/6] Refactor item shares as well --- src/onepassword/items_shares.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/onepassword/items_shares.py b/src/onepassword/items_shares.py index e71cadda..31eaadaf 100644 --- a/src/onepassword/items_shares.py +++ b/src/onepassword/items_shares.py @@ -6,8 +6,9 @@ class ItemsShares: - def __init__(self, client_id): + def __init__(self, client_id, core): self.client_id = client_id + self.core = core async def get_account_policy( self, vault_id: str, item_id: str From fd67b9ce72e023254bf7374a40cf6cedab429a6c Mon Sep 17 00:00:00 2001 From: AndyTitu Date: Mon, 8 Sep 2025 21:37:53 +0200 Subject: [PATCH 4/6] Refactor finlizer --- src/onepassword/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/onepassword/client.py b/src/onepassword/client.py index c1de1095..37f5fe1d 100644 --- a/src/onepassword/client.py +++ b/src/onepassword/client.py @@ -38,7 +38,7 @@ async def authenticate( authenticated_client.items = Items(client_id, core) authenticated_client.vaults = Vaults(client_id, core) authenticated_client._finalizer = weakref.finalize( - cls, _release_client, client_id + cls, core.release_client, client_id ) return authenticated_client From 381023a88b8f9195d80fb546a1df6e6caf0e8c94 Mon Sep 17 00:00:00 2001 From: AndyTitu Date: Mon, 8 Sep 2025 21:42:30 +0200 Subject: [PATCH 5/6] Remove py version 3.9 from tests --- .github/workflows/validate.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index b68c6c8e..89269365 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -19,7 +19,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.10", "3.11", "3.12", "3.13"] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v3 From dd4ea87a86b316fae564b2c83b0c93414f6bcd30 Mon Sep 17 00:00:00 2001 From: AndyTitu Date: Mon, 13 Oct 2025 19:51:44 +0300 Subject: [PATCH 6/6] Update code to latestcore data passing model --- example/desktop_app.py | 32 +++++++++++++++ src/onepassword/client.py | 6 +-- src/onepassword/defaults.py | 2 - src/onepassword/desktop_core.py | 73 +++++++++++++++++---------------- src/onepassword/types.py | 5 +++ src/onepassword/vaults.py | 6 +-- 6 files changed, 80 insertions(+), 44 deletions(-) create mode 100644 example/desktop_app.py diff --git a/example/desktop_app.py b/example/desktop_app.py new file mode 100644 index 00000000..54908028 --- /dev/null +++ b/example/desktop_app.py @@ -0,0 +1,32 @@ +# [developer-docs.sdk.python.sdk-import]-start +from onepassword import * +import asyncio + + +async def main(): + # [developer-docs.sdk.python.client-initialization]-start + # Connects to the 1Password desktop app. + client = await Client.authenticate( + auth=DesktopAuth( + account_name="AndiTituTest" # Set to your 1Password account name. + ), + # Set the following to your own integration name and version. + integration_name="My 1Password Integration", + integration_version="v1.0.0", + ) + + # [developer-docs.sdk.python.list-vaults]-start + vaults = await client.vaults.list() + for vault in vaults: + print(vault) + # [developer-docs.sdk.python.list-vaults]-end + + # [developer-docs.sdk.python.list-items]-start + overviews = await client.items.list("xw33qlvug6moegr3wkk5zkenoa") + for overview in overviews: + print(overview.title) + # [developer-docs.sdk.python.list-items]-end + + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file diff --git a/src/onepassword/client.py b/src/onepassword/client.py index 37f5fe1d..88f1e72a 100644 --- a/src/onepassword/client.py +++ b/src/onepassword/client.py @@ -25,10 +25,10 @@ async def authenticate( integration_version=integration_version, ) - if isinstance(auth, str): + if isinstance(auth, DesktopAuth): + core = DesktopCore(auth.account_name) + else: core = UniffiCore() - elif isinstance(auth, DesktopAuth): - core = DesktopCore() client_id = int(await core.init_client(config)) diff --git a/src/onepassword/defaults.py b/src/onepassword/defaults.py index 350f61ec..8e30ba1d 100644 --- a/src/onepassword/defaults.py +++ b/src/onepassword/defaults.py @@ -34,7 +34,5 @@ def new_default_config(auth: DesktopAuth | str, integration_name, integration_ve } if isinstance(auth, str): client_config_dict["serviceAccountToken"] = auth - elif isinstance(auth, DesktopAuth): - client_config_dict["accountName"] = auth.account_name return client_config_dict diff --git a/src/onepassword/desktop_core.py b/src/onepassword/desktop_core.py index 8af173c3..c50ded8a 100644 --- a/src/onepassword/desktop_core.py +++ b/src/onepassword/desktop_core.py @@ -1,32 +1,16 @@ import ctypes import json import os -import platform -import sys +import base64 from ctypes import c_uint8, c_size_t, c_int32, POINTER, byref, c_void_p from .core import UniffiCore +from onepassword.errors import raise_typed_exception def find_1password_lib_path(): - host_os = platform.system().lower() # "darwin", "linux", "windows" - - core = UniffiCore() - if core is None: - raise RuntimeError("failed to get ExtismCore") - - locations_raw = core.invoke({ - "invocation": { - "parameters": { - "methodName": "GetDesktopAppIPCClientLocations", - "serializedParams": {"host_os": host_os}, - } - } - }) - - try: - locations = json.loads(locations_raw) - except Exception as e: - raise RuntimeError(f"failed to parse core response: {e}") + locations = [ + "/Users/andititu/core/target/debug/libop_sdk_ipc_client.dylib" + ] for lib_path in locations: if os.path.exists(lib_path): @@ -35,11 +19,12 @@ def find_1password_lib_path(): raise FileNotFoundError("1Password desktop application not found") class DesktopCore: - def __init__(self): + def __init__(self, account_name: str): # Determine the path to the desktop app. path = find_1password_lib_path() self.lib = ctypes.CDLL(path) + self.account_name = account_name # Bind the Rust-exported functions self.send_message = self.lib.op_sdk_ipc_send_message @@ -56,21 +41,31 @@ def __init__(self): self.free_message.argtypes = [POINTER(c_uint8), c_size_t, c_size_t] self.free_message.restype = None - def call_shared_library(self, payload: bytes) -> bytes: + def call_shared_library(self, payload: str, operation_kind: str) -> bytes: + # Prepare the input + encoded_payload = base64.b64encode(payload.encode("utf-8")).decode("utf-8") + data = { + "kind": operation_kind, + "account_name": self.account_name, + "payload": encoded_payload, + } + message = json.dumps(data).encode("utf-8") + + # Prepare output parameters out_buf = POINTER(c_uint8)() out_len = c_size_t() out_cap = c_size_t() ret = self.send_message( - (ctypes.cast(payload, POINTER(c_uint8))), - len(payload), + (ctypes.cast(message, POINTER(c_uint8))), + len(message), byref(out_buf), byref(out_len), byref(out_cap), ) if ret != 0: - raise RuntimeError(f"send_message failed with code {ret}") + raise RuntimeError(f"send_message failed with code {ret}. Please make sure the Desktop app intehration setting is enabled, or contact 1Password support.") # Copy bytes into Python data = ctypes.string_at(out_buf, out_len.value) @@ -78,21 +73,27 @@ def call_shared_library(self, payload: bytes) -> bytes: # Free memory via Rust's exported function self.free_message(out_buf, out_len, out_cap) - return data + parsed = json.loads(data) + payload = bytes(parsed.get("payload", [])).decode("utf-8") + + success = parsed.get("success", False) + if not success: + raise_typed_exception(Exception(str(payload))) + + return payload - def init_client(self, config: dict) -> int: - payload = json.dumps(config).encode("utf-8") - resp = self.call_shared_library(payload) + async def init_client(self, config: dict) -> int: + payload = json.dumps(config) + resp = self.call_shared_library(payload, "init_client") return json.loads(resp) - def invoke(self, invoke_config: dict) -> str: - payload = json.dumps(invoke_config).encode("utf-8") - resp = self.call_shared_library(payload) - return resp.decode("utf-8") + async def invoke(self, invoke_config: dict) -> str: + payload = json.dumps(invoke_config) + return self.call_shared_library(payload, "invoke") def release_client(self, client_id: int): - payload = json.dumps(client_id).encode("utf-8") + payload = json.dumps(client_id) try: - self.call_shared_library(payload) + self.call_shared_library(payload, "release_client") except Exception as e: print(f"failed to release client: {e}") diff --git a/src/onepassword/types.py b/src/onepassword/types.py index 37d2e333..012fbece 100644 --- a/src/onepassword/types.py +++ b/src/onepassword/types.py @@ -1159,3 +1159,8 @@ class WordListType(str, Enum): """ Three (random) letter "words" """ + +class VaultListParams(BaseModel): + model_config = ConfigDict(populate_by_name=True) + + decrypt_details: Optional[bool] = Field(alias="decryptDetails", default=None) diff --git a/src/onepassword/vaults.py b/src/onepassword/vaults.py index 7bfa46d4..f851fa58 100644 --- a/src/onepassword/vaults.py +++ b/src/onepassword/vaults.py @@ -3,7 +3,7 @@ from .core import Core from typing import Optional, List from pydantic import TypeAdapter -from .types import VaultOverview +from .types import VaultOverview, VaultListParams class Vaults: @@ -15,7 +15,7 @@ def __init__(self, client_id, core): self.client_id = client_id self.core = core - async def list(self) -> List[VaultOverview]: + async def list(self, params: Optional[VaultListParams] = None) -> List[VaultOverview]: """ List all vaults """ @@ -23,7 +23,7 @@ async def list(self) -> List[VaultOverview]: { "invocation": { "clientId": self.client_id, - "parameters": {"name": "VaultsList", "parameters": {}}, + "parameters": {"name": "VaultsList", "parameters": {"params": params.model_dump(by_alias=True) if params else {}}}, } } )