feat: integrate Just Works app and native business setup

Preserve hosted website UX inside Archipelago, add isolated Business and owner-authorized native wallet setup, and include deployment handoff. Based on main with native signer import already accepted.
This commit is contained in:
yaya
2026-10-08 19:39:38 +01:00
parent 8d17597202
commit 94899e69d3
152 changed files with 22873 additions and 2 deletions
+90
View File
@@ -0,0 +1,90 @@
"""Owner-authorized, fixed-origin payment destination update. No secret storage."""
import json
import re
import threading
from urllib.request import Request, build_opener, HTTPRedirectHandler
CORE = 'https://justworks.cash'
LOCK = threading.Lock()
class PublishUnverified(Exception):
pass
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, *args, **kwargs):
return None
def request_core(method, path, body=None):
data = json.dumps(body).encode() if body is not None else None
request = Request(CORE + path, data=data, method=method,
headers={'Content-Type': 'application/json', 'Accept': 'application/json',
'User-Agent': 'Archipelago-JustWorks/0.1'})
with build_opener(NoRedirect).open(request, timeout=100 if path.endswith('/deploy') else 20) as response:
raw = response.read(2 * 1024 * 1024 + 1)
if len(raw) > 2 * 1024 * 1024:
raise ValueError('Response too large')
return json.loads(raw)
def checked_input(body):
if not isinstance(body, dict):
raise ValueError('Invalid request')
slug = body.get('siteId', '')
address = body.get('address', '')
if not isinstance(slug, str) or not re.fullmatch(r'[a-z0-9]+(?:-[a-z0-9]+)*', slug) or len(slug) > 160 or slug.startswith(('nsec1', 'ncryptsec1')):
raise ValueError('Invalid website')
# This integration connects only the native wallet address returned by the OS.
if not isinstance(address, str) or not re.fullmatch(r'[a-z0-9._+-]{1,100}@minibits\.cash', address):
raise ValueError('Invalid native wallet address')
credentials = body.get('credentials')
if not isinstance(credentials, dict):
raise ValueError('Owner sign-in required')
allowed = ('ownerIdentifier', 'ownerPassword', 'ownerKey', 'password')
clean = {}
for key in allowed:
value = credentials.get(key, '')
if not isinstance(value, str) or len(value) > 2048:
raise ValueError('Invalid owner sign-in')
if value:
clean[key] = value
if not clean.get('ownerKey') and not (clean.get('ownerIdentifier') and clean.get('ownerPassword')):
raise ValueError('Owner sign-in required')
return slug, address, clean
def destination_matches(record, slug, address):
return (isinstance(record, dict) and record.get('id') == slug
and record.get('nsite', {}).get('deploymentStatus') == 'published'
and record.get('identity', {}).get('profile', {}).get('lud16') == address)
def connect_payment(body):
slug, address, credentials = checked_input(body)
if not LOCK.acquire(blocking=False):
raise ValueError('Another update is in progress. Wait and retry.')
try:
# Confirm ownership before attempting a change. Never keep the returned
# owner session or any recovery material from Core's response.
owner = request_core('POST', f'/api/nsites/{slug}/login', credentials).get('nsite', {})
if owner.get('id') != slug:
raise ValueError('Owner sign-in did not match this website')
try:
# Only payment changes; do not replay or overwrite other profile fields.
request_core('PATCH', f'/api/nsites/{slug}', {**credentials, 'payment': address})
published = request_core('POST', f'/api/nsites/{slug}/deploy', credentials)
if not published.get('artifact', {}).get('verified') or not destination_matches(published.get('nsite'), slug, address):
raise PublishUnverified()
# This is the public record consumed by the hosted customer tip page.
public = request_core('GET', f'/api/nsites/{slug}').get('nsite')
if not destination_matches(public, slug, address):
raise PublishUnverified()
except Exception as error:
raise PublishUnverified('The address may be saved, but publication could not be verified. Retry to publish and check again.') from error
return {'verified': True, 'siteId': slug, 'address': address,
'url': f'{CORE}/{slug}'}
finally:
credentials.clear()
LOCK.release()