dsh-crate-web 0.1.0
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 +302 -0
- package/README.zh-CN.md +304 -0
- package/cordis.patch.yml +11 -0
- package/core/dsh_pack/__init__.py +77 -0
- package/core/dsh_pack/__main__.py +4 -0
- package/core/dsh_pack/cli.py +372 -0
- package/core/dsh_pack/errors.py +167 -0
- package/core/dsh_pack/export.py +912 -0
- package/core/dsh_pack/importer.py +1347 -0
- package/core/dsh_pack/network.py +77 -0
- package/core/dsh_pack/pack.py +248 -0
- package/core/dsh_pack/preflight.py +811 -0
- package/core/dsh_pack/user_cli.py +462 -0
- package/core/dsh_pack/validation.py +272 -0
- package/core/dsh_pack/verify.py +1336 -0
- package/lib/client.js +1623 -0
- package/lib/index.d.ts +30 -0
- package/lib/index.js +1275 -0
- package/package.json +73 -0
- package/restart-helper.mjs +121 -0
- package/runtime/python/LICENSE.txt +676 -0
- package/runtime/python/__install__.json +82 -0
- package/runtime/python/_asyncio.pyd +0 -0
- package/runtime/python/_bz2.pyd +0 -0
- package/runtime/python/_ctypes.pyd +0 -0
- package/runtime/python/_decimal.pyd +0 -0
- package/runtime/python/_elementtree.pyd +0 -0
- package/runtime/python/_hashlib.pyd +0 -0
- package/runtime/python/_lzma.pyd +0 -0
- package/runtime/python/_multiprocessing.pyd +0 -0
- package/runtime/python/_overlapped.pyd +0 -0
- package/runtime/python/_queue.pyd +0 -0
- package/runtime/python/_remote_debugging.pyd +0 -0
- package/runtime/python/_socket.pyd +0 -0
- package/runtime/python/_sqlite3.pyd +0 -0
- package/runtime/python/_ssl.pyd +0 -0
- package/runtime/python/_uuid.pyd +0 -0
- package/runtime/python/_wmi.pyd +0 -0
- package/runtime/python/_zoneinfo.pyd +0 -0
- package/runtime/python/_zstd.pyd +0 -0
- package/runtime/python/libcrypto-3.dll +0 -0
- package/runtime/python/libffi-8.dll +0 -0
- package/runtime/python/libssl-3.dll +0 -0
- package/runtime/python/pyexpat.pyd +0 -0
- package/runtime/python/python.cat +0 -0
- package/runtime/python/python.exe +0 -0
- package/runtime/python/python3.dll +0 -0
- package/runtime/python/python314._pth +6 -0
- package/runtime/python/python314.dll +0 -0
- package/runtime/python/python314.zip +0 -0
- package/runtime/python/pythonw.exe +0 -0
- package/runtime/python/select.pyd +0 -0
- package/runtime/python/sqlite3.dll +0 -0
- package/runtime/python/unicodedata.pyd +0 -0
- package/runtime/python/vcruntime140.dll +0 -0
- package/runtime/python/vcruntime140_1.dll +0 -0
- package/runtime/python/winsound.pyd +0 -0
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
"""Strict Phase 1 schema and archive-path validation.
|
|
2
|
+
|
|
3
|
+
The validators intentionally describe only the first Pack format. They do not
|
|
4
|
+
try to infer DSH compatibility, install packages, or interpret third-party
|
|
5
|
+
plugin behavior.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
from collections.abc import Mapping, Sequence
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from .errors import PathSafetyError, SchemaValidationError
|
|
15
|
+
|
|
16
|
+
SCHEMA_VERSION = 1
|
|
17
|
+
PACK_FORMAT = "dshcrate"
|
|
18
|
+
LEGACY_PACK_FORMATS = {PACK_FORMAT, "dshpack"}
|
|
19
|
+
PACK_VERSION = "0.1.0"
|
|
20
|
+
|
|
21
|
+
_SHA256_RE = re.compile(r"^[0-9a-f]{64}$")
|
|
22
|
+
_SECRET_NAME_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
|
|
23
|
+
_PACKAGE_NAME_RE = re.compile(r"^(?:@[^/\s]+/)?[^/\s]+$")
|
|
24
|
+
_SOURCE_TYPES = {"registry", "git", "file", "link", "tarball", "alias", "workspace", "unknown"}
|
|
25
|
+
_RUNTIME_SOURCES = {"installation-anchor", "profile-dependency", "unknown"}
|
|
26
|
+
_ARTIFACT_MODES = {"embedded", "reference-only"}
|
|
27
|
+
_FORBIDDEN_PATH_COMPONENTS = {"node_modules", ".git", "cache", "logs"}
|
|
28
|
+
_FORBIDDEN_FILENAMES = {
|
|
29
|
+
".credentials.yaml",
|
|
30
|
+
".credentials.yml",
|
|
31
|
+
".env",
|
|
32
|
+
".env.local",
|
|
33
|
+
"credentials.json",
|
|
34
|
+
"credentials.yaml",
|
|
35
|
+
"credentials.yml",
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _object(value: Any, path: str) -> dict[str, Any]:
|
|
40
|
+
if not isinstance(value, Mapping) or isinstance(value, list):
|
|
41
|
+
raise SchemaValidationError(f"{path} must be an object")
|
|
42
|
+
return dict(value)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _strict_keys(
|
|
46
|
+
value: Mapping[str, Any],
|
|
47
|
+
allowed: set[str],
|
|
48
|
+
required: set[str],
|
|
49
|
+
path: str,
|
|
50
|
+
) -> None:
|
|
51
|
+
unknown = set(value) - allowed
|
|
52
|
+
missing = required - set(value)
|
|
53
|
+
if unknown:
|
|
54
|
+
raise SchemaValidationError(f"{path} has unknown fields: {sorted(unknown)}")
|
|
55
|
+
if missing:
|
|
56
|
+
raise SchemaValidationError(f"{path} is missing fields: {sorted(missing)}")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _string(value: Any, path: str) -> str:
|
|
60
|
+
if not isinstance(value, str) or not value.strip():
|
|
61
|
+
raise SchemaValidationError(f"{path} must be a non-empty string")
|
|
62
|
+
return value
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _boolean(value: Any, path: str) -> bool:
|
|
66
|
+
if not isinstance(value, bool):
|
|
67
|
+
raise SchemaValidationError(f"{path} must be a boolean")
|
|
68
|
+
return value
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _integer(value: Any, path: str, *, minimum: int | None = None) -> int:
|
|
72
|
+
if isinstance(value, bool) or not isinstance(value, int):
|
|
73
|
+
raise SchemaValidationError(f"{path} must be an integer")
|
|
74
|
+
if minimum is not None and value < minimum:
|
|
75
|
+
raise SchemaValidationError(f"{path} must be >= {minimum}")
|
|
76
|
+
return value
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _sha256(value: Any, path: str) -> str:
|
|
80
|
+
result = _string(value, path)
|
|
81
|
+
if not _SHA256_RE.fullmatch(result):
|
|
82
|
+
raise SchemaValidationError(f"{path} must be a lowercase SHA-256 hex digest")
|
|
83
|
+
return result
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def validate_archive_path(value: Any, *, expected_root: str | None = None) -> str:
|
|
87
|
+
"""Validate one canonical, relative ZIP member path.
|
|
88
|
+
|
|
89
|
+
ZIP names are required to use forward slashes and cannot contain dot
|
|
90
|
+
segments, empty segments, drive letters, UNC roots, or forbidden runtime
|
|
91
|
+
directories. Requiring the input to already be canonical also prevents
|
|
92
|
+
two spellings from representing the same logical member.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
if not isinstance(value, str) or not value:
|
|
96
|
+
raise PathSafetyError("archive path must be a non-empty string")
|
|
97
|
+
if "\x00" in value or "\\" in value:
|
|
98
|
+
raise PathSafetyError(f"unsafe archive path: {value!r}")
|
|
99
|
+
if value.startswith("/") or value.startswith("//") or re.match(r"^[A-Za-z]:", value):
|
|
100
|
+
raise PathSafetyError(f"absolute archive path is not allowed: {value!r}")
|
|
101
|
+
parts = value.split("/")
|
|
102
|
+
if any(part in {"", ".", ".."} for part in parts):
|
|
103
|
+
raise PathSafetyError(f"non-canonical archive path: {value!r}")
|
|
104
|
+
if any(part in _FORBIDDEN_PATH_COMPONENTS for part in parts):
|
|
105
|
+
raise PathSafetyError(f"runtime directory is not allowed in Pack: {value!r}")
|
|
106
|
+
if parts[-1] in _FORBIDDEN_FILENAMES:
|
|
107
|
+
raise PathSafetyError(f"credential-bearing file is not allowed in Pack: {value!r}")
|
|
108
|
+
if expected_root is not None and (value == expected_root or not value.startswith(f"{expected_root}/")):
|
|
109
|
+
raise PathSafetyError(f"archive path must be below {expected_root}/: {value!r}")
|
|
110
|
+
return value
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def validate_payload_key(value: Any, root: str, path: str) -> str:
|
|
114
|
+
"""Validate a PackData map key and return its canonical archive path."""
|
|
115
|
+
|
|
116
|
+
if not isinstance(value, str) or not value:
|
|
117
|
+
raise PathSafetyError(f"{path} key must be a non-empty relative path")
|
|
118
|
+
if value.startswith(("/", "\\")) or re.match(r"^[A-Za-z]:", value):
|
|
119
|
+
raise PathSafetyError(f"{path} key must be relative: {value!r}")
|
|
120
|
+
if value.startswith(f"{root}/"):
|
|
121
|
+
raise PathSafetyError(f"{path} key must omit the {root}/ prefix: {value!r}")
|
|
122
|
+
return validate_archive_path(f"{root}/{value}", expected_root=root)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def validate_manifest(value: Any, *, require_integrity: bool = False) -> dict[str, Any]:
|
|
126
|
+
manifest = _object(value, "manifest")
|
|
127
|
+
# Report an unsupported schema before checking version-specific required
|
|
128
|
+
# fields, so a future schema cannot be misdiagnosed as a malformed v1 Pack.
|
|
129
|
+
if "schemaVersion" in manifest and manifest["schemaVersion"] != SCHEMA_VERSION:
|
|
130
|
+
raise SchemaValidationError(
|
|
131
|
+
f"unsupported manifest.schemaVersion: {manifest['schemaVersion']!r}; expected {SCHEMA_VERSION}"
|
|
132
|
+
)
|
|
133
|
+
allowed = {"schemaVersion", "format", "packVersion", "profile", "environment", "requiredSecrets", "integrity"}
|
|
134
|
+
required = {"schemaVersion", "format", "packVersion", "profile", "environment"}
|
|
135
|
+
if require_integrity:
|
|
136
|
+
required.add("integrity")
|
|
137
|
+
_strict_keys(manifest, allowed, required, "manifest")
|
|
138
|
+
if manifest["schemaVersion"] != SCHEMA_VERSION:
|
|
139
|
+
raise SchemaValidationError(
|
|
140
|
+
f"unsupported manifest.schemaVersion: {manifest['schemaVersion']!r}; expected {SCHEMA_VERSION}"
|
|
141
|
+
)
|
|
142
|
+
if manifest["format"] not in LEGACY_PACK_FORMATS:
|
|
143
|
+
raise SchemaValidationError(f"unsupported manifest.format: {manifest['format']!r}")
|
|
144
|
+
_string(manifest["packVersion"], "manifest.packVersion")
|
|
145
|
+
|
|
146
|
+
profile = _object(manifest["profile"], "manifest.profile")
|
|
147
|
+
_strict_keys(profile, {"name"}, {"name"}, "manifest.profile")
|
|
148
|
+
profile_name = _string(profile["name"], "manifest.profile.name")
|
|
149
|
+
if any(char in profile_name for char in "/\\") or profile_name in {".", ".."}:
|
|
150
|
+
raise SchemaValidationError("manifest.profile.name must be a single profile name")
|
|
151
|
+
|
|
152
|
+
validate_environment(manifest["environment"])
|
|
153
|
+
required_secrets = manifest.get("requiredSecrets", [])
|
|
154
|
+
if not isinstance(required_secrets, Sequence) or isinstance(required_secrets, (str, bytes, bytearray)):
|
|
155
|
+
raise SchemaValidationError("manifest.requiredSecrets must be an array")
|
|
156
|
+
for index, name in enumerate(required_secrets):
|
|
157
|
+
secret_name = _string(name, f"manifest.requiredSecrets[{index}]")
|
|
158
|
+
if not _SECRET_NAME_RE.fullmatch(secret_name):
|
|
159
|
+
raise SchemaValidationError(f"manifest.requiredSecrets[{index}] must be an environment variable name")
|
|
160
|
+
if "integrity" in manifest:
|
|
161
|
+
validate_integrity(manifest["integrity"])
|
|
162
|
+
return manifest
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def validate_environment(value: Any) -> dict[str, Any]:
|
|
166
|
+
environment = _object(value, "manifest.environment")
|
|
167
|
+
_strict_keys(environment, {"schemaVersion", "os", "node", "dsh"}, {"schemaVersion", "os", "node", "dsh"}, "manifest.environment")
|
|
168
|
+
if environment["schemaVersion"] != SCHEMA_VERSION:
|
|
169
|
+
raise SchemaValidationError(
|
|
170
|
+
f"unsupported manifest.environment.schemaVersion: {environment['schemaVersion']!r}; expected {SCHEMA_VERSION}"
|
|
171
|
+
)
|
|
172
|
+
os_info = _object(environment["os"], "manifest.environment.os")
|
|
173
|
+
_strict_keys(os_info, {"name", "version", "arch"}, {"name", "version", "arch"}, "manifest.environment.os")
|
|
174
|
+
for key in ("name", "version", "arch"):
|
|
175
|
+
_string(os_info[key], f"manifest.environment.os.{key}")
|
|
176
|
+
for section in ("node", "dsh"):
|
|
177
|
+
info = _object(environment[section], f"manifest.environment.{section}")
|
|
178
|
+
_strict_keys(info, {"version"}, {"version"}, f"manifest.environment.{section}")
|
|
179
|
+
_string(info["version"], f"manifest.environment.{section}.version")
|
|
180
|
+
return environment
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def validate_integrity(value: Any) -> dict[str, Any]:
|
|
184
|
+
integrity = _object(value, "manifest.integrity")
|
|
185
|
+
_strict_keys(integrity, {"algorithm", "files"}, {"algorithm", "files"}, "manifest.integrity")
|
|
186
|
+
if integrity["algorithm"] != "sha256":
|
|
187
|
+
raise SchemaValidationError("manifest.integrity.algorithm must be sha256")
|
|
188
|
+
files = _object(integrity["files"], "manifest.integrity.files")
|
|
189
|
+
if not files:
|
|
190
|
+
raise SchemaValidationError("manifest.integrity.files must not be empty")
|
|
191
|
+
for archive_path, digest in files.items():
|
|
192
|
+
validate_archive_path(archive_path)
|
|
193
|
+
_sha256(digest, f"manifest.integrity.files[{archive_path!r}]")
|
|
194
|
+
return integrity
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _validate_package_name(value: Any, path: str) -> str:
|
|
198
|
+
name = _string(value, path)
|
|
199
|
+
if not _PACKAGE_NAME_RE.fullmatch(name):
|
|
200
|
+
raise SchemaValidationError(f"{path} is not a valid package name")
|
|
201
|
+
return name
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def validate_plugins_lock(value: Any, *, artifact_paths: set[str] | None = None) -> dict[str, Any]:
|
|
205
|
+
lock = _object(value, "plugins.lock")
|
|
206
|
+
_strict_keys(lock, {"schemaVersion", "plugins"}, {"schemaVersion", "plugins"}, "plugins.lock")
|
|
207
|
+
if lock["schemaVersion"] != SCHEMA_VERSION:
|
|
208
|
+
raise SchemaValidationError(
|
|
209
|
+
f"unsupported plugins.lock.schemaVersion: {lock['schemaVersion']!r}; expected {SCHEMA_VERSION}"
|
|
210
|
+
)
|
|
211
|
+
plugins = lock["plugins"]
|
|
212
|
+
if not isinstance(plugins, list):
|
|
213
|
+
raise SchemaValidationError("plugins.lock.plugins must be an array")
|
|
214
|
+
seen: set[str] = set()
|
|
215
|
+
for index, raw_plugin in enumerate(plugins):
|
|
216
|
+
path = f"plugins.lock.plugins[{index}]"
|
|
217
|
+
plugin = _object(raw_plugin, path)
|
|
218
|
+
_strict_keys(plugin, {"name", "required", "requested", "resolved", "runtime", "bundle", "artifact"},
|
|
219
|
+
{"name", "required", "requested", "resolved", "runtime", "bundle", "artifact"}, path)
|
|
220
|
+
name = _validate_package_name(plugin["name"], f"{path}.name")
|
|
221
|
+
if name in seen:
|
|
222
|
+
raise SchemaValidationError(f"duplicate plugin name: {name}")
|
|
223
|
+
seen.add(name)
|
|
224
|
+
_boolean(plugin["required"], f"{path}.required")
|
|
225
|
+
|
|
226
|
+
requested = _object(plugin["requested"], f"{path}.requested")
|
|
227
|
+
_strict_keys(requested, {"specifier", "sourceType"}, {"specifier", "sourceType"}, f"{path}.requested")
|
|
228
|
+
_string(requested["specifier"], f"{path}.requested.specifier")
|
|
229
|
+
source_type = _string(requested["sourceType"], f"{path}.requested.sourceType")
|
|
230
|
+
if source_type not in _SOURCE_TYPES:
|
|
231
|
+
raise SchemaValidationError(f"{path}.requested.sourceType is unsupported: {source_type!r}")
|
|
232
|
+
|
|
233
|
+
resolved = _object(plugin["resolved"], f"{path}.resolved")
|
|
234
|
+
_strict_keys(resolved, {"version", "locator", "integrity"}, {"version", "locator"}, f"{path}.resolved")
|
|
235
|
+
_string(resolved["version"], f"{path}.resolved.version")
|
|
236
|
+
_string(resolved["locator"], f"{path}.resolved.locator")
|
|
237
|
+
if "integrity" in resolved:
|
|
238
|
+
_string(resolved["integrity"], f"{path}.resolved.integrity")
|
|
239
|
+
|
|
240
|
+
runtime = _object(plugin["runtime"], f"{path}.runtime")
|
|
241
|
+
_strict_keys(runtime, {"version", "source", "entry"}, {"version", "source", "entry"}, f"{path}.runtime")
|
|
242
|
+
_string(runtime["version"], f"{path}.runtime.version")
|
|
243
|
+
runtime_source = _string(runtime["source"], f"{path}.runtime.source")
|
|
244
|
+
if runtime_source not in _RUNTIME_SOURCES:
|
|
245
|
+
raise SchemaValidationError(f"{path}.runtime.source is unsupported: {runtime_source!r}")
|
|
246
|
+
_string(runtime["entry"], f"{path}.runtime.entry")
|
|
247
|
+
|
|
248
|
+
bundle = _object(plugin["bundle"], f"{path}.bundle")
|
|
249
|
+
_strict_keys(bundle, {"enabled", "order", "patch"}, {"enabled", "order", "patch"}, f"{path}.bundle")
|
|
250
|
+
bundle_enabled = _boolean(bundle["enabled"], f"{path}.bundle.enabled")
|
|
251
|
+
if bundle_enabled:
|
|
252
|
+
_integer(bundle["order"], f"{path}.bundle.order", minimum=0)
|
|
253
|
+
_string(bundle["patch"], f"{path}.bundle.patch")
|
|
254
|
+
else:
|
|
255
|
+
if bundle["order"] is not None or bundle["patch"] is not None:
|
|
256
|
+
raise SchemaValidationError(f"{path}.bundle.order and patch must be null when bundle.enabled is false")
|
|
257
|
+
|
|
258
|
+
artifact = _object(plugin["artifact"], f"{path}.artifact")
|
|
259
|
+
_strict_keys(artifact, {"mode", "path", "sha256"}, {"mode"}, f"{path}.artifact")
|
|
260
|
+
mode = _string(artifact["mode"], f"{path}.artifact.mode")
|
|
261
|
+
if mode not in _ARTIFACT_MODES:
|
|
262
|
+
raise SchemaValidationError(f"{path}.artifact.mode is unsupported: {mode!r}")
|
|
263
|
+
if mode == "embedded":
|
|
264
|
+
if "path" not in artifact or "sha256" not in artifact:
|
|
265
|
+
raise SchemaValidationError(f"{path}.artifact requires path and sha256 when embedded")
|
|
266
|
+
artifact_path = validate_archive_path(artifact["path"], expected_root="plugins")
|
|
267
|
+
_sha256(artifact["sha256"], f"{path}.artifact.sha256")
|
|
268
|
+
if artifact_paths is not None and artifact_path not in artifact_paths:
|
|
269
|
+
raise SchemaValidationError(f"{path}.artifact.path is missing from plugins/: {artifact_path}")
|
|
270
|
+
elif "path" in artifact or "sha256" in artifact:
|
|
271
|
+
raise SchemaValidationError(f"{path}.artifact cannot contain embedded fields in reference-only mode")
|
|
272
|
+
return lock
|