Skip to content

Commit 155545b

Browse files
committed
Refactor code to add support for desktop integration
1 parent c29275d commit 155545b

File tree

11 files changed

+222
-90
lines changed

11 files changed

+222
-90
lines changed

example/example.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ async def main():
1717

1818
# Connects to 1Password.
1919
client = await Client.authenticate(
20-
auth=token,
20+
auth=DesktopAuth("AndiTituTest"),
2121
# Set the following to your own integration name and version.
2222
integration_name="My 1Password Integration",
2323
integration_version="v1.0.0",

src/onepassword/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Code generated by op-codegen - DO NO EDIT MANUALLY
22

3-
from .client import Client
3+
from .client import Client, DesktopAuth
44
from .defaults import DEFAULT_INTEGRATION_NAME, DEFAULT_INTEGRATION_VERSION
55
from .types import * # noqa F403
66
from .errors import * # noqa F403
@@ -20,6 +20,7 @@
2020
"Vaults",
2121
"DEFAULT_INTEGRATION_NAME",
2222
"DEFAULT_INTEGRATION_VERSION",
23+
"DesktopAuth",
2324
]
2425

2526
for name, obj in inspect.getmembers(sys.modules["onepassword.types"]):

src/onepassword/client.py

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22

33
from __future__ import annotations
44
import weakref
5-
from .core import _init_client, _release_client
6-
from .defaults import new_default_config
5+
from .core import Core, UniffiCore
6+
from .desktop_core import DesktopCore
7+
from .defaults import new_default_config, DesktopAuth
78
from .secrets import Secrets
89
from .items import Items
910
from .vaults import Vaults
@@ -16,21 +17,26 @@ class Client:
1617

1718
@classmethod
1819
async def authenticate(
19-
cls, auth: str, integration_name: str, integration_version: str
20+
cls, auth: str | DesktopAuth, integration_name: str, integration_version: str
2021
) -> Client:
2122
config = new_default_config(
22-
auth=auth or "",
23+
auth=auth,
2324
integration_name=integration_name,
2425
integration_version=integration_version,
2526
)
2627

27-
client_id = int(await _init_client(config))
28+
if isinstance(auth, str):
29+
core = UniffiCore()
30+
elif isinstance(auth, DesktopAuth):
31+
core = DesktopCore()
32+
33+
client_id = int(await core.init_client(config))
2834

2935
authenticated_client = cls()
3036

31-
authenticated_client.secrets = Secrets(client_id)
32-
authenticated_client.items = Items(client_id)
33-
authenticated_client.vaults = Vaults(client_id)
37+
authenticated_client.secrets = Secrets(client_id, core)
38+
authenticated_client.items = Items(client_id, core)
39+
authenticated_client.vaults = Vaults(client_id, core)
3440
authenticated_client._finalizer = weakref.finalize(
3541
cls, _release_client, client_id
3642
)

src/onepassword/core.py

Lines changed: 63 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,72 @@
11
import json
22
import platform
3-
3+
from typing import Protocol
44
from onepassword.errors import raise_typed_exception
55

66
# In empirical tests, we determined that maximum message size that can cross the FFI boundary
77
# is ~128MB. Past this limit, FFI will throw an error and the program will crash.
88
# 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
99
MESSAGE_LIMIT = 50 * 1024 * 1024
1010

11-
machine_arch = platform.machine().lower()
12-
13-
if machine_arch in ["x86_64", "amd64"]:
14-
import onepassword.lib.x86_64.op_uniffi_core as core
15-
elif machine_arch in ["aarch64", "arm64"]:
16-
import onepassword.lib.aarch64.op_uniffi_core as core
17-
else:
18-
raise ImportError(
19-
f"Your machine's architecture is not currently supported: {machine_arch}"
20-
)
21-
22-
23-
# InitClient creates a client instance in the current core module and returns its unique ID.
24-
async def _init_client(client_config):
25-
try:
26-
return await core.init_client(json.dumps(client_config))
27-
except Exception as e:
28-
raise_typed_exception(e)
29-
30-
31-
# Invoke calls specified business logic from the SDK core.
32-
async def _invoke(invoke_config):
33-
serialized_config = json.dumps(invoke_config)
34-
if len(serialized_config.encode()) > MESSAGE_LIMIT:
35-
raise ValueError(
36-
f"message size exceeds the limit of {MESSAGE_LIMIT} bytes, please contact 1Password at [email protected] or https://developer.1password.com/joinslack if you need help."
37-
)
38-
try:
39-
return await core.invoke(serialized_config)
40-
except Exception as e:
41-
raise_typed_exception(e)
42-
43-
44-
# Invoke calls specified business logic from the SDK core.
45-
def _invoke_sync(invoke_config):
46-
serialized_config = json.dumps(invoke_config)
47-
if len(serialized_config.encode()) > MESSAGE_LIMIT:
48-
raise ValueError(
49-
f"message size exceeds the limit of {MESSAGE_LIMIT} bytes, please contact 1Password at [email protected] or https://developer.1password.com/joinslack if you need help."
50-
)
51-
try:
52-
return core.invoke_sync(serialized_config)
53-
except Exception as e:
54-
raise_typed_exception(e)
55-
56-
57-
# ReleaseClient releases memory in the SDK core associated with the given client ID.
58-
def _release_client(client_id):
59-
return core.release_client(json.dumps(client_id))
11+
class Core(Protocol):
12+
async def init_client(self, client_config: dict) -> str: ...
13+
async def invoke(self, invoke_config: dict) -> str: ...
14+
def invoke_sync(self, invoke_config: dict) -> str: ...
15+
def release_client(self, client_id: int) -> None: ...
16+
17+
class UniffiCore:
18+
def __init__(self):
19+
machine_arch = platform.machine().lower()
20+
21+
if machine_arch in ["x86_64", "amd64"]:
22+
import onepassword.lib.x86_64.op_uniffi_core as core
23+
elif machine_arch in ["aarch64", "arm64"]:
24+
import onepassword.lib.aarch64.op_uniffi_core as core
25+
else:
26+
raise ImportError(
27+
f"Your machine's architecture is not currently supported: {machine_arch}"
28+
)
29+
30+
self.core = core
31+
32+
async def init_client(self, client_config: dict):
33+
"""Creates a client instance in the current core module and returns its unique ID."""
34+
try:
35+
return await self.core.init_client(json.dumps(client_config))
36+
except Exception as e:
37+
raise_typed_exception(e)
38+
39+
async def invoke(self, invoke_config: dict):
40+
"""Invoke business logic asynchronously."""
41+
serialized_config = json.dumps(invoke_config)
42+
if len(serialized_config.encode()) > MESSAGE_LIMIT:
43+
raise ValueError(
44+
f"message size exceeds the limit of {MESSAGE_LIMIT} bytes, "
45+
"please contact 1Password at [email protected] or "
46+
"https://developer.1password.com/joinslack if you need help."
47+
)
48+
try:
49+
return await self.core.invoke(serialized_config)
50+
except Exception as e:
51+
raise_typed_exception(e)
52+
53+
def invoke_sync(self, invoke_config: dict):
54+
"""Invoke business logic synchronously."""
55+
serialized_config = json.dumps(invoke_config)
56+
if len(serialized_config.encode()) > MESSAGE_LIMIT:
57+
raise ValueError(
58+
f"message size exceeds the limit of {MESSAGE_LIMIT} bytes, "
59+
"please contact 1Password at [email protected] or "
60+
"https://developer.1password.com/joinslack if you need help."
61+
)
62+
try:
63+
return self.core.invoke_sync(serialized_config)
64+
except Exception as e:
65+
raise_typed_exception(e)
66+
67+
def release_client(self, client_id: int):
68+
"""Releases memory in the SDK core associated with the given client ID."""
69+
try:
70+
return self.core.release_client(json.dumps(client_id))
71+
except Exception as e:
72+
raise_typed_exception(e)

src/onepassword/defaults.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,19 @@
99
DEFAULT_REQUEST_LIBRARY_VERSION = "0.11.24"
1010
DEFAULT_OS_VERSION = "0.0.0"
1111

12+
class DesktopAuth:
13+
def __init__(self, account_name: str):
14+
"""
15+
Initialize a DesktopAuth instance.
16+
17+
Args:
18+
account_name (str): The name of the account.
19+
"""
20+
self.account_name = account_name
1221

1322
# Generates a configuration dictionary with the user's parameters
14-
def new_default_config(auth, integration_name, integration_version):
23+
def new_default_config(auth: DesktopAuth | str, integration_name, integration_version):
1524
client_config_dict = {
16-
"serviceAccountToken": auth,
1725
"programmingLanguage": SDK_LANGUAGE,
1826
"sdkVersion": SDK_VERSION,
1927
"integrationName": integration_name,
@@ -24,4 +32,9 @@ def new_default_config(auth, integration_name, integration_version):
2432
"osVersion": DEFAULT_OS_VERSION,
2533
"architecture": platform.machine(),
2634
}
35+
if isinstance(auth, str):
36+
client_config_dict["serviceAccountToken"] = auth
37+
elif isinstance(auth, DesktopAuth):
38+
client_config_dict["accountName"] = auth.account_name
39+
2740
return client_config_dict

src/onepassword/desktop_core.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import ctypes
2+
import json
3+
import os
4+
import platform
5+
import sys
6+
from ctypes import c_uint8, c_size_t, c_int32, POINTER, byref, c_void_p
7+
from .core import UniffiCore
8+
9+
10+
def find_1password_lib_path():
11+
host_os = platform.system().lower() # "darwin", "linux", "windows"
12+
13+
core = UniffiCore()
14+
if core is None:
15+
raise RuntimeError("failed to get ExtismCore")
16+
17+
locations_raw = core.invoke({
18+
"invocation": {
19+
"parameters": {
20+
"methodName": "GetDesktopAppIPCClientLocations",
21+
"serializedParams": {"host_os": host_os},
22+
}
23+
}
24+
})
25+
26+
try:
27+
locations = json.loads(locations_raw)
28+
except Exception as e:
29+
raise RuntimeError(f"failed to parse core response: {e}")
30+
31+
for lib_path in locations:
32+
if os.path.exists(lib_path):
33+
return lib_path
34+
35+
raise FileNotFoundError("1Password desktop application not found")
36+
37+
class DesktopCore:
38+
def __init__(self):
39+
# Determine the path to the desktop app.
40+
path = find_1password_lib_path()
41+
42+
self.lib = ctypes.CDLL(path)
43+
44+
# Bind the Rust-exported functions
45+
self.send_message = self.lib.op_sdk_ipc_send_message
46+
self.send_message.argtypes = [
47+
POINTER(c_uint8), # msg_ptr
48+
c_size_t, # msg_len
49+
POINTER(POINTER(c_uint8)), # out_buf
50+
POINTER(c_size_t), # out_len
51+
POINTER(c_size_t), # out_cap
52+
]
53+
self.send_message.restype = c_int32
54+
55+
self.free_message = self.lib.op_sdk_ipc_free_response
56+
self.free_message.argtypes = [POINTER(c_uint8), c_size_t, c_size_t]
57+
self.free_message.restype = None
58+
59+
def call_shared_library(self, payload: bytes) -> bytes:
60+
out_buf = POINTER(c_uint8)()
61+
out_len = c_size_t()
62+
out_cap = c_size_t()
63+
64+
ret = self.send_message(
65+
(ctypes.cast(payload, POINTER(c_uint8))),
66+
len(payload),
67+
byref(out_buf),
68+
byref(out_len),
69+
byref(out_cap),
70+
)
71+
72+
if ret != 0:
73+
raise RuntimeError(f"send_message failed with code {ret}")
74+
75+
# Copy bytes into Python
76+
data = ctypes.string_at(out_buf, out_len.value)
77+
78+
# Free memory via Rust's exported function
79+
self.free_message(out_buf, out_len, out_cap)
80+
81+
return data
82+
83+
def init_client(self, config: dict) -> int:
84+
payload = json.dumps(config).encode("utf-8")
85+
resp = self.call_shared_library(payload)
86+
return json.loads(resp)
87+
88+
def invoke(self, invoke_config: dict) -> str:
89+
payload = json.dumps(invoke_config).encode("utf-8")
90+
resp = self.call_shared_library(payload)
91+
return resp.decode("utf-8")
92+
93+
def release_client(self, client_id: int):
94+
payload = json.dumps(client_id).encode("utf-8")
95+
try:
96+
self.call_shared_library(payload)
97+
except Exception as e:
98+
print(f"failed to release client: {e}")

src/onepassword/items.py

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
# Code generated by op-codegen - DO NO EDIT MANUALLY
22

3-
from .core import _invoke, _invoke_sync
43
from typing import Optional, List
54
from pydantic import TypeAdapter
65
from .items_shares import ItemsShares
@@ -13,17 +12,17 @@ class Items:
1312
The Items API holds all operations the SDK client can perform on 1Password items.
1413
"""
1514

16-
def __init__(self, client_id):
15+
def __init__(self, client_id, core):
1716
self.client_id = client_id
17+
self.core = core
1818
self.shares = ItemsShares(client_id)
19-
2019
self.files = ItemsFiles(client_id)
2120

2221
async def create(self, params: ItemCreateParams) -> Item:
2322
"""
2423
Create a new item.
2524
"""
26-
response = await _invoke(
25+
response = await self.core.invoke(
2726
{
2827
"invocation": {
2928
"clientId": self.client_id,
@@ -42,7 +41,7 @@ async def get(self, vault_id: str, item_id: str) -> Item:
4241
"""
4342
Get an item by vault and item ID
4443
"""
45-
response = await _invoke(
44+
response = await self.core.invoke(
4645
{
4746
"invocation": {
4847
"clientId": self.client_id,
@@ -61,7 +60,7 @@ async def put(self, item: Item) -> Item:
6160
"""
6261
Update an existing item.
6362
"""
64-
response = await _invoke(
63+
response = await self.core.invoke(
6564
{
6665
"invocation": {
6766
"clientId": self.client_id,
@@ -80,7 +79,7 @@ async def delete(self, vault_id: str, item_id: str) -> None:
8079
"""
8180
Delete an item.
8281
"""
83-
response = await _invoke(
82+
response = await self.core.invoke(
8483
{
8584
"invocation": {
8685
"clientId": self.client_id,
@@ -98,7 +97,7 @@ async def archive(self, vault_id: str, item_id: str) -> None:
9897
"""
9998
Archive an item.
10099
"""
101-
response = await _invoke(
100+
response = await self.core.invoke(
102101
{
103102
"invocation": {
104103
"clientId": self.client_id,
@@ -116,7 +115,7 @@ async def list(self, vault_id: str, *filters: ItemListFilter) -> List[ItemOvervi
116115
"""
117116
List items based on filters.
118117
"""
119-
response = await _invoke(
118+
response = await self.core.invoke(
120119
{
121120
"invocation": {
122121
"clientId": self.client_id,

0 commit comments

Comments
 (0)