@follenfang/fupload 0.0.0-bootstrap.0 → 0.0.2
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/README.md +236 -3
- package/fupload/SKILL.md +142 -0
- package/fupload/agents/openai.yaml +4 -0
- package/fupload/examples/curseforge-plugin-upload.json +21 -0
- package/fupload/examples/dd-config-delete.json +5 -0
- package/fupload/examples/dd-config-update.json +25 -0
- package/fupload/examples/dd-plugin-delete.json +5 -0
- package/fupload/examples/dd-plugin-update.json +9 -0
- package/fupload/examples/dd-wa-delete.json +5 -0
- package/fupload/examples/dd-wa-edit.json +9 -0
- package/fupload/examples/newbee-config-delete.json +5 -0
- package/fupload/examples/newbee-config-update.json +10 -0
- package/fupload/examples/newbee-plugin-create.json +14 -0
- package/fupload/examples/newbee-plugin-delete.json +5 -0
- package/fupload/examples/newbee-wa-delete.json +5 -0
- package/fupload/examples/newbee-wa-update.json +8 -0
- package/fupload/references/curseforge.md +233 -0
- package/fupload/references/dd.md +105 -0
- package/fupload/references/newbee-official-cli.md +288 -0
- package/fupload/references/newbee.md +80 -0
- package/fupload/references/workflow.md +67 -0
- package/fupload/scripts/fupload.py +17 -0
- package/fupload/scripts/fupload_cli/__init__.py +3 -0
- package/fupload/scripts/fupload_cli/cli.py +281 -0
- package/fupload/scripts/fupload_cli/curseforge.py +186 -0
- package/fupload/scripts/fupload_cli/dd.py +2406 -0
- package/fupload/scripts/fupload_cli/dd_broker.py +634 -0
- package/fupload/scripts/fupload_cli/dd_sidecar.py +860 -0
- package/fupload/scripts/fupload_cli/errors.py +94 -0
- package/fupload/scripts/fupload_cli/io.py +125 -0
- package/fupload/scripts/fupload_cli/newbee.py +1412 -0
- package/fupload/scripts/fupload_cli/newbee_auth.py +135 -0
- package/fupload/scripts/fupload_cli/schema.py +587 -0
- package/fupload/scripts/fupload_cli/transport.py +125 -0
- package/fupload/scripts/fupload_cli/trust.py +207 -0
- package/npm/bin/fupload.mjs +92 -0
- package/npm/lib/curseforge-config.mjs +36 -0
- package/npm/lib/managed-install.mjs +86 -0
- package/npm/lib/options.mjs +38 -0
- package/npm/lib/python.mjs +45 -0
- package/npm/lib/skill-installer.mjs +228 -0
- package/npm/lib/uninstall.mjs +211 -0
- package/npm/lib/update.mjs +102 -0
- package/npm/lib/versions.mjs +63 -0
- package/npm/postinstall.mjs +21 -0
- package/npm/skill-manifest.json +179 -0
- package/package.json +50 -6
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Stable, redacted CLI errors."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from typing import Any, Dict, Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
_BEARER = re.compile(r"(?i)Bearer\s+[A-Za-z0-9._~+/=-]+")
|
|
10
|
+
_JWT = re.compile(r"\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]+\b")
|
|
11
|
+
_WA_VALUE = re.compile(r"!WA:\d+![^\s\"']+")
|
|
12
|
+
_SIGNED = re.compile(r"(?i)(X-Amz-(?:Credential|Signature)=)[^&\s]+")
|
|
13
|
+
_SIGNED_TOKEN = re.compile(r"(?i)(X-Amz-(?:Security-Token|Date|Expires)=)[^&\s]+")
|
|
14
|
+
_URL_TOKEN = re.compile(r"(?i)([?&](?:token|jwt|clientNo|client_no|signature|credential)=)[^&\s]+")
|
|
15
|
+
_COOKIE = re.compile(r"(?i)(Cookie|Set-Cookie|Authentication|Authorization)(\s*[:=]\s*)[^\s;]+")
|
|
16
|
+
_SECRET_NAME = (
|
|
17
|
+
r"(?:access[_-]?token|refresh[_-]?token|resource[_-]?token|token|clientNo|client_no|"
|
|
18
|
+
r"clientId|client_id|client[_-]?secret|device[_-]?(?:id|proof)|login[_-]?code|jwt|"
|
|
19
|
+
r"credential|signature|api[_-]?key|auth[_-]?key|password|secret|cookie|set[_-]?cookie|"
|
|
20
|
+
r"authorization|authentication|signed[_-]?url|upload[_-]?url|presigned[_-]?(?:uri|url))"
|
|
21
|
+
)
|
|
22
|
+
_TOKEN_PAIR = re.compile(
|
|
23
|
+
r'''(?i)((?:["']?''' + _SECRET_NAME + r'''["']?)\s*[:=]\s*["']?)[^"',}&\s]+'''
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def redact(value: str) -> str:
|
|
28
|
+
value = _BEARER.sub("Bearer [REDACTED]", str(value))
|
|
29
|
+
value = _JWT.sub("[REDACTED_JWT]", value)
|
|
30
|
+
value = _WA_VALUE.sub("[REDACTED_WA]", value)
|
|
31
|
+
value = _SIGNED.sub(r"\1[REDACTED]", value)
|
|
32
|
+
value = _SIGNED_TOKEN.sub(r"\1[REDACTED]", value)
|
|
33
|
+
value = _URL_TOKEN.sub(r"\1[REDACTED]", value)
|
|
34
|
+
value = _COOKIE.sub(r"\1\2[REDACTED]", value)
|
|
35
|
+
return _TOKEN_PAIR.sub(r"\1[REDACTED]", value)[:1000]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class FuploadError(Exception):
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
message: str,
|
|
42
|
+
*,
|
|
43
|
+
kind: str = "operation_failed",
|
|
44
|
+
stage: Optional[str] = None,
|
|
45
|
+
endpoint: Optional[str] = None,
|
|
46
|
+
http_status: Optional[int] = None,
|
|
47
|
+
business_code: Optional[Any] = None,
|
|
48
|
+
verification_required: bool = False,
|
|
49
|
+
details: Optional[Dict[str, Any]] = None,
|
|
50
|
+
) -> None:
|
|
51
|
+
super().__init__(redact(message))
|
|
52
|
+
self.kind = kind
|
|
53
|
+
self.stage = stage
|
|
54
|
+
self.endpoint = endpoint
|
|
55
|
+
self.http_status = http_status
|
|
56
|
+
self.business_code = business_code
|
|
57
|
+
self.verification_required = verification_required
|
|
58
|
+
self.details = details or {}
|
|
59
|
+
|
|
60
|
+
def as_dict(self) -> Dict[str, Any]:
|
|
61
|
+
result: Dict[str, Any] = {
|
|
62
|
+
"kind": self.kind,
|
|
63
|
+
"message": redact(str(self)),
|
|
64
|
+
"verification_required": bool(self.verification_required),
|
|
65
|
+
}
|
|
66
|
+
if self.stage:
|
|
67
|
+
result["stage"] = self.stage
|
|
68
|
+
if self.endpoint:
|
|
69
|
+
result["endpoint"] = self.endpoint
|
|
70
|
+
if self.http_status is not None:
|
|
71
|
+
result["http_status"] = self.http_status
|
|
72
|
+
if self.business_code is not None:
|
|
73
|
+
result["business_code"] = self.business_code
|
|
74
|
+
if self.details:
|
|
75
|
+
result["details"] = self.details
|
|
76
|
+
return result
|
|
77
|
+
|
|
78
|
+
@classmethod
|
|
79
|
+
def from_dict(cls, value: Dict[str, Any]) -> "FuploadError":
|
|
80
|
+
return cls(
|
|
81
|
+
str(value.get("message") or "operation failed"),
|
|
82
|
+
kind=str(value.get("kind") or "operation_failed"),
|
|
83
|
+
stage=str(value.get("stage")) if value.get("stage") else None,
|
|
84
|
+
endpoint=str(value.get("endpoint")) if value.get("endpoint") else None,
|
|
85
|
+
http_status=value.get("http_status"),
|
|
86
|
+
business_code=value.get("business_code"),
|
|
87
|
+
verification_required=bool(value.get("verification_required")),
|
|
88
|
+
details=value.get("details") if isinstance(value.get("details"), dict) else None,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class ValidationError(FuploadError):
|
|
93
|
+
def __init__(self, message: str, *, path: str = "$") -> None:
|
|
94
|
+
super().__init__(message, kind="validation_error", stage="dependency_get", details={"path": path})
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""Input and stable output helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, Dict
|
|
10
|
+
|
|
11
|
+
from .errors import FuploadError, ValidationError, redact
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
OUTPUT_SCHEMA = "fupload.output.v1"
|
|
15
|
+
_SENSITIVE_KEYS = {
|
|
16
|
+
"token", "access_token", "refresh_token", "resource_token", "jwt", "jwttoken",
|
|
17
|
+
"cookie", "set_cookie", "authorization", "authentication", "clientno", "client_no",
|
|
18
|
+
"clientid", "client_id", "client_secret", "device_id", "device_proof", "cred",
|
|
19
|
+
"credential", "signature", "x_amz_credential", "x_amz_signature",
|
|
20
|
+
"x_amz_security_token", "signed_url", "upload_url", "presigneduri", "presigned_uri",
|
|
21
|
+
"api_key", "auth_key", "password", "secret",
|
|
22
|
+
}
|
|
23
|
+
_RAW_CONTENT_KEYS = {
|
|
24
|
+
"content", "wa_str", "t_wa_str", "raw_wtf", "wtf_zip", "download_url",
|
|
25
|
+
"import_string",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class _DuplicateKey(ValueError):
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _strict_object(pairs: Any) -> Dict[str, Any]:
|
|
34
|
+
result: Dict[str, Any] = {}
|
|
35
|
+
for key, value in pairs:
|
|
36
|
+
if key in result:
|
|
37
|
+
raise _DuplicateKey(str(key))
|
|
38
|
+
result[key] = value
|
|
39
|
+
return result
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _reject_constant(value: str) -> Any:
|
|
43
|
+
raise ValueError(value)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def read_json(path: str) -> Dict[str, Any]:
|
|
47
|
+
if not path:
|
|
48
|
+
raise ValidationError("--input is required", path="--input")
|
|
49
|
+
try:
|
|
50
|
+
text = sys.stdin.read() if path == "-" else Path(path).read_text(encoding="utf-8-sig")
|
|
51
|
+
text = text.lstrip("\ufeff")
|
|
52
|
+
except OSError as exc:
|
|
53
|
+
raise ValidationError("cannot read input: %s" % exc, path="--input") from exc
|
|
54
|
+
try:
|
|
55
|
+
value = json.loads(
|
|
56
|
+
text,
|
|
57
|
+
object_pairs_hook=_strict_object,
|
|
58
|
+
parse_constant=_reject_constant,
|
|
59
|
+
)
|
|
60
|
+
except _DuplicateKey as exc:
|
|
61
|
+
raise ValidationError("input contains duplicate key: %s" % exc, path="--input") from exc
|
|
62
|
+
except json.JSONDecodeError as exc:
|
|
63
|
+
raise ValidationError(
|
|
64
|
+
"input must be valid JSON: line %d column %d" % (exc.lineno, exc.colno),
|
|
65
|
+
path="--input",
|
|
66
|
+
) from exc
|
|
67
|
+
except ValueError as exc:
|
|
68
|
+
raise ValidationError("input contains a non-standard numeric value", path="--input") from exc
|
|
69
|
+
if not isinstance(value, dict):
|
|
70
|
+
raise ValidationError("input document must be a JSON object")
|
|
71
|
+
return value
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def write_output(platform: str, operation: str, data: Any, *, dry_run: bool = False) -> None:
|
|
75
|
+
payload = {
|
|
76
|
+
"schema": OUTPUT_SCHEMA,
|
|
77
|
+
"platform": platform,
|
|
78
|
+
"operation": operation,
|
|
79
|
+
"success": True,
|
|
80
|
+
"dry_run": bool(dry_run),
|
|
81
|
+
"data": sanitize_output(data),
|
|
82
|
+
}
|
|
83
|
+
# Stable ASCII JSON avoids inheriting a Windows console code-page contract.
|
|
84
|
+
print(json.dumps(payload, ensure_ascii=True, sort_keys=True, separators=(",", ":"), allow_nan=False))
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def sanitize_output(value: Any) -> Any:
|
|
88
|
+
if isinstance(value, dict):
|
|
89
|
+
result: Dict[str, Any] = {}
|
|
90
|
+
for key, item in value.items():
|
|
91
|
+
normalized = str(key).replace("-", "_").lower()
|
|
92
|
+
if normalized in _SENSITIVE_KEYS or any(
|
|
93
|
+
marker in normalized
|
|
94
|
+
for marker in ("token", "cookie", "credential", "signature", "password", "secret")
|
|
95
|
+
):
|
|
96
|
+
result[key] = "[REDACTED]"
|
|
97
|
+
elif normalized in _RAW_CONTENT_KEYS:
|
|
98
|
+
text = str(item or "").encode("utf-8")
|
|
99
|
+
result[str(key) + "_summary"] = {
|
|
100
|
+
"bytes": len(text),
|
|
101
|
+
"sha256": hashlib.sha256(text).hexdigest(),
|
|
102
|
+
}
|
|
103
|
+
else:
|
|
104
|
+
result[key] = sanitize_output(item)
|
|
105
|
+
return result
|
|
106
|
+
if isinstance(value, list):
|
|
107
|
+
return [sanitize_output(item) for item in value]
|
|
108
|
+
if isinstance(value, str):
|
|
109
|
+
return redact(value)
|
|
110
|
+
return value
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def write_error(platform: str, operation: str, error: BaseException) -> None:
|
|
114
|
+
if isinstance(error, FuploadError):
|
|
115
|
+
detail = sanitize_output(error.as_dict())
|
|
116
|
+
else:
|
|
117
|
+
detail = sanitize_output(FuploadError(str(error)).as_dict())
|
|
118
|
+
payload = {
|
|
119
|
+
"schema": OUTPUT_SCHEMA,
|
|
120
|
+
"platform": platform,
|
|
121
|
+
"operation": operation,
|
|
122
|
+
"success": False,
|
|
123
|
+
"error": detail,
|
|
124
|
+
}
|
|
125
|
+
print(json.dumps(payload, ensure_ascii=True, sort_keys=True, separators=(",", ":"), allow_nan=False))
|