From 998f9c69308ee88fac36cfb77dfb6d07c6fa926a Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Wed, 23 Sep 2026 17:46:48 +0200 Subject: feat(hub): invitation-link tickets bound to a verified address MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit group_invite_links holds sha256(ticket) and the invitee's address blind index; redeeming grants membership to that account only. Owner-only create/list/cancel (a node token may create, never mail), 20 outstanding per group, optional mail written by the hub itself and capped at 10 per sender per day (mail.invite_link_daily_cap). MESHBAY_DESIGN.md ยง3.4 now carries the whole link design. Co-Authored-By: Claude Opus 5.5 --- .../b2c3d4e5f6a7_add_group_invite_links.py | 46 ++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/b2c3d4e5f6a7_add_group_invite_links.py (limited to 'packages/meshbay-hub/src/meshbay_hub/db/migrations') diff --git a/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/b2c3d4e5f6a7_add_group_invite_links.py b/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/b2c3d4e5f6a7_add_group_invite_links.py new file mode 100644 index 0000000..75b4d49 --- /dev/null +++ b/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/b2c3d4e5f6a7_add_group_invite_links.py @@ -0,0 +1,46 @@ +"""add group_invite_links + +The hub's half of an invitation link: a ticket, stored hashed, that grants +membership of one group to the one account whose verified address matches. + +Revision ID: b2c3d4e5f6a7 +Revises: a9b8c7d6e5f4 +""" + +from collections.abc import Sequence + +import sqlalchemy as sa +from alembic import op + +revision: str = "b2c3d4e5f6a7" +down_revision: str | Sequence[str] | None = "a9b8c7d6e5f4" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None + + +def upgrade() -> None: + op.create_table( + "group_invite_links", + sa.Column("id", sa.String(36), primary_key=True), + sa.Column("group_id", sa.String(36), sa.ForeignKey("groups.id"), nullable=False), + sa.Column("created_by", sa.String(36), sa.ForeignKey("users.id"), nullable=False), + sa.Column("ticket_hash", sa.String(64), nullable=False), + sa.Column("email_hash", sa.String(64), nullable=False), + sa.Column("email_masked", sa.String(128), nullable=False), + sa.Column("node_invite_id", sa.String(32), nullable=False, server_default=""), + sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, + server_default=sa.func.now()), + sa.Column("expires_at", sa.DateTime(timezone=True), nullable=False), + sa.Column("redeemed_by", sa.String(36), sa.ForeignKey("users.id")), + sa.Column("redeemed_at", sa.DateTime(timezone=True)), + ) + op.create_index("uq_invite_links_ticket", "group_invite_links", ["ticket_hash"], + unique=True) + op.create_index("ix_invite_links_group", "group_invite_links", ["group_id"]) + + +def downgrade() -> None: + # Outstanding links stop working; the node halves stay until they expire. + op.drop_index("ix_invite_links_group", table_name="group_invite_links") + op.drop_index("uq_invite_links_ticket", table_name="group_invite_links") + op.drop_table("group_invite_links") -- cgit v1.2.3