aurasprout 0.1.1__py3-none-any.whl
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/__init__.py +4 -0
- aurasprout/__main__.py +8 -0
- aurasprout/catalog/__init__.py +2 -0
- aurasprout/catalog/crypto.py +101 -0
- aurasprout/catalog/html_generator.py +202 -0
- aurasprout/catalog/manager.py +422 -0
- aurasprout/catalog/models.py +640 -0
- aurasprout/catalog/packager.py +124 -0
- aurasprout/catalog/sources.py +122 -0
- aurasprout/cli.py +219 -0
- aurasprout/cli_catalog.py +305 -0
- aurasprout/cli_volunteer.py +122 -0
- aurasprout/contracts/__init__.py +2 -0
- aurasprout/contracts/paths.py +225 -0
- aurasprout/coordination/__init__.py +10 -0
- aurasprout/coordination/batches.py +240 -0
- aurasprout/coordination/leases.py +577 -0
- aurasprout/installer/__init__.py +2 -0
- aurasprout/installer/autostart.py +473 -0
- aurasprout/installer/engine.py +924 -0
- aurasprout/installer/registry.py +203 -0
- aurasprout/installer/self_install.py +739 -0
- aurasprout/ipc/__init__.py +9 -0
- aurasprout/ipc/server.py +705 -0
- aurasprout/storage/__init__.py +23 -0
- aurasprout/storage/cell_state.py +424 -0
- aurasprout/storage/events.py +278 -0
- aurasprout/storage/mutex.py +244 -0
- aurasprout/supervisor/__init__.py +9 -0
- aurasprout/supervisor/runner.py +335 -0
- aurasprout/ui/__init__.py +11 -0
- aurasprout/ui/desktop.py +874 -0
- aurasprout/ui/server.py +788 -0
- aurasprout/ui/static/app.js +326 -0
- aurasprout/ui/static/aurasprout.ico +0 -0
- aurasprout/ui/static/aurasprout.png +0 -0
- aurasprout/ui/static/index.html +135 -0
- aurasprout-0.1.1.dist-info/METADATA +74 -0
- aurasprout-0.1.1.dist-info/RECORD +43 -0
- aurasprout-0.1.1.dist-info/WHEEL +5 -0
- aurasprout-0.1.1.dist-info/entry_points.txt +2 -0
- aurasprout-0.1.1.dist-info/licenses/LICENSE +21 -0
- aurasprout-0.1.1.dist-info/top_level.txt +1 -0
aurasprout/__init__.py
ADDED
aurasprout/__main__.py
ADDED
|
@@ -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
|
+
|