opencode-swap 0.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,186 @@
1
+ """OpenAI provider: reads/writes the ``"openai"`` key of OpenCode's auth.json.
2
+
3
+ Record shapes verified against OpenCode source
4
+ (packages/opencode/src/auth/index.ts:14-33, the Oauth/Api/WellKnown union):
5
+
6
+ oauth: {type:"oauth", refresh:str, access:str, expires:int,
7
+ accountId?:str, enterpriseUrl?:str}
8
+ api: {type:"api", key:str, metadata?:dict}
9
+ wellknown: {type:"wellknown", key:str, token:str}
10
+
11
+ Identity derivation mirrors opencode-balancer's authIdentityKey
12
+ (src/core/pending.ts:116-123): prefer the stable account id, fall back to
13
+ the refresh token itself so two records for the same account are still
14
+ recognized as the same identity even before an accountId claim is known.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import math
20
+ import time
21
+
22
+ from opencode_swap import oauth_refresh, usage
23
+ from opencode_swap.exceptions import SchemaError
24
+ from opencode_swap.models import AccountDesc, AuthRecord, JsonObject, Validity
25
+ from opencode_swap.oauth_jwt import decode_claims, extract_account_id, extract_email
26
+ from opencode_swap.providers.common import credential_values, is_json_number, key_account_hint, published_raw, require_expiry
27
+
28
+ PROVIDER_ID = "openai"
29
+
30
+
31
+ def _require_str(raw: JsonObject, field: str, entry_type: str) -> str:
32
+ value = raw.get(field)
33
+ if not isinstance(value, str) or not value:
34
+ raise SchemaError(f"openai {entry_type} entry missing/invalid field {field!r}")
35
+ return value
36
+
37
+
38
+ def _optional_str(raw: JsonObject, field: str, entry_type: str) -> str | None:
39
+ if field not in raw:
40
+ return None
41
+ value = raw[field]
42
+ if not isinstance(value, str):
43
+ raise SchemaError(f"openai {entry_type} entry has invalid field {field!r}")
44
+ return value
45
+
46
+
47
+ def _json_value(value: object) -> bool:
48
+ if value is None or isinstance(value, (str, bool)):
49
+ return True
50
+ if is_json_number(value):
51
+ return True
52
+ if isinstance(value, list):
53
+ return all(_json_value(item) for item in value)
54
+ if isinstance(value, dict):
55
+ return all(isinstance(key, str) and _json_value(item) for key, item in value.items())
56
+ return False
57
+
58
+
59
+ def _safe_display(value: object, record: JsonObject) -> str | None:
60
+ if not isinstance(value, str) or not value:
61
+ return None
62
+ if any(ord(char) < 32 or 127 <= ord(char) < 160 for char in value):
63
+ return None
64
+ if any(
65
+ secret and secret in value for secret in (record.get(field) for field in ("refresh", "access", "key", "token")) if isinstance(secret, str)
66
+ ):
67
+ return None
68
+ return value
69
+
70
+
71
+ class OpenAiProvider:
72
+ id = PROVIDER_ID
73
+ usage_record_types = frozenset({"oauth"})
74
+
75
+ def extract(self, auth: JsonObject) -> AuthRecord | None:
76
+ raw = auth.get(PROVIDER_ID)
77
+ if raw is None:
78
+ return None
79
+ if not isinstance(raw, dict) or "type" not in raw:
80
+ raise SchemaError("openai entry is not a recognizable auth record")
81
+ if not _json_value(raw):
82
+ raise SchemaError("openai entry contains values not representable as JSON")
83
+
84
+ entry_type = raw["type"]
85
+ if entry_type == "oauth":
86
+ _require_str(raw, "refresh", "oauth")
87
+ _require_str(raw, "access", "oauth")
88
+ require_expiry(raw, PROVIDER_ID)
89
+ _optional_str(raw, "accountId", "oauth")
90
+ _optional_str(raw, "enterpriseUrl", "oauth")
91
+ elif entry_type == "api":
92
+ _require_str(raw, "key", "api")
93
+ metadata = raw.get("metadata")
94
+ if "metadata" in raw and (not isinstance(metadata, dict) or not _json_value(metadata)):
95
+ raise SchemaError("openai api entry has invalid field 'metadata'")
96
+ elif entry_type == "wellknown":
97
+ _require_str(raw, "key", "wellknown")
98
+ _require_str(raw, "token", "wellknown")
99
+ else:
100
+ raise SchemaError("unknown openai auth type")
101
+
102
+ return AuthRecord(type=entry_type, raw=dict(raw))
103
+
104
+ def splice(self, auth: JsonObject, record: AuthRecord) -> JsonObject:
105
+ new_auth = dict(auth)
106
+ new_auth[PROVIDER_ID] = published_raw(record.raw)
107
+ return new_auth
108
+
109
+ def identity(self, record: AuthRecord) -> str:
110
+ if record.type == "oauth":
111
+ account_id = record.raw.get("accountId")
112
+ if not isinstance(account_id, str) or not account_id:
113
+ access = record.raw.get("access")
114
+ claims = decode_claims(access if isinstance(access, str) else "")
115
+ account_id = extract_account_id(claims)
116
+ if account_id:
117
+ return f"oauth-account\0{account_id}"
118
+ refresh = record.raw.get("refresh")
119
+ return f"oauth-refresh\0{refresh if isinstance(refresh, str) else ''}"
120
+ if record.type == "api":
121
+ key = record.raw.get("key")
122
+ return f"api-key\0{key if isinstance(key, str) else ''}"
123
+ if record.type == "wellknown":
124
+ key = record.raw.get("key")
125
+ token = record.raw.get("token")
126
+ return f"wellknown\0{key if isinstance(key, str) else ''}\0{token if isinstance(token, str) else ''}"
127
+ raise SchemaError("unknown openai auth type")
128
+
129
+ def identity_is_stable(self, record: AuthRecord) -> bool:
130
+ if record.type != "oauth":
131
+ return True
132
+ return self.identity(record).startswith("oauth-account\0")
133
+
134
+ def credential_values(self, record: AuthRecord) -> set[str]:
135
+ return credential_values(record)
136
+
137
+ def describe(self, record: AuthRecord) -> AccountDesc:
138
+ if record.type == "oauth":
139
+ access = record.raw.get("access")
140
+ claims = decode_claims(access if isinstance(access, str) else "")
141
+ account_id = record.raw.get("accountId")
142
+ expires = record.raw.get("expires")
143
+ return AccountDesc(
144
+ type="oauth",
145
+ email=_safe_display(extract_email(claims), record.raw),
146
+ account_id=_safe_display(account_id if isinstance(account_id, str) else extract_account_id(claims), record.raw),
147
+ expires=expires if isinstance(expires, (int, float)) else None,
148
+ )
149
+ if record.type == "api":
150
+ return AccountDesc(type="api", email=None, account_id=key_account_hint(record), expires=None)
151
+ return AccountDesc(type="wellknown", email=None, account_id=None, expires=None)
152
+
153
+ def validate(self, record: AuthRecord) -> Validity:
154
+ if record.type == "oauth":
155
+ expires = record.raw.get("expires")
156
+ if isinstance(expires, bool) or not isinstance(expires, (int, float)) or not math.isfinite(expires):
157
+ return Validity.INVALID
158
+ if expires < time.time() * 1000:
159
+ return Validity.EXPIRED
160
+ return Validity.OK
161
+ if record.type in ("api", "wellknown"):
162
+ return Validity.OK
163
+ return Validity.INVALID
164
+
165
+ def refresh(self, record: AuthRecord) -> AuthRecord | None:
166
+ if record.type != "oauth":
167
+ return None
168
+ refresh_token = record.raw.get("refresh")
169
+ if not isinstance(refresh_token, str) or not refresh_token:
170
+ raise SchemaError("openai oauth entry missing/invalid field 'refresh'")
171
+ tokens = oauth_refresh.refresh_openai_oauth(refresh_token, now_ms=time.time() * 1000)
172
+ new_raw = dict(record.raw)
173
+ new_raw["access"] = tokens.access
174
+ new_raw["refresh"] = tokens.refresh
175
+ new_raw["expires"] = tokens.expires
176
+ account_id = tokens.account_id or record.raw.get("accountId")
177
+ if account_id:
178
+ new_raw["accountId"] = account_id
179
+ return AuthRecord(type="oauth", raw=new_raw)
180
+
181
+ def fetch_usage(self, record: AuthRecord) -> usage.UsageSnapshot | None:
182
+ access = record.raw.get("access")
183
+ if not isinstance(access, str):
184
+ return None
185
+ account_id = record.raw.get("accountId")
186
+ return usage.fetch_openai_oauth_usage(access, account_id if isinstance(account_id, str) else None)
@@ -0,0 +1,64 @@
1
+ """Poe OAuth/API credentials; OAuth yields a non-rotating API key."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+
7
+ from opencode_swap.exceptions import SchemaError
8
+ from opencode_swap.models import AccountDesc, AuthRecord, JsonObject, Validity
9
+ from opencode_swap.providers.common import (
10
+ credential_values,
11
+ extract_raw,
12
+ is_json_number,
13
+ key_account_hint,
14
+ published_raw,
15
+ validate_api,
16
+ validate_oauth,
17
+ )
18
+
19
+
20
+ class PoeProvider:
21
+ id = "poe"
22
+ usage_record_types: frozenset[str] = frozenset() # no known usage endpoint
23
+
24
+ def extract(self, auth: JsonObject) -> AuthRecord | None:
25
+ raw = extract_raw(auth, self.id)
26
+ if raw is None:
27
+ return None
28
+ if raw.get("type") == "api":
29
+ return validate_api(raw, self.id)
30
+ if raw.get("type") == "oauth":
31
+ return validate_oauth(raw, self.id)
32
+ raise SchemaError("poe auth type is not supported by opencode-swap")
33
+
34
+ def splice(self, auth: JsonObject, record: AuthRecord) -> JsonObject:
35
+ return {**auth, self.id: published_raw(record.raw)}
36
+
37
+ def identity(self, record: AuthRecord) -> str:
38
+ field = "key" if record.type == "api" else "access"
39
+ token = record.raw.get(field)
40
+ return f"{record.type}\0{token if isinstance(token, str) else ''}"
41
+
42
+ def identity_is_stable(self, record: AuthRecord) -> bool:
43
+ return True
44
+
45
+ def credential_values(self, record: AuthRecord) -> set[str]:
46
+ return credential_values(record)
47
+
48
+ def describe(self, record: AuthRecord) -> AccountDesc:
49
+ expires = record.raw.get("expires")
50
+ return AccountDesc(type=record.type, email=None, account_id=key_account_hint(record), expires=expires if is_json_number(expires) else None)
51
+
52
+ def validate(self, record: AuthRecord) -> Validity:
53
+ if record.type == "api":
54
+ return Validity.OK
55
+ expires = record.raw.get("expires")
56
+ if not is_json_number(expires):
57
+ return Validity.INVALID
58
+ return Validity.EXPIRED if expires < time.time() * 1000 else Validity.OK
59
+
60
+ def refresh(self, record: AuthRecord) -> AuthRecord | None:
61
+ return None # no verified standalone refresh flow for this provider yet
62
+
63
+ def fetch_usage(self, record: AuthRecord) -> None:
64
+ return None # unreachable: usage_record_types is empty
@@ -0,0 +1,72 @@
1
+ """xAI API/OAuth handling with guarded stable JWT subject identity."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+
7
+ from opencode_swap.exceptions import SchemaError
8
+ from opencode_swap.models import AccountDesc, AuthRecord, JsonObject, Validity
9
+ from opencode_swap.oauth_jwt import decode_claims
10
+ from opencode_swap.providers.common import (
11
+ credential_values,
12
+ extract_raw,
13
+ is_json_number,
14
+ key_account_hint,
15
+ published_raw,
16
+ validate_api,
17
+ validate_oauth,
18
+ )
19
+
20
+
21
+ class XaiProvider:
22
+ id = "xai"
23
+ usage_record_types: frozenset[str] = frozenset() # no known usage endpoint
24
+
25
+ def extract(self, auth: JsonObject) -> AuthRecord | None:
26
+ raw = extract_raw(auth, self.id)
27
+ if raw is None:
28
+ return None
29
+ if raw.get("type") == "api":
30
+ return validate_api(raw, self.id)
31
+ if raw.get("type") == "oauth":
32
+ return validate_oauth(raw, self.id)
33
+ raise SchemaError("xai auth type is not supported by opencode-swap")
34
+
35
+ def splice(self, auth: JsonObject, record: AuthRecord) -> JsonObject:
36
+ return {**auth, self.id: published_raw(record.raw)}
37
+
38
+ def identity(self, record: AuthRecord) -> str:
39
+ if record.type == "api":
40
+ key = record.raw.get("key")
41
+ return f"api-key\0{key if isinstance(key, str) else ''}"
42
+ access = record.raw.get("access")
43
+ claims = decode_claims(access if isinstance(access, str) else "")
44
+ subject = claims.get("sub")
45
+ issuer = claims.get("iss", "")
46
+ if not isinstance(subject, str) or not subject or not isinstance(issuer, str) or not issuer:
47
+ raise SchemaError("xai oauth access token has no stable JWT subject; refusing unsafe account switching")
48
+ return f"oauth-subject\0{issuer}\0{subject}"
49
+
50
+ def identity_is_stable(self, record: AuthRecord) -> bool:
51
+ return True
52
+
53
+ def credential_values(self, record: AuthRecord) -> set[str]:
54
+ return credential_values(record)
55
+
56
+ def describe(self, record: AuthRecord) -> AccountDesc:
57
+ expires = record.raw.get("expires")
58
+ return AccountDesc(type=record.type, email=None, account_id=key_account_hint(record), expires=expires if is_json_number(expires) else None)
59
+
60
+ def validate(self, record: AuthRecord) -> Validity:
61
+ if record.type == "api":
62
+ return Validity.OK
63
+ expires = record.raw.get("expires")
64
+ if not is_json_number(expires):
65
+ return Validity.INVALID
66
+ return Validity.EXPIRED if expires < time.time() * 1000 else Validity.OK
67
+
68
+ def refresh(self, record: AuthRecord) -> AuthRecord | None:
69
+ return None # no verified standalone refresh flow for this provider yet
70
+
71
+ def fetch_usage(self, record: AuthRecord) -> None:
72
+ return None # unreachable: usage_record_types is empty
@@ -0,0 +1,31 @@
1
+ """Z.AI GLM Coding Plan: a canonical API-key record plus a live usage lookup.
2
+
3
+ Record handling is exactly the generic API path (`ApiProvider`) -- z.ai stores
4
+ `{"type":"api","key":"..."}` like any other static-key provider. The only
5
+ reason this provider exists as its own class is the GLM Coding Plan quota
6
+ endpoint (see `usage.fetch_zai_usage`), which is keyed to the same API key.
7
+
8
+ Only `zai-coding-plan` is registered, not a bare `zai`: the quota endpoint is
9
+ coding-plan-specific, and a pay-as-you-go `zai` key would only ever report an
10
+ inactive plan.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from opencode_swap import usage
16
+ from opencode_swap.models import AuthRecord
17
+ from opencode_swap.providers.api import ApiProvider
18
+
19
+
20
+ class ZaiProvider(ApiProvider):
21
+ id = "zai-coding-plan"
22
+ usage_record_types = frozenset({"api"})
23
+
24
+ def __init__(self) -> None:
25
+ super().__init__(self.id)
26
+
27
+ def fetch_usage(self, record: AuthRecord) -> usage.UsageSnapshot | None:
28
+ key = record.raw.get("key")
29
+ if not isinstance(key, str) or not key:
30
+ return None
31
+ return usage.fetch_zai_usage(key)
@@ -0,0 +1,83 @@
1
+ """AES-256-GCM envelope format for the macOS Keychain backend.
2
+
3
+ Why this exists: a single OpenAI OAuth record is ~2KB, and hex-encoded for
4
+ ``security -i`` that's over the 4095-char stdin line limit (see
5
+ macos_keychain.py) — so the raw credential can never go into a Keychain
6
+ item directly. Instead the Keychain holds a small (32-byte, 64 hex chars)
7
+ per-account data key, and the credential itself is AES-256-GCM ciphertext
8
+ in the existing atomic 0600 file store. This keeps every operation to one
9
+ ``security`` spawn regardless of credential size, with no size classes.
10
+
11
+ pycryptodomex is not a new dependency: pyzipper (already required for
12
+ transfer.py's export/import archives) depends on it, and this module
13
+ formalizes that as a direct dependency.
14
+
15
+ Blob layout: b"OCS3" | nonce(12) | tag(16) | ciphertext.
16
+ AAD is the caller's key_id (e.g. "openai:work"), so a blob copied or
17
+ renamed onto a different account fails to decrypt instead of silently
18
+ producing garbage under the wrong identity.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ from Cryptodome.Cipher import AES
24
+ from Cryptodome.Random import get_random_bytes
25
+
26
+ from opencode_swap.exceptions import SecretStoreError
27
+
28
+ _MAGIC = b"OCS3"
29
+ _KEY_BYTES = 32
30
+ _NONCE_BYTES = 12
31
+ _TAG_BYTES = 16
32
+
33
+ # Generic message only: never let a decode/decrypt failure path leak
34
+ # ciphertext, key material, or plaintext content into an error string.
35
+ _CORRUPT_MSG = "stored file credential is corrupt"
36
+
37
+
38
+ def new_data_key() -> str:
39
+ """A fresh 32-byte AES key, hex-encoded (64 chars — printable, tiny)."""
40
+ random_bytes: bytes = get_random_bytes(_KEY_BYTES)
41
+ return random_bytes.hex()
42
+
43
+
44
+ def seal(key_id: str, data_key_hex: str, plaintext: str) -> bytes:
45
+ """Encrypt `plaintext` under `data_key_hex`, bound to `key_id` via AAD."""
46
+ key = _decode_key(data_key_hex)
47
+ nonce: bytes = get_random_bytes(_NONCE_BYTES)
48
+ cipher = AES.new(key, AES.MODE_GCM, nonce=nonce)
49
+ cipher.update(key_id.encode("utf-8"))
50
+ ciphertext: bytes
51
+ tag: bytes
52
+ ciphertext, tag = cipher.encrypt_and_digest(plaintext.encode("utf-8"))
53
+ return _MAGIC + nonce + tag + ciphertext
54
+
55
+
56
+ def unseal(key_id: str, data_key_hex: str, blob: bytes) -> str:
57
+ """Decrypt a `seal`-produced blob. Raises SecretStoreError on any
58
+ tampering, wrong key, or wrong key_id (AAD mismatch)."""
59
+ key = _decode_key(data_key_hex)
60
+ if len(blob) < len(_MAGIC) + _NONCE_BYTES + _TAG_BYTES or not blob.startswith(_MAGIC):
61
+ raise SecretStoreError(_CORRUPT_MSG)
62
+ rest = blob[len(_MAGIC) :]
63
+ nonce, tag, ciphertext = rest[:_NONCE_BYTES], rest[_NONCE_BYTES : _NONCE_BYTES + _TAG_BYTES], rest[_NONCE_BYTES + _TAG_BYTES :]
64
+ cipher = AES.new(key, AES.MODE_GCM, nonce=nonce)
65
+ cipher.update(key_id.encode("utf-8"))
66
+ try:
67
+ plaintext: bytes = cipher.decrypt_and_verify(ciphertext, tag)
68
+ except ValueError as exc:
69
+ raise SecretStoreError(_CORRUPT_MSG) from exc
70
+ try:
71
+ return plaintext.decode("utf-8")
72
+ except UnicodeDecodeError as exc:
73
+ raise SecretStoreError(_CORRUPT_MSG) from exc
74
+
75
+
76
+ def _decode_key(data_key_hex: str) -> bytes:
77
+ try:
78
+ key = bytes.fromhex(data_key_hex)
79
+ except ValueError as exc:
80
+ raise SecretStoreError(_CORRUPT_MSG) from exc
81
+ if len(key) != _KEY_BYTES:
82
+ raise SecretStoreError(_CORRUPT_MSG)
83
+ return key