@follenfang/fupload 0.0.5 → 0.0.6
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.
package/fupload/SKILL.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: fupload
|
|
3
3
|
description: Explicit author-publishing workflow for World of Warcraft plugins, configuration shares, and WA/strings on NewBeeBox, NetEase DD, CurseForge, and Heybox Workshop, including CurseForge author project lookup, plugin ZIP upload, and Heybox client-session version management. Use only when the user explicitly invokes `$fupload`, explicitly asks to use the Fupload Skill, or loads this Skill by path. Do not trigger from ordinary mentions of publishing, NewBeeBox, DD, CurseForge, Heybox, plugins, configurations, or WA.
|
|
4
4
|
metadata:
|
|
5
|
-
version: "0.0.
|
|
5
|
+
version: "0.0.6"
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Fupload
|
|
@@ -6,19 +6,35 @@ from urllib.parse import urlencode
|
|
|
6
6
|
from urllib.request import Request, urlopen
|
|
7
7
|
from typing import Any, Mapping
|
|
8
8
|
from .errors import FuploadError, ValidationError
|
|
9
|
-
from .blackbox_auth import API_BASE, hkey, load_session
|
|
9
|
+
from .blackbox_auth import API_BASE, CLIENT_VERSION, hkey, load_session
|
|
10
|
+
|
|
11
|
+
API_MISC_BASE = "https://api.xiaoheihe.cn"
|
|
10
12
|
|
|
11
13
|
class Blackbox:
|
|
12
14
|
def __init__(self, config: Mapping[str, Any] | None = None, transport=None):
|
|
13
15
|
self.config = dict(config or {})
|
|
14
16
|
self.profile = Path(self.config.get("client_profile") or Path.home() / "AppData/Roaming/heybox-pc-launcher")
|
|
15
17
|
self._transport = transport
|
|
16
|
-
def _request(self, method, path, body=None, query=None):
|
|
18
|
+
def _request(self, method, path, body=None, query=None, *, base=API_BASE):
|
|
17
19
|
if self._transport: return self._transport(method, path, body or {}, query or {})
|
|
18
20
|
cookies, identity = load_session(self.profile); ts=int(time.time()); nonce=hashlib.md5((str(ts)+secrets.token_hex(16)).encode()).hexdigest().upper()
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
version = identity.get("version") or CLIENT_VERSION
|
|
22
|
+
params={
|
|
23
|
+
"app": "heybox",
|
|
24
|
+
"os_type": "Windows",
|
|
25
|
+
"web_version": "",
|
|
26
|
+
**identity,
|
|
27
|
+
"version": version,
|
|
28
|
+
"hkey": hkey(path,ts + 1,nonce),
|
|
29
|
+
"_time": ts,
|
|
30
|
+
"_chat_time": int(time.time()*1000),
|
|
31
|
+
"nonce": nonce,
|
|
32
|
+
**(query or {}),
|
|
33
|
+
}
|
|
34
|
+
json_body = base == API_MISC_BASE or path.startswith("/bbs/app/api/qcloud/")
|
|
35
|
+
headers={"Referer":"https://chat.xiaoheihe.cn","User-Agent":f"HeyboxApp/{identity.get('exe_version') or version}","x_xhh_tokenid":cookies["x_xhh_tokenid"],"Cookie":" ".join(f"{k}={v};" for k,v in cookies.items()),"Content-Type":("application/json;charset=utf-8" if json_body else "application/x-www-form-urlencoded;charset=utf-8")}
|
|
36
|
+
url=base.rstrip("/")+path+"?"+urlencode(params)
|
|
37
|
+
data=(json.dumps(body or {}, separators=(",", ":")).encode() if json_body else urlencode(body or {},doseq=True).encode()) if method=="POST" else None
|
|
22
38
|
try:
|
|
23
39
|
with urlopen(Request(url,data=data,headers=headers,method=method),timeout=60) as r: result=json.loads(r.read().decode())
|
|
24
40
|
except Exception as exc: raise FuploadError("Workshop API request failed", endpoint=path, verification_required=method=="POST") from exc
|
|
@@ -72,8 +88,10 @@ class Blackbox:
|
|
|
72
88
|
def version_upsert(self,doc):
|
|
73
89
|
aliases={"module_id":"moduleId","version_id":"versionId","game_versions":"gameVersions","file_url":"fileUrl"}
|
|
74
90
|
normalized={aliases.get(k,k):v for k,v in doc.items() if k not in {"schema","dry_run"}}
|
|
91
|
+
upload_result = None
|
|
75
92
|
if "file" in normalized and "fileUrl" not in normalized:
|
|
76
|
-
|
|
93
|
+
upload_result=self.upload_zip(int(normalized["moduleId"]),str(normalized["file"]))
|
|
94
|
+
normalized["fileUrl"]=upload_result["url"]
|
|
77
95
|
if "fileUrl" not in normalized and "versionId" in normalized:
|
|
78
96
|
existing = self._find_version(int(normalized["moduleId"]), int(normalized["versionId"]))
|
|
79
97
|
if existing and existing.get("fileUrlHeybox"):
|
|
@@ -91,7 +109,10 @@ class Blackbox:
|
|
|
91
109
|
if not found.get("fileUrlHeybox"):
|
|
92
110
|
mismatches.append("fileUrl")
|
|
93
111
|
if mismatches: raise FuploadError("version upsert readback mismatch",kind="verification_required",verification_required=True,details={"fields":mismatches})
|
|
94
|
-
|
|
112
|
+
result={"accepted":True,"verified":True,"module_id":body["moduleId"],"version_id":found.get("id"),"readback":self._redact(found),"response":self._redact(response)}
|
|
113
|
+
if upload_result:
|
|
114
|
+
result["upload"]={key:upload_result[key] for key in ("protocol","bytes","sha256") if key in upload_result}
|
|
115
|
+
return result
|
|
95
116
|
def version_delete(self,doc):
|
|
96
117
|
version_id=doc.get("versionId",doc.get("version_id")); module_id=doc.get("moduleId",doc.get("module_id"))
|
|
97
118
|
if version_id is None or module_id is None: raise ValidationError("versionId and moduleId are required")
|
|
@@ -133,6 +154,53 @@ class Blackbox:
|
|
|
133
154
|
path=Path(file_path)
|
|
134
155
|
if not path.is_file(): raise ValidationError("file does not exist",path="$.file")
|
|
135
156
|
if dry_run: return {"dry_run":True,"bytes":path.stat().st_size,"sha256":hashlib.sha256(path.read_bytes()).hexdigest()}
|
|
157
|
+
try:
|
|
158
|
+
return self._upload_zip_v2(path)
|
|
159
|
+
except FuploadError:
|
|
160
|
+
# The web creator still exposes the legacy Workshop token route.
|
|
161
|
+
# Fall back to it when the desktop generic uploader is unavailable.
|
|
162
|
+
return self._upload_zip_legacy(path)
|
|
163
|
+
|
|
164
|
+
def _upload_zip_v2(self, path: Path):
|
|
165
|
+
file_info = {"name": path.name, "mimetype": "application/zip", "fsize": path.stat().st_size}
|
|
166
|
+
info = self._result(self._request(
|
|
167
|
+
"POST", "/bbs/app/api/qcloud/cos/upload/info/v2", base=API_MISC_BASE,
|
|
168
|
+
body={"file_infos": json.dumps([file_info], separators=(",", ":")), "scope": "any", "need_cache": 0},
|
|
169
|
+
))
|
|
170
|
+
keys = info.get("keys") or []
|
|
171
|
+
if not keys or not info.get("bucket"):
|
|
172
|
+
raise FuploadError("Current COS upload info response is incomplete", endpoint="/bbs/app/api/qcloud/cos/upload/info/v2", verification_required=True)
|
|
173
|
+
key = str(keys[0])
|
|
174
|
+
token = self._result(self._request(
|
|
175
|
+
"POST", "/bbs/app/api/qcloud/cos/upload/token/v2", base=API_MISC_BASE,
|
|
176
|
+
body={"bucket": info["bucket"], "keys": json.dumps([key], separators=(",", ":")), "mimetypes": json.dumps([file_info["mimetype"]]), "is_multipart_upload": 0},
|
|
177
|
+
))
|
|
178
|
+
credentials = token.get("credentials") or {}
|
|
179
|
+
secret_id = credentials.get("tmpSecretId") or credentials.get("TmpSecretID")
|
|
180
|
+
secret_key = credentials.get("tmpSecretKey") or credentials.get("TmpSecretKey")
|
|
181
|
+
session_token = credentials.get("sessionToken") or credentials.get("Token")
|
|
182
|
+
if not all((secret_id, secret_key, session_token)):
|
|
183
|
+
raise FuploadError("Current COS upload credentials are incomplete", endpoint="/bbs/app/api/qcloud/cos/upload/token/v2", verification_required=True)
|
|
184
|
+
try:
|
|
185
|
+
from qcloud_cos import CosConfig, CosS3Client
|
|
186
|
+
except ModuleNotFoundError as exc:
|
|
187
|
+
raise FuploadError("The managed Fuploader Python runtime is missing the Heybox COS SDK", kind="environment_error", details={"repair_command":"fupload update"}) from exc
|
|
188
|
+
try:
|
|
189
|
+
cos=CosS3Client(CosConfig(Region=info.get("region") or "ap-shanghai",SecretId=secret_id,SecretKey=secret_key,Token=session_token,Scheme="https"))
|
|
190
|
+
with path.open("rb") as stream: cos.put_object(Bucket=info["bucket"],Key=key,Body=stream)
|
|
191
|
+
except Exception as exc:
|
|
192
|
+
raise FuploadError("COS upload failed", endpoint="/bbs/app/api/qcloud/cos/upload/token/v2", verification_required=True) from exc
|
|
193
|
+
callback = self._result(self._request(
|
|
194
|
+
"POST", "/bbs/app/api/qcloud/cos/upload/callback/v2", base=API_MISC_BASE,
|
|
195
|
+
query={"is_finished": "true"}, body={"keys": json.dumps([key], separators=(",", ":"))},
|
|
196
|
+
))
|
|
197
|
+
urls = callback.get("preview_urls") or callback.get("previewUrls") or []
|
|
198
|
+
url = urls[0] if urls else callback.get("url")
|
|
199
|
+
if not url:
|
|
200
|
+
raise FuploadError("Current COS upload callback did not return a URL", endpoint="/bbs/app/api/qcloud/cos/upload/callback/v2", verification_required=True)
|
|
201
|
+
return {"url":url,"bytes":path.stat().st_size,"sha256":hashlib.sha256(path.read_bytes()).hexdigest(),"protocol":"v2"}
|
|
202
|
+
|
|
203
|
+
def _upload_zip_legacy(self, path: Path):
|
|
136
204
|
size_mb=path.stat().st_size/1024/1024
|
|
137
205
|
token=self._result(self._request("POST","/wow/cos/upload/token/",body={"jsonData":json.dumps({"upload_infos":[{"filename":path.name,"file_size":size_mb,"type":"module"}]},separators=(",",":"))}))
|
|
138
206
|
info=token["info"]; f=token["files"][0]; creds=info["Credentials"].get("Credentials",info["Credentials"])
|
|
@@ -148,4 +216,4 @@ class Blackbox:
|
|
|
148
216
|
cos=CosS3Client(CosConfig(Region=info.get("region") or "ap-shanghai",SecretId=creds["TmpSecretID"],SecretKey=creds["TmpSecretKey"],Token=creds["Token"],Scheme="https"))
|
|
149
217
|
with path.open("rb") as stream: cos.put_object(Bucket=info["bucket"],Key=f["key"],Body=stream)
|
|
150
218
|
except Exception as exc: raise FuploadError("COS upload failed",verification_required=True) from exc
|
|
151
|
-
return {"url":"https://%s/%s"%(info["host"],f["key"]),"bytes":path.stat().st_size,"sha256":hashlib.sha256(path.read_bytes()).hexdigest()}
|
|
219
|
+
return {"url":"https://%s/%s"%(info["host"],f["key"]),"bytes":path.stat().st_size,"sha256":hashlib.sha256(path.read_bytes()).hexdigest(),"protocol":"legacy"}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
"""Authentication helpers for the Heybox desktop Workshop client."""
|
|
2
2
|
from __future__ import annotations
|
|
3
|
-
import hashlib, json, secrets, sqlite3, time
|
|
3
|
+
import hashlib, json, platform, secrets, sqlite3, time
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
from urllib.parse import parse_qs, urlparse
|
|
6
6
|
from .errors import FuploadError
|
|
7
7
|
|
|
8
8
|
API_BASE = "https://workshopapi.xiaoheihe.cn"
|
|
9
|
+
CLIENT_VERSION = "1.14.1"
|
|
10
|
+
_CONFIG_AES_KEY = bytes.fromhex("5f1d7f11e6e90dbb5c2f0c1e614a6a8c4b9e16b50fa724e4c54d6f25b1208b93")
|
|
9
11
|
_ALPHABET = "AB45STUVWZEFGJ6CH01D237IXYPQRKLMN89"
|
|
10
12
|
|
|
11
13
|
def _n8(value: str, limit: int) -> str:
|
|
@@ -28,27 +30,130 @@ def hkey(path: str, timestamp: int, nonce: str) -> str:
|
|
|
28
30
|
digest = hashlib.md5(_interleave([_n8(str(timestamp), -2), _i8(normalized), _i8(nonce)]).encode()).hexdigest()
|
|
29
31
|
return _n8(digest[:5], -4) + "%02d" % (sum(_mix([ord(x) for x in digest[-6:]])) % 100)
|
|
30
32
|
|
|
31
|
-
def
|
|
32
|
-
|
|
33
|
+
def _decrypt_user_pkey(value: str) -> str:
|
|
34
|
+
"""Decrypt the desktop config's ``iv:ciphertext`` AES-256-CBC value.
|
|
35
|
+
|
|
36
|
+
Older Chromium cookie databases contain the already decrypted pkey, so this
|
|
37
|
+
helper deliberately returns ordinary values unchanged and only attempts
|
|
38
|
+
crypto for the new config format.
|
|
39
|
+
"""
|
|
40
|
+
if not isinstance(value, str) or ":" not in value:
|
|
41
|
+
return value
|
|
42
|
+
iv_hex, ciphertext_hex = value.split(":", 1)
|
|
43
|
+
try:
|
|
44
|
+
iv = bytes.fromhex(iv_hex)
|
|
45
|
+
ciphertext = bytes.fromhex(ciphertext_hex)
|
|
46
|
+
if len(iv) != 16 or not ciphertext or len(ciphertext) % 16:
|
|
47
|
+
return value
|
|
48
|
+
except ValueError:
|
|
49
|
+
return value
|
|
50
|
+
try:
|
|
51
|
+
from Crypto.Cipher import AES
|
|
52
|
+
plaintext = AES.new(_CONFIG_AES_KEY, AES.MODE_CBC, iv).decrypt(ciphertext)
|
|
53
|
+
except ImportError:
|
|
54
|
+
try:
|
|
55
|
+
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
|
56
|
+
decryptor = Cipher(algorithms.AES(_CONFIG_AES_KEY), modes.CBC(iv)).decryptor()
|
|
57
|
+
plaintext = decryptor.update(ciphertext) + decryptor.finalize()
|
|
58
|
+
except ImportError as exc:
|
|
59
|
+
raise FuploadError(
|
|
60
|
+
"AES support is required to read the Heybox desktop session",
|
|
61
|
+
kind="environment_error",
|
|
62
|
+
details={"install": "Crypto or cryptography"},
|
|
63
|
+
) from exc
|
|
64
|
+
if not plaintext:
|
|
65
|
+
return value
|
|
66
|
+
pad = plaintext[-1]
|
|
67
|
+
if not 1 <= pad <= 16 or plaintext[-pad:] != bytes([pad]) * pad:
|
|
68
|
+
raise FuploadError("Heybox desktop pkey could not be decrypted", kind="authentication_error")
|
|
69
|
+
try:
|
|
70
|
+
return plaintext[:-pad].decode("utf-8")
|
|
71
|
+
except UnicodeDecodeError as exc:
|
|
72
|
+
raise FuploadError("Heybox desktop pkey is invalid", kind="authentication_error") from exc
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _read_json(path: Path) -> dict:
|
|
76
|
+
try:
|
|
77
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
78
|
+
except (OSError, UnicodeError, ValueError):
|
|
79
|
+
return {}
|
|
80
|
+
return data if isinstance(data, dict) else {}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _load_cookie_db(profile: Path) -> dict[str, str]:
|
|
33
84
|
db = profile / "Network/Cookies"
|
|
34
85
|
try:
|
|
35
86
|
with sqlite3.connect("file:%s?mode=ro" % db, uri=True) as con:
|
|
36
87
|
rows = con.execute("select name,value from cookies where host_key like '%xiaoheihe.cn'").fetchall()
|
|
37
|
-
except (OSError, sqlite3.Error)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
88
|
+
except (OSError, sqlite3.Error):
|
|
89
|
+
return {}
|
|
90
|
+
return {str(k): str(v) for k, v in rows if k in {"user_heybox_id", "user_pkey", "x_xhh_tokenid"} and v}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _read_sentry_identity(profile: Path) -> dict[str, str]:
|
|
94
|
+
identity: dict[str, str] = {}
|
|
43
95
|
scope = profile / "sentry/scope_v3.json"
|
|
44
96
|
try:
|
|
45
|
-
crumbs =
|
|
97
|
+
crumbs = _read_json(scope).get("scope", {}).get("breadcrumbs", [])
|
|
46
98
|
for crumb in reversed(crumbs):
|
|
47
99
|
raw = crumb.get("data",{}).get("url","")
|
|
48
100
|
if "x_app=heybox_pc" in raw:
|
|
49
101
|
query = parse_qs(urlparse(raw).query)
|
|
50
|
-
allowed = {
|
|
51
|
-
|
|
102
|
+
allowed = {
|
|
103
|
+
"app", "client_type", "device_id", "exe_version", "heybox_id",
|
|
104
|
+
"os_type", "os_version", "version", "web_version", "x_app",
|
|
105
|
+
"x_client_type", "x_client_version", "x_os_type",
|
|
106
|
+
}
|
|
107
|
+
identity = {k: v[-1] for k, v in query.items() if k in allowed and v}
|
|
108
|
+
break
|
|
52
109
|
except (OSError, ValueError, TypeError):
|
|
53
110
|
pass
|
|
111
|
+
return identity
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def load_session(profile: Path | None = None):
|
|
115
|
+
"""Load the current desktop session without exposing credential material.
|
|
116
|
+
|
|
117
|
+
Heybox 1.14 stores its authoritative session in ``config.json``. The
|
|
118
|
+
Chromium database is retained as a compatibility fallback because older
|
|
119
|
+
installations and upgrade paths may not have written the new config yet.
|
|
120
|
+
"""
|
|
121
|
+
profile = profile or Path.home() / "AppData/Roaming/heybox-pc-launcher"
|
|
122
|
+
config = _read_json(profile / "config.json")
|
|
123
|
+
db_cookies = _load_cookie_db(profile)
|
|
124
|
+
cookies: dict[str, str] = dict(db_cookies)
|
|
125
|
+
config_cookies = config.get("cookies")
|
|
126
|
+
if isinstance(config_cookies, list):
|
|
127
|
+
for item in config_cookies:
|
|
128
|
+
if not isinstance(item, dict):
|
|
129
|
+
continue
|
|
130
|
+
name, value = item.get("name"), item.get("value")
|
|
131
|
+
if isinstance(name, str) and isinstance(value, str) and value:
|
|
132
|
+
if name in {"user_heybox_id", "user_pkey", "x_xhh_tokenid"}:
|
|
133
|
+
cookies[name] = value
|
|
134
|
+
acc_config = config.get("acc_config")
|
|
135
|
+
if isinstance(acc_config, dict) and acc_config.get("xhh_token_id"):
|
|
136
|
+
cookies["x_xhh_tokenid"] = str(acc_config["xhh_token_id"])
|
|
137
|
+
if cookies.get("user_pkey"):
|
|
138
|
+
cookies["user_pkey"] = _decrypt_user_pkey(cookies["user_pkey"])
|
|
139
|
+
account = config.get("account") if isinstance(config.get("account"), dict) else {}
|
|
140
|
+
if not cookies.get("user_heybox_id") and account.get("heybox_id"):
|
|
141
|
+
cookies["user_heybox_id"] = str(account["heybox_id"])
|
|
142
|
+
if not {"user_heybox_id", "user_pkey", "x_xhh_tokenid"} <= set(cookies):
|
|
143
|
+
raise FuploadError("Heybox desktop login state is incomplete", kind="authentication_error")
|
|
144
|
+
|
|
145
|
+
identity = _read_sentry_identity(profile)
|
|
146
|
+
version = identity.get("version") or CLIENT_VERSION
|
|
147
|
+
identity = {
|
|
148
|
+
"x_client_type": "pc",
|
|
149
|
+
"x_os_type": "Windows",
|
|
150
|
+
"x_app": "heybox_pc",
|
|
151
|
+
"version": version,
|
|
152
|
+
"exe_version": identity.get("exe_version") or version,
|
|
153
|
+
"os_version": identity.get("os_version") or platform.platform(aliased=True),
|
|
154
|
+
**identity,
|
|
155
|
+
}
|
|
156
|
+
identity["version"] = version
|
|
157
|
+
identity["exe_version"] = identity.get("exe_version") or version
|
|
158
|
+
identity["heybox_id"] = cookies["user_heybox_id"]
|
|
54
159
|
return cookies, identity
|
package/npm/skill-manifest.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema": "fupload.npm-skill-manifest.v1",
|
|
3
3
|
"package_name": "@follenfang/fupload",
|
|
4
|
-
"package_version": "0.0.
|
|
5
|
-
"skill_version": "0.0.
|
|
6
|
-
"tree_sha256": "
|
|
4
|
+
"package_version": "0.0.6",
|
|
5
|
+
"skill_version": "0.0.6",
|
|
6
|
+
"tree_sha256": "89170e9d93a44d9aa72a1c041f93d941609eb9d762375358090c5a96129405d9",
|
|
7
7
|
"files": [
|
|
8
8
|
{
|
|
9
9
|
"path": "agents/openai.yaml",
|
|
@@ -108,17 +108,17 @@
|
|
|
108
108
|
{
|
|
109
109
|
"path": "scripts/fupload_cli/__init__.py",
|
|
110
110
|
"bytes": 49,
|
|
111
|
-
"sha256": "
|
|
111
|
+
"sha256": "acf9b050750b58742981c5b9ae836149b89bb86daad01d18dd1948e71817d2c2"
|
|
112
112
|
},
|
|
113
113
|
{
|
|
114
114
|
"path": "scripts/fupload_cli/blackbox_auth.py",
|
|
115
|
-
"bytes":
|
|
116
|
-
"sha256": "
|
|
115
|
+
"bytes": 7211,
|
|
116
|
+
"sha256": "797ca228c57d2b845a7105f132891b71d3e494adcdb8f44027ce835125206177"
|
|
117
117
|
},
|
|
118
118
|
{
|
|
119
119
|
"path": "scripts/fupload_cli/blackbox.py",
|
|
120
|
-
"bytes":
|
|
121
|
-
"sha256": "
|
|
120
|
+
"bytes": 16666,
|
|
121
|
+
"sha256": "c6a793cacc7d9ceb8995cac62a076c1944345eaa113bed21068c75a05b1b0ee6"
|
|
122
122
|
},
|
|
123
123
|
{
|
|
124
124
|
"path": "scripts/fupload_cli/cli.py",
|
|
@@ -188,7 +188,7 @@
|
|
|
188
188
|
{
|
|
189
189
|
"path": "SKILL.md",
|
|
190
190
|
"bytes": 22890,
|
|
191
|
-
"sha256": "
|
|
191
|
+
"sha256": "af700f4e6eb924d955c8e5ae1205934c0bc08b425c85ce5f279eb0c630a0f77a"
|
|
192
192
|
}
|
|
193
193
|
]
|
|
194
194
|
}
|