aurasprout 0.1.1__tar.gz
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.
- aurasprout-0.1.1/LICENSE +21 -0
- aurasprout-0.1.1/PKG-INFO +74 -0
- aurasprout-0.1.1/README.md +61 -0
- aurasprout-0.1.1/pyproject.toml +31 -0
- aurasprout-0.1.1/setup.cfg +4 -0
- aurasprout-0.1.1/src/aurasprout/__init__.py +4 -0
- aurasprout-0.1.1/src/aurasprout/__main__.py +8 -0
- aurasprout-0.1.1/src/aurasprout/catalog/__init__.py +2 -0
- aurasprout-0.1.1/src/aurasprout/catalog/crypto.py +101 -0
- aurasprout-0.1.1/src/aurasprout/catalog/html_generator.py +202 -0
- aurasprout-0.1.1/src/aurasprout/catalog/manager.py +422 -0
- aurasprout-0.1.1/src/aurasprout/catalog/models.py +640 -0
- aurasprout-0.1.1/src/aurasprout/catalog/packager.py +124 -0
- aurasprout-0.1.1/src/aurasprout/catalog/sources.py +122 -0
- aurasprout-0.1.1/src/aurasprout/cli.py +219 -0
- aurasprout-0.1.1/src/aurasprout/cli_catalog.py +305 -0
- aurasprout-0.1.1/src/aurasprout/cli_volunteer.py +122 -0
- aurasprout-0.1.1/src/aurasprout/contracts/__init__.py +2 -0
- aurasprout-0.1.1/src/aurasprout/contracts/paths.py +225 -0
- aurasprout-0.1.1/src/aurasprout/coordination/__init__.py +10 -0
- aurasprout-0.1.1/src/aurasprout/coordination/batches.py +240 -0
- aurasprout-0.1.1/src/aurasprout/coordination/leases.py +577 -0
- aurasprout-0.1.1/src/aurasprout/installer/__init__.py +2 -0
- aurasprout-0.1.1/src/aurasprout/installer/autostart.py +473 -0
- aurasprout-0.1.1/src/aurasprout/installer/engine.py +924 -0
- aurasprout-0.1.1/src/aurasprout/installer/registry.py +203 -0
- aurasprout-0.1.1/src/aurasprout/installer/self_install.py +739 -0
- aurasprout-0.1.1/src/aurasprout/ipc/__init__.py +9 -0
- aurasprout-0.1.1/src/aurasprout/ipc/server.py +705 -0
- aurasprout-0.1.1/src/aurasprout/storage/__init__.py +23 -0
- aurasprout-0.1.1/src/aurasprout/storage/cell_state.py +424 -0
- aurasprout-0.1.1/src/aurasprout/storage/events.py +278 -0
- aurasprout-0.1.1/src/aurasprout/storage/mutex.py +244 -0
- aurasprout-0.1.1/src/aurasprout/supervisor/__init__.py +9 -0
- aurasprout-0.1.1/src/aurasprout/supervisor/runner.py +335 -0
- aurasprout-0.1.1/src/aurasprout/ui/__init__.py +11 -0
- aurasprout-0.1.1/src/aurasprout/ui/desktop.py +874 -0
- aurasprout-0.1.1/src/aurasprout/ui/server.py +788 -0
- aurasprout-0.1.1/src/aurasprout/ui/static/app.js +326 -0
- aurasprout-0.1.1/src/aurasprout/ui/static/aurasprout.ico +0 -0
- aurasprout-0.1.1/src/aurasprout/ui/static/aurasprout.png +0 -0
- aurasprout-0.1.1/src/aurasprout/ui/static/index.html +135 -0
- aurasprout-0.1.1/src/aurasprout.egg-info/PKG-INFO +74 -0
- aurasprout-0.1.1/src/aurasprout.egg-info/SOURCES.txt +51 -0
- aurasprout-0.1.1/src/aurasprout.egg-info/dependency_links.txt +1 -0
- aurasprout-0.1.1/src/aurasprout.egg-info/entry_points.txt +2 -0
- aurasprout-0.1.1/src/aurasprout.egg-info/requires.txt +4 -0
- aurasprout-0.1.1/src/aurasprout.egg-info/top_level.txt +1 -0
- aurasprout-0.1.1/tests/test_catalog_and_installer.py +337 -0
- aurasprout-0.1.1/tests/test_coordination_and_deconfliction.py +168 -0
- aurasprout-0.1.1/tests/test_http_catalog_and_self_install.py +333 -0
- aurasprout-0.1.1/tests/test_ipc_bridge_and_events.py +157 -0
- aurasprout-0.1.1/tests/test_ui_openapi_sync.py +152 -0
aurasprout-0.1.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 AuraCoreDynamics
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aurasprout
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: User-space zero-privilege AuraGrid cell coordination, application catalog, installer, and supervision layer.
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Dist: auraseed>=1.1.0
|
|
9
|
+
Requires-Dist: auragrid>=4.3.0
|
|
10
|
+
Requires-Dist: pydantic>=2.0.0
|
|
11
|
+
Requires-Dist: httpx>=0.25.0
|
|
12
|
+
Dynamic: license-file
|
|
13
|
+
|
|
14
|
+
# AuraSprout
|
|
15
|
+
|
|
16
|
+
Zero-privilege, user-space application distribution, local supervision, and shared-filesystem coordination layer for **AuraCore**.
|
|
17
|
+
|
|
18
|
+
## Architecture Boundaries
|
|
19
|
+
|
|
20
|
+
- **`auragrid-api`**: Canonical OpenAPI (`ipc-bridge.openapi.json`, `aurasprout-management.openapi.json`) and JSON Schemas (`app.manifest.schema.json`, `catalog.schema.json`, `lease-collaboration.schema.json`).
|
|
21
|
+
- **`auragrid-py`**: Typed Python SDK consumed by `LocalOnly`, `CellSingleton`, and `Distributed` MAS workloads.
|
|
22
|
+
- **`auraseed`**: Strictly Python discovery & sovereign virtual environment (`mvenv`) management (`aurasprout` composes `auraseed.gui.MvenvManagerPanel`).
|
|
23
|
+
- **`aurasprout`**: Application cataloging & packaging (`.apkg`), HTTP catalog hosting/push, user-space installation/updates/rollback (`active_version.json` + preserved `app_data/`), cleanroom standalone self-install/cleanup, SMB-safe SQLite event bus (`PRAGMA journal_mode = TRUNCATE`), atomic directory mutex & fenced leases/batches (`SproutMutex`), Tier 2 loopback IPC bridge (`X-AuraGrid-IPC-Token`), Tier 3 loopback Management Web UI (`aurasprout_session` + `X-CSRF-Token`), and Tkinter Desktop Hub.
|
|
24
|
+
|
|
25
|
+
## Quick Start
|
|
26
|
+
|
|
27
|
+
Both `aurasprout` (installed console script / standalone launcher) and `python -m aurasprout` expose the full CLI (supporting global `--cell-root` and `--user-root` flags):
|
|
28
|
+
|
|
29
|
+
```powershell
|
|
30
|
+
# Install AuraSprout standalone into %LOCALAPPDATA%\AuraSprout\standalone
|
|
31
|
+
python -m aurasprout self-install --add-to-path --shortcut --json
|
|
32
|
+
|
|
33
|
+
# Package & publish an app to a static zero-CDN catalog (supports --wheelhouse, --format zip|tar.gz)
|
|
34
|
+
aurasprout package .\my_app --out .\dist --publish .\catalog --channel stable --json
|
|
35
|
+
|
|
36
|
+
# Push an .apkg or source directory to a running AuraSprout HTTP server
|
|
37
|
+
aurasprout catalog push .\dist\my-app-1.0.0.apkg --server http://127.0.0.1:8765 --token-file ui_daemon.json --json
|
|
38
|
+
|
|
39
|
+
# Install locally from directory, .apkg, or HTTP/FS catalog (user-space only, zero admin privileges)
|
|
40
|
+
aurasprout install my-app --from-catalog http://127.0.0.1:8765/catalog --channel stable --shortcut --json
|
|
41
|
+
|
|
42
|
+
# Manage catalog entries & attached sources
|
|
43
|
+
aurasprout catalog list --installed --json
|
|
44
|
+
aurasprout catalog sources --json
|
|
45
|
+
aurasprout catalog add-source https://example.internal/catalog --default
|
|
46
|
+
aurasprout catalog remove-source https://example.internal/catalog
|
|
47
|
+
aurasprout catalog delete my-app --json
|
|
48
|
+
|
|
49
|
+
# Rollback, uninstall, or clean up user state completely
|
|
50
|
+
aurasprout rollback my-app --json
|
|
51
|
+
aurasprout uninstall my-app --remove-mvenv --json
|
|
52
|
+
aurasprout self-uninstall --yes --json
|
|
53
|
+
|
|
54
|
+
# Inspect or opt in to cell volunteering (opt-in default: false) & resolve conflicts
|
|
55
|
+
aurasprout volunteer status --json
|
|
56
|
+
aurasprout volunteer enable my-service --mode CellSingleton --json
|
|
57
|
+
aurasprout volunteer disable my-service --json
|
|
58
|
+
aurasprout conflicts list --json
|
|
59
|
+
aurasprout conflicts resolve <conflictId> TakeOverStaleLock --json
|
|
60
|
+
|
|
61
|
+
# Start Tier 3 loopback Management Web UI (plus /catalog hosting), Tier 2 IPC bridge, or Desktop Hub
|
|
62
|
+
aurasprout ui --port 8765 --token-file ui_daemon.json --json
|
|
63
|
+
aurasprout ipc --port 8766
|
|
64
|
+
aurasprout desktop
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## HTTP Endpoints & Storage
|
|
68
|
+
|
|
69
|
+
- **Tier 3 Management UI & Hosted Catalog (`aurasprout ui`)**:
|
|
70
|
+
- Public: `GET /`, `GET /api/health`, `GET /api/v1/health`, `GET /catalog/`, `GET /catalog/index.html`, `GET /catalog/catalog.json`, `GET /catalog/apps/<path>`, `GET /catalog/channels/<path>`.
|
|
71
|
+
- Auth & CSRF (`POST /api/v1/auth/session`): `/api/v1/apps`, `/api/v1/apps/install`, `/api/v1/apps/{appId}`, `/api/v1/apps/{appId}/launch`, `/api/v1/catalog`, `/api/v1/catalog/sources`, `POST /api/v1/catalog/publish` (binary/multipart/base64/path), `/api/v1/catalog/apps/{appId}/download`, `/api/v1/volunteer*`, `/api/v1/conflicts*`, `/api/v1/runtimes`.
|
|
72
|
+
- **Tier 2 IPC Bridge (`aurasprout ipc`)**: Loopback `X-AuraGrid-IPC-Token` endpoints under `/health`, `/ready`, `/cell/config*`, `/cell/members*`, `/cell/governance*`, `/cell/events*`, `/cell/lease*`, `/cell/collaboration*`, `/cell/registry*`, `/cell/storage*`, `/cell/tattle`, and `/cell/chaos*`.
|
|
73
|
+
- **User Data Roots**: Resolved via `AURASPROUT_HOME` -> `%LOCALAPPDATA%\AuraSprout` (Windows), `~/Library/Application Support/aurasprout` (macOS), or `$XDG_DATA_HOME/aurasprout` / `~/.local/share/aurasprout` (Linux).
|
|
74
|
+
- **Environment Variables**: `AURASPROUT_HOME`, `AURASPROUT_CELL_ROOT`, `AURAGRID_CELL_ROOT`, `AURASPROUT_STANDALONE_DIR`, `AURASPROUT_UI_HOST`, `AURASPROUT_UI_PORT`, `AURASPROUT_UI_TOKEN`, `AURASPROUT_DEFAULT_CATALOG`, `AURAGRID_IPC_TOKEN`, `AURAGRID_NODE_ID`.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# AuraSprout
|
|
2
|
+
|
|
3
|
+
Zero-privilege, user-space application distribution, local supervision, and shared-filesystem coordination layer for **AuraCore**.
|
|
4
|
+
|
|
5
|
+
## Architecture Boundaries
|
|
6
|
+
|
|
7
|
+
- **`auragrid-api`**: Canonical OpenAPI (`ipc-bridge.openapi.json`, `aurasprout-management.openapi.json`) and JSON Schemas (`app.manifest.schema.json`, `catalog.schema.json`, `lease-collaboration.schema.json`).
|
|
8
|
+
- **`auragrid-py`**: Typed Python SDK consumed by `LocalOnly`, `CellSingleton`, and `Distributed` MAS workloads.
|
|
9
|
+
- **`auraseed`**: Strictly Python discovery & sovereign virtual environment (`mvenv`) management (`aurasprout` composes `auraseed.gui.MvenvManagerPanel`).
|
|
10
|
+
- **`aurasprout`**: Application cataloging & packaging (`.apkg`), HTTP catalog hosting/push, user-space installation/updates/rollback (`active_version.json` + preserved `app_data/`), cleanroom standalone self-install/cleanup, SMB-safe SQLite event bus (`PRAGMA journal_mode = TRUNCATE`), atomic directory mutex & fenced leases/batches (`SproutMutex`), Tier 2 loopback IPC bridge (`X-AuraGrid-IPC-Token`), Tier 3 loopback Management Web UI (`aurasprout_session` + `X-CSRF-Token`), and Tkinter Desktop Hub.
|
|
11
|
+
|
|
12
|
+
## Quick Start
|
|
13
|
+
|
|
14
|
+
Both `aurasprout` (installed console script / standalone launcher) and `python -m aurasprout` expose the full CLI (supporting global `--cell-root` and `--user-root` flags):
|
|
15
|
+
|
|
16
|
+
```powershell
|
|
17
|
+
# Install AuraSprout standalone into %LOCALAPPDATA%\AuraSprout\standalone
|
|
18
|
+
python -m aurasprout self-install --add-to-path --shortcut --json
|
|
19
|
+
|
|
20
|
+
# Package & publish an app to a static zero-CDN catalog (supports --wheelhouse, --format zip|tar.gz)
|
|
21
|
+
aurasprout package .\my_app --out .\dist --publish .\catalog --channel stable --json
|
|
22
|
+
|
|
23
|
+
# Push an .apkg or source directory to a running AuraSprout HTTP server
|
|
24
|
+
aurasprout catalog push .\dist\my-app-1.0.0.apkg --server http://127.0.0.1:8765 --token-file ui_daemon.json --json
|
|
25
|
+
|
|
26
|
+
# Install locally from directory, .apkg, or HTTP/FS catalog (user-space only, zero admin privileges)
|
|
27
|
+
aurasprout install my-app --from-catalog http://127.0.0.1:8765/catalog --channel stable --shortcut --json
|
|
28
|
+
|
|
29
|
+
# Manage catalog entries & attached sources
|
|
30
|
+
aurasprout catalog list --installed --json
|
|
31
|
+
aurasprout catalog sources --json
|
|
32
|
+
aurasprout catalog add-source https://example.internal/catalog --default
|
|
33
|
+
aurasprout catalog remove-source https://example.internal/catalog
|
|
34
|
+
aurasprout catalog delete my-app --json
|
|
35
|
+
|
|
36
|
+
# Rollback, uninstall, or clean up user state completely
|
|
37
|
+
aurasprout rollback my-app --json
|
|
38
|
+
aurasprout uninstall my-app --remove-mvenv --json
|
|
39
|
+
aurasprout self-uninstall --yes --json
|
|
40
|
+
|
|
41
|
+
# Inspect or opt in to cell volunteering (opt-in default: false) & resolve conflicts
|
|
42
|
+
aurasprout volunteer status --json
|
|
43
|
+
aurasprout volunteer enable my-service --mode CellSingleton --json
|
|
44
|
+
aurasprout volunteer disable my-service --json
|
|
45
|
+
aurasprout conflicts list --json
|
|
46
|
+
aurasprout conflicts resolve <conflictId> TakeOverStaleLock --json
|
|
47
|
+
|
|
48
|
+
# Start Tier 3 loopback Management Web UI (plus /catalog hosting), Tier 2 IPC bridge, or Desktop Hub
|
|
49
|
+
aurasprout ui --port 8765 --token-file ui_daemon.json --json
|
|
50
|
+
aurasprout ipc --port 8766
|
|
51
|
+
aurasprout desktop
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## HTTP Endpoints & Storage
|
|
55
|
+
|
|
56
|
+
- **Tier 3 Management UI & Hosted Catalog (`aurasprout ui`)**:
|
|
57
|
+
- Public: `GET /`, `GET /api/health`, `GET /api/v1/health`, `GET /catalog/`, `GET /catalog/index.html`, `GET /catalog/catalog.json`, `GET /catalog/apps/<path>`, `GET /catalog/channels/<path>`.
|
|
58
|
+
- Auth & CSRF (`POST /api/v1/auth/session`): `/api/v1/apps`, `/api/v1/apps/install`, `/api/v1/apps/{appId}`, `/api/v1/apps/{appId}/launch`, `/api/v1/catalog`, `/api/v1/catalog/sources`, `POST /api/v1/catalog/publish` (binary/multipart/base64/path), `/api/v1/catalog/apps/{appId}/download`, `/api/v1/volunteer*`, `/api/v1/conflicts*`, `/api/v1/runtimes`.
|
|
59
|
+
- **Tier 2 IPC Bridge (`aurasprout ipc`)**: Loopback `X-AuraGrid-IPC-Token` endpoints under `/health`, `/ready`, `/cell/config*`, `/cell/members*`, `/cell/governance*`, `/cell/events*`, `/cell/lease*`, `/cell/collaboration*`, `/cell/registry*`, `/cell/storage*`, `/cell/tattle`, and `/cell/chaos*`.
|
|
60
|
+
- **User Data Roots**: Resolved via `AURASPROUT_HOME` -> `%LOCALAPPDATA%\AuraSprout` (Windows), `~/Library/Application Support/aurasprout` (macOS), or `$XDG_DATA_HOME/aurasprout` / `~/.local/share/aurasprout` (Linux).
|
|
61
|
+
- **Environment Variables**: `AURASPROUT_HOME`, `AURASPROUT_CELL_ROOT`, `AURAGRID_CELL_ROOT`, `AURASPROUT_STANDALONE_DIR`, `AURASPROUT_UI_HOST`, `AURASPROUT_UI_PORT`, `AURASPROUT_UI_TOKEN`, `AURASPROUT_DEFAULT_CATALOG`, `AURAGRID_IPC_TOKEN`, `AURAGRID_NODE_ID`.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "aurasprout"
|
|
7
|
+
version = "0.1.1"
|
|
8
|
+
description = "User-space zero-privilege AuraGrid cell coordination, application catalog, installer, and supervision layer."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"auraseed>=1.1.0",
|
|
13
|
+
"auragrid>=4.3.0",
|
|
14
|
+
"pydantic>=2.0.0",
|
|
15
|
+
"httpx>=0.25.0",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[project.scripts]
|
|
19
|
+
aurasprout = "aurasprout.cli:main"
|
|
20
|
+
|
|
21
|
+
[tool.setuptools.packages.find]
|
|
22
|
+
where = ["src"]
|
|
23
|
+
include = ["aurasprout*"]
|
|
24
|
+
|
|
25
|
+
[tool.setuptools.package-data]
|
|
26
|
+
aurasprout = ["ui/static/*"]
|
|
27
|
+
|
|
28
|
+
[tool.pytest.ini_options]
|
|
29
|
+
testpaths = ["tests"]
|
|
30
|
+
pythonpath = ["src"]
|
|
31
|
+
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Cryptographic SHA-256 verification and traversal-safe archive extraction."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import stat
|
|
7
|
+
import tarfile
|
|
8
|
+
import zipfile
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import List
|
|
11
|
+
|
|
12
|
+
from aurasprout.contracts.paths import validate_within_root
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def compute_sha256_bytes(data: bytes) -> str:
|
|
16
|
+
return hashlib.sha256(data).hexdigest()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def compute_sha256(file_path: Path) -> str:
|
|
20
|
+
target = Path(file_path).resolve()
|
|
21
|
+
h = hashlib.sha256()
|
|
22
|
+
with open(target, "rb") as handle:
|
|
23
|
+
while True:
|
|
24
|
+
chunk = handle.read(65536)
|
|
25
|
+
if not chunk:
|
|
26
|
+
break
|
|
27
|
+
h.update(chunk)
|
|
28
|
+
return h.hexdigest()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def verify_sha256(file_path: Path, expected_sha256: str) -> str:
|
|
32
|
+
"""Compute SHA-256 of ``file_path`` and raise ValueError on mismatch."""
|
|
33
|
+
expected = (expected_sha256 or "").strip().lower()
|
|
34
|
+
if len(expected) != 64:
|
|
35
|
+
raise ValueError(f"Invalid SHA-256 checksum format: {expected_sha256!r}")
|
|
36
|
+
actual = compute_sha256(file_path).lower()
|
|
37
|
+
if actual != expected:
|
|
38
|
+
raise ValueError(f"SHA-256 mismatch for {file_path}: expected {expected}, got {actual}")
|
|
39
|
+
return actual
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _validate_archive_member_name(name: str) -> None:
|
|
43
|
+
if not name or "\x00" in name:
|
|
44
|
+
raise ValueError(f"Invalid archive member name: {name!r}")
|
|
45
|
+
normalized = name.replace("\\", "/")
|
|
46
|
+
if normalized.startswith("/") or (len(normalized) >= 2 and normalized[1] == ":"):
|
|
47
|
+
raise ValueError(f"Absolute member path rejected in archive: {name!r}")
|
|
48
|
+
parts = [p for p in normalized.split("/") if p]
|
|
49
|
+
if ".." in parts:
|
|
50
|
+
raise ValueError(f"Path traversal member rejected in archive: {name!r}")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def extract_archive_safely(archive_path: Path, dest_dir: Path) -> List[Path]:
|
|
54
|
+
"""Safely extract a `.apkg` / `.zip` / `.tar.gz` archive into ``dest_dir``.
|
|
55
|
+
|
|
56
|
+
Rejects Zip-Slip, Tar-Slip (`..`, rooted paths, Windows drive letters),
|
|
57
|
+
symlinks, hardlinks, and device nodes BEFORE writing any files to ``dest_dir``.
|
|
58
|
+
"""
|
|
59
|
+
src = Path(archive_path).resolve()
|
|
60
|
+
dst = Path(dest_dir).resolve()
|
|
61
|
+
dst.mkdir(parents=True, exist_ok=True)
|
|
62
|
+
extracted: List[Path] = []
|
|
63
|
+
|
|
64
|
+
if zipfile.is_zipfile(src):
|
|
65
|
+
with zipfile.ZipFile(src, "r") as zf:
|
|
66
|
+
members = zf.infolist()
|
|
67
|
+
for info in members:
|
|
68
|
+
_validate_archive_member_name(info.filename)
|
|
69
|
+
mode = (info.external_attr >> 16) & 0o170000
|
|
70
|
+
if stat.S_ISLNK(mode):
|
|
71
|
+
raise ValueError(f"ZIP symlink member rejected: {info.filename!r}")
|
|
72
|
+
validate_within_root(dst, info.filename)
|
|
73
|
+
|
|
74
|
+
for info in members:
|
|
75
|
+
target = validate_within_root(dst, info.filename)
|
|
76
|
+
if info.is_dir():
|
|
77
|
+
target.mkdir(parents=True, exist_ok=True)
|
|
78
|
+
else:
|
|
79
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
80
|
+
with zf.open(info, "r") as reader, open(target, "wb") as writer:
|
|
81
|
+
writer.write(reader.read())
|
|
82
|
+
extracted.append(target)
|
|
83
|
+
return extracted
|
|
84
|
+
|
|
85
|
+
if tarfile.is_tarfile(src):
|
|
86
|
+
with tarfile.open(src, "r:*") as tf:
|
|
87
|
+
members = tf.getmembers()
|
|
88
|
+
for member in members:
|
|
89
|
+
_validate_archive_member_name(member.name)
|
|
90
|
+
if member.issym() or member.islnk() or member.isdev() or member.isfifo():
|
|
91
|
+
raise ValueError(f"Unsafe tar member type rejected: {member.name!r}")
|
|
92
|
+
validate_within_root(dst, member.name)
|
|
93
|
+
|
|
94
|
+
tf.extractall(path=dst, members=members, filter="data")
|
|
95
|
+
for member in members:
|
|
96
|
+
if member.isfile():
|
|
97
|
+
extracted.append(validate_within_root(dst, member.name))
|
|
98
|
+
return extracted
|
|
99
|
+
|
|
100
|
+
raise ValueError(f"Unsupported package archive format: {src}")
|
|
101
|
+
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""Zero-CDN self-contained static HTML5 Application Catalog Portal generator."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import html
|
|
6
|
+
import json
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
from aurasprout.catalog.models import CatalogManifest
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def generate_catalog_html(manifest: CatalogManifest, catalog_url: Optional[str] = None) -> str:
|
|
13
|
+
"""Render a completely offline, zero-CDN HTML5 portal for a CatalogManifest."""
|
|
14
|
+
title = html.escape(manifest.catalog_name)
|
|
15
|
+
desc = html.escape(manifest.description or "Unified Zero-Privilege Application Catalog")
|
|
16
|
+
updated = html.escape(manifest.updated_at[:19].replace("T", " ") + " UTC")
|
|
17
|
+
effective_catalog_url = html.escape(catalog_url or manifest.base_url or "./catalog.json")
|
|
18
|
+
|
|
19
|
+
all_tags = sorted({tag for app in manifest.apps.values() for tag in app.tags})
|
|
20
|
+
cards_html = []
|
|
21
|
+
for app in sorted(manifest.apps.values(), key=lambda a: a.app_id):
|
|
22
|
+
v_info = app.get_version(app.latest_version)
|
|
23
|
+
v_str = html.escape(v_info.version if v_info else app.latest_version)
|
|
24
|
+
ch_str = html.escape(v_info.channel if v_info else app.channel)
|
|
25
|
+
app_disp = html.escape(app.display_name or app.name)
|
|
26
|
+
app_id_esc = html.escape(app.app_id)
|
|
27
|
+
desc_esc = html.escape(app.description or "No description provided.")
|
|
28
|
+
tags_html = " ".join(f'<span class="pill">{html.escape(t)}</span>' for t in app.tags)
|
|
29
|
+
sha_str = html.escape(v_info.bundle_sha256 if v_info else "")
|
|
30
|
+
sha_short = f"{sha_str[:16]}..." if sha_str else "none"
|
|
31
|
+
letter = html.escape((app.display_name or app.app_id)[:1].upper() or "A")
|
|
32
|
+
bundle_url = html.escape(
|
|
33
|
+
(v_info.bundle_url if v_info and v_info.bundle_url else f"apps/{app_id_esc}/{app_id_esc}-{v_str}.apkg")
|
|
34
|
+
)
|
|
35
|
+
ver_options = "".join(
|
|
36
|
+
f'<option value="{html.escape(ver_k)}">v{html.escape(ver_k)} ({html.escape(ver_obj.channel)})</option>'
|
|
37
|
+
for ver_k, ver_obj in sorted(app.versions.items(), reverse=True)
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
cards_html.append(
|
|
41
|
+
f"""
|
|
42
|
+
<div class="app-card" data-app-id="{app_id_esc}" data-channel="{ch_str}">
|
|
43
|
+
<div>
|
|
44
|
+
<div class="card-header">
|
|
45
|
+
<div class="app-avatar">{letter}</div>
|
|
46
|
+
<div class="header-text">
|
|
47
|
+
<div class="app-title" title="{app_disp}">{app_disp}</div>
|
|
48
|
+
<div class="app-meta">
|
|
49
|
+
<span class="app-slug">{app_id_esc}</span>
|
|
50
|
+
<span class="version-badge">v{v_str}</span>
|
|
51
|
+
<span class="channel-badge">{ch_str}</span>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
<p class="app-desc">{desc_esc}</p>
|
|
56
|
+
<div class="tags-list">{tags_html}</div>
|
|
57
|
+
</div>
|
|
58
|
+
<div class="card-actions">
|
|
59
|
+
<select class="ver-select" aria-label="Available versions">{ver_options}</select>
|
|
60
|
+
<div class="checksum-bar" title="SHA-256: {sha_str}">
|
|
61
|
+
<span>SHA-256:</span>
|
|
62
|
+
<span class="checksum-hash">{sha_short}</span>
|
|
63
|
+
<button class="copy-btn" type="button" onclick="copyText('{sha_str}', this)">Copy</button>
|
|
64
|
+
</div>
|
|
65
|
+
<div class="buttons-row">
|
|
66
|
+
<button class="btn-install" type="button" onclick="openInstallModal('{app_id_esc}', '{v_str}')">
|
|
67
|
+
Install CLI Command
|
|
68
|
+
</button>
|
|
69
|
+
<a href="{bundle_url}" class="btn-download" download>Download .apkg</a>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
</div>"""
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
tag_buttons = '<button class="tag-btn active" type="button" data-tag="all">All</button>\n' + "\n".join(
|
|
76
|
+
f'<button class="tag-btn" type="button" data-tag="{html.escape(t)}">{html.escape(t)}</button>'
|
|
77
|
+
for t in all_tags
|
|
78
|
+
)
|
|
79
|
+
manifest_json_escaped = json.dumps(manifest.to_dict()).replace("</script>", "<\\/script>")
|
|
80
|
+
rendered_cards = "\n".join(cards_html) or "<p class='empty'>No applications published yet.</p>"
|
|
81
|
+
|
|
82
|
+
return f"""<!DOCTYPE html>
|
|
83
|
+
<html lang="en">
|
|
84
|
+
<head>
|
|
85
|
+
<meta charset="UTF-8">
|
|
86
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
87
|
+
<title>{title}</title>
|
|
88
|
+
<style>
|
|
89
|
+
:root {{
|
|
90
|
+
--bg: #0d1117;
|
|
91
|
+
--card-bg: #161b22;
|
|
92
|
+
--card-border: #30363d;
|
|
93
|
+
--text: #e6edf3;
|
|
94
|
+
--muted: #8b949e;
|
|
95
|
+
--accent: #58a6ff;
|
|
96
|
+
--green: #238636;
|
|
97
|
+
--font: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
|
|
98
|
+
}}
|
|
99
|
+
* {{ box-sizing: border-box; margin: 0; padding: 0; }}
|
|
100
|
+
body {{ font-family: var(--font); background: var(--bg); color: var(--text); padding: 1.5rem; }}
|
|
101
|
+
header {{ max-width: 1200px; margin: 0 auto 1.5rem auto; border-bottom: 1px solid var(--card-border); padding-bottom: 1rem; }}
|
|
102
|
+
h1 {{ font-size: 1.75rem; margin-bottom: 0.25rem; }}
|
|
103
|
+
.sub {{ color: var(--muted); font-size: 0.9rem; }}
|
|
104
|
+
.controls {{ max-width: 1200px; margin: 0 auto 1.25rem auto; display: flex; gap: 0.75rem; flex-wrap: wrap; align-items: center; }}
|
|
105
|
+
input, select {{ background: var(--card-bg); color: var(--text); border: 1px solid var(--card-border); border-radius: 6px; padding: 0.45rem 0.75rem; }}
|
|
106
|
+
.tags-bar {{ display: flex; gap: 0.4rem; flex-wrap: wrap; }}
|
|
107
|
+
.tag-btn {{ background: var(--card-bg); color: var(--text); border: 1px solid var(--card-border); border-radius: 999px; padding: 0.3rem 0.75rem; cursor: pointer; }}
|
|
108
|
+
.tag-btn.active {{ border-color: var(--accent); color: var(--accent); }}
|
|
109
|
+
.grid {{ max-width: 1200px; margin: 0 auto; display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 1rem; }}
|
|
110
|
+
.app-card {{ background: var(--card-bg); border: 1px solid var(--card-border); border-radius: 10px; padding: 1rem; display: flex; flex-direction: column; justify-content: space-between; gap: 0.85rem; }}
|
|
111
|
+
.card-header {{ display: flex; align-items: center; gap: 0.75rem; }}
|
|
112
|
+
.app-avatar {{ width: 38px; height: 38px; border-radius: 8px; background: #1f6feb; display: grid; place-items: center; font-weight: 700; }}
|
|
113
|
+
.app-title {{ font-weight: 600; font-size: 1.05rem; }}
|
|
114
|
+
.app-meta {{ font-size: 0.8rem; color: var(--muted); display: flex; gap: 0.4rem; margin-top: 0.15rem; }}
|
|
115
|
+
.version-badge, .channel-badge, .pill {{ background: #21262d; padding: 0.1rem 0.45rem; border-radius: 6px; font-size: 0.75rem; }}
|
|
116
|
+
.app-desc {{ color: var(--muted); font-size: 0.88rem; margin-top: 0.6rem; }}
|
|
117
|
+
.tags-list {{ display: flex; gap: 0.3rem; flex-wrap: wrap; margin-top: 0.5rem; }}
|
|
118
|
+
.checksum-bar {{ font-size: 0.75rem; color: var(--muted); display: flex; align-items: center; justify-content: space-between; margin: 0.4rem 0; }}
|
|
119
|
+
.copy-btn {{ background: #21262d; color: var(--text); border: 1px solid var(--card-border); border-radius: 5px; padding: 0.15rem 0.45rem; cursor: pointer; }}
|
|
120
|
+
.buttons-row {{ display: flex; gap: 0.5rem; }}
|
|
121
|
+
.btn-install {{ flex: 1; background: var(--green); color: #fff; border: none; border-radius: 6px; padding: 0.45rem; cursor: pointer; font-weight: 600; }}
|
|
122
|
+
.btn-download {{ background: #21262d; color: var(--text); border: 1px solid var(--card-border); border-radius: 6px; padding: 0.45rem 0.75rem; text-decoration: none; font-size: 0.85rem; }}
|
|
123
|
+
.modal {{ position: fixed; inset: 0; background: rgba(0, 0, 0, 0.7); display: none; align-items: center; justify-content: center; }}
|
|
124
|
+
.modal-box {{ background: var(--card-bg); border: 1px solid var(--card-border); border-radius: 10px; max-width: 600px; width: 92%; padding: 1.25rem; }}
|
|
125
|
+
pre {{ background: #0d1117; border: 1px solid var(--card-border); padding: 0.75rem; border-radius: 6px; overflow-x: auto; margin: 0.75rem 0; }}
|
|
126
|
+
</style>
|
|
127
|
+
</head>
|
|
128
|
+
<body>
|
|
129
|
+
<header>
|
|
130
|
+
<h1>{title}</h1>
|
|
131
|
+
<p class="sub">{desc} | Updated: {updated} | Apps: {len(manifest.apps)}</p>
|
|
132
|
+
</header>
|
|
133
|
+
<div class="controls">
|
|
134
|
+
<input id="search-input" type="search" placeholder="Filter applications...">
|
|
135
|
+
<select id="channel-filter">
|
|
136
|
+
<option value="all">All channels</option>
|
|
137
|
+
<option value="stable">stable</option>
|
|
138
|
+
<option value="preview">preview</option>
|
|
139
|
+
<option value="canary">canary</option>
|
|
140
|
+
<option value="nightly">nightly</option>
|
|
141
|
+
</select>
|
|
142
|
+
<div class="tags-bar">{tag_buttons}</div>
|
|
143
|
+
</div>
|
|
144
|
+
<div id="app-grid" class="grid">{rendered_cards}</div>
|
|
145
|
+
<div id="install-modal" class="modal">
|
|
146
|
+
<div class="modal-box">
|
|
147
|
+
<h2 id="modal-app-title">Deploy Application</h2>
|
|
148
|
+
<pre id="modal-code"></pre>
|
|
149
|
+
<button id="modal-copy-btn" class="btn-install" type="button" onclick="copyModalCode()">Copy Command</button>
|
|
150
|
+
<button class="copy-btn" type="button" onclick="closeModal()">Close</button>
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
<script>
|
|
154
|
+
const CATALOG_URL = {json.dumps(effective_catalog_url)};
|
|
155
|
+
const MANIFEST = {manifest_json_escaped};
|
|
156
|
+
let activeTag = "all";
|
|
157
|
+
function filterCards() {{
|
|
158
|
+
const q = (document.getElementById("search-input").value || "").toLowerCase();
|
|
159
|
+
const ch = document.getElementById("channel-filter").value;
|
|
160
|
+
document.querySelectorAll(".app-card").forEach((card) => {{
|
|
161
|
+
const appId = card.dataset.appId;
|
|
162
|
+
const entry = (MANIFEST.apps || {{}})[appId] || {{}};
|
|
163
|
+
const text = (appId + " " + (entry.displayName || "") + " " + (entry.description || "")).toLowerCase();
|
|
164
|
+
const tags = entry.tags || [];
|
|
165
|
+
const matchQ = !q || text.includes(q);
|
|
166
|
+
const matchCh = ch === "all" || card.dataset.channel === ch;
|
|
167
|
+
const matchTag = activeTag === "all" || tags.includes(activeTag);
|
|
168
|
+
card.style.display = (matchQ && matchCh && matchTag) ? "flex" : "none";
|
|
169
|
+
}});
|
|
170
|
+
}}
|
|
171
|
+
document.getElementById("search-input").addEventListener("input", filterCards);
|
|
172
|
+
document.getElementById("channel-filter").addEventListener("change", filterCards);
|
|
173
|
+
document.querySelectorAll(".tag-btn").forEach((btn) => {{
|
|
174
|
+
btn.addEventListener("click", () => {{
|
|
175
|
+
document.querySelectorAll(".tag-btn").forEach((b) => b.classList.remove("active"));
|
|
176
|
+
btn.classList.add("active");
|
|
177
|
+
activeTag = btn.dataset.tag;
|
|
178
|
+
filterCards();
|
|
179
|
+
}});
|
|
180
|
+
}});
|
|
181
|
+
function copyText(text, btn) {{
|
|
182
|
+
if (navigator.clipboard) navigator.clipboard.writeText(text);
|
|
183
|
+
const orig = btn.textContent;
|
|
184
|
+
btn.textContent = "Copied";
|
|
185
|
+
setTimeout(() => {{ btn.textContent = orig; }}, 1200);
|
|
186
|
+
}}
|
|
187
|
+
function openInstallModal(appId, version) {{
|
|
188
|
+
document.getElementById("modal-app-title").textContent = "Install " + appId + " v" + version;
|
|
189
|
+
document.getElementById("modal-code").textContent =
|
|
190
|
+
'aurasprout install ' + appId + ' --version ' + version + ' --from-catalog "' + CATALOG_URL + '"';
|
|
191
|
+
document.getElementById("install-modal").style.display = "flex";
|
|
192
|
+
}}
|
|
193
|
+
function closeModal() {{ document.getElementById("install-modal").style.display = "none"; }}
|
|
194
|
+
function copyModalCode() {{
|
|
195
|
+
const code = document.getElementById("modal-code").textContent;
|
|
196
|
+
copyText(code, document.getElementById("modal-copy-btn"));
|
|
197
|
+
}}
|
|
198
|
+
</script>
|
|
199
|
+
</body>
|
|
200
|
+
</html>
|
|
201
|
+
"""
|
|
202
|
+
|