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"
];
};
Lois = {
isNormalUser = true;
extraGroups = [ "wheel" ];
openssh.authorizedKeys.keys = [
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKVQ7yYXr4ZguGWYHZ7v2L3kPmYjaFo46PTgAEviW5D8 lois@mouse"
];
};
};
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 =
''
systemd-run --pty \
--same-dir \
--wait \
--collect \
--service-type=exec \
--unit "manage-${name}.service" \
--property "User=${name}" \
--property "Group=${name}" \
--property "WorkingDirectory=/var/lib/${name}" \
--property "Environment=DATABASE_URL=${database-url} USER_SETTINGS_FILE=${configFile}" \
''
+ 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(
name='Configuration',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('version', models.PositiveIntegerField(default=1, help_text='Configuration schema version')),
('value', models.JSONField(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)),
('id', models.BigAutoField(auto_created=True,
primary_key=True, serialize=False, verbose_name='ID')),
('value', models.JSONField(
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.contrib.auth.models import User
from panel.configuration import Version
from panel.configuration import forms
from pydantic import BaseModel
def get_default_config():
return forms.Configuration().model_dump_json()
class Configuration(models.Model):
operator = models.ForeignKey(
User,
@ -14,16 +19,11 @@ class Configuration(models.Model):
help_text="Operator who owns the configuration",
)
version = models.PositiveIntegerField(
help_text="Configuration schema version",
default=Version.latest,
)
value = models.JSONField(
help_text="Stored configuration value",
default=Version(Version.latest).model().model_dump_json,
default=get_default_config,
)
@property
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" type="submit" >Save</button>
</form>
<p><sub>Configuration schema version {{ version }}</sub></p>
{% 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 panel import models
from panel.configuration import Version
from panel.configuration import forms
class Index(TemplateView):
template_name = 'index.html'
class AccountDetail(LoginRequiredMixin, DetailView):
model = User
template_name = 'account_detail.html'
def get_object(self):
return self.request.user
class ServiceList(TemplateView):
template_name = 'service_list.html'
class ConfigurationForm(LoginRequiredMixin, FormView):
template_name = 'configuration_form.html'
success_url = reverse_lazy('configuration_form')
form_class = forms.Form
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["version"] = self.get_object().version
return context
def get_form_class(self):
config = self.get_object()
return Version(config.version).form
def get_object(self):
"""Get or create the configuration object for the current user"""
obj, created = models.Configuration.objects.get_or_create(
operator=self.request.user)
operator=self.request.user,
)
return obj
# TODO(@fricklerhandwerk):

View file

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

View file

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

View file

@ -1,14 +1,16 @@
age-encryption.org/v1
-> ssh-ed25519 Jpc21A v5ystrO0dW9a/n31TpDkXwgEYYPkd5NNNsrhA57rSQc
tjGQl8KXiDfxe1dOuVPu8Yl7jOcfolisRdzgO4lLpYI
-> ssh-ed25519 BAs8QA y0O4//knxtwtowt8DAhvlXZ8JaX7qK/bkbDcDc3NXBA
sTKHklMRut3C+L81Er3tZJrIhpRjeHihfXEbavqjf9I
-> ssh-ed25519 ofQnlg bRrkoMi+Yb4MURd6e6rzzGKaf4ZH+zOLsFdfA+MLu0E
P4P3VLWsbs1gC0L4ULpvdtMQF0DAGLIqueZDGPKxfcg
-> ssh-ed25519 COspvA BgFgHb9u73/zrkn6mfd3tgS1cxEJ1YWcByZZbQMl9gs
L5NTd1sQ6xbdvD3XnhlIJ33PANCl5owhsXm2wMDp0C0
-> ssh-ed25519 1MUEqQ K/sLMv7neETy3a9MXgPoNnG9+hLb3HWRSL1u/y/5v0o
mF8ipNxm2J0P/Gact0ixmJ9IccG0+ftUwBiWlhjuAHg
--- HXsWuDGdzSMQii48ZDaIKOuKeD8JcJT6PKhYgJCMirs
9熏)]ナ<>ホ气Y。明<EFBDA1>?シ嗜7薙m懌I枴勧<E69EB4><E58BA7>GDX&」ケ<EFBDA3>tシ<74>2エ゚、V:<3A>.ォ<><E883A4>:0じ%<25>
T
-> ssh-ed25519 Jpc21A 7QzPABl0IuQlZ9nsqfdbmWT9zb6iHGEOQiJ5sBpuLWU
zusphKpt0yVxQj0zh4AGtYFORIhUoJHJBYfEAHb/VN0
-> ssh-ed25519 BAs8QA PBfmSc0pag5zCqB/EKvirDXyleNi2sgMZ0xTAMiANFo
gugtjl9TSsGQuPypQiKZZqr7JfMKSdwVHGGKevyfF+k
-> ssh-ed25519 ofQnlg RQd9jPyn8nv1zCLbETa9/JB39fwX3X0X9gElcHWEfCA
7ZEBE4qaZUtrXLc0caZ/tfJFYT9UDYkVvuaBc0SVBhc
-> ssh-ed25519 COspvA plM6PJykVR0NuSrOkRkA6ucOzUpijFaKbe99jaVrQSw
7AkcdijjOSckx3GxXwXo1K34ReU3x0yWamlxdaA1FWA
-> ssh-ed25519 2XrTgw +2ZOwvZRUmnuHVV8poyMR6eIvPQoxWQRngKJdL2kVHc
rzWJIuZUtuurvIdV/47N2CNu8x4T/vca8IeRqi+mk/I
-> ssh-ed25519 1MUEqQ 9U13b5LO4pSKhlvWEtkdrjTmVO5oGqcdT3Ime1AHjH4
8wC87WZZYMZaR2YlG3oEt79QcZMA86TrUbvTekAUkqw
--- 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
-> ssh-ed25519 BAs8QA 0TS+HcjtKeUAsLyzrsnCbj53GAq7pvXF12yQSxaxuFs
IjmmZV2Zh4cwj1+7r/fAKnuftpl46P5fO6SxtRMevIM
-> ssh-ed25519 ofQnlg b4maqJdxyyi7b3arE9sxySwqeFjFlC6oT+PgQjIGj0Y
Gi5d4sJa0te/MsbkKYIOByIQ+TXBgu7hh2InES1pvXw
-> ssh-ed25519 COspvA RiXEgUbPi3vep/8fM/RuRUYhCfBHO1XZt6Ov3WPnkV0
tTMLMb92ct5Zkqt42y8R3UI/zblAbsuEammavVcwGOU
-> ssh-ed25519 1MUEqQ XxxSvZrI9S6FI7CwYOSKDlfVBdLTur7/07Sm2HHLJwg
iW5PduiY/7N2kSJpBzmfnt8aNWKPfLZ43Kq6fyLeydw
-> ssh-ed25519 ChtTUw zixDXeL07d4+pzFBSt/1f8yB+QxXOMv6sE6h469YzVs
rSC9S8v9gmtBw9FMKLg0h0muCmfMRuFD24JpTVw3ALc
--- vf2SwG1rpxjri3TGARwdMBc/mccj6RSTgf54YeQeR/8
În9K±¼‰îÁä ÞÈ9÷y¼¿«dMÈdWn@õYç0ì.ü½ž1uÜoÚ«¨Á¾jý<6A>iý`
<EFBFBD>;1
-> ssh-ed25519 Jpc21A fBhVzGFs61K63QtA8RdOuuGfHFjMe/Dp0M6TXGLGWDU
qppnUZ+LQCXhuMCFMYv2D2CkmEfjb7mpmJufIeVjjaM
-> ssh-ed25519 BAs8QA PNicZCWLkbvM4ih77/F4z6FzHomL9EsJCuSCjbdRTwA
qIpTl/v7Xl08qBB//dFeW9qQiZg10YrYLnfyQrgDRfQ
-> ssh-ed25519 ofQnlg 9/vSN3V25ysXBOvS4UJQEzm0734zqO0gXjhgzX63tTs
AH9Q1lWr+RgICfW3h+D2SgCTFr+azI0x3J3eFnaz/XA
-> ssh-ed25519 COspvA IB1nWOMaVZVcvEog6UaqCak2fcKxIUN2yXvvRSTDxGw
Ti7JuBgU6phlI+oXfDDvx42dRu95kTwesRUKu4QsXZ4
-> ssh-ed25519 2XrTgw 7S9ZhJvUFMw9tDCc0HvkRsRqjvmn47GFGVg/jkxIy1I
cj27gqqihSZG3Jcab9h9FyNJ1J8FjlUiyVlDot+sbWQ
-> ssh-ed25519 1MUEqQ l9mVTLD9rZXisBEz0sU2AdFNrJQ/+zuFTiIod5R/HCI
2q3csSEvMW5vtzqGHYTtZ1nZ0J1vT23bjhuj9HTsdWk
-> 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
-> ssh-ed25519 Jpc21A oYtKMwPB/M034KiJAmYsjg46+IC525Uul+VoFzHWwDE
v6YVVia1UVBwd4RoJsvozbCdy/3jYHMmFKyiDfaGR+g
-> ssh-ed25519 BAs8QA 42kHHxx7k6jkazovkEIJ+uhOKr485RCQgvTXKY/+eWQ
4YAGNgdsWk7dC32SMRyicifvrkz/wCor/98lCQ2Kl/I
-> ssh-ed25519 ofQnlg nfPgeBQ9674MP0dKJLLkvcJSTSAOLs0cnRc/Pp2Z1E8
Lm+GLkCAQ2B9wxVIaDjw18Ru3KQat79mJVF8nLFb7+A
-> ssh-ed25519 COspvA u07qlzPhj4e6qoilGLa9L7x2QMgjD6D40ROeUKiFGyk
gpPwOMfZM/DMxnzg67mxZCyetSxJEmqpiKs1WqdLO7E
-> ssh-ed25519 1MUEqQ x8aGrPIHJymzUAXrQHeYrG3SeeEkc6bQcRcKCdHmJH0
5C3tswXVRpEzj+Plkt+F4DLhhX/m1jJoKv9iwIWfxlk
-> ssh-ed25519 dgBsjw 4pSsRKJIJWaDAOPrdnNM5O/u0mh99ZAniUHjjiT59jM
EhEjGusUWQ//3r62GklyjuPSS3VXfzivDkYhrAh4tRE
--- fr5CmQ26pYk4qLbSXyVjEP20BYeALQ0sD5mPW0JsJpM
æ&E9êùñIÙð¾T ÄŽ4êÆ¶§es4Fø·ÃÓ;v7ÇÐIu
|åQݸ°ˆVÅò®¿Æ\­,K£Íý¼ŠÐ·n­ùí`WÉ l2RÓªä¤TGN$
-> ssh-ed25519 Jpc21A BfHJN3vILbsfY91kEjSQ+STrn6vQfn83Fx3cBCNshRQ
0O8GJYfF8WFS4Xsgj5v1cly4JP1MgSN40OgRdW/i0rA
-> ssh-ed25519 BAs8QA Ue0NLMpmZDSTGvwZ8lhzes7pcmit9F6uwzeT4XhiwC0
jsvvuOW344i8GR4B139SX0LwTqzKQEgBvsy8oRppqBU
-> ssh-ed25519 ofQnlg 9iSMQeTJn1OUqTF+M2sHpp69lblb8E6TVbgZs7vgD2U
uMQI1gTTMvYW7ea9xBAln118JEeNvv3nqbq32zJoat8
-> ssh-ed25519 COspvA YxCyfe0li23JoI2q4XFVUx4vrWApLwSnJD31PHXuPBg
8xuT9+W2mnTag9tm6F6LXzHkIh2Nou/8lgxd64OpvWk
-> ssh-ed25519 2XrTgw jEzw0A9Wd1b1Zoryzp/W/QZ6bd99E7sySnr/W2xcnDs
IyMrojJ3AChS6lhj599caNM+02i16qtpc6cocln14b4
-> ssh-ed25519 1MUEqQ haiI/5EkuTZ2YHxsqSVlqfM0VVR24DIDrMS3RmXwAhU
qVIAvLp2qG4A3f3OKUqAKqH1eOicJz54nfblPSUKrSw
-> ssh-ed25519 dgBsjw /vCnznu73U99onCWcM0aQlW0azscyUe4BB2kKeZvtHs
MPnvXR/WVsl/tJ1YPoc7nk2Ls2x9bbtJdNp3CQTuuWI
--- 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
-> ssh-ed25519 Jpc21A 6LN8aSeHyRfQv+eGVgtopTb/WpAF1zR1dwW0YVt0qxg
wSM6R7r9SjKmhjryhEZ1+JSetqfYNlRv/uZi2ME8UAM
-> ssh-ed25519 BAs8QA x5ttKskqaHhf/StOzSwprTutcneG/S06nZ1w6+B/YkI
UwlynUOoncgg1JU6qP/EzaPZrAP28fZq5q6vdXlHBss
-> ssh-ed25519 ofQnlg Vrs8dDVekAMZNyFQvijC/Q3xqtJ3+elMYE89t8D4L14
AxfYaTk8JGDgef6tpB/SgeY5u0Jt3Mz7FWBVseAVQX4
-> ssh-ed25519 COspvA 3xCdPxnF003niI8XFzgO9Y7QD5bru6faLWeVUu6b8xI
5vjR7d9xkWepGhfLyJAhBrun87yC9NSSxmD9WOls6W0
-> ssh-ed25519 1MUEqQ KkiwnNTIZngy8A4m3DE3Bd3XqTBC+gq/StwW/0iBMXg
6ElKKheqr0wedd9SBdaGXgVoJJiyvwmVptB3LMf8Yzw
-> ssh-ed25519 dgBsjw tmd9R9c/Im+e4vDOZ5jBbfKnaALVOZQ9d1q2gp+HNEY
vOQn5KuKYO0ObfxqCHcNnzaBSOjj4Sonca/cEyBNppw
--- zKHl+VjurB7fjxLskhi+XBaqFrbL6TNv6uL/fPMLffw
Šåp¼ ,bv=ÉÙ÷Âæh“ÂS<>¸_AáÛÚ]ü2
]óŠ<15>ɨˆÇ!døáCk W5F>èÄjš
-> ssh-ed25519 Jpc21A RFzPu3fD28STex7ND5lE9bfCxQq/xeHEb7h7BFt9pVA
8K+ECDGs71V91jEVQjrRVQNbdTzBb6W9jkp0+K1trzw
-> ssh-ed25519 BAs8QA F2L9Eh9OItaPfAcR4qNnOQnvCyTeGdR5lSu0WqXiuU0
3jLlt4qAL3/VKyfbP7R0/7SUwwPpWf5YUWwzjDONEy8
-> ssh-ed25519 ofQnlg BLdBNuJExNlYED/XFU5zmYPtO1bxumuyPPgcs8qSLEY
wIi31st4WS9O2a7VJmYpE8PimgzLvwU6zWkvHCy84yQ
-> ssh-ed25519 COspvA GmC8YZVv3ZwidaDKUkLhx0l8UOmRw5ZBiM8r4/Ub7xA
wK994Zs1aLspqY84Ik77qdMaEsjs0ZFNuKQDOGXsnmM
-> ssh-ed25519 2XrTgw s62q7KOHZRqimCTwazX9LUvnpcYuzxwflumXe6NVF38
WnOpHI9ejvRrZrQuasTEYyqP8ny3Hx9Q9bJzbK0pOI4
-> ssh-ed25519 1MUEqQ 9NLHR5OwOngiLRguTkf5KnUHrc80mambCw19dPPKPQ8
GDg9yRoRUaP1KOa/pOCFiLCCAxuFsuCIiDP/ERl8YLA
-> ssh-ed25519 dgBsjw NulCMPtc2miJlHYpXMjQHUlc/HIaX4AqzxXZxt8cWkE
tCh3WD91A89258F8THeddXvab77tTIjNjGxYNDVoaBQ
--- oIUm35maOqmHL0nifKpyEvLpHSKmthxIT5DDueCVZDc
ÈaT~œú£Úôf4÷„Ô$\—ì;¿@Äý`¦<18>(<28>3{VWgM}#<1A>ï9aû6¢½ª^öúT6éÞ!Þ %¿

Binary file not shown.