Compare commits

...

7 commits

Author SHA1 Message Date
8f0bcc35f0 fix: run manage in service directory
for everything else it will error on CHDIR, even if $PWD has the right owner and permissions.
2025-03-18 09:52:14 +01:00
98c4490b4e update fedi201 host public key (#251)
this is a hack in order to be able to redeploy; that machine was still provisioned
manually

Reviewed-on: Fediversity/Fediversity#251
2025-03-18 09:51:27 +01:00
4d748c91d7
remove paragraph: Configuration schema version, following #247 2025-03-17 14:11:58 +01:00
08d109cc82 Remove versions 2025-03-13 15:31:09 +01:00
e41f9c572a add basic service configuration (#236)
- test the form interaction for a fixed schema version
- also add a database migration missed in the last commit

Closes #73

Reviewed-on: Fediversity/Fediversity#236
Co-authored-by: Valentin Gagarin <valentin.gagarin@tweag.io>
Co-committed-by: Valentin Gagarin <valentin.gagarin@tweag.io>
2025-03-13 15:28:54 +01:00
607b17a10a
rekey for public key lois 2025-03-12 14:34:57 +01:00
7afae84b6c Add pub key Lois 2025-03-12 12:01:50 +01:00
21 changed files with 319 additions and 189 deletions

View file

@ -33,6 +33,14 @@
"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;

1
keys/contributors/lois Normal file
View file

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

View file

@ -1 +1 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILhSlUo7L/TjoAILfLv/BDxlBT+rGudh9VoK50Uiu2lZ root@fedi201 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKBpnV6zzgdJN5pjw2oWryneE6kZ5rQ343Ut4ed12Cm9 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

@ -1,36 +0,0 @@
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

@ -0,0 +1,118 @@
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

@ -1,44 +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
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,10 +17,12 @@ class Migration(migrations.Migration):
migrations.CreateModel( migrations.CreateModel(
name='Configuration', name='Configuration',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True,
('version', models.PositiveIntegerField(default=1, help_text='Configuration schema version')), primary_key=True, serialize=False, verbose_name='ID')),
('value', models.JSONField(help_text='Stored configuration value', null=True)), ('value', models.JSONField(
('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)), 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)),
], ],
), ),
] ]

View file

@ -0,0 +1,22 @@
# 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,10 +1,15 @@
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 Version from panel.configuration import forms
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,
@ -14,16 +19,11 @@ 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=Version(Version.latest).model().model_dump_json, default=get_default_config,
) )
@property @property
def parsed_value(self) -> BaseModel: def parsed_value(self) -> BaseModel:
return Version(self.version).model.model_validate_json(self.value) return forms.Configuration.model_validate_json(self.value)

View file

@ -8,6 +8,4 @@
<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

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

View file

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

View file

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

View file

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

View file

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

View file

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

Binary file not shown.