"""Tech Tools plugin. A section for small technician utilities. MOST tools here run entirely in the browser - no API, no tables, no state - which is deliberate: these are the things someone reaches for at a bench with no network guarantee, and an air-gapped site gets them for free. SECURE SEND IS THE EXCEPTION, and it is why this plugin now has a blueprint, a table and a migration chain where it once had none. A link that expires and dies on first read cannot be browser-only: something has to hold the ciphertext until it is claimed, and forget it afterwards. The secret itself is still encrypted in the browser - the key travels in the URL fragment and never reaches this server - so what is stored is ciphertext nobody on this side can read. The plugin also exists so the section can carry a nav entry and be included in or left out of a per-site build (ADR-013). """ import logging from typing import List, Optional, Type from flask import Blueprint, Flask from shopdb.plugins.base import BasePlugin, PluginMeta from .models import SecureSecret from .api import tools_bp logger = logging.getLogger(__name__) class ToolsPlugin(BasePlugin): """Registers the Tech Tools section and the one tool that needs a server.""" @property def meta(self) -> PluginMeta: return PluginMeta( name='tools', version='1.0.0', description=('Tech Tools: technician utilities, mostly browser-side ' '(barcode/QR labels) plus secure send for handing ' 'somebody a credential by one-view link'), author='ShopDB Team', dependencies=[], core_version='>=0.16.0,<1.0.0', api_prefix='/api/tools', ) def get_blueprint(self) -> Optional[Blueprint]: # Secure send only. Every other tool is still browser-side. return tools_bp def get_models(self) -> List[Type]: return [SecureSecret] def get_permissions(self) -> List: """One permission, on CREATING a secret only. There is deliberately no read permission: the recipient usually has no account here, and the unguessable token is what authorises the read. Gating that side would make the tool useless for the case it exists for - handing somebody a credential while you are on a call with them. No delete permission either. A secret is spent by being read or by expiring, and both remove the row; a hand-delete route would only add a way to destroy the record of a secret that had not been read yet. """ return [ ('tools.secrets', 'Create secure send links', 'tools'), ] def get_navigation_items(self) -> List[dict]: return [ { 'name': 'Tech Tools', 'icon': 'wrench', 'route': '/tools', 'position': 47, }, ] def on_install(self, app: Flask) -> None: logger.info('Tools plugin installed')