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.
Files changed (57) hide show
  1. package/README.md +302 -0
  2. package/README.zh-CN.md +304 -0
  3. package/cordis.patch.yml +11 -0
  4. package/core/dsh_pack/__init__.py +77 -0
  5. package/core/dsh_pack/__main__.py +4 -0
  6. package/core/dsh_pack/cli.py +372 -0
  7. package/core/dsh_pack/errors.py +167 -0
  8. package/core/dsh_pack/export.py +912 -0
  9. package/core/dsh_pack/importer.py +1347 -0
  10. package/core/dsh_pack/network.py +77 -0
  11. package/core/dsh_pack/pack.py +248 -0
  12. package/core/dsh_pack/preflight.py +811 -0
  13. package/core/dsh_pack/user_cli.py +462 -0
  14. package/core/dsh_pack/validation.py +272 -0
  15. package/core/dsh_pack/verify.py +1336 -0
  16. package/lib/client.js +1623 -0
  17. package/lib/index.d.ts +30 -0
  18. package/lib/index.js +1275 -0
  19. package/package.json +73 -0
  20. package/restart-helper.mjs +121 -0
  21. package/runtime/python/LICENSE.txt +676 -0
  22. package/runtime/python/__install__.json +82 -0
  23. package/runtime/python/_asyncio.pyd +0 -0
  24. package/runtime/python/_bz2.pyd +0 -0
  25. package/runtime/python/_ctypes.pyd +0 -0
  26. package/runtime/python/_decimal.pyd +0 -0
  27. package/runtime/python/_elementtree.pyd +0 -0
  28. package/runtime/python/_hashlib.pyd +0 -0
  29. package/runtime/python/_lzma.pyd +0 -0
  30. package/runtime/python/_multiprocessing.pyd +0 -0
  31. package/runtime/python/_overlapped.pyd +0 -0
  32. package/runtime/python/_queue.pyd +0 -0
  33. package/runtime/python/_remote_debugging.pyd +0 -0
  34. package/runtime/python/_socket.pyd +0 -0
  35. package/runtime/python/_sqlite3.pyd +0 -0
  36. package/runtime/python/_ssl.pyd +0 -0
  37. package/runtime/python/_uuid.pyd +0 -0
  38. package/runtime/python/_wmi.pyd +0 -0
  39. package/runtime/python/_zoneinfo.pyd +0 -0
  40. package/runtime/python/_zstd.pyd +0 -0
  41. package/runtime/python/libcrypto-3.dll +0 -0
  42. package/runtime/python/libffi-8.dll +0 -0
  43. package/runtime/python/libssl-3.dll +0 -0
  44. package/runtime/python/pyexpat.pyd +0 -0
  45. package/runtime/python/python.cat +0 -0
  46. package/runtime/python/python.exe +0 -0
  47. package/runtime/python/python3.dll +0 -0
  48. package/runtime/python/python314._pth +6 -0
  49. package/runtime/python/python314.dll +0 -0
  50. package/runtime/python/python314.zip +0 -0
  51. package/runtime/python/pythonw.exe +0 -0
  52. package/runtime/python/select.pyd +0 -0
  53. package/runtime/python/sqlite3.dll +0 -0
  54. package/runtime/python/unicodedata.pyd +0 -0
  55. package/runtime/python/vcruntime140.dll +0 -0
  56. package/runtime/python/vcruntime140_1.dll +0 -0
  57. 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