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:
@@ -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()
|
||||
Reference in New Issue
Block a user