Move secure send into Tech Tools, where somebody would look for it
Some checks failed
CI / backend (push) Failing after 7m10s
CI / naming (push) Failing after 7m14s
CI / frontend (push) Failing after 7m10s
CI / migrations-mysql (push) Failing after 7m10s

9845867 gave it its own plugin because Tech Tools is browser-only and this needs
storage. That was the right reading of the code and the wrong answer for the
person using it: Tech Tools is where you go for a utility, and a tool nobody can
find is a tool nobody uses.

So Tech Tools now has a blueprint, a table and a migration chain where it had
none. Its docstring and manifest say why, because "every tool here runs in the
browser" was load-bearing documentation and is no longer true.

WHAT MOVED
  /api/securesend       -> /api/tools/secrets
  securesend.create     -> tools.secrets
  securesend0001baseline -> tools0001securesend
  PLUGIN_TABLE_OWNERS, EXPECTED_HEAD_REVISION, EXEMPT_ENDPOINTS, the universal
  profile and the api-inventory surface all follow it.

THE OLD "NO BACKEND" TEST IS NARROWED, NOT DELETED. It asserted
get_blueprint() is None, which is the sort of guard that gets removed when it
becomes inconvenient. It now asserts the blueprint serves EXACTLY the three
secret routes, with their methods - so a future tool that quietly grows an
endpoint or a table has to edit this test, and therefore make the decision on
purpose rather than by drift.

Asserting methods rather than paths also pins the detail this design turns on:
redeeming is POST-ONLY. Chat and mail clients prefetch GET urls to build
previews, and a preview that could redeem would consume a one-view secret before
its recipient opened the link. Previously that was a comment; now it fails a
test.

UNINSTALLING THE OLD PLUGIN COULD NOT BE DONE WITH THE PLUGIN TOOLING. `flask
plugin uninstall securesend` loads a plugin before uninstalling it, so a plugin
whose directory has been deleted cannot be removed by the command that manages
plugins - the registry entry had to be edited by hand. Worth fixing separately;
it is the same registry-versus-code divergence the device work hit from the
other side.

MIGRATING A CHAIN THAT HAS ALREADY RUN needs the old table and its version row
dropped first, because the relocated chain has no stamp and tries to create a
table that exists. Only relevant to anyone who installed securesend during the
hour it existed - it did reach the publication repo in 4242028 - and the fix is
DROP TABLE securesendsecrets plus DROP TABLE alembic_version_securesend before
running plugin upgrade-all.

1829 tests green. Still backend only: there are no pages yet, so nothing here is
reachable by a person.
This commit is contained in:
cproudlock
2026-08-21 14:17:57 -04:00
parent 98458672eb
commit d4b49b91b4
26 changed files with 156 additions and 281 deletions

View File

@@ -27,11 +27,39 @@ def test_meta_matches_manifest():
assert meta.core_version == manifest['core_version']
def test_no_backend_surface():
"""Client-side only: no blueprint to register, no tables to migrate."""
def test_the_only_backend_surface_is_secure_send():
"""Tech Tools used to have NO backend, and mostly still should.
This asserted `get_blueprint() is None` until secure send arrived: a link
that expires and dies on first read cannot be browser-only, because
something has to hold the ciphertext until it is claimed. The guard is kept
rather than deleted, narrowed to the one exception - a future tool that
quietly grows an endpoint or a table has to change this test, and therefore
make the decision on purpose rather than by accident.
"""
from flask import Flask
from plugins.tools.models import SecureSecret
plugin = ToolsPlugin()
assert plugin.get_blueprint() is None
assert plugin.get_models() == []
assert plugin.get_models() == [SecureSecret]
probe = Flask(__name__)
probe.register_blueprint(plugin.get_blueprint(), url_prefix='/api/tools')
served = sorted(
(str(rule), sorted(rule.methods - {'HEAD', 'OPTIONS'}))
for rule in probe.url_map.iter_rules()
if not str(rule).startswith('/static'))
# Methods are asserted, not just paths: REDEEMING MUST BE POST-ONLY. Chat
# and mail clients prefetch GET urls to build previews, and a preview that
# could redeem would consume a one-view secret before its recipient ever
# opened the link. The GET on the same path reports availability only.
assert served == [
('/api/tools/secrets', ['POST']),
('/api/tools/secrets/<token>', ['GET']),
('/api/tools/secrets/<token>', ['POST']),
], served
def test_navigation_item():