port: from the old factory (tea-mcp/src/persimmons + human_vault), as it was, 2026-10-14
This commit is contained in:
commit
c50ff52fcf
14 files changed
+1396
No files matched your search
@@ -0,0 +1,94 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Replace a loaded Persimmons folder from the dedicated HTTPS release.
|
||||
|
||||
Run: python3 update-extension.py /path/to/persimmons-extension
|
||||
Close the popup first; afterward use chrome://extensions -> Reload.
|
||||
No browser profile, local vault storage, credentials or native host is accessed.
|
||||
"""
|
||||
import argparse
|
||||
import base64
|
||||
import hashlib
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path, PurePosixPath
|
||||
import tempfile
|
||||
import urllib.request
|
||||
import zipfile
|
||||
|
||||
EXTENSION_ID = 'kiolgjoohhgdohcafgelhikpgkbmckbg'
|
||||
URL = 'https://persimmons.app/assets/extension.zip'
|
||||
LIMIT = 4 * 1024 * 1024
|
||||
|
||||
|
||||
def identity(manifest):
|
||||
digest = hashlib.sha256(base64.b64decode(manifest['key'], validate=True)).hexdigest()[:32]
|
||||
return ''.join(chr(ord('a') + int(c, 16)) for c in digest)
|
||||
|
||||
|
||||
def update(folder, raw):
|
||||
folder = Path(folder).expanduser().absolute()
|
||||
if folder.is_symlink() or any(p.is_symlink() for p in folder.parents):
|
||||
raise ValueError('Use a real extension folder, not a symbolic link.')
|
||||
installed = json.loads((folder / 'manifest.json').read_text())
|
||||
if identity(installed) != EXTENSION_ID:
|
||||
raise ValueError('The installed extension identity does not match.')
|
||||
files = {}
|
||||
with zipfile.ZipFile(io.BytesIO(raw)) as archive:
|
||||
entries = archive.infolist()
|
||||
if len(entries) > 100 or sum(e.file_size for e in entries) > LIMIT:
|
||||
raise ValueError('Release exceeds size limit.')
|
||||
for entry in entries:
|
||||
path = PurePosixPath(entry.filename)
|
||||
if (not path.parts or path.parts[0] != 'persimmons-extension' or
|
||||
'..' in path.parts or '\\' in entry.filename or entry.is_dir() or
|
||||
(entry.external_attr >> 16) & 0o170000 == 0o120000):
|
||||
raise ValueError('Unexpected release path.')
|
||||
relative = Path(*path.parts[1:])
|
||||
if relative.suffix not in {'.mjs', '.json', '.html', '.css', '.png'} or relative in files:
|
||||
raise ValueError('Unexpected release file.')
|
||||
destination = folder / relative
|
||||
if destination.is_symlink() or any(p.is_symlink() for p in destination.parents):
|
||||
raise ValueError('Refusing to replace symbolic links.')
|
||||
files[relative] = archive.read(entry)
|
||||
candidate = json.loads(files[Path('manifest.json')])
|
||||
if identity(candidate) != EXTENSION_ID or candidate['key'] != installed['key']:
|
||||
raise ValueError('Release identity does not match; no files replaced.')
|
||||
version = lambda value: tuple(int(n) for n in value.split('.'))
|
||||
if version(candidate['version']) < version(installed['version']):
|
||||
raise ValueError('Refusing an older release.')
|
||||
# Validate the entire archive before any write. Stage beside the destination
|
||||
# for atomic per-file replacements; manifest goes last. Retry after interruption.
|
||||
with tempfile.TemporaryDirectory(prefix='.persimmons-update-', dir=folder.parent) as staging:
|
||||
stage = Path(staging)
|
||||
for relative, data in files.items():
|
||||
(stage / relative).parent.mkdir(parents=True, exist_ok=True)
|
||||
(stage / relative).write_bytes(data)
|
||||
for relative in sorted(files, key=lambda p: (p.name == 'manifest.json', str(p))):
|
||||
(folder / relative).parent.mkdir(parents=True, exist_ok=True)
|
||||
os.replace(stage / relative, folder / relative)
|
||||
return candidate['version']
|
||||
|
||||
|
||||
class NoRedirect(urllib.request.HTTPRedirectHandler):
|
||||
def redirect_request(self, *args, **kwargs):
|
||||
raise ValueError('Release URL redirected; no files replaced.')
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('folder', help='Existing folder shown under chrome://extensions -> Details -> Loaded from')
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
with urllib.request.build_opener(NoRedirect).open(URL, timeout=30) as response:
|
||||
raw = response.read(LIMIT + 1)
|
||||
if len(raw) > LIMIT:
|
||||
raise ValueError('Release exceeds size limit.')
|
||||
release = update(args.folder, raw)
|
||||
print(f'Installed files for {release}. Now click Reload at chrome://extensions, or Reload installed files in the popup.')
|
||||
except Exception:
|
||||
parser.exit(1, 'Update failed safely. Check the folder, network and release; retry before reloading.\n')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in new issue
Block a user