Compare commits

..

No commits in common. "8f0bcc35f0c0f5909215db7dea01b5ef8c2bc9a9" and "9dd92b4cc180e88ffc14a96ec979a90c84e1676f" have entirely different histories.

21 changed files with 189 additions and 319 deletions

View file

@ -33,14 +33,6 @@
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDHTIqF4CAylSxKPiSo5JOPuocn0y2z38wOSsQ1MUaZ2" "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDHTIqF4CAylSxKPiSo5JOPuocn0y2z38wOSsQ1MUaZ2"
]; ];
}; };
Lois = {
isNormalUser = true;
extraGroups = [ "wheel" ];
openssh.authorizedKeys.keys = [
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKVQ7yYXr4ZguGWYHZ7v2L3kPmYjaFo46PTgAEviW5D8 lois@mouse"
];
};
}; };
security.sudo.wheelNeedsPassword = false; security.sudo.wheelNeedsPassword = false;

View file

@ -1 +0,0 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKVQ7yYXr4ZguGWYHZ7v2L3kPmYjaFo46PTgAEviW5D8 lois@mouse

View file

@ -1 +1 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKBpnV6zzgdJN5pjw2oWryneE6kZ5rQ343Ut4ed12Cm9 root@fedi201 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILhSlUo7L/TjoAILfLv/BDxlBT+rGudh9VoK50Uiu2lZ root@fedi201

View file

@ -58,13 +58,13 @@ let
text = text =
'' ''
systemd-run --pty \ systemd-run --pty \
--same-dir \
--wait \ --wait \
--collect \ --collect \
--service-type=exec \ --service-type=exec \
--unit "manage-${name}.service" \ --unit "manage-${name}.service" \
--property "User=${name}" \ --property "User=${name}" \
--property "Group=${name}" \ --property "Group=${name}" \
--property "WorkingDirectory=/var/lib/${name}" \
--property "Environment=DATABASE_URL=${database-url} USER_SETTINGS_FILE=${configFile}" \ --property "Environment=DATABASE_URL=${database-url} USER_SETTINGS_FILE=${configFile}" \
'' ''
+ optionalString (credentials != [ ]) ( + optionalString (credentials != [ ]) (

View file

@ -0,0 +1,36 @@
import os
import re
import sys
from importlib import import_module
from importlib.util import find_spec
from django.utils.functional import classproperty
from django_pydantic_field import SchemaField
from django.apps import apps
from django.db import models
from django import forms
class Version():
model: models.Model
form: forms.ModelForm
@classproperty
def latest(cls):
current_dir = os.path.dirname(os.path.abspath(__file__))
version_pattern = re.compile(r'v(\d+)\.py$')
versions = []
for filename in os.listdir(current_dir):
match = version_pattern.match(filename)
if match:
versions.append(int(match.group(1)))
return max(versions)
def __init__(self, version: int):
module = import_module(f"{__name__}.v{version}")
self.model = getattr(module, "Configuration")
self.form = getattr(module, "Form")

View file

@ -1,118 +0,0 @@
from django import forms
from django.db import models
from pydantic import BaseModel, Field
from enum import Enum
from django_pydantic_field import SchemaField
# TODO(@fricklerhandwerk):
# Eventually should probably maintain a separate series of configuration schema versions for each service.
# I didn't start it here yet, mainly to keep it readable.
class PeerTube(BaseModel):
enable: bool = Field(
default=False,
description="Enable PeerTube",
)
class Pixelfed(BaseModel):
enable: bool = Field(
default=False,
description="Enable Pixelfed",
)
class Mastodon(BaseModel):
enable: bool = Field(
default=False,
description="Enable Mastodon",
)
class Configuration(BaseModel):
enable: bool = Field(
default=False,
description="Enable the configuration",
)
# XXX: hard-code available apex domains for now,
# they will be prefixed by the user name
class Domain(Enum):
EU = "fediversity.eu"
NET = "fediversity.net"
domain: Domain = Field(
default=Domain.EU,
description="DNS domain where to expose services"
)
peertube: PeerTube = Field(
default=PeerTube(),
description="Configuration for PeerTube",
)
pixelfed: Pixelfed = Field(
default=Pixelfed(),
description="Configuration for Pixelfed",
)
mastodon: Mastodon = Field(
default=Mastodon(),
description="Configuration for Mastodon",
)
# TODO@(fricklerhandwerk):
# generate this automatically from the Pydantic model so this code can go away:
# - add custom types, splicing `forms.Form` into `pydantic.BaseModel` and `forms.Field` into `pydantic.Field` so we can attach Django validators to Pydantic models
# - map primitive or well-known field types as captured by `pydantic.Field` to `forms.Field` constructors, and `BaseModel` to `Form` using `BaseModel.model_fields`
# - use a custom'{"name": "John Doe", "age": 30 }' widget that renders nested forms in e.g. a `<div>`
# - likely this also needs overriding `as_p()`, `as_li()`
# more inspiration: https://levelup.gitconnected.com/how-to-export-pydantic-models-as-django-forms-c1b59ddca580
# that work goes through the JSON Schema generated by Pydantic, which seems unnecessary to me, but it follows the same princple
# TODO(@fricklerhandwerk):
# eventually we probably want to validate each field separately,
# so we should also auto-generate views per field that will be called by htmx directives defined in the templates.
# those htmx parts can be generated by each form field using `attrs`
class Form(forms.Form):
enable = forms.BooleanField(required=False)
domain = forms.ChoiceField(
required=False,
choices=[(d.name, d.value) for d in Configuration.Domain],
)
peertube_enable = forms.BooleanField(
required=False,
label=Configuration.model_fields["peertube"].annotation.model_fields["enable"].description,
)
pixelfed_enable = forms.BooleanField(
required=False,
label=Configuration.model_fields["pixelfed"].annotation.model_fields["enable"].description,
)
mastodon_enable = forms.BooleanField(
required=False,
label=Configuration.model_fields["mastodon"].annotation.model_fields["enable"].description,
)
# HACK: take out nested dict fields manually
# TODO: make this generic
def __init__(self, *args, **kwargs):
initial = kwargs.pop('initial')
initial["pixelfed_enable"] = initial.pop('pixelfed')["enable"]
initial["peertube_enable"] = initial.pop('peertube')["enable"]
initial["mastodon_enable"] = initial.pop('mastodon')["enable"]
kwargs["initial"] = initial
super().__init__(*args, **kwargs)
def to_python(self):
return Configuration(
enable=self.cleaned_data['enable'],
domain=Configuration.Domain[self.cleaned_data['domain']],
peertube=PeerTube(enable=self.cleaned_data['peertube_enable']),
pixelfed=Pixelfed(enable=self.cleaned_data['pixelfed_enable']),
mastodon=Mastodon(enable=self.cleaned_data['mastodon_enable']),
)

View file

@ -0,0 +1,44 @@
from django import forms
from django.db import models
from pydantic import BaseModel, Field
from enum import Enum
from django_pydantic_field import SchemaField
class Configuration(BaseModel):
enable: bool = Field(
default=False,
description="Enable the configuration",
)
# XXX: hard-code available apex domains for now,
# they will be prefixed by the user name
class Domain(Enum):
EU = "fediversity.eu"
NET = "fediversity.net"
domain: Domain = Field(
default=Domain.EU,
description="DNS domain where to expose services"
)
# TODO@(fricklerhandwerk):
# generate this automatically from the Pydantic model so this code can go away:
# - add custom types, splicing `forms.Form` into `pydantic.BaseModel` and `forms.Field` into `pydantic.Field` so we can attach Django validators to Pydantic models
# - map primitive or well-known field types as captured by `pydantic.Field` to `forms.Field` constructors, and `BaseModel` to `Form` using `BaseModel.model_fields`
# - use a custom widget that renders nested forms in e.g. a `<div>`
# - likely this also needs overriding `as_p()`, `as_li()`
# more inspiration: https://levelup.gitconnected.com/how-to-export-pydantic-models-as-django-forms-c1b59ddca580
# that work goes through the JSON Schema generated by Pydantic, which seems unnecessary to me, but it follows the same princple
# TODO(@fricklerhandwerk):
# eventually we probably want to validate each field separately,
# so we should also auto-generate views per field that will be called by htmx directives defined in the templates.
# those htmx parts can be generated by each form field using `attrs`
class Form(forms.Form):
enable = forms.BooleanField(required=False)
domain = forms.ChoiceField(choices=[(d.name, d.value) for d in Configuration.Domain])
def to_python(self):
return Configuration(
enable=self.cleaned_data['enable'],
domain=Configuration.Domain[self.cleaned_data['domain']]
)

View file

@ -17,12 +17,10 @@ class Migration(migrations.Migration):
migrations.CreateModel( migrations.CreateModel(
name='Configuration', name='Configuration',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
primary_key=True, serialize=False, verbose_name='ID')), ('version', models.PositiveIntegerField(default=1, help_text='Configuration schema version')),
('value', models.JSONField( ('value', models.JSONField(help_text='Stored configuration value', null=True)),
help_text='Stored configuration value', null=True)), ('operator', models.ForeignKey(help_text='Operator who owns the configuration', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='configurations', to=settings.AUTH_USER_MODEL)),
('operator', models.ForeignKey(help_text='Operator who owns the configuration', null=True,
on_delete=django.db.models.deletion.SET_NULL, related_name='configurations', to=settings.AUTH_USER_MODEL)),
], ],
), ),
] ]

View file

@ -1,22 +0,0 @@
# Generated by Django 4.2.16 on 2025-03-09 21:25
from django.conf import settings
from django.db import migrations, models
from panel.configuration.forms import Configuration
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('panel', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='configuration',
name='value',
field=models.JSONField(default=Configuration(
).model_dump_json(), help_text='Stored configuration value'),
),
]

View file

@ -1,15 +1,10 @@
from django.db import models from django.db import models
from django.contrib.auth.models import User from django.contrib.auth.models import User
from panel.configuration import forms from panel.configuration import Version
from pydantic import BaseModel from pydantic import BaseModel
def get_default_config():
return forms.Configuration().model_dump_json()
class Configuration(models.Model): class Configuration(models.Model):
operator = models.ForeignKey( operator = models.ForeignKey(
User, User,
@ -19,11 +14,16 @@ class Configuration(models.Model):
help_text="Operator who owns the configuration", help_text="Operator who owns the configuration",
) )
version = models.PositiveIntegerField(
help_text="Configuration schema version",
default=Version.latest,
)
value = models.JSONField( value = models.JSONField(
help_text="Stored configuration value", help_text="Stored configuration value",
default=get_default_config, default=Version(Version.latest).model().model_dump_json,
) )
@property @property
def parsed_value(self) -> BaseModel: def parsed_value(self) -> BaseModel:
return forms.Configuration.model_validate_json(self.value) return Version(self.version).model.model_validate_json(self.value)

View file

@ -8,4 +8,6 @@
<button class="button" disabled>Deploy</button> <button class="button" disabled>Deploy</button>
<button class="button" type="submit" >Save</button> <button class="button" type="submit" >Save</button>
</form> </form>
<p><sub>Configuration schema version {{ version }}</sub></p>
{% endblock %} {% endblock %}

View file

@ -1,48 +0,0 @@
from django.test import TestCase
from django.contrib.auth.models import User
from django.urls import reverse
from panel.models import Configuration
class ConfigurationForm(TestCase):
def setUp(self):
self.username = 'testuser'
self.password = 'securepassword123'
self.user = User.objects.create_user(
username=self.username,
password=self.password
)
self.config_url = reverse('configuration_form')
def test_configuration_form_submission(self):
config = Configuration.objects.create(
operator=self.user,
)
self.client.login(username=self.username, password=self.password)
response = self.client.get(self.config_url)
self.assertEqual(response.status_code, 200)
context = response.context[0]
# configuration should be disabled by default
self.assertFalse(context['view'].get_object().parsed_value.enable)
# ...and be displayed as such
self.assertFalse(context['form'].initial["enable"])
form_data = context['form'].initial.copy()
form_data.update(
enable=True,
mastodon_enable=True,
)
response = self.client.post(self.config_url, data=form_data)
self.assertEqual(response.status_code, 302)
config.refresh_from_db()
self.assertTrue(config.parsed_value.enable)
self.assertTrue(config.parsed_value.mastodon.enable)
# this should not have changed
self.assertFalse(config.parsed_value.peertube.enable)

View file

@ -7,40 +7,37 @@ from django.views.generic import TemplateView, DetailView
from django.views.generic.edit import FormView from django.views.generic.edit import FormView
from panel import models from panel import models
from panel.configuration import forms from panel.configuration import Version
class Index(TemplateView): class Index(TemplateView):
template_name = 'index.html' template_name = 'index.html'
class AccountDetail(LoginRequiredMixin, DetailView): class AccountDetail(LoginRequiredMixin, DetailView):
model = User model = User
template_name = 'account_detail.html' template_name = 'account_detail.html'
def get_object(self): def get_object(self):
return self.request.user return self.request.user
class ServiceList(TemplateView): class ServiceList(TemplateView):
template_name = 'service_list.html' template_name = 'service_list.html'
class ConfigurationForm(LoginRequiredMixin, FormView): class ConfigurationForm(LoginRequiredMixin, FormView):
template_name = 'configuration_form.html' template_name = 'configuration_form.html'
success_url = reverse_lazy('configuration_form') success_url = reverse_lazy('configuration_form')
form_class = forms.Form
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context["version"] = self.get_object().version
return context return context
def get_form_class(self):
config = self.get_object()
return Version(config.version).form
def get_object(self): def get_object(self):
"""Get or create the configuration object for the current user""" """Get or create the configuration object for the current user"""
obj, created = models.Configuration.objects.get_or_create( obj, created = models.Configuration.objects.get_or_create(
operator=self.request.user, operator=self.request.user)
)
return obj return obj
# TODO(@fricklerhandwerk): # TODO(@fricklerhandwerk):

View file

@ -1,17 +1,16 @@
age-encryption.org/v1 age-encryption.org/v1
-> ssh-ed25519 Jpc21A JzLWMEH98I5/A8O55mKUMy5zo2kg3Qk8SfXnHvkjwT4 -> ssh-ed25519 Jpc21A 5HeEmeKYFHH3Wko4afd4SG2LcQfxvvCv81dnfUMkCzw
8f7zDHSp3AHoAQy0dVWMa1TurCBLnsHNtbNjaD++7ow B4CuJx6eGFU3bdz4y/V1M+XW5lfao99Lig1+rrq//9s
-> ssh-ed25519 BAs8QA eCD3saYXdv1bjAoQghmyVqHjMBu/o2lWgu7grk1vgRs -> ssh-ed25519 BAs8QA AJtZJIwSZTFXccbPVrXwGsqD3CT7n7PRTCpkE3OQiQs
//pOnkzqQTK3xmeCjruo46ju2X136KEt6DpsegMouFQ gVlxB8ABw83lqOFr2rBSh2Y212/yr1SheZSjW3d2bqw
-> ssh-ed25519 ofQnlg ePjq7GmM36qaGxcJ0qnW8FdKDjwlXtFqOBK8OgWY3Co -> ssh-ed25519 ofQnlg jZtGq/Xqr9pXMvGJqIBAeIlB1BxXyxR9HKljETvkqTg
gVmsDP9rMcQD/B6BpNhCn+avdgjhyyohNUXlatXpXo0 6RikmxoLwUOy7IOp4V9zTCC8UMWyjS5KyLxWXvuuxC0
-> ssh-ed25519 COspvA lrQB/NEmMUR2RWxfRzE2iTDkjMYsrIaiKn8thxZR+RA -> ssh-ed25519 COspvA 0tlSQU5/iVenCwuu9CPK5EQC39FYfCho8bmBOCyZGXw
MU23Z28v+cNk2VxpAYaYoFb53js2Zr9/KAM9uMe6+EA 1dOShM2CM1fJqqV1e1LgEfmxLq+e9tt71eWhvSUixsE
-> ssh-ed25519 2XrTgw z1ixx5dYCNbgw6wWV45b4wn69X/5/4MzesTomWa4WB4 -> ssh-ed25519 1MUEqQ O/1idQox9vPu19mdLyLoqLQWImBg1pC+PPMfItQDhz0
eNSlP6+nUW9rpsGyzqOEQ+7IVpGeU3UcZpyfB9XT2/4 eBkqIelOFQ+CUPSwPdI5SsQhBZ3JI9gD0b3SVUIK9ag
-> ssh-ed25519 1MUEqQ c6ps9RB6Dw9JtR0+4eB1NDx44uUes8YjLrY7RCpD0jg -> ssh-ed25519 Fa25Dw ZT+LO4ml0P6DeKi9u7QBP9e/UWkQRN2gDYJ6g1bbg2s
GwVRqR5t07ctbWhwH76T+SAe2Y6Vv1uY/AHkzd/gw/c CNULJZIGfU2bnjzQ28Ozkhs6HlM0964vDLBi5hRirPQ
-> ssh-ed25519 Fa25Dw jTqtV2RWsXBH4zgWAYr9tBGC/BbXKBvr3uyL8IgmI1o --- aHFDJRy4XUQCLbDF9Yeb8iXFvEutu3PS0oM5l3oW3r8
qBirnzIpi9hB61xwyS+5U6XBobAquEJrV3cleDtG8/4 Ž×hí¨¢;ÎB—
--- j/vJgDV+47UmKokdvztXntBIhCLEyUm2aYoGJ2WMKbU <EFBFBD>e)¿÷qÝo©ùIºÇ¹Á­FÁïY½-çX?kà
¢¹ŽÀi±ËQõf §¥ÐÅ·DN§àB"—ÍvsëB6QžorF‡<46>Å

View file

@ -1,17 +1,16 @@
age-encryption.org/v1 age-encryption.org/v1
-> ssh-ed25519 Jpc21A l6Xwv4JBlTeRTC7RgjxY9gDrCk96atMUH/62P+u55Qs -> ssh-ed25519 Jpc21A KDoPj7ywZDlw2tVIWBtuypVarOUdBOrLmuBdoWV9FGY
CrYsLZgDFAiR8up87lhGZqsbAEZtOXG+l5IzLh2uaqg gnEtIo5tfBAQeDztEK4i6+ZHx02WG/cqWjTJ/BNS3OM
-> ssh-ed25519 BAs8QA lgtmfoc4vKfRpI/XbIS258BMyIB4mTdquEx/Kxm5OTI -> ssh-ed25519 BAs8QA 8nmAEHH9zLjrWdjSxlW6c3xWP/kBZQ+P7SmJo5umC1o
3gQL8Rnqc7JfqsRmKYU3rD0cWMKdnIeVXbY3eFM07RU 9WLBny4Hk1llZB/mwQ9nzG80atCHkOtqMElJuCL7yt8
-> ssh-ed25519 ofQnlg 0vwuCrduMLjssA3CK3gfVPMSPYKO9cF7HH1JF/oJv18 -> ssh-ed25519 ofQnlg qEiN2Xn5P9UhzosnZYwsk/pv0Gwg+qDQIjEi7Y6R+ls
2KrZgQmpvw/tNDJrDArinnbEjopkkmuG8s7t6klBXcQ 4Ka0LaC8jWwzkMdwHVT4KW8ALGU7I2XoWChSkX/4ibA
-> ssh-ed25519 COspvA NT+/h2KsiZN2XbaWAlrTlDwyAPmHWrwgr6f0uhSbEGs -> ssh-ed25519 COspvA fcT8kt8mukX73QHcH+a2sbsmUc8U3U6fKM2n5U3+Szc
QpoAd+69VYrZwAC0LwDm1m/zfslVgzxpVFihQWDcqzE TC9JFZo6YWohCskzy0vCSSKVctnpLQUlyMfC+08MDWM
-> ssh-ed25519 2XrTgw QoJ/74FOqYFxHJYXJEkyzbGY0xptSjorNvnyUS1p6zk -> ssh-ed25519 1MUEqQ x+lo3jlXQc5hJUVmIk594j8RZkgHAfxoivWqte/Nd0o
0sJ2F6IFuTrRvXO5ND1QL4CZ2lr1BAU3iQffC6Uc3h4 rmC0Y2yL/d3iZH3LKtyY5ggY3bza4+F0xJBLXPsH2yU
-> ssh-ed25519 1MUEqQ xxgEUIhvWN/ZfRMGfu3fKQ+fWM5WSz8OexXPm6jaXDk -> ssh-ed25519 Fa25Dw McE5h1x23+JjZRI2IJ9FvElghshij/+cZxT82RorjkM
RXe0JMZ0sYMdQvrbi+zAs9F3d98ocRFnsSGUuUWccRk vMZSy1a4BE0XEaTTVBAkbBViCuYcPOsV/FOnEn1Sh8I
-> ssh-ed25519 Fa25Dw tw4sqQcO86Gh0FGUD+O3bJ+8OcaN5rm8R6qocXvDbRg --- cP4vWMTBTfLLh8Xm0D/4ArTE4xmq2w4/kj8VijWSGt4
7hiWa4qznHTV45kvC7ucj7j7FbPrqYK5OcCcByrcSxg þ½HE4jDËr&üo<C3BC>Ñ«·럱tER+áÂ8[¶ý
--- kvZDYq5n/OXu7xe2Kf5vGN0zosl9fgH4CAf3K0Tq3U4 ò ÐS„òż%jilÚ2
¤D!´Ój]2ï݃T+• ÃUó¬æcgÝoP”‰ ˆF.Þ¿_vbÕÓÔdª9

View file

@ -1,16 +1,14 @@
age-encryption.org/v1 age-encryption.org/v1
-> ssh-ed25519 Jpc21A 7QzPABl0IuQlZ9nsqfdbmWT9zb6iHGEOQiJ5sBpuLWU -> ssh-ed25519 Jpc21A v5ystrO0dW9a/n31TpDkXwgEYYPkd5NNNsrhA57rSQc
zusphKpt0yVxQj0zh4AGtYFORIhUoJHJBYfEAHb/VN0 tjGQl8KXiDfxe1dOuVPu8Yl7jOcfolisRdzgO4lLpYI
-> ssh-ed25519 BAs8QA PBfmSc0pag5zCqB/EKvirDXyleNi2sgMZ0xTAMiANFo -> ssh-ed25519 BAs8QA y0O4//knxtwtowt8DAhvlXZ8JaX7qK/bkbDcDc3NXBA
gugtjl9TSsGQuPypQiKZZqr7JfMKSdwVHGGKevyfF+k sTKHklMRut3C+L81Er3tZJrIhpRjeHihfXEbavqjf9I
-> ssh-ed25519 ofQnlg RQd9jPyn8nv1zCLbETa9/JB39fwX3X0X9gElcHWEfCA -> ssh-ed25519 ofQnlg bRrkoMi+Yb4MURd6e6rzzGKaf4ZH+zOLsFdfA+MLu0E
7ZEBE4qaZUtrXLc0caZ/tfJFYT9UDYkVvuaBc0SVBhc P4P3VLWsbs1gC0L4ULpvdtMQF0DAGLIqueZDGPKxfcg
-> ssh-ed25519 COspvA plM6PJykVR0NuSrOkRkA6ucOzUpijFaKbe99jaVrQSw -> ssh-ed25519 COspvA BgFgHb9u73/zrkn6mfd3tgS1cxEJ1YWcByZZbQMl9gs
7AkcdijjOSckx3GxXwXo1K34ReU3x0yWamlxdaA1FWA L5NTd1sQ6xbdvD3XnhlIJ33PANCl5owhsXm2wMDp0C0
-> ssh-ed25519 2XrTgw +2ZOwvZRUmnuHVV8poyMR6eIvPQoxWQRngKJdL2kVHc -> ssh-ed25519 1MUEqQ K/sLMv7neETy3a9MXgPoNnG9+hLb3HWRSL1u/y/5v0o
rzWJIuZUtuurvIdV/47N2CNu8x4T/vca8IeRqi+mk/I mF8ipNxm2J0P/Gact0ixmJ9IccG0+ftUwBiWlhjuAHg
-> ssh-ed25519 1MUEqQ 9U13b5LO4pSKhlvWEtkdrjTmVO5oGqcdT3Ime1AHjH4 --- HXsWuDGdzSMQii48ZDaIKOuKeD8JcJT6PKhYgJCMirs
8wC87WZZYMZaR2YlG3oEt79QcZMA86TrUbvTekAUkqw 9熏)]ナ<>ホ气Y。明<EFBDA1>?シ嗜7薙m懌I枴勧<E69EB4><E58BA7>GDX&」ケ<EFBDA3>tシ<74>2エ゚、V:<3A>.ォ<><E883A4>:0じ%<25>
--- X8Szdu5EyyPKR6xaL/8uKdHRc/D7wVGizk5k0XanreQ T
¯<EFBFBD>欿Æj{Ò“¦!q-ò˜G:£ÂjQò[_Ä„=ò<>#ÇŸd¾(i@/f3ºn°¦ý
r :Ú‰ ¾×txð¬|<<øX¡ÜºÊ¼™¨

View file

@ -1,17 +1,14 @@
age-encryption.org/v1 age-encryption.org/v1
-> ssh-ed25519 Jpc21A fBhVzGFs61K63QtA8RdOuuGfHFjMe/Dp0M6TXGLGWDU -> ssh-ed25519 BAs8QA 0TS+HcjtKeUAsLyzrsnCbj53GAq7pvXF12yQSxaxuFs
qppnUZ+LQCXhuMCFMYv2D2CkmEfjb7mpmJufIeVjjaM IjmmZV2Zh4cwj1+7r/fAKnuftpl46P5fO6SxtRMevIM
-> ssh-ed25519 BAs8QA PNicZCWLkbvM4ih77/F4z6FzHomL9EsJCuSCjbdRTwA -> ssh-ed25519 ofQnlg b4maqJdxyyi7b3arE9sxySwqeFjFlC6oT+PgQjIGj0Y
qIpTl/v7Xl08qBB//dFeW9qQiZg10YrYLnfyQrgDRfQ Gi5d4sJa0te/MsbkKYIOByIQ+TXBgu7hh2InES1pvXw
-> ssh-ed25519 ofQnlg 9/vSN3V25ysXBOvS4UJQEzm0734zqO0gXjhgzX63tTs -> ssh-ed25519 COspvA RiXEgUbPi3vep/8fM/RuRUYhCfBHO1XZt6Ov3WPnkV0
AH9Q1lWr+RgICfW3h+D2SgCTFr+azI0x3J3eFnaz/XA tTMLMb92ct5Zkqt42y8R3UI/zblAbsuEammavVcwGOU
-> ssh-ed25519 COspvA IB1nWOMaVZVcvEog6UaqCak2fcKxIUN2yXvvRSTDxGw -> ssh-ed25519 1MUEqQ XxxSvZrI9S6FI7CwYOSKDlfVBdLTur7/07Sm2HHLJwg
Ti7JuBgU6phlI+oXfDDvx42dRu95kTwesRUKu4QsXZ4 iW5PduiY/7N2kSJpBzmfnt8aNWKPfLZ43Kq6fyLeydw
-> ssh-ed25519 2XrTgw 7S9ZhJvUFMw9tDCc0HvkRsRqjvmn47GFGVg/jkxIy1I -> ssh-ed25519 ChtTUw zixDXeL07d4+pzFBSt/1f8yB+QxXOMv6sE6h469YzVs
cj27gqqihSZG3Jcab9h9FyNJ1J8FjlUiyVlDot+sbWQ rSC9S8v9gmtBw9FMKLg0h0muCmfMRuFD24JpTVw3ALc
-> ssh-ed25519 1MUEqQ l9mVTLD9rZXisBEz0sU2AdFNrJQ/+zuFTiIod5R/HCI --- vf2SwG1rpxjri3TGARwdMBc/mccj6RSTgf54YeQeR/8
2q3csSEvMW5vtzqGHYTtZ1nZ0J1vT23bjhuj9HTsdWk În9K±¼‰îÁä ÞÈ9÷y¼¿«dMÈdWn@õYç0ì.ü½ž1uÜoÚ«¨Á¾jý<6A>iý`
-> ssh-ed25519 kXy85Q BCrDvkPZLvx2Kvgapa3BT+AmpS6Fa5kpkgBnRVso2BE <EFBFBD>;1
ZBi+x/2ilJIzhzGipdZQJoGOjSqCuAttsqCDVFlYJ8Y
--- iWtseKyfUMBkQTUl9QzwXXLQcodEJeZt1Wuj5sR18yY
+2,エ ゚恝Jrメィ><7F>z?ホi<EFBE8E><69><EFBFBD>x<EFBFBD>0z<30>ヤ智メ劬呱^ゥネコ食・濤礫+1」ァ<EFBDA3>

View file

@ -1,18 +1,16 @@
age-encryption.org/v1 age-encryption.org/v1
-> ssh-ed25519 Jpc21A BfHJN3vILbsfY91kEjSQ+STrn6vQfn83Fx3cBCNshRQ -> ssh-ed25519 Jpc21A oYtKMwPB/M034KiJAmYsjg46+IC525Uul+VoFzHWwDE
0O8GJYfF8WFS4Xsgj5v1cly4JP1MgSN40OgRdW/i0rA v6YVVia1UVBwd4RoJsvozbCdy/3jYHMmFKyiDfaGR+g
-> ssh-ed25519 BAs8QA Ue0NLMpmZDSTGvwZ8lhzes7pcmit9F6uwzeT4XhiwC0 -> ssh-ed25519 BAs8QA 42kHHxx7k6jkazovkEIJ+uhOKr485RCQgvTXKY/+eWQ
jsvvuOW344i8GR4B139SX0LwTqzKQEgBvsy8oRppqBU 4YAGNgdsWk7dC32SMRyicifvrkz/wCor/98lCQ2Kl/I
-> ssh-ed25519 ofQnlg 9iSMQeTJn1OUqTF+M2sHpp69lblb8E6TVbgZs7vgD2U -> ssh-ed25519 ofQnlg nfPgeBQ9674MP0dKJLLkvcJSTSAOLs0cnRc/Pp2Z1E8
uMQI1gTTMvYW7ea9xBAln118JEeNvv3nqbq32zJoat8 Lm+GLkCAQ2B9wxVIaDjw18Ru3KQat79mJVF8nLFb7+A
-> ssh-ed25519 COspvA YxCyfe0li23JoI2q4XFVUx4vrWApLwSnJD31PHXuPBg -> ssh-ed25519 COspvA u07qlzPhj4e6qoilGLa9L7x2QMgjD6D40ROeUKiFGyk
8xuT9+W2mnTag9tm6F6LXzHkIh2Nou/8lgxd64OpvWk gpPwOMfZM/DMxnzg67mxZCyetSxJEmqpiKs1WqdLO7E
-> ssh-ed25519 2XrTgw jEzw0A9Wd1b1Zoryzp/W/QZ6bd99E7sySnr/W2xcnDs -> ssh-ed25519 1MUEqQ x8aGrPIHJymzUAXrQHeYrG3SeeEkc6bQcRcKCdHmJH0
IyMrojJ3AChS6lhj599caNM+02i16qtpc6cocln14b4 5C3tswXVRpEzj+Plkt+F4DLhhX/m1jJoKv9iwIWfxlk
-> ssh-ed25519 1MUEqQ haiI/5EkuTZ2YHxsqSVlqfM0VVR24DIDrMS3RmXwAhU -> ssh-ed25519 dgBsjw 4pSsRKJIJWaDAOPrdnNM5O/u0mh99ZAniUHjjiT59jM
qVIAvLp2qG4A3f3OKUqAKqH1eOicJz54nfblPSUKrSw EhEjGusUWQ//3r62GklyjuPSS3VXfzivDkYhrAh4tRE
-> ssh-ed25519 dgBsjw /vCnznu73U99onCWcM0aQlW0azscyUe4BB2kKeZvtHs --- fr5CmQ26pYk4qLbSXyVjEP20BYeALQ0sD5mPW0JsJpM
MPnvXR/WVsl/tJ1YPoc7nk2Ls2x9bbtJdNp3CQTuuWI æ&E9êùñIÙð¾T ÄŽ4êÆ¶§es4Fø·ÃÓ;v7ÇÐIu
--- OzkqKlw4xu3McMk20orQN0h+VPYfUUSDC+DsgRU1tSw |åQݸ°ˆVÅò®¿Æ\­,K£Íý¼ŠÐ·n­ùí`WÉ l2RÓªä¤TGN$
ü^@ÃmŽÚé`B˜³#{¡Ÿ‚ GUu´|¹Á œ¡ rÚïjb¥:Ô“d²ù] GØÎ©¦ú²-«ÂMÁ³ŸÜálÙ3mÌí)½š@¢—±e?¯ªêe¤üZ
Y}ÿ!fÐ

View file

@ -1,17 +1,16 @@
age-encryption.org/v1 age-encryption.org/v1
-> ssh-ed25519 Jpc21A RFzPu3fD28STex7ND5lE9bfCxQq/xeHEb7h7BFt9pVA -> ssh-ed25519 Jpc21A 6LN8aSeHyRfQv+eGVgtopTb/WpAF1zR1dwW0YVt0qxg
8K+ECDGs71V91jEVQjrRVQNbdTzBb6W9jkp0+K1trzw wSM6R7r9SjKmhjryhEZ1+JSetqfYNlRv/uZi2ME8UAM
-> ssh-ed25519 BAs8QA F2L9Eh9OItaPfAcR4qNnOQnvCyTeGdR5lSu0WqXiuU0 -> ssh-ed25519 BAs8QA x5ttKskqaHhf/StOzSwprTutcneG/S06nZ1w6+B/YkI
3jLlt4qAL3/VKyfbP7R0/7SUwwPpWf5YUWwzjDONEy8 UwlynUOoncgg1JU6qP/EzaPZrAP28fZq5q6vdXlHBss
-> ssh-ed25519 ofQnlg BLdBNuJExNlYED/XFU5zmYPtO1bxumuyPPgcs8qSLEY -> ssh-ed25519 ofQnlg Vrs8dDVekAMZNyFQvijC/Q3xqtJ3+elMYE89t8D4L14
wIi31st4WS9O2a7VJmYpE8PimgzLvwU6zWkvHCy84yQ AxfYaTk8JGDgef6tpB/SgeY5u0Jt3Mz7FWBVseAVQX4
-> ssh-ed25519 COspvA GmC8YZVv3ZwidaDKUkLhx0l8UOmRw5ZBiM8r4/Ub7xA -> ssh-ed25519 COspvA 3xCdPxnF003niI8XFzgO9Y7QD5bru6faLWeVUu6b8xI
wK994Zs1aLspqY84Ik77qdMaEsjs0ZFNuKQDOGXsnmM 5vjR7d9xkWepGhfLyJAhBrun87yC9NSSxmD9WOls6W0
-> ssh-ed25519 2XrTgw s62q7KOHZRqimCTwazX9LUvnpcYuzxwflumXe6NVF38 -> ssh-ed25519 1MUEqQ KkiwnNTIZngy8A4m3DE3Bd3XqTBC+gq/StwW/0iBMXg
WnOpHI9ejvRrZrQuasTEYyqP8ny3Hx9Q9bJzbK0pOI4 6ElKKheqr0wedd9SBdaGXgVoJJiyvwmVptB3LMf8Yzw
-> ssh-ed25519 1MUEqQ 9NLHR5OwOngiLRguTkf5KnUHrc80mambCw19dPPKPQ8 -> ssh-ed25519 dgBsjw tmd9R9c/Im+e4vDOZ5jBbfKnaALVOZQ9d1q2gp+HNEY
GDg9yRoRUaP1KOa/pOCFiLCCAxuFsuCIiDP/ERl8YLA vOQn5KuKYO0ObfxqCHcNnzaBSOjj4Sonca/cEyBNppw
-> ssh-ed25519 dgBsjw NulCMPtc2miJlHYpXMjQHUlc/HIaX4AqzxXZxt8cWkE --- zKHl+VjurB7fjxLskhi+XBaqFrbL6TNv6uL/fPMLffw
tCh3WD91A89258F8THeddXvab77tTIjNjGxYNDVoaBQ Šåp¼ ,bv=ÉÙ÷Âæh“ÂS<>¸_AáÛÚ]ü2
--- oIUm35maOqmHL0nifKpyEvLpHSKmthxIT5DDueCVZDc ]óŠ<15>ɨˆÇ!døáCk W5F>èÄjš
ÈaT~œú£Úôf4÷„Ô$\—ì;¿@Äý`¦<18>(<28>3{VWgM}#<1A>ï9aû6¢½ª^öúT6éÞ!Þ %¿

Binary file not shown.