@follenfang/fupload 0.0.21 → 0.0.23

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.21"
5
+ version: "0.0.23"
6
6
  ---
7
7
 
8
8
  # Fupload
@@ -136,7 +136,7 @@ PUT are separate stages.
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` |
138
138
  | `path` | `path` | string, max 500 | metadata optional | Creator/object response | local path is never binary upload | metadata create/update | `path` |
139
- | `toc_version` | `tocVersion` | string, max 80 | derived | every ZIP `.toc` `Interface` | incompatible/missing/mixed values fail before write | metadata create/update | `tocVersion` |
139
+ | `toc_version` | `tocVersion` | string, max 80 | derived | each addon `.toc` `Interface`; TOCs under a `Libs` directory are ignored | incompatible/missing/mixed addon values fail before write | metadata create/update | `tocVersion` |
140
140
  | `changelog` | `changelog` | string/null, max 10000 | optional | free text | omitted/empty behavior is action-specific | metadata create/update | `changelog` |
141
141
  | `file` | not in JSON; raw bytes | local path | create/upload required | local filesystem | valid ZIP, max 200 MiB; bytes PUT only after metadata/signature | signed URL from `GET .../upload/signature/{projectId}/{fileId}`, then HTTP `PUT` | object upload status plus release detail hashes/sizes |
142
142
  | `transaction_log` | not sent | local path string | optional | caller | redacted durable stage record | local record only | transaction JSON |
@@ -147,9 +147,9 @@ binary upload stages that ran; failed records include `failed_stage`, a redacted
147
147
  error, and whether the ZIP was retained. The transaction record is established
148
148
  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
- status when available, and a bounded redacted response summary. The provider parses every ZIP `.toc`
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`; explicit caller values must match.
152
+ `toc_version` and `supported_game_versions`, and ignores TOCs under a `Libs` directory. Explicit caller values must match.
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.21"
3
+ __version__ = "0.0.23"
@@ -1486,13 +1486,15 @@ def plugin_form(
1486
1486
  if isinstance(author_value, Mapping) and isinstance(author_value.get("latest_version"), dict)
1487
1487
  else {}
1488
1488
  )
1489
- # Official detail dialog projection followed by the editor's pick list.
1489
+ # The author list is the live publication projection. DD detail_v2 can
1490
+ # retain an older latest_version, so an available author-list value wins
1491
+ # over detail for the version/package fields used to rebuild modify form.
1490
1492
  source = {name: copy.deepcopy(value[name]) for name in PLUGIN_FIELDS if name in value}
1491
1493
  for name in ("detail_url", "release_type", "version"):
1492
1494
  latest_name = {"detail_url": "file_path"}.get(name, name)
1493
- projected = latest.get(latest_name)
1495
+ projected = author_latest.get(latest_name)
1494
1496
  if projected is None:
1495
- projected = author_latest.get(latest_name)
1497
+ projected = latest.get(latest_name)
1496
1498
  if projected is None:
1497
1499
  source.pop(name, None)
1498
1500
  else:
@@ -1517,22 +1519,29 @@ def plugin_form(
1517
1519
  return form
1518
1520
 
1519
1521
 
1520
- def plugin_version_projection(value: Mapping[str, Any]) -> Dict[str, Any]:
1522
+ def plugin_version_projection(
1523
+ value: Mapping[str, Any], author_value: Optional[Mapping[str, Any]] = None,
1524
+ ) -> Dict[str, Any]:
1521
1525
  """Project version fields for readback without feeding them into modify."""
1522
1526
  latest = value.get("latest_version") if isinstance(value.get("latest_version"), dict) else {}
1527
+ author_latest = (
1528
+ author_value.get("latest_version")
1529
+ if isinstance(author_value, Mapping) and isinstance(author_value.get("latest_version"), dict)
1530
+ else {}
1531
+ )
1523
1532
  projection: Dict[str, Any] = {}
1524
1533
  sources = {
1525
- "game_versions": (latest, "game_versions", value, "game_versions"),
1526
- "detail_url": (latest, "file_path", value, "detail_url"),
1527
- "release_type": (latest, "release_type", value, "release_type"),
1528
- "version": (latest, "version", value, "version"),
1529
- "update_desc": (latest, "update_desc", value, "update_desc"),
1534
+ "game_versions": ((author_latest, "game_versions"), (latest, "game_versions"), (value, "game_versions")),
1535
+ "detail_url": ((author_latest, "file_path"), (latest, "file_path"), (value, "detail_url")),
1536
+ "release_type": ((author_latest, "release_type"), (latest, "release_type"), (value, "release_type")),
1537
+ "version": ((author_latest, "version"), (latest, "version"), (value, "version")),
1538
+ "update_desc": ((author_latest, "update_desc"), (latest, "update_desc"), (value, "update_desc")),
1530
1539
  }
1531
- for name, (preferred, preferred_name, fallback, fallback_name) in sources.items():
1532
- if preferred.get(preferred_name) is not None:
1533
- projection[name] = copy.deepcopy(preferred[preferred_name])
1534
- elif fallback.get(fallback_name) is not None:
1535
- projection[name] = copy.deepcopy(fallback[fallback_name])
1540
+ for name, candidates in sources.items():
1541
+ for source, source_name in candidates:
1542
+ if source.get(source_name) is not None:
1543
+ projection[name] = copy.deepcopy(source[source_name])
1544
+ break
1536
1545
  return projection
1537
1546
 
1538
1547
 
@@ -2088,11 +2097,33 @@ class DD:
2088
2097
  apply_present(form, doc, PLUGIN_FIELDS)
2089
2098
  else:
2090
2099
  current = self._fresh_detail(session, "plugin", doc["sn"])
2100
+ if current.get("assign_user_sn") and doc.get("scope") == "private":
2101
+ raise ValidationError("assigned plugins can only use public scope", path="$.scope")
2091
2102
  game_type = current.get("game_type") or (current.get("game_types") or [None])[0]
2092
2103
  listing = author_item(
2093
2104
  session, "plugin", str(doc["sn"]), str(current.get("name") or ""), game_type,
2094
2105
  )
2095
- form = plugin_form(current, listing)
2106
+ if action == "edit":
2107
+ if not listing:
2108
+ raise FuploadError(
2109
+ "DD author-list projection is required to preserve the published plugin version",
2110
+ kind="verification_required", stage="dependency_get", endpoint="/addon/list",
2111
+ verification_required=True,
2112
+ )
2113
+ published_version = plugin_version_projection({}, listing)
2114
+ missing_published = [
2115
+ name for name in ("detail_url", "release_type", "version")
2116
+ if name not in published_version
2117
+ ]
2118
+ if missing_published:
2119
+ raise FuploadError(
2120
+ "DD author-list projection is missing published plugin field(s): %s"
2121
+ % ", ".join(missing_published),
2122
+ kind="verification_required", stage="dependency_get", endpoint="/addon/list",
2123
+ verification_required=True,
2124
+ details={"fields": missing_published},
2125
+ )
2126
+ form = plugin_form(current, listing) if action == "edit" else plugin_form(current)
2096
2127
  allowed = PLUGIN_EDIT_FIELDS if action == "edit" else ("game_versions", "detail_url", "release_type", "version", "update_desc")
2097
2128
  current_version = str(form.get("version") or "").strip()
2098
2129
  apply_present(form, doc, allowed)
@@ -2152,6 +2183,7 @@ class DD:
2152
2183
  "creation_statement", "need_buy",
2153
2184
  "price_fen", "buy_life_type", "jump_room", "room_id", "channel_id", "channel_type",
2154
2185
  "sync_room", "with_associate", "associated_acts", "need_anchor_vip", "vip_levels",
2186
+ "game_versions", "detail_url", "release_type", "version",
2155
2187
  )
2156
2188
  if action != "update":
2157
2189
  raw_readback: Any = {}
@@ -46,6 +46,17 @@ _INTERFACE_VALUE_RE = re.compile(r"^\d+$")
46
46
  _SOURCE = Union[str, os.PathLike[str], bytes, bytearray, memoryview, BinaryIO]
47
47
 
48
48
 
49
+ def _is_library_toc(name: str) -> bool:
50
+ """Return whether ``name`` is an embedded library TOC.
51
+
52
+ Creator's release form reads the addon TOC. Libraries such as Ace3 and
53
+ LibDeflate live under a ``Libs`` directory and declare their own Interface
54
+ values, so treating them as addons rejects packages the Creator accepts.
55
+ """
56
+ parts = name.replace("\\", "/").split("/")
57
+ return any(part.casefold() == "libs" for part in parts[:-1])
58
+
59
+
49
60
  def _read_source(source: _SOURCE) -> Tuple[bytes, str]:
50
61
  """Read a path, byte buffer, or seekable stream without leaking content."""
51
62
  if isinstance(source, (bytes, bytearray, memoryview)):
@@ -127,18 +138,26 @@ def _zip_entries(raw: bytes, source_name: str) -> Iterable[Tuple[str, bytes]]:
127
138
  def parse_modus_zip(source: _SOURCE) -> Dict[str, Any]:
128
139
  """Return ModUs metadata inferred from ``source``.
129
140
 
130
- Every ``.toc`` in the archive must declare the same Interface set. This
131
- avoids choosing an arbitrary addon when a multi-addon archive contains
132
- incompatible game versions. Multiple Interface values in one TOC are
133
- supported and become a deterministic comma-separated ``toc_version``.
141
+ Addon ``.toc`` files must declare the same Interface set. This avoids
142
+ choosing an arbitrary addon when a multi-addon archive contains
143
+ incompatible game versions. TOCs under a ``Libs`` directory are ignored.
144
+ Multiple Interface values in one TOC are supported and become a
145
+ deterministic comma-separated ``toc_version``.
134
146
 
135
147
  Returned keys are JSON-ready and use the exact snake_case names accepted
136
148
  by the Fupload ModUs schema. ``interface_values`` and ``toc_files`` are
137
149
  diagnostic fields for callers and can be omitted from the wire request.
138
150
  """
139
151
  raw, source_name = _read_source(source)
152
+ addon_entries = [
153
+ (name, toc_raw)
154
+ for name, toc_raw in _zip_entries(raw, source_name)
155
+ if not _is_library_toc(name)
156
+ ]
157
+ if not addon_entries:
158
+ raise ValidationError("ZIP contains no addon .toc file outside a Libs directory", path="$.file")
140
159
  signatures: List[Tuple[str, Tuple[str, ...]]] = []
141
- for name, toc_raw in _zip_entries(raw, source_name):
160
+ for name, toc_raw in addon_entries:
142
161
  values = tuple(_interface_values(_decode_toc(toc_raw, name), name))
143
162
  signatures.append((name, values))
144
163
  expected = signatures[0][1]
@@ -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.21",
5
- "skill_version": "0.0.21",
6
- "tree_sha256": "06e04c9d9820c812a5bce0e530887212460eb5aa55ada6f9cfa0f68b92222ae9",
4
+ "package_version": "0.0.23",
5
+ "skill_version": "0.0.23",
6
+ "tree_sha256": "92ada2b43090f9d3d44c90da840fba1ed420e79ef8166275dc790c5a200d53db",
7
7
  "files": [
8
8
  {
9
9
  "path": "agents/openai.yaml",
@@ -92,8 +92,8 @@
92
92
  },
93
93
  {
94
94
  "path": "references/modus.md",
95
- "bytes": 37142,
96
- "sha256": "84e1524bc3404de3c3bc0582d85651b884c616ff009b1d01839fb84e348917b3"
95
+ "bytes": 37236,
96
+ "sha256": "396bf899cef28dfdd66558a555c37bc9af7afaa444f96b2b5b5b1f9b68508783"
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": "72bdf8ff38debf5bf6e764f214f7381a9dca2a17d9a0396952ac63b58bba679d"
116
+ "sha256": "36c61f2ff8e0593fc5d731f120d8406354634b1ae61805d20c2d73da59ced3d0"
117
117
  },
118
118
  {
119
119
  "path": "scripts/fupload_cli/blackbox_web.py",
@@ -147,8 +147,8 @@
147
147
  },
148
148
  {
149
149
  "path": "scripts/fupload_cli/dd.py",
150
- "bytes": 115484,
151
- "sha256": "bf42e40784111e07ded17e7f7ad3cfacd007cf8efad6f61df475d0b3873a2793"
150
+ "bytes": 117372,
151
+ "sha256": "19a303e84ce08dd3af9a905d04d24f7224c3302f83f0d62f899618cf45fe0433"
152
152
  },
153
153
  {
154
154
  "path": "scripts/fupload_cli/errors.py",
@@ -162,8 +162,8 @@
162
162
  },
163
163
  {
164
164
  "path": "scripts/fupload_cli/modus_zip.py",
165
- "bytes": 7833,
166
- "sha256": "3b35b30266dd48874f679b40d12739534ede3dc58f0c34cdd35ac2deca4e07db"
165
+ "bytes": 8590,
166
+ "sha256": "6c3949d59356e3e28aa15458eb09019f64fdb0d383af26e7bb0c4c262b857559"
167
167
  },
168
168
  {
169
169
  "path": "scripts/fupload_cli/modus.py",
@@ -208,7 +208,7 @@
208
208
  {
209
209
  "path": "SKILL.md",
210
210
  "bytes": 28582,
211
- "sha256": "d24b44882de93eb9fc43655ef995ff33c075b3b2fb5230b1a357ab43bedd082c"
211
+ "sha256": "f150b547db8ce091c1d1e5493026f2c30fce8c9458042a76df33844bac214972"
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.21",
3
+ "version": "0.0.23",
4
4
  "description": "Install and run the Fuploader Agent Skill and Python CLI.",
5
5
  "type": "module",
6
6
  "bin": {