@follenfang/fupload 0.0.24 → 0.0.26

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, Heybox Workshop, and ModUs.Creator, including local Creator login reuse and plugin ZIP publishing. 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, ModUs, plugins, configurations, or WA.
4
4
  metadata:
5
- version: "0.0.24"
5
+ version: "0.0.26"
6
6
  ---
7
7
 
8
8
  # Fupload
@@ -81,7 +81,7 @@ When rebuilding a plugin update/edit form, preserve field presence from the curr
81
81
 
82
82
  Before `plugin update`, traverse `/addon/addon_versions` pages as an optional duplicate guard. If it returns version rows, reject any candidate version already present anywhere in that history before upload; if it is empty or unavailable, retain the current-version check and continue. After update, confirm the submitted version fields from the matching item in the author plugin list, whose `latest_version` is the official update projection. `detail_v2` is supplementary. An empty history never marks a successful private-plugin update as failed and never triggers replay.
83
83
 
84
- Read `plugin categories`, choose `primary_category_id`, then choose only returned `second_category_ids`. Read `plugin game-versions --game-type <id>` and use its stable values. The package accepts `.zip` only. Authorization always uses `file_type=a19-ui-res`, `business_id=addon`, fixed `file_name=addon.zip`, and `mime_type=application/x-zip-compressed`. Plugin image authorization uses `a19-ui-media/img` with an explicit empty wire file name.
84
+ Read `plugin categories`, choose `primary_category_id`, then choose only returned `second_category_ids`. Read `plugin game-versions --game-type <id>` and use its stable values. The package accepts `.zip` only. Before login/upload, local plugin ZIP files are checked for backslash paths in both the central directory and local file headers; DD cannot recognize these paths reliably. Rebuild such ZIPs with forward-slash (`/`) entry names (Windows PowerShell 5 `Compress-Archive` may produce incompatible backslash names). Fupload rejects the package rather than rewriting it. External `detail_url` packages are not downloaded for this check. Authorization always uses `file_type=a19-ui-res`, `business_id=addon`, fixed `file_name=addon.zip`, and `mime_type=application/x-zip-compressed`. Plugin image authorization uses `a19-ui-media/img` with an explicit empty wire file name.
85
85
 
86
86
  ## Configuration share
87
87
 
@@ -131,7 +131,7 @@ PUT are separate stages.
131
131
  | `file_id` | `fileId` | positive integer | update/edit/delete required; upload allocates | `GET /game/data/author/project/fileId/{projectId}` | allocated ID is used by signature; create metadata omits it | update/delete/signature | `id`/`fileId` in release detail |
132
132
  | `version` | `version` | non-empty string, max 120 | create/upload required after ZIP preflight | ZIP `.toc` plus caller label | caller value preserved | metadata create/update | `version` |
133
133
  | `type` | `type` | non-empty string, max 40 | create/upload required | Creator release-type choices/API | must be a server-supported value | metadata create/update | `type` |
134
- | `supported_game_versions` | `supportedGameVersionsReqs[]` | non-empty object array | required; ZIP-derived | installed `.toc` `Interface` values and game config | each entry requires string `gameVersion` and `server`; explicit caller value must be a nonempty subset of ZIP and live `wow_builds` choices | metadata create/update | `supportedGameVersionsReqs`/version compatibility rows |
134
+ | `supported_game_versions` | `supportedGameVersionsReqs[]` | non-empty object array | required; defaults to ZIP-derived builds | installed `.toc` `Interface` values and game config | each entry requires string `gameVersion` and `server`; explicit caller values must be live `wow_builds` choices on client branches declared in the ZIP | metadata create/update | `supportedGameVersionsReqs`/version compatibility rows |
135
135
  | `md5` | `md5` | hex string, max 64 | derived from exact ZIP bytes | local ZIP preflight | must describe bytes later PUT | metadata create/update | `md5` |
136
136
  | `zip_size` | `zipSize` | non-negative integer | derived | local ZIP stat | must describe exact ZIP | metadata create/update | `zipSize` |
137
137
  | `unzip_size` | `unzipSize` | non-negative integer | derived | ZIP central directory | bounded by ZIP preflight | metadata create/update | `unzipSize` |
@@ -149,7 +149,7 @@ before file-ID allocation and ZIP preflight, so those early failures are also
149
149
  durable. Binary-upload errors expose only a query-free endpoint identifier,
150
150
  status when available, and a bounded redacted response summary. The provider parses each addon `.toc`
151
151
  `Interface` value to derive
152
- `toc_version` and `supported_game_versions`, and ignores TOCs under a `Libs` directory. Explicit `toc_version` must match. Known clients absent from live `wow_builds` are excluded from automatic selection; unknown Interface codes still fail. Explicit `supported_game_versions` may narrow selection to a product's supported scope, but must be a nonempty subset of both ZIP and live choices. Creator `plugin get/versions` uses the Creator session; main-client addon reads retain their separate session.
152
+ `toc_version` and `supported_game_versions`, and ignores TOCs under a `Libs` directory. Explicit `toc_version` must match. Interface `16001` maps to `wow_forever` / `1.60.1`. Known clients absent from live `wow_builds` are excluded from automatic selection; unknown Interface codes still fail. Omitted `supported_game_versions` continues to select only exact TOC-derived builds offered by the live service. Explicit values are the author's compatibility declaration: they may select a newer live build (for example `12.1.5` for a `120100` retail TOC), but cannot add a client branch absent from the ZIP or a build absent from live choices. Empty and duplicate selections are rejected. This does not rewrite the ZIP or `toc_version`, or automatically assert compatibility with newer builds. Creator `plugin get/versions` uses the Creator session; main-client addon reads retain their separate session.
153
153
  The provider validates the ZIP, computes `md5`, `zip_size`, and `unzip_size`,
154
154
  registers metadata, obtains a signed URL, then uploads the ZIP bytes with HTTP
155
155
  `PUT` and `Content-Type: application/zip`. A local path is metadata only; it is
@@ -1,3 +1,3 @@
1
1
  """Fupload Python CLI."""
2
2
 
3
- __version__ = "0.0.24"
3
+ __version__ = "0.0.26"
@@ -40,6 +40,7 @@ INTERFACE_GAME_VERSION_MAP: Mapping[str, Mapping[str, str]] = {
40
40
  "11508": {"gameVersion": "Classic Era", "server": "wow_classic_era"},
41
41
  "40401": {"gameVersion": "Cataclysm Classic", "server": "wow_classic_cata"},
42
42
  "11509": {"gameVersion": "1.15.9", "server": "wow_classic_era"},
43
+ "16001": {"gameVersion": "1.60.1", "server": "wow_forever"},
43
44
  "20506": {"gameVersion": "2.5.6", "server": "wow_anniversary"},
44
45
  "38002": {"gameVersion": "3.80.2", "server": "wow_classic_titan"},
45
46
  "40402": {"gameVersion": "4.4.2", "server": "wow_classic_cata"},
@@ -48,7 +49,7 @@ INTERFACE_GAME_VERSION_MAP: Mapping[str, Mapping[str, str]] = {
48
49
 
49
50
  # Recognized WoW clients in universal ZIPs which currently have no Creator
50
51
  # server mapping. Preserve their Interface codes, without inventing wire keys.
51
- KNOWN_UNMAPPED_INTERFACES = {"16001", "30405"}
52
+ KNOWN_UNMAPPED_INTERFACES = {"30405"}
52
53
 
53
54
  _INTERFACE_RE = re.compile(r"^\s*##\s*Interface\s*:\s*(.*?)\s*$", re.IGNORECASE)
54
55
  _INTERFACE_VALUE_RE = re.compile(r"^\d+$")
@@ -213,7 +214,7 @@ parse_zip_metadata = parse_modus_zip
213
214
 
214
215
  def select_game_versions(derived: Sequence[Mapping[str, str]], config: Any,
215
216
  supplied: Any = None) -> List[Dict[str, str]]:
216
- """Intersect known ZIP clients with live Creator choices and caller scope."""
217
+ """Default to exact TOC builds; allow explicit live builds on ZIP clients."""
217
218
  rows = config if isinstance(config, list) else []
218
219
  row = next((item for item in rows if isinstance(item, Mapping) and item.get("key") == "wow_builds"), None)
219
220
  try:
@@ -228,9 +229,16 @@ def select_game_versions(derived: Sequence[Mapping[str, str]], config: Any,
228
229
  raise ValidationError("live wow_builds config is missing or malformed", path="$.supported_game_versions")
229
230
  available = [dict(item) for item in derived
230
231
  if item["server"] in builds and item["gameVersion"] in builds[item["server"]]["versions"]]
232
+ # An explicit build is the author's compatibility declaration, like the
233
+ # Creator dropdown. It must still belong to a client present in the ZIP
234
+ # and be offered by the live service. Never opt into newer builds by default.
235
+ clients = {item["server"] for item in derived}
236
+ live_choices = [{"server": server, "gameVersion": version}
237
+ for server in builds if server in clients
238
+ for version in builds[server]["versions"]]
231
239
  selected = available if supplied is None else supplied
232
- if not isinstance(selected, list) or not selected or any(item not in available for item in selected):
233
- raise ValidationError("supported_game_versions must be a nonempty subset of ZIP and live Creator choices", path="$.supported_game_versions")
240
+ if not isinstance(selected, list) or not selected or any(item not in live_choices for item in selected):
241
+ raise ValidationError("supported_game_versions must be a nonempty subset of ZIP client branches and live Creator choices", path="$.supported_game_versions")
234
242
  if len({(item["server"], item["gameVersion"]) for item in selected}) != len(selected):
235
243
  raise ValidationError("supported_game_versions must not contain duplicates", path="$.supported_game_versions")
236
244
  return [dict(item) for item in selected]
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import os
6
+ import struct
6
7
  import zipfile
7
8
  from dataclasses import dataclass, field
8
9
  from typing import Any, Dict, Iterable, Mapping, Optional, Sequence, Tuple
@@ -10,6 +11,33 @@ from typing import Any, Dict, Iterable, Mapping, Optional, Sequence, Tuple
10
11
  from .errors import ValidationError
11
12
 
12
13
 
14
+ def _validate_dd_plugin_zip(path: str) -> None:
15
+ """Inspect stored paths before Windows ZipInfo normalizes separators."""
16
+ try:
17
+ with open(path, "rb") as source, zipfile.ZipFile(source) as archive:
18
+ for info in archive.infolist():
19
+ if "\\" in info.orig_filename:
20
+ raise ValidationError(
21
+ "DD plugin ZIP contains backslash paths; rebuild the ZIP with forward-slash (/) entry names",
22
+ path="$.file",
23
+ )
24
+ source.seek(info.header_offset)
25
+ header = source.read(30)
26
+ if len(header) != 30 or header[:4] != b"PK\x03\x04":
27
+ raise zipfile.BadZipFile("invalid local file header")
28
+ name_length = struct.unpack_from("<H", header, 26)[0]
29
+ name = source.read(name_length)
30
+ if len(name) != name_length:
31
+ raise zipfile.BadZipFile("truncated local entry name")
32
+ if b"\\" in name:
33
+ raise ValidationError(
34
+ "DD plugin ZIP contains backslash paths in a local file header; rebuild the ZIP with forward-slash (/) entry names",
35
+ path="$.file",
36
+ )
37
+ except (OSError, zipfile.BadZipFile) as exc:
38
+ raise ValidationError("file must be a valid ZIP archive", path="$.file") from exc
39
+
40
+
13
41
  JSON_TYPES = {
14
42
  "string": (str,),
15
43
  "integer": (int,),
@@ -308,6 +336,8 @@ class Schema:
308
336
  ):
309
337
  scalar_array(name, (str,), "array must contain nonempty strings")
310
338
  if self.name.startswith("fupload.v1.dd"):
339
+ if self.name.startswith("fupload.v1.dd.plugin.") and "file" in value:
340
+ _validate_dd_plugin_zip(value["file"])
311
341
  object_array("associated_acts", {"sn", "act_type"}, ("sn", "act_type"))
312
342
  for name in ("second_category_ids", "vip_levels"):
313
343
  if name in value and any(isinstance(item, bool) or not isinstance(item, int) for item in value[name]):
@@ -191,6 +191,7 @@ def verify_dd_executable(executable: Path) -> Dict[str, str]:
191
191
  [powershell, "-NoProfile", "-NonInteractive", "-Command", script],
192
192
  capture_output=True, text=True, encoding="utf-8", errors="strict",
193
193
  timeout=20, check=False, env=environment,
194
+ creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
194
195
  )
195
196
  if completed.returncode != 0:
196
197
  raise FuploadError("DD Authenticode verification process failed", kind="trust_boundary")
@@ -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.24",
5
- "skill_version": "0.0.24",
6
- "tree_sha256": "53bc8cdcaab317ddace896026d8668614dfaeb19f421cf0195fe4846f6e9933c",
4
+ "package_version": "0.0.26",
5
+ "skill_version": "0.0.26",
6
+ "tree_sha256": "4af4e44df9949daa9432d47bc5d7913d6dd58442e8436981c22922a1e365b63f",
7
7
  "files": [
8
8
  {
9
9
  "path": "agents/openai.yaml",
@@ -87,13 +87,13 @@
87
87
  },
88
88
  {
89
89
  "path": "references/dd.md",
90
- "bytes": 21111,
91
- "sha256": "18895d95e5221b3c2243922bbfc6f2e826f9084a0d5d11c37c3eb923a9dd7547"
90
+ "bytes": 21542,
91
+ "sha256": "2bff00091259535980e42fac12f9765f9e51f8beea43dee9bff377c67fa2bce3"
92
92
  },
93
93
  {
94
94
  "path": "references/modus.md",
95
- "bytes": 37716,
96
- "sha256": "f462731d05d786cdfd290eceaea9104b0f28814c9a5bd793358e4dc1a7abcdce"
95
+ "bytes": 38146,
96
+ "sha256": "1aa357333988e076b2ab3754a6f530b6e0b8ed30cc6bdd0091bab3cece970f18"
97
97
  },
98
98
  {
99
99
  "path": "references/newbee-official-cli.md",
@@ -113,7 +113,7 @@
113
113
  {
114
114
  "path": "scripts/fupload_cli/__init__.py",
115
115
  "bytes": 50,
116
- "sha256": "8903ed57cd646124d87b730c4c5923b97cacd906cef920de53c6b91acfb554f4"
116
+ "sha256": "8affb4fcf748b4f6b76907e88051a6a4aa5110146ea5adbe56231509962b1de4"
117
117
  },
118
118
  {
119
119
  "path": "scripts/fupload_cli/blackbox_web.py",
@@ -162,8 +162,8 @@
162
162
  },
163
163
  {
164
164
  "path": "scripts/fupload_cli/modus_zip.py",
165
- "bytes": 11445,
166
- "sha256": "ddf603cb24d5e9758be0428446f134e02aa1a44a8be39f697b8985ac34463e2f"
165
+ "bytes": 11993,
166
+ "sha256": "45e84c175feccfc1f05368048ea3e6f24058fe30bbc770396f3be7a43868d188"
167
167
  },
168
168
  {
169
169
  "path": "scripts/fupload_cli/modus.py",
@@ -182,8 +182,8 @@
182
182
  },
183
183
  {
184
184
  "path": "scripts/fupload_cli/schema.py",
185
- "bytes": 55131,
186
- "sha256": "b651963c504896cfef46f65b61d5fb638884aacb14a8e09593162526e239133f"
185
+ "bytes": 56696,
186
+ "sha256": "9d690fea8fe7b249c089a24a6c59d783f83d2ffd0215def770cd9849d09e52f8"
187
187
  },
188
188
  {
189
189
  "path": "scripts/fupload_cli/state_machine.py",
@@ -197,8 +197,8 @@
197
197
  },
198
198
  {
199
199
  "path": "scripts/fupload_cli/trust.py",
200
- "bytes": 9392,
201
- "sha256": "83298b6aaf63144381451b9e486469215aa5fa5fbde115a20c376d5daf4cef11"
200
+ "bytes": 9462,
201
+ "sha256": "79d31833a16d71ec132612a4a2fd2f015770df469e7db06933b3ea530022c97d"
202
202
  },
203
203
  {
204
204
  "path": "scripts/fupload.py",
@@ -208,7 +208,7 @@
208
208
  {
209
209
  "path": "SKILL.md",
210
210
  "bytes": 28729,
211
- "sha256": "9625fb102b1fcc347978d0b9fe73a51703ec8296d344550938e8a2dbe593565f"
211
+ "sha256": "863223ca2e78adb854511a18fea9c9bead7238d5014b229a0ea21a335409fc90"
212
212
  }
213
213
  ]
214
214
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@follenfang/fupload",
3
- "version": "0.0.24",
3
+ "version": "0.0.26",
4
4
  "description": "Install and run the Fuploader Agent Skill and Python CLI.",
5
5
  "type": "module",
6
6
  "bin": {