+3
.gitignore
+3
.gitignore
+182
-35
client/app.js
+182
-35
client/app.js
···
1
1
/**
2
-
* Client-side application for submitting autonomy declarations
2
+
* Autonomy Declaration Form - Client-side validation and submission
3
3
*/
4
+
import { BskyAgent } from '@atproto/api';
5
+
6
+
// Get form elements
7
+
const form = document.getElementById('declaration-form');
8
+
const submitBtn = document.getElementById('submit-btn');
9
+
const resultDiv = document.getElementById('result');
10
+
11
+
// Validation functions
12
+
const validators = {
13
+
handle: (value) => {
14
+
if (!value) return 'Handle is required';
15
+
// Basic check for handle format (username.bsky.social) or DID
16
+
if (!value.includes('.') && !value.startsWith('did:')) {
17
+
return 'Handle must be a valid Bluesky handle (e.g., username.bsky.social) or DID';
18
+
}
19
+
return null;
20
+
},
21
+
22
+
password: (value) => {
23
+
if (!value) return 'App password is required';
24
+
if (value.length !== 19) {
25
+
return 'App password must be exactly 19 characters';
26
+
}
27
+
// Check for exactly 3 dashes in positions 4, 9, 14 (xxxx-xxxx-xxxx-xxxx)
28
+
const dashCount = (value.match(/-/g) || []).length;
29
+
if (dashCount !== 3) {
30
+
return 'App password must contain exactly 3 dashes';
31
+
}
32
+
// Validate format: xxxx-xxxx-xxxx-xxxx
33
+
if (!/^[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{7}$/.test(value)) {
34
+
return 'App password must match format: xxxx-xxxx-xxxx-xxxx';
35
+
}
36
+
return null;
37
+
},
38
+
39
+
description: (value) => {
40
+
if (value && value.length > 300) {
41
+
return 'Description must be 300 characters or less';
42
+
}
43
+
return null;
44
+
},
45
+
46
+
responsibleName: (value) => {
47
+
if (value && value.length > 100) {
48
+
return 'Name must be 100 characters or less';
49
+
}
50
+
return null;
51
+
},
52
+
53
+
responsibleContact: (value) => {
54
+
if (value && value.length > 300) {
55
+
return 'Contact must be 300 characters or less';
56
+
}
57
+
return null;
58
+
},
59
+
60
+
responsibleDid: (value) => {
61
+
if (value && !value.startsWith('did:')) {
62
+
return 'DID must start with "did:" (e.g., did:plc:... or did:web:...)';
63
+
}
64
+
return null;
65
+
},
66
+
67
+
disclosureUrl: (value) => {
68
+
if (value) {
69
+
try {
70
+
new URL(value);
71
+
} catch {
72
+
return 'Must be a valid URL (e.g., https://example.com)';
73
+
}
74
+
}
75
+
return null;
76
+
},
77
+
78
+
externalServices: (value) => {
79
+
if (value) {
80
+
const services = value.split('\n').map(s => s.trim()).filter(s => s.length > 0);
81
+
if (services.length > 20) {
82
+
return 'Maximum 20 external services allowed';
83
+
}
84
+
for (const service of services) {
85
+
if (service.length > 200) {
86
+
return `Service name too long (max 200 chars): ${service.substring(0, 50)}...`;
87
+
}
88
+
}
89
+
}
90
+
return null;
91
+
}
92
+
};
93
+
94
+
// Display error for a field
95
+
function showError(fieldId, message) {
96
+
const errorElement = document.getElementById(`${fieldId}-error`);
97
+
if (errorElement) {
98
+
errorElement.textContent = message;
99
+
errorElement.hidden = false;
100
+
}
101
+
}
102
+
103
+
// Clear error for a field
104
+
function clearError(fieldId) {
105
+
const errorElement = document.getElementById(`${fieldId}-error`);
106
+
if (errorElement) {
107
+
errorElement.textContent = '';
108
+
errorElement.hidden = true;
109
+
}
110
+
}
4
111
5
-
import { BskyAgent } from "https://esm.sh/@atproto/api@0.13.0";
112
+
// Clear all errors
113
+
function clearAllErrors() {
114
+
const errorFields = ['handle', 'password', 'description', 'responsibleName',
115
+
'responsibleContact', 'responsibleDid', 'disclosureUrl',
116
+
'externalServices'];
117
+
errorFields.forEach(clearError);
118
+
}
6
119
7
-
const form = document.getElementById("declaration-form");
8
-
const submitBtn = document.getElementById("submit-btn");
9
-
const resultDiv = document.getElementById("result");
120
+
// Validate all fields
121
+
function validateForm(formData) {
122
+
clearAllErrors();
123
+
let isValid = true;
10
124
11
-
form.addEventListener("submit", async (e) => {
12
-
e.preventDefault();
125
+
// Validate required and optional fields
126
+
for (const [fieldId, validator] of Object.entries(validators)) {
127
+
const value = formData.get(fieldId) || '';
128
+
const error = validator(value);
129
+
if (error) {
130
+
showError(fieldId, error);
131
+
isValid = false;
132
+
}
133
+
}
13
134
14
-
// Get form values
15
-
const handle = document.getElementById("handle").value.trim();
16
-
const password = document.getElementById("password").value;
135
+
return isValid;
136
+
}
17
137
18
-
const automationLevel = document.getElementById("automationLevel").value;
19
-
const usesGenerativeAI = document.getElementById("usesGenerativeAI").checked;
20
-
const description = document.getElementById("description").value.trim();
138
+
// Form submission handler
139
+
form.addEventListener('submit', async (e) => {
140
+
e.preventDefault();
21
141
22
-
const responsibleType = document.getElementById("responsibleType").value;
23
-
const responsibleName = document.getElementById("responsibleName").value.trim();
24
-
const responsibleContact = document.getElementById("responsibleContact").value.trim();
25
-
const responsibleDid = document.getElementById("responsibleDid").value.trim();
142
+
// Get form data
143
+
const formData = new FormData(form);
26
144
27
-
const disclosureUrl = document.getElementById("disclosureUrl").value.trim();
28
-
const externalServicesText = document.getElementById("externalServices").value.trim();
145
+
// Validate form
146
+
if (!validateForm(formData)) {
147
+
resultDiv.textContent = 'Please fix the errors above before submitting.';
148
+
resultDiv.hidden = false;
149
+
return;
150
+
}
29
151
30
152
// Disable form during submission
31
153
submitBtn.disabled = true;
32
-
submitBtn.textContent = "Submitting...";
33
-
resultDiv.className = "result hidden";
154
+
submitBtn.textContent = 'Submitting...';
155
+
resultDiv.hidden = true;
34
156
35
157
try {
158
+
// Get form values
159
+
const handle = formData.get('handle').trim();
160
+
const password = formData.get('password');
161
+
const automationLevel = formData.get('automationLevel');
162
+
const usesGenerativeAI = formData.get('usesGenerativeAI') === 'on';
163
+
const description = formData.get('description').trim();
164
+
const responsibleType = formData.get('responsibleType');
165
+
const responsibleName = formData.get('responsibleName').trim();
166
+
const responsibleContact = formData.get('responsibleContact').trim();
167
+
const responsibleDid = formData.get('responsibleDid').trim();
168
+
const disclosureUrl = formData.get('disclosureUrl').trim();
169
+
const externalServicesText = formData.get('externalServices').trim();
170
+
36
171
// Create agent and login
37
172
const agent = new BskyAgent({
38
-
service: "https://bsky.social"
173
+
service: 'https://bsky.social'
39
174
});
40
175
41
176
await agent.login({
···
45
180
46
181
// Build the record
47
182
const record = {
48
-
$type: "studio.voyager.account.autonomy",
183
+
$type: 'studio.voyager.account.autonomy',
49
184
createdAt: new Date().toISOString()
50
185
};
51
186
52
-
// Add optional fields
187
+
// Add optional fields only if they have values
53
188
if (automationLevel) {
54
189
record.automationLevel = automationLevel;
55
190
}
56
191
57
192
if (usesGenerativeAI) {
58
-
record.usesGenerativeAI = usesGenerativeAI;
193
+
record.usesGenerativeAI = true;
59
194
}
60
195
61
196
if (description) {
···
89
224
90
225
// Submit to PDS
91
226
const response = await agent.api.com.atproto.repo.createRecord({
92
-
repo: agent.session?.did || "",
93
-
collection: "studio.voyager.account.autonomy",
227
+
repo: agent.session?.did || '',
228
+
collection: 'studio.voyager.account.autonomy',
94
229
record: record
95
230
});
96
231
97
232
// Show success
98
-
resultDiv.className = "result success";
99
-
resultDiv.textContent = `โ
Declaration submitted successfully! URI: ${response.data.uri}`;
233
+
resultDiv.textContent = `Declaration submitted successfully! Record URI: ${response.data.uri}`;
234
+
resultDiv.hidden = false;
100
235
101
236
// Clear form
102
237
form.reset();
103
238
104
239
} catch (error) {
105
-
// Show error
106
-
resultDiv.className = "result error";
107
-
resultDiv.textContent = `โ Error: ${error.message}`;
108
-
console.error("Submission error:", error);
240
+
// Show helpful error messages
241
+
let errorMessage = 'Error: ';
242
+
243
+
if (error.message.includes('Invalid identifier or password')) {
244
+
errorMessage += 'Invalid handle or app password. Please check your credentials.';
245
+
} else if (error.message.includes('Network')) {
246
+
errorMessage += 'Network error. Please check your connection and try again.';
247
+
} else if (error.message.includes('AuthenticationRequired')) {
248
+
errorMessage += 'Authentication failed. Please verify your app password.';
249
+
} else {
250
+
errorMessage += error.message;
251
+
}
252
+
253
+
resultDiv.textContent = errorMessage;
254
+
resultDiv.hidden = false;
255
+
console.error('Submission error:', error);
256
+
109
257
} finally {
110
258
// Re-enable form
111
259
submitBtn.disabled = false;
112
-
submitBtn.textContent = "Submit Declaration";
113
-
resultDiv.classList.remove("hidden");
260
+
submitBtn.textContent = 'Submit Declaration';
114
261
}
115
262
});
+986
-98
client/index.html
+986
-98
client/index.html
···
1
-
<!DOCTYPE html>
1
+
<!doctype html>
2
2
<html lang="en">
3
-
<head>
4
-
<meta charset="UTF-8">
5
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6
-
<title>Autonomy Declaration - ATProtocol</title>
7
-
<link rel="stylesheet" href="styles.css">
8
-
</head>
9
-
<body>
10
-
<div class="container">
11
-
<header>
3
+
<head>
4
+
<meta charset="UTF-8" />
5
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+
<title>Autonomy Declaration - ATProtocol</title>
7
+
<script type="importmap">
8
+
{
9
+
"imports": {
10
+
"@atproto/api": "https://esm.sh/@atproto/api@0.13.11"
11
+
}
12
+
}
13
+
</script>
14
+
<style>
15
+
/*stylebase v0.11.0*/
16
+
@layer webfont, stylebase-token, token, stylebase-default, default, stylebase-utility, utility, stylebase-layout, layout;
17
+
@layer stylebase-default {
18
+
article {
19
+
font-size: var(--fs-2);
20
+
}
21
+
article :where(h1, h2, h3) {
22
+
font-family: (--ff-heading);
23
+
}
24
+
article :where(h4, h5, h6) {
25
+
font-family: (--ff-sans);
26
+
}
27
+
article :where(h1, h2, h3, h4, h5, h6) {
28
+
max-inline-size: 40ch;
29
+
text-wrap: balance;
30
+
}
31
+
article hgroup p {
32
+
font-family: var(--ff-heading);
33
+
font-size: var(--fs-2);
34
+
}
35
+
article h1 {
36
+
font-size: var(--fs-7);
37
+
}
38
+
article hr {
39
+
margin: 0 10ch;
40
+
max-inline-size: var(--hr-width, 40ch);
41
+
}
42
+
article p {
43
+
font-family: var(--ff-content);
44
+
font-size: inherit;
45
+
max-inline-size: 68ch;
46
+
text-wrap: pretty;
47
+
}
48
+
}
49
+
@layer stylebase-token {
50
+
:root {
51
+
--hue-red-50: oklch(98.83% 0.005 20);
52
+
--hue-red-100: oklch(96.68% 0.02 20);
53
+
--hue-red-200: oklch(92.19% 0.04 20);
54
+
--hue-red-300: oklch(86.13% 0.08 20);
55
+
--hue-red-400: oklch(80.08% 0.11 20);
56
+
--hue-red-500: oklch(74.22% 0.15 20);
57
+
--hue-red-600: oklch(62.7% 0.14 20);
58
+
--hue-red-700: oklch(53.52% 0.12 20);
59
+
--hue-red-800: oklch(41.99% 0.09 20);
60
+
--hue-red-900: oklch(30.66% 0.07 20);
61
+
--hue-red-950: oklch(19.34% 0.04 20);
62
+
--hue-orange-50: oklch(98.83% 0.005 43.33);
63
+
--hue-orange-100: oklch(96.68% 0.02 43.33);
64
+
--hue-orange-200: oklch(92.19% 0.04 43.33);
65
+
--hue-orange-300: oklch(85.94% 0.08 43.33);
66
+
--hue-orange-400: oklch(79.88% 0.11 43.33);
67
+
--hue-orange-500: oklch(73.83% 0.15 43.33);
68
+
--hue-orange-600: oklch(62.3% 0.14 43.33);
69
+
--hue-orange-700: oklch(53.13% 0.12 43.33);
70
+
--hue-orange-800: oklch(41.8% 0.09 43.33);
71
+
--hue-orange-900: oklch(30.47% 0.07 43.33);
72
+
--hue-orange-950: oklch(19.14% 0.04 43.33);
73
+
--hue-amber-50: oklch(98.83% 0.005 66.67);
74
+
--hue-amber-100: oklch(96.68% 0.02 66.67);
75
+
--hue-amber-200: oklch(91.99% 0.04 66.67);
76
+
--hue-amber-300: oklch(85.74% 0.08 66.67);
77
+
--hue-amber-400: oklch(79.49% 0.11 66.67);
78
+
--hue-amber-500: oklch(73.24% 0.15 66.67);
79
+
--hue-amber-600: oklch(61.91% 0.14 66.67);
80
+
--hue-amber-700: oklch(52.73% 0.12 66.67);
81
+
--hue-amber-800: oklch(41.41% 0.09 66.67);
82
+
--hue-amber-900: oklch(30.27% 0.07 66.67);
83
+
--hue-amber-950: oklch(19.14% 0.04 66.67);
84
+
--hue-yellow-50: oklch(98.83% 0.005 90);
85
+
--hue-yellow-100: oklch(96.48% 0.02 90);
86
+
--hue-yellow-200: oklch(91.8% 0.04 90);
87
+
--hue-yellow-300: oklch(85.35% 0.08 90);
88
+
--hue-yellow-400: oklch(78.91% 0.11 90);
89
+
--hue-yellow-500: oklch(72.66% 0.15 90);
90
+
--hue-yellow-600: oklch(61.13% 0.14 90);
91
+
--hue-yellow-700: oklch(52.34% 0.12 90);
92
+
--hue-yellow-800: oklch(41.02% 0.09 90);
93
+
--hue-yellow-900: oklch(30.08% 0.07 90);
94
+
--hue-yellow-950: oklch(19.14% 0.04 90);
95
+
--hue-lime-50: oklch(98.83% 0.005 106.67);
96
+
--hue-lime-100: oklch(96.48% 0.02 106.67);
97
+
--hue-lime-200: oklch(91.6% 0.04 106.67);
98
+
--hue-lime-300: oklch(84.96% 0.08 106.67);
99
+
--hue-lime-400: oklch(78.52% 0.11 106.67);
100
+
--hue-lime-500: oklch(72.07% 0.15 106.67);
101
+
--hue-lime-600: oklch(60.74% 0.14 106.67);
102
+
--hue-lime-700: oklch(51.95% 0.12 106.67);
103
+
--hue-lime-800: oklch(40.82% 0.09 106.67);
104
+
--hue-lime-900: oklch(29.88% 0.07 106.67);
105
+
--hue-lime-950: oklch(18.95% 0.04 106.67);
106
+
--hue-green-50: oklch(98.83% 0.005 123.33);
107
+
--hue-green-100: oklch(96.29% 0.02 123.33);
108
+
--hue-green-200: oklch(91.41% 0.04 123.33);
109
+
--hue-green-300: oklch(84.77% 0.08 123.33);
110
+
--hue-green-400: oklch(78.13% 0.11 123.33);
111
+
--hue-green-500: oklch(71.48% 0.15 123.33);
112
+
--hue-green-600: oklch(60.35% 0.14 123.33);
113
+
--hue-green-700: oklch(51.56% 0.12 123.33);
114
+
--hue-green-800: oklch(40.43% 0.09 123.33);
115
+
--hue-green-900: oklch(29.69% 0.07 123.33);
116
+
--hue-green-950: oklch(18.75% 0.04 123.33);
117
+
--hue-emerald-50: oklch(98.83% 0.005 140);
118
+
--hue-emerald-100: oklch(96.29% 0.02 140);
119
+
--hue-emerald-200: oklch(91.41% 0.04 140);
120
+
--hue-emerald-300: oklch(84.57% 0.08 140);
121
+
--hue-emerald-400: oklch(77.73% 0.11 140);
122
+
--hue-emerald-500: oklch(71.09% 0.15 140);
123
+
--hue-emerald-600: oklch(59.77% 0.14 140);
124
+
--hue-emerald-700: oklch(51.17% 0.12 140);
125
+
--hue-emerald-800: oklch(40.04% 0.09 140);
126
+
--hue-emerald-900: oklch(29.49% 0.07 140);
127
+
--hue-emerald-950: oklch(18.75% 0.04 140);
128
+
--hue-teal-50: oklch(98.83% 0.005 160);
129
+
--hue-teal-100: oklch(96.29% 0.02 160);
130
+
--hue-teal-200: oklch(91.21% 0.04 160);
131
+
--hue-teal-300: oklch(84.38% 0.08 160);
132
+
--hue-teal-400: oklch(77.54% 0.11 160);
133
+
--hue-teal-500: oklch(70.51% 0.15 160);
134
+
--hue-teal-600: oklch(59.38% 0.14 160);
135
+
--hue-teal-700: oklch(50.78% 0.12 160);
136
+
--hue-teal-800: oklch(39.84% 0.09 160);
137
+
--hue-teal-900: oklch(29.1% 0.07 160);
138
+
--hue-teal-950: oklch(18.36% 0.04 160);
139
+
--hue-cyan-50: oklch(98.83% 0.005 180);
140
+
--hue-cyan-100: oklch(96.29% 0.02 180);
141
+
--hue-cyan-200: oklch(91.21% 0.04 180);
142
+
--hue-cyan-300: oklch(84.38% 0.08 180);
143
+
--hue-cyan-400: oklch(77.34% 0.11 180);
144
+
--hue-cyan-500: oklch(70.51% 0.15 180);
145
+
--hue-cyan-600: oklch(59.18% 0.14 180);
146
+
--hue-cyan-700: oklch(50.59% 0.12 180);
147
+
--hue-cyan-800: oklch(39.65% 0.09 180);
148
+
--hue-cyan-900: oklch(28.91% 0.07 180);
149
+
--hue-cyan-950: oklch(18.16% 0.04 180);
150
+
--hue-lightBlue-50: oklch(98.83% 0.005 210);
151
+
--hue-lightBlue-100: oklch(96.29% 0.02 210);
152
+
--hue-lightBlue-200: oklch(91.41% 0.04 210);
153
+
--hue-lightBlue-300: oklch(84.57% 0.08 210);
154
+
--hue-lightBlue-400: oklch(77.73% 0.11 210);
155
+
--hue-lightBlue-500: oklch(70.9% 0.15 210);
156
+
--hue-lightBlue-600: oklch(59.57% 0.14 210);
157
+
--hue-lightBlue-700: oklch(50.78% 0.12 210);
158
+
--hue-lightBlue-800: oklch(39.84% 0.09 210);
159
+
--hue-lightBlue-900: oklch(29.1% 0.07 210);
160
+
--hue-lightBlue-950: oklch(18.16% 0.04 210);
161
+
--hue-blue-50: oklch(98.83% 0.005 240);
162
+
--hue-blue-100: oklch(96.48% 0.02 240);
163
+
--hue-blue-200: oklch(91.6% 0.04 240);
164
+
--hue-blue-300: oklch(84.96% 0.08 240);
165
+
--hue-blue-400: oklch(78.32% 0.11 240);
166
+
--hue-blue-500: oklch(71.88% 0.15 240);
167
+
--hue-blue-600: oklch(60.55% 0.14 240);
168
+
--hue-blue-700: oklch(51.76% 0.12 240);
169
+
--hue-blue-800: oklch(40.63% 0.09 240);
170
+
--hue-blue-900: oklch(29.69% 0.07 240);
171
+
--hue-blue-950: oklch(18.75% 0.04 240);
172
+
--hue-indigo-50: oklch(98.83% 0.005 260);
173
+
--hue-indigo-100: oklch(96.48% 0.02 260);
174
+
--hue-indigo-200: oklch(91.8% 0.04 260);
175
+
--hue-indigo-300: oklch(85.35% 0.08 260);
176
+
--hue-indigo-400: oklch(78.91% 0.11 260);
177
+
--hue-indigo-500: oklch(72.66% 0.15 260);
178
+
--hue-indigo-600: oklch(61.33% 0.14 260);
179
+
--hue-indigo-700: oklch(52.34% 0.12 260);
180
+
--hue-indigo-800: oklch(41.21% 0.09 260);
181
+
--hue-indigo-900: oklch(30.08% 0.07 260);
182
+
--hue-indigo-950: oklch(19.14% 0.04 260);
183
+
--hue-violet-50: oklch(98.83% 0.005 280);
184
+
--hue-violet-100: oklch(96.48% 0.02 280);
185
+
--hue-violet-200: oklch(91.99% 0.04 280);
186
+
--hue-violet-300: oklch(85.74% 0.08 280);
187
+
--hue-violet-400: oklch(79.49% 0.11 280);
188
+
--hue-violet-500: oklch(73.44% 0.15 280);
189
+
--hue-violet-600: oklch(61.91% 0.14 280);
190
+
--hue-violet-700: oklch(52.93% 0.12 280);
191
+
--hue-violet-800: oklch(41.6% 0.09 280);
192
+
--hue-violet-900: oklch(30.47% 0.07 280);
193
+
--hue-violet-950: oklch(19.34% 0.04 280);
194
+
--hue-purple-50: oklch(98.83% 0.005 300);
195
+
--hue-purple-100: oklch(96.68% 0.02 300);
196
+
--hue-purple-200: oklch(92.19% 0.04 300);
197
+
--hue-purple-300: oklch(85.94% 0.08 300);
198
+
--hue-purple-400: oklch(79.88% 0.11 300);
199
+
--hue-purple-500: oklch(73.83% 0.15 300);
200
+
--hue-purple-600: oklch(62.5% 0.14 300);
201
+
--hue-purple-700: oklch(53.32% 0.12 300);
202
+
--hue-purple-800: oklch(41.8% 0.09 300);
203
+
--hue-purple-900: oklch(30.66% 0.07 300);
204
+
--hue-purple-950: oklch(19.53% 0.04 300);
205
+
--hue-fuschia-50: oklch(98.83% 0.005 320);
206
+
--hue-fuschia-100: oklch(96.68% 0.02 320);
207
+
--hue-fuschia-200: oklch(92.19% 0.04 320);
208
+
--hue-fuschia-300: oklch(86.13% 0.08 320);
209
+
--hue-fuschia-400: oklch(80.08% 0.11 320);
210
+
--hue-fuschia-500: oklch(74.22% 0.15 320);
211
+
--hue-fuschia-600: oklch(62.7% 0.14 320);
212
+
--hue-fuschia-700: oklch(53.52% 0.12 320);
213
+
--hue-fuschia-800: oklch(41.99% 0.09 320);
214
+
--hue-fuschia-900: oklch(30.86% 0.07 320);
215
+
--hue-fuschia-950: oklch(19.53% 0.04 320);
216
+
--hue-pink-50: oklch(98.83% 0.005 340);
217
+
--hue-pink-100: oklch(96.68% 0.02 340);
218
+
--hue-pink-200: oklch(92.38% 0.04 340);
219
+
--hue-pink-300: oklch(86.33% 0.08 340);
220
+
--hue-pink-400: oklch(80.27% 0.11 340);
221
+
--hue-pink-500: oklch(74.41% 0.15 340);
222
+
--hue-pink-600: oklch(62.89% 0.14 340);
223
+
--hue-pink-700: oklch(53.71% 0.12 340);
224
+
--hue-pink-800: oklch(41.99% 0.09 340);
225
+
--hue-pink-900: oklch(30.86% 0.07 340);
226
+
--hue-pink-950: oklch(19.53% 0.04 340);
227
+
--hue-rose-50: oklch(98.83% 0.005 0);
228
+
--hue-rose-100: oklch(96.68% 0.02 0);
229
+
--hue-rose-200: oklch(92.38% 0.04 0);
230
+
--hue-rose-300: oklch(86.33% 0.08 0);
231
+
--hue-rose-400: oklch(80.27% 0.11 0);
232
+
--hue-rose-500: oklch(74.41% 0.15 0);
233
+
--hue-rose-600: oklch(62.7% 0.14 0);
234
+
--hue-rose-700: oklch(53.52% 0.12 0);
235
+
--hue-rose-800: oklch(41.99% 0.09 0);
236
+
--hue-rose-900: oklch(30.66% 0.07 0);
237
+
--hue-rose-950: oklch(19.34% 0.04 0);
238
+
--hue-slate-50: oklch(98.83% 0.005 275);
239
+
--hue-slate-100: oklch(96.48% 0.02 275);
240
+
--hue-slate-200: oklch(91.8% 0.02 275);
241
+
--hue-slate-300: oklch(85.35% 0.02 275);
242
+
--hue-slate-400: oklch(78.91% 0.02 275);
243
+
--hue-slate-500: oklch(72.66% 0.02 275);
244
+
--hue-slate-600: oklch(61.33% 0.02 275);
245
+
--hue-slate-700: oklch(52.34% 0.02 275);
246
+
--hue-slate-800: oklch(41.21% 0.02 275);
247
+
--hue-slate-900: oklch(30.27% 0.02 275);
248
+
--hue-slate-950: oklch(19.34% 0.02 275);
249
+
--hue-gray-50: oklch(98.83% 0.005 275);
250
+
--hue-gray-100: oklch(96.48% 0.02 275);
251
+
--hue-gray-200: oklch(91.8% 0.02 275);
252
+
--hue-gray-300: oklch(85.35% 0.02 275);
253
+
--hue-gray-400: oklch(78.91% 0.02 275);
254
+
--hue-gray-500: oklch(72.66% 0.02 275);
255
+
--hue-gray-600: oklch(61.33% 0.02 275);
256
+
--hue-gray-700: oklch(52.34% 0.02 275);
257
+
--hue-gray-800: oklch(41.21% 0.02 275);
258
+
--hue-gray-900: oklch(30.27% 0.02 275);
259
+
--hue-gray-950: oklch(19.34% 0.02 275);
260
+
--hue-zinc-50: oklch(98.83% 0.005 275);
261
+
--hue-zinc-100: oklch(96.48% 0.01 275);
262
+
--hue-zinc-200: oklch(91.8% 0.01 275);
263
+
--hue-zinc-300: oklch(85.35% 0.01 275);
264
+
--hue-zinc-400: oklch(78.91% 0.01 275);
265
+
--hue-zinc-500: oklch(72.66% 0.01 275);
266
+
--hue-zinc-600: oklch(61.33% 0.01 275);
267
+
--hue-zinc-700: oklch(52.34% 0.01 275);
268
+
--hue-zinc-800: oklch(41.21% 0.01 275);
269
+
--hue-zinc-900: oklch(30.27% 0.01 275);
270
+
--hue-zinc-950: oklch(19.34% 0.01 275);
271
+
--hue-neutral-50: oklch(98.83% 0.005 0);
272
+
--hue-neutral-100: oklch(96.48% 0 0);
273
+
--hue-neutral-200: oklch(91.8% 0 0);
274
+
--hue-neutral-300: oklch(85.35% 0 0);
275
+
--hue-neutral-400: oklch(78.91% 0 0);
276
+
--hue-neutral-500: oklch(72.66% 0 0);
277
+
--hue-neutral-600: oklch(61.13% 0 0);
278
+
--hue-neutral-700: oklch(52.34% 0 0);
279
+
--hue-neutral-800: oklch(41.21% 0 0);
280
+
--hue-neutral-900: oklch(30.08% 0 0);
281
+
--hue-neutral-950: oklch(19.34% 0 0);
282
+
--hue-stone-50: oklch(98.83% 0.008 75);
283
+
--hue-stone-100: oklch(96.48% 0.01 75);
284
+
--hue-stone-200: oklch(91.8% 0.01 75);
285
+
--hue-stone-300: oklch(85.35% 0.01 75);
286
+
--hue-stone-400: oklch(78.91% 0.01 75);
287
+
--hue-stone-500: oklch(72.66% 0.01 75);
288
+
--hue-stone-600: oklch(61.33% 0.01 75);
289
+
--hue-stone-700: oklch(52.34% 0.01 75);
290
+
--hue-stone-800: oklch(41.21% 0.01 75);
291
+
--hue-stone-900: oklch(30.27% 0.01 75);
292
+
--hue-stone-950: oklch(19.34% 0.01 75);
293
+
--hue-sand-50: oklch(98.83% 0.008 75);
294
+
--hue-sand-100: oklch(96.48% 0.01 75);
295
+
--hue-sand-200: oklch(91.8% 0.01 75);
296
+
--hue-sand-300: oklch(85.35% 0.01 75);
297
+
--hue-sand-400: oklch(78.91% 0.01 75);
298
+
--hue-sand-500: oklch(72.66% 0.01 75);
299
+
--hue-sand-600: oklch(61.33% 0.01 75);
300
+
--hue-sand-700: oklch(52.34% 0.01 75);
301
+
--hue-sand-800: oklch(41.21% 0.01 75);
302
+
--hue-sand-900: oklch(30.27% 0.01 75);
303
+
--hue-sand-950: oklch(30.27% 0.01 75);
304
+
--hue-olive-50: oklch(98.83% 0.008 120);
305
+
--hue-olive-100: oklch(96.48% 0.01 120);
306
+
--hue-olive-200: oklch(91.8% 0.01 120);
307
+
--hue-olive-300: oklch(85.16% 0.01 120);
308
+
--hue-olive-400: oklch(78.91% 0.01 120);
309
+
--hue-olive-500: oklch(72.46% 0.01 120);
310
+
--hue-olive-600: oklch(61.13% 0.01 120);
311
+
--hue-olive-700: oklch(52.34% 0.01 120);
312
+
--hue-olive-800: oklch(41.02% 0.01 120);
313
+
--hue-olive-900: oklch(30.08% 0.01 120);
314
+
--hue-olive-950: oklch(19.14% 0.01 120);
315
+
--hue-mauve-50: oklch(98.83% 0.008 325);
316
+
--hue-mauve-100: oklch(96.68% 0.01 325);
317
+
--hue-mauve-200: oklch(91.8% 0.01 325);
318
+
--hue-mauve-300: oklch(85.35% 0.01 325);
319
+
--hue-mauve-400: oklch(78.91% 0.01 325);
320
+
--hue-mauve-500: oklch(72.66% 0.01 325);
321
+
--hue-mauve-600: oklch(61.33% 0.01 325);
322
+
--hue-mauve-700: oklch(52.34% 0.01 325);
323
+
--hue-mauve-800: oklch(41.21% 0.01 325);
324
+
--hue-mauve-900: oklch(30.27% 0.01 325);
325
+
--hue-mauve-950: oklch(19.34% 0.01 325);
326
+
}
327
+
}
328
+
@layer stylebase-default {
329
+
:root {
330
+
--hue-z0-bg: var(--hue-neutral-50);
331
+
--hue-z0-fg: var(--hue-neutral-950);
332
+
--hue-z0-divider: color-mix(in oklch, currentColor 50%, transparent);
333
+
--hue-z1-bg: ;
334
+
--hue-z1-fg: ;
335
+
}
336
+
body,
337
+
html {
338
+
background-color: var(--hue-z0-bg);
339
+
color: var(--hue-z0-fg);
340
+
}
341
+
@media (prefers-color-scheme: dark) {
342
+
:root {
343
+
--hue-z0-bg: var(--hue-neutral-950);
344
+
--hue-z0-fg: var(--hue-neutral-200);
345
+
}
346
+
}
347
+
}
348
+
@layer stylebase-token {
349
+
:root {
350
+
--ff-antique-display:
351
+
Superclarendon, "Bookman Old Style", "URW Bookman", "URW Bookman L",
352
+
"Georgia Pro", Georgia, serif;
353
+
--ff-didone-display:
354
+
Didot, "Bodoni MT", "Noto Serif Display", "URW Palladio L", P052,
355
+
Sylfaen, serif;
356
+
--ff-handwritten-display:
357
+
"Segoe Print", "Bradley Hand", Chilanka, TSCu_Comic, casual, cursive;
358
+
--ff-humanist-classical:
359
+
Optima, Candara, "Noto Sans", source-sans-pro, sans-serif;
360
+
--ff-humanist-geometric:
361
+
Avenir, Montserrat, Corbel, "URW Gothic", source-sans-pro,
362
+
sans-serif;
363
+
--ff-humanist:
364
+
Seravek, "Gill Sans Nova", Ubuntu, Calibri, "DejaVu Sans",
365
+
source-sans-pro, sans-serif;
366
+
--ff-industrial-display:
367
+
Bahnschrift, "DIN Alternate", "Franklin Gothic Medium",
368
+
"Nimbus Sans Narrow", sans-serif-condensed, sans-serif;
369
+
--ff-mono-slab-serif: "Nimbus Mono PS", "Courier New", monospace;
370
+
--ff-mono:
371
+
ui-monospace, "Cascadia Code", "Source Code Pro", Menlo, Consolas,
372
+
"DejaVu Sans Mono", monospace;
373
+
--ff-neo-grotesque:
374
+
Inter, Roboto, "Helvetica Neue", "Arial Nova", "Nimbus Sans", Arial,
375
+
sans-serif;
376
+
--ff-old-style:
377
+
"Iowan Old Style", "Palatino Linotype", "URW Palladio L", P052,
378
+
serif;
379
+
--ff-rounded-sans-display:
380
+
ui-rounded, "Hiragino Maru Gothic ProN", Quicksand, Comfortaa,
381
+
Manjari, "Arial Rounded MT", "Arial Rounded MT Bold", Calibri,
382
+
source-sans-pro, sans-serif;
383
+
--ff-slab-serif-display:
384
+
Rockwell, "Rockwell Nova", "Roboto Slab", "DejaVu Serif",
385
+
"Sitka Small", serif;
386
+
--ff-system: system-ui, sans-serif;
387
+
--ff-transitional:
388
+
Charter, "Bitstream Charter", "Sitka Text", Cambria, serif;
389
+
--ff-content: var(--ff-old-style);
390
+
--ff-heading: var(--ff-rounded-sans-display);
391
+
--ff-sans: var(--ff-neo-grotesque);
392
+
--ff-serif: var(--fftransitional);
393
+
--ff-ui: var(--ff-system);
394
+
--fs-0: clamp(0.625rem, 0.5979rem + 0.1379vw, 0.75rem);
395
+
--fs-1: clamp(0.75rem, 0.7094rem + 0.2069vw, 0.9375rem);
396
+
--fs-2: clamp(0.9rem, 0.8411rem + 0.3vw, 1.1719rem);
397
+
--fs-3: clamp(1.08rem, 0.9967rem + 0.4247vw, 1.4648rem);
398
+
--fs-4: clamp(1.296rem, 1.1801rem + 0.5904vw, 1.8311rem);
399
+
--fs-5: clamp(1.5552rem, 1.3963rem + 0.8095vw, 2.2888rem);
400
+
--fs-6: clamp(1.8662rem, 1.6508rem + 1.0977vw, 2.861rem);
401
+
--fs-7: clamp(2.2395rem, 1.95rem + 1.4751vw, 3.5763rem);
402
+
--fs-8: clamp(2.6874rem, 2.3013rem + 1.9674vw, 4.4703rem);
403
+
--fs-9: clamp(3.2249rem, 2.7131rem + 2.6075vw, 5.5879rem);
404
+
--fs-10: clamp(3.8698rem, 3.1953rem + 3.4373vw, 6.9849rem);
405
+
--lh-ui: 1;
406
+
--lh-snug: 1.15;
407
+
--lh-condensed: 1.35;
408
+
--lh-standard: 1.5;
409
+
--lh-expanded: 1.62;
410
+
--lh-loose: 1.75;
411
+
}
412
+
}
413
+
@layer stylebase-token {
414
+
}
415
+
@layer stylebase-default {
416
+
html {
417
+
-moz-text-size-adjust: none;
418
+
-webkit-text-size-adjust: none;
419
+
color-scheme: light dark;
420
+
text-size-adjust: none;
421
+
}
422
+
body {
423
+
margin: 0;
424
+
}
425
+
hr {
426
+
background-color: var(--hue-z0-divider);
427
+
border: unset;
428
+
height: 1px;
429
+
}
430
+
img,
431
+
svg,
432
+
video {
433
+
display: block;
434
+
max-width: 100%;
435
+
}
436
+
}
437
+
@layer stylebase-layout {
438
+
.l\:grid {
439
+
column-gap: var(--grid-gutter);
440
+
display: grid;
441
+
grid-template-columns: repeat(var(--grid-columns), minmax(0, 1fr));
442
+
margin-inline: auto;
443
+
max-width: var(--grid-max-width);
444
+
padding-inline: var(--grid-gutter);
445
+
row-gap: 0;
446
+
}
447
+
[data-grid-columns="quarter"] {
448
+
grid-column: span calc(var(--grid-columns) / 4);
449
+
}
450
+
[data-grid-columns="third"] {
451
+
grid-column: span calc(var(--grid-columns) / 3);
452
+
}
453
+
[data-grid-columns="half"] {
454
+
grid-column: span calc(var(--grid-columns) / 2);
455
+
}
456
+
[data-grid-columns="full"] {
457
+
grid-column: span var(--grid-columns);
458
+
}
459
+
}
460
+
@layer stylebase-token {
461
+
:root {
462
+
--grid-max-width: 83.25rem;
463
+
--grid-gutter: var(
464
+
--space-s-xl,
465
+
clamp(0.625rem, 0.1321rem + 2.544vw, 2.25rem)
466
+
);
467
+
--grid-columns: 12;
468
+
}
469
+
}
470
+
@layer stylebase-layout {
471
+
.l\:repel {
472
+
align-items: center;
473
+
display: flex !important;
474
+
justify-content: space-between;
475
+
}
476
+
.l\:river > * {
477
+
margin-inline: 0;
478
+
}
479
+
.l\:river > * + * {
480
+
margin-inline-start: var(--river-gap, 1em);
481
+
}
482
+
.l\:root {
483
+
display: block;
484
+
margin-inline: auto;
485
+
max-width: var(--grid-max-width);
486
+
padding-inline: var(--grid-gutter);
487
+
}
488
+
}
489
+
@layer stylebase-layout {
490
+
}
491
+
@layer stylebase-layout {
492
+
}
493
+
@layer stylebase-token {
494
+
:root {
495
+
--space-5xs: clamp(0.56px, 0.5362px + 0.0076vw, 0.67px);
496
+
--space-4xs: clamp(0.9px, 0.861px + 0.0124vw, 1.08px);
497
+
--space-3xs: clamp(1.46px, 1.3972px + 0.02vw, 1.75px);
498
+
--space-2xs: clamp(2.36px, 2.2582px + 0.0324vw, 2.83px);
499
+
--space-xs: clamp(3.82px, 3.6533px + 0.0531vw, 4.59px);
500
+
--space-sm: clamp(6.18px, 5.9115px + 0.0855vw, 7.42px);
501
+
--space-medium: clamp(10px, 9.5669px + 0.1379vw, 12px);
502
+
--space-lg: clamp(16.18px, 15.4784px + 0.2234vw, 19.42px);
503
+
--space-xl: clamp(26.18px, 25.0453px + 0.3614vw, 31.42px);
504
+
--space-2xl: clamp(42.36px, 40.5258px + 0.5841vw, 50.83px);
505
+
--space-3xl: clamp(68.54px, 65.5711px + 0.9455vw, 82.25px);
506
+
--space-4xl: clamp(110.9px, 106.0969px + 1.5297vw, 133.08px);
507
+
--space-5xl: clamp(179.44px, 171.668px + 2.4752vw, 215.33px);
508
+
}
509
+
}
510
+
@layer stylebase-default {
511
+
body {
512
+
font-family: var(--ff-sans);
513
+
font-size: var(--fs-1);
514
+
line-height: var(--lh-standard);
515
+
}
516
+
h1,
517
+
h2,
518
+
h3,
519
+
h4,
520
+
h5,
521
+
h6 {
522
+
font-weight: unset;
523
+
}
524
+
h1,
525
+
h2,
526
+
h3 {
527
+
line-height: var(--lh-snug);
528
+
}
529
+
h4,
530
+
h5,
531
+
h6 {
532
+
line-height: var(--lh-condensed);
533
+
}
534
+
h1 {
535
+
font-size: var(--fs-5);
536
+
}
537
+
h2 {
538
+
font-size: var(--fs-4);
539
+
}
540
+
h3 {
541
+
font-size: var(--fs-3);
542
+
}
543
+
h4 {
544
+
font-size: var(--fs-2);
545
+
}
546
+
h5 {
547
+
font-size: var(--fs-1);
548
+
}
549
+
h6 {
550
+
font-size: var(--fs-0);
551
+
}
552
+
:where(h5, h6):not([class]) {
553
+
text-transform: uppercase;
554
+
}
555
+
p {
556
+
font-size: var(--fs-1);
557
+
line-height: var(--lh-standard);
558
+
}
559
+
p code {
560
+
font-family: var(--ff-mono);
561
+
font-size: 0.9em;
562
+
}
563
+
}
564
+
@layer stylebase-layout {
565
+
.l\:ui-list {
566
+
list-style-type: none;
567
+
padding-inline: unset;
568
+
li {
569
+
display: inline-block;
570
+
}
571
+
}
572
+
}
573
+
@layer stylebase-layout {
574
+
.l\:waterfall > * {
575
+
margin-block: 0;
576
+
}
577
+
.l\:waterfall > * + * {
578
+
margin-block-start: var(--waterfall-gap, 2em);
579
+
}
580
+
}
581
+
@layer utility {
582
+
.u\:fs-0 {
583
+
font-size: var(--fs-0);
584
+
}
585
+
.u\:fs-1 {
586
+
font-size: var(--fs-1);
587
+
}
588
+
.u\:fs-2 {
589
+
font-size: var(--fs-2);
590
+
}
591
+
.u\:fs-3 {
592
+
font-size: var(--fs-3);
593
+
}
594
+
.u\:fs-4 {
595
+
font-size: var(--fs-4);
596
+
}
597
+
.u\:fs-5 {
598
+
font-size: var(--fs-5);
599
+
}
600
+
.u\:fs-6 {
601
+
font-size: var(--fs-6);
602
+
}
603
+
.u\:fs-7 {
604
+
font-size: var(--fs-7);
605
+
}
606
+
.u\:fs-8 {
607
+
font-size: var(--fs-8);
608
+
}
609
+
.u\:fs-9 {
610
+
font-size: var(--fs-9);
611
+
}
612
+
.u\:fs-10 {
613
+
font-size: var(--fs-10);
614
+
}
615
+
.u\:lh-ui {
616
+
line-height: var(--lh-ui);
617
+
}
618
+
.u\:lh-snug {
619
+
line-height: var(--lh-snug);
620
+
}
621
+
.u\:lh-condensed {
622
+
line-height: var(--lh-condensed);
623
+
}
624
+
.u\:lh-standard {
625
+
line-height: var(--lh-standard);
626
+
}
627
+
.u\:lh-expanded {
628
+
line-height: var(--lh-expanded);
629
+
}
630
+
.u\:lh-loose {
631
+
line-height: var(--lh-loose);
632
+
}
633
+
}
634
+
</style>
635
+
</head>
636
+
<body>
637
+
<header class="l:root">
12
638
<h1>Autonomy Declaration</h1>
13
-
<p>Declare automation and AI usage for transparency and accountability</p>
639
+
<p>
640
+
Declare automation and AI usage for transparency and accountability on
641
+
Bluesky
642
+
</p>
14
643
</header>
15
644
16
-
<main>
17
-
<form id="declaration-form">
18
-
<div class="form-group">
19
-
<label for="handle">Your Handle</label>
20
-
<input
21
-
type="text"
22
-
id="handle"
23
-
name="handle"
24
-
placeholder="username.bsky.social"
25
-
required
26
-
/>
27
-
<small>Your Bluesky handle or DID</small>
28
-
</div>
645
+
<main class="l:root">
646
+
<form id="declaration-form" class="l:waterfall" novalidate>
647
+
<section>
648
+
<h2>Authentication</h2>
649
+
650
+
<div>
651
+
<label for="handle">Bluesky Handle (required)</label>
652
+
<input
653
+
type="text"
654
+
id="handle"
655
+
name="handle"
656
+
placeholder="username.bsky.social"
657
+
required
658
+
autocomplete="username"
659
+
/>
660
+
<small>Your Bluesky handle or DID</small>
661
+
<p id="handle-error" role="alert" hidden></p>
662
+
</div>
663
+
664
+
<div>
665
+
<label for="password">App Password (required)</label>
666
+
<input
667
+
type="password"
668
+
id="password"
669
+
name="password"
670
+
placeholder="xxxx-xxxx-xxxx-xxxx"
671
+
required
672
+
autocomplete="current-password"
673
+
pattern=".{19,19}"
674
+
/>
675
+
<small
676
+
>19 characters with 3 dashes (generate in Bluesky settings)</small
677
+
>
678
+
<p id="password-error" role="alert" hidden></p>
679
+
</div>
680
+
</section>
29
681
30
-
<div class="form-group">
31
-
<label for="password">App Password</label>
32
-
<input
33
-
type="password"
34
-
id="password"
35
-
name="password"
36
-
placeholder="xxxx-xxxx-xxxx-xxxx"
37
-
required
38
-
/>
39
-
<small>Generate an app password in Bluesky settings</small>
40
-
</div>
682
+
<section>
683
+
<h2>Declaration Details</h2>
41
684
42
-
<div class="form-group">
43
-
<label for="automationLevel">Automation Level</label>
44
-
<select id="automationLevel" name="automationLevel">
45
-
<option value="">Select level (optional)</option>
46
-
<option value="human">Human - No automation</option>
47
-
<option value="assisted">Assisted - Tools help but human decides</option>
48
-
<option value="collaborative">Collaborative - Human and AI work together</option>
49
-
<option value="automated">Automated - Primarily AI-driven</option>
50
-
</select>
51
-
<small>Level of automation in account management and content creation</small>
52
-
</div>
685
+
<div>
686
+
<label for="automationLevel">Automation Level</label>
687
+
<select id="automationLevel" name="automationLevel">
688
+
<option value="">Select level (optional)</option>
689
+
<option value="human">Human - No automation</option>
690
+
<option value="assisted">
691
+
Assisted - Tools help but human decides
692
+
</option>
693
+
<option value="collaborative">
694
+
Collaborative - Human and AI work together
695
+
</option>
696
+
<option value="automated">Automated - Primarily AI-driven</option>
697
+
</select>
698
+
<small
699
+
>Level of automation in account management and content
700
+
creation</small
701
+
>
702
+
</div>
53
703
54
-
<div class="form-group">
55
-
<label>
56
-
<input type="checkbox" id="usesGenerativeAI" name="usesGenerativeAI" />
57
-
Uses Generative AI
58
-
</label>
59
-
<small>Check if this account uses LLMs, image generation, etc.</small>
60
-
</div>
704
+
<div>
705
+
<label>
706
+
<input
707
+
type="checkbox"
708
+
id="usesGenerativeAI"
709
+
name="usesGenerativeAI"
710
+
/>
711
+
Uses Generative AI
712
+
</label>
713
+
<small
714
+
>Check if this account uses LLMs, image generation, etc.</small
715
+
>
716
+
</div>
61
717
62
-
<div class="form-group">
63
-
<label for="description">Description</label>
64
-
<textarea
65
-
id="description"
66
-
name="description"
67
-
rows="4"
68
-
maxlength="300"
69
-
placeholder="Explain how this account is automated and what it does..."
70
-
></textarea>
71
-
<small>Plain language explanation (max 300 characters)</small>
72
-
</div>
718
+
<div>
719
+
<label for="description">Description</label>
720
+
<textarea
721
+
id="description"
722
+
name="description"
723
+
rows="4"
724
+
maxlength="300"
725
+
placeholder="Explain how this account is automated and what it does..."
726
+
></textarea>
727
+
<small
728
+
>Plain language explanation (max 300 characters, optional)</small
729
+
>
730
+
<p id="description-error" role="alert" hidden></p>
731
+
</div>
732
+
</section>
73
733
74
-
<fieldset class="form-group">
75
-
<legend>Responsible Party</legend>
734
+
<fieldset>
735
+
<legend>Responsible Party (optional)</legend>
76
736
77
-
<div class="form-group">
737
+
<div>
78
738
<label for="responsibleType">Type</label>
79
739
<select id="responsibleType" name="responsibleType">
80
740
<option value="">Select type (optional)</option>
···
83
743
</select>
84
744
</div>
85
745
86
-
<div class="form-group">
746
+
<div>
87
747
<label for="responsibleName">Name</label>
88
748
<input
89
749
type="text"
···
92
752
maxlength="100"
93
753
placeholder="Name of person or organization"
94
754
/>
755
+
<p id="responsibleName-error" role="alert" hidden></p>
95
756
</div>
96
757
97
-
<div class="form-group">
758
+
<div>
98
759
<label for="responsibleContact">Contact</label>
99
760
<input
100
761
type="text"
···
103
764
maxlength="300"
104
765
placeholder="Email, URL, handle, or DID"
105
766
/>
767
+
<p id="responsibleContact-error" role="alert" hidden></p>
106
768
</div>
107
769
108
-
<div class="form-group">
109
-
<label for="responsibleDid">DID (optional)</label>
770
+
<div>
771
+
<label for="responsibleDid">DID</label>
110
772
<input
111
773
type="text"
112
774
id="responsibleDid"
···
114
776
placeholder="did:plc:..."
115
777
/>
116
778
<small>ATProto DID if they have an identity</small>
779
+
<p id="responsibleDid-error" role="alert" hidden></p>
117
780
</div>
118
781
</fieldset>
119
782
120
-
<div class="form-group">
121
-
<label for="disclosureUrl">Disclosure URL</label>
122
-
<input
123
-
type="url"
124
-
id="disclosureUrl"
125
-
name="disclosureUrl"
126
-
placeholder="https://..."
127
-
/>
128
-
<small>URL with additional information about automation</small>
129
-
</div>
783
+
<section>
784
+
<h2>Additional Information</h2>
130
785
131
-
<div class="form-group">
132
-
<label for="externalServices">External Services</label>
133
-
<textarea
134
-
id="externalServices"
135
-
name="externalServices"
136
-
rows="3"
137
-
placeholder="Letta, Railway, Google Gemini 2.5-pro (one per line)"
138
-
></textarea>
139
-
<small>External tools and services used (one per line, max 20)</small>
140
-
</div>
786
+
<div>
787
+
<label for="disclosureUrl">Disclosure URL</label>
788
+
<input
789
+
type="url"
790
+
id="disclosureUrl"
791
+
name="disclosureUrl"
792
+
placeholder="https://..."
793
+
/>
794
+
<small
795
+
>URL with additional information about automation
796
+
(optional)</small
797
+
>
798
+
<p id="disclosureUrl-error" role="alert" hidden></p>
799
+
</div>
141
800
142
-
<button type="submit" id="submit-btn">
143
-
Submit Declaration
144
-
</button>
801
+
<div>
802
+
<label for="externalServices">External Services</label>
803
+
<textarea
804
+
id="externalServices"
805
+
name="externalServices"
806
+
rows="5"
807
+
placeholder="One service per line, e.g.: Letta Railway Google Gemini 2.5-pro"
808
+
></textarea>
809
+
<small
810
+
>External tools and services used (one per line, max 20 services,
811
+
200 chars each)</small
812
+
>
813
+
<p id="externalServices-error" role="alert" hidden></p>
814
+
</div>
815
+
</section>
816
+
817
+
<button type="submit" id="submit-btn">Submit Declaration</button>
145
818
</form>
146
819
147
-
<div id="result" class="result hidden"></div>
820
+
<div id="result" role="status" aria-live="polite" hidden></div>
148
821
</main>
149
-
</div>
822
+
823
+
<footer class="l:root">
824
+
<p>
825
+
This form creates a record in your Personal Data Server (PDS) using the
826
+
<code>studio.voyager.account.autonomy</code> lexicon.
827
+
</p>
828
+
</footer>
829
+
830
+
<script type="module">
831
+
import { BskyAgent } from "@atproto/api";
832
+
833
+
const form = document.getElementById("declaration-form");
834
+
const submitBtn = document.getElementById("submit-btn");
835
+
const resultDiv = document.getElementById("result");
836
+
837
+
const validators = {
838
+
handle: (value) => {
839
+
if (!value) return "Handle is required";
840
+
if (!value.includes(".") && !value.startsWith("did:")) {
841
+
return "Handle must be a valid Bluesky handle (e.g., username.bsky.social) or DID";
842
+
}
843
+
return null;
844
+
},
845
+
password: (value) => {
846
+
if (!value) return "App password is required";
847
+
if (value.length !== 19)
848
+
return "App password must be exactly 19 characters";
849
+
const dashCount = (value.match(/-/g) || []).length;
850
+
if (dashCount !== 3)
851
+
return "App password must contain exactly 3 dashes";
852
+
return null;
853
+
},
854
+
description: (value) =>
855
+
value && value.length > 300
856
+
? "Description must be 300 characters or less"
857
+
: null,
858
+
responsibleName: (value) =>
859
+
value && value.length > 100
860
+
? "Name must be 100 characters or less"
861
+
: null,
862
+
responsibleContact: (value) =>
863
+
value && value.length > 300
864
+
? "Contact must be 300 characters or less"
865
+
: null,
866
+
responsibleDid: (value) =>
867
+
value && !value.startsWith("did:")
868
+
? 'DID must start with "did:"'
869
+
: null,
870
+
disclosureUrl: (value) => {
871
+
if (value) {
872
+
try {
873
+
new URL(value);
874
+
} catch {
875
+
return "Must be a valid URL";
876
+
}
877
+
}
878
+
return null;
879
+
},
880
+
externalServices: (value) => {
881
+
if (value) {
882
+
const services = value
883
+
.split("\n")
884
+
.map((s) => s.trim())
885
+
.filter((s) => s.length > 0);
886
+
if (services.length > 20)
887
+
return "Maximum 20 external services allowed";
888
+
for (const service of services) {
889
+
if (service.length > 200)
890
+
return `Service name too long (max 200 chars): ${service.substring(0, 50)}...`;
891
+
}
892
+
}
893
+
return null;
894
+
},
895
+
};
896
+
897
+
function showError(fieldId, message) {
898
+
const errorElement = document.getElementById(`${fieldId}-error`);
899
+
if (errorElement) {
900
+
errorElement.textContent = message;
901
+
errorElement.hidden = false;
902
+
}
903
+
}
904
+
905
+
function clearError(fieldId) {
906
+
const errorElement = document.getElementById(`${fieldId}-error`);
907
+
if (errorElement) {
908
+
errorElement.textContent = "";
909
+
errorElement.hidden = true;
910
+
}
911
+
}
912
+
913
+
function clearAllErrors() {
914
+
[
915
+
"handle",
916
+
"password",
917
+
"description",
918
+
"responsibleName",
919
+
"responsibleContact",
920
+
"responsibleDid",
921
+
"disclosureUrl",
922
+
"externalServices",
923
+
].forEach(clearError);
924
+
}
925
+
926
+
function validateForm(formData) {
927
+
clearAllErrors();
928
+
let isValid = true;
929
+
for (const [fieldId, validator] of Object.entries(validators)) {
930
+
const value = formData.get(fieldId) || "";
931
+
const error = validator(value);
932
+
if (error) {
933
+
console.log("Validation error for", fieldId, ":", error);
934
+
showError(fieldId, error);
935
+
isValid = false;
936
+
}
937
+
}
938
+
return isValid;
939
+
}
940
+
941
+
form.addEventListener("submit", async (e) => {
942
+
e.preventDefault();
943
+
const formData = new FormData(form);
944
+
945
+
if (!validateForm(formData)) {
946
+
resultDiv.textContent =
947
+
"Please fix the errors above before submitting.";
948
+
resultDiv.hidden = false;
949
+
return;
950
+
}
951
+
952
+
submitBtn.disabled = true;
953
+
submitBtn.textContent = "Submitting...";
954
+
resultDiv.hidden = true;
955
+
956
+
try {
957
+
const agent = new BskyAgent({ service: "https://bsky.social" });
958
+
await agent.login({
959
+
identifier: formData.get("handle").trim(),
960
+
password: formData.get("password"),
961
+
});
962
+
963
+
const record = {
964
+
$type: "studio.voyager.account.autonomy",
965
+
createdAt: new Date().toISOString(),
966
+
};
967
+
968
+
const automationLevel = formData.get("automationLevel");
969
+
const usesGenerativeAI = formData.get("usesGenerativeAI") === "on";
970
+
const description = formData.get("description").trim();
971
+
const responsibleType = formData.get("responsibleType");
972
+
const responsibleName = formData.get("responsibleName").trim();
973
+
const responsibleContact = formData.get("responsibleContact").trim();
974
+
const responsibleDid = formData.get("responsibleDid").trim();
975
+
const disclosureUrl = formData.get("disclosureUrl").trim();
976
+
const externalServicesText = formData.get("externalServices").trim();
977
+
978
+
if (automationLevel) record.automationLevel = automationLevel;
979
+
if (usesGenerativeAI) record.usesGenerativeAI = true;
980
+
if (description) record.description = description;
981
+
982
+
if (
983
+
responsibleType ||
984
+
responsibleName ||
985
+
responsibleContact ||
986
+
responsibleDid
987
+
) {
988
+
record.responsibleParty = {};
989
+
if (responsibleType) record.responsibleParty.type = responsibleType;
990
+
if (responsibleName) record.responsibleParty.name = responsibleName;
991
+
if (responsibleContact)
992
+
record.responsibleParty.contact = responsibleContact;
993
+
if (responsibleDid) record.responsibleParty.did = responsibleDid;
994
+
}
995
+
996
+
if (disclosureUrl) record.disclosureUrl = disclosureUrl;
997
+
998
+
if (externalServicesText) {
999
+
const services = externalServicesText
1000
+
.split("\n")
1001
+
.map((s) => s.trim())
1002
+
.filter((s) => s.length > 0)
1003
+
.slice(0, 20);
1004
+
if (services.length > 0) record.externalServices = services;
1005
+
}
1006
+
1007
+
const response = await agent.api.com.atproto.repo.putRecord({
1008
+
repo: agent.session?.did || "",
1009
+
collection: "studio.voyager.account.autonomy",
1010
+
rkey: "self",
1011
+
record: record,
1012
+
});
150
1013
151
-
<script type="module" src="./app.js"></script>
152
-
</body>
1014
+
resultDiv.textContent = `Declaration submitted successfully! Record URI: ${response.data.uri}`;
1015
+
resultDiv.hidden = false;
1016
+
form.reset();
1017
+
} catch (error) {
1018
+
let errorMessage = "Error: ";
1019
+
if (error.message.includes("Invalid identifier or password")) {
1020
+
errorMessage +=
1021
+
"Invalid handle or app password. Please check your credentials.";
1022
+
} else if (error.message.includes("Network")) {
1023
+
errorMessage +=
1024
+
"Network error. Please check your connection and try again.";
1025
+
} else if (error.message.includes("AuthenticationRequired")) {
1026
+
errorMessage +=
1027
+
"Authentication failed. Please verify your app password.";
1028
+
} else {
1029
+
errorMessage += error.message;
1030
+
}
1031
+
resultDiv.textContent = errorMessage;
1032
+
resultDiv.hidden = false;
1033
+
console.error("Submission error:", error);
1034
+
} finally {
1035
+
submitBtn.disabled = false;
1036
+
submitBtn.textContent = "Submit Declaration";
1037
+
}
1038
+
});
1039
+
</script>
1040
+
</body>
153
1041
</html>
+35
-10
client/main.ts
+35
-10
client/main.ts
···
1
1
/**
2
-
* Simple development server for the client
2
+
* Deno HTTP server for local development
3
+
* Serves static files from the client directory
3
4
*/
4
5
5
-
import { serveDir } from "https://deno.land/std@0.224.0/http/file_server.ts";
6
+
const PORT = 8000;
7
+
8
+
const MIME_TYPES: Record<string, string> = {
9
+
'.html': 'text/html',
10
+
'.js': 'application/javascript',
11
+
'.css': 'text/css',
12
+
'.json': 'application/json',
13
+
};
14
+
15
+
Deno.serve({ port: PORT }, async (req: Request) => {
16
+
const url = new URL(req.url);
17
+
let filepath = url.pathname;
18
+
19
+
// Default to index.html for root path
20
+
if (filepath === '/') {
21
+
filepath = '/index.html';
22
+
}
23
+
24
+
try {
25
+
// Serve file from client directory
26
+
const file = await Deno.readFile(`./client${filepath}`);
6
27
7
-
Deno.serve({
8
-
port: 8000,
9
-
handler: (req) => {
10
-
return serveDir(req, {
11
-
fsRoot: "./client",
12
-
showDirListing: false,
28
+
// Determine content type
29
+
const ext = filepath.substring(filepath.lastIndexOf('.'));
30
+
const contentType = MIME_TYPES[ext] || 'application/octet-stream';
31
+
32
+
return new Response(file, {
33
+
headers: {
34
+
'content-type': contentType,
35
+
},
13
36
});
14
-
},
37
+
} catch {
38
+
return new Response('Not Found', { status: 404 });
39
+
}
15
40
});
16
41
17
-
console.log("๐ Client running at http://localhost:8000");
42
+
console.log(`Server running at http://localhost:${PORT}/`);
-148
client/styles.css
-148
client/styles.css
···
1
-
* {
2
-
margin: 0;
3
-
padding: 0;
4
-
box-sizing: border-box;
5
-
}
6
-
7
-
body {
8
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
9
-
line-height: 1.6;
10
-
color: #333;
11
-
background: #f5f5f5;
12
-
padding: 20px;
13
-
}
14
-
15
-
.container {
16
-
max-width: 600px;
17
-
margin: 0 auto;
18
-
background: white;
19
-
padding: 40px;
20
-
border-radius: 8px;
21
-
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
22
-
}
23
-
24
-
header {
25
-
text-align: center;
26
-
margin-bottom: 40px;
27
-
}
28
-
29
-
h1 {
30
-
font-size: 2rem;
31
-
margin-bottom: 10px;
32
-
color: #1a1a1a;
33
-
}
34
-
35
-
header p {
36
-
color: #666;
37
-
}
38
-
39
-
.form-group {
40
-
margin-bottom: 24px;
41
-
}
42
-
43
-
label {
44
-
display: block;
45
-
margin-bottom: 8px;
46
-
font-weight: 500;
47
-
color: #333;
48
-
}
49
-
50
-
input[type="text"],
51
-
input[type="password"],
52
-
input[type="url"],
53
-
select,
54
-
textarea {
55
-
width: 100%;
56
-
padding: 12px;
57
-
border: 1px solid #ddd;
58
-
border-radius: 4px;
59
-
font-size: 14px;
60
-
font-family: inherit;
61
-
transition: border-color 0.2s;
62
-
}
63
-
64
-
input[type="text"]:focus,
65
-
input[type="password"]:focus,
66
-
input[type="url"]:focus,
67
-
select:focus,
68
-
textarea:focus {
69
-
outline: none;
70
-
border-color: #0085ff;
71
-
}
72
-
73
-
select {
74
-
cursor: pointer;
75
-
}
76
-
77
-
textarea {
78
-
resize: vertical;
79
-
min-height: 100px;
80
-
}
81
-
82
-
input[type="checkbox"] {
83
-
margin-right: 8px;
84
-
}
85
-
86
-
fieldset {
87
-
border: 1px solid #ddd;
88
-
border-radius: 4px;
89
-
padding: 20px;
90
-
margin-bottom: 24px;
91
-
}
92
-
93
-
legend {
94
-
font-weight: 600;
95
-
color: #333;
96
-
padding: 0 8px;
97
-
}
98
-
99
-
small {
100
-
display: block;
101
-
margin-top: 4px;
102
-
color: #666;
103
-
font-size: 12px;
104
-
}
105
-
106
-
button[type="submit"] {
107
-
width: 100%;
108
-
padding: 14px;
109
-
background: #0085ff;
110
-
color: white;
111
-
border: none;
112
-
border-radius: 4px;
113
-
font-size: 16px;
114
-
font-weight: 500;
115
-
cursor: pointer;
116
-
transition: background 0.2s;
117
-
}
118
-
119
-
button[type="submit"]:hover {
120
-
background: #0070dd;
121
-
}
122
-
123
-
button[type="submit"]:disabled {
124
-
background: #ccc;
125
-
cursor: not-allowed;
126
-
}
127
-
128
-
.result {
129
-
margin-top: 24px;
130
-
padding: 16px;
131
-
border-radius: 4px;
132
-
}
133
-
134
-
.result.hidden {
135
-
display: none;
136
-
}
137
-
138
-
.result.success {
139
-
background: #d4edda;
140
-
color: #155724;
141
-
border: 1px solid #c3e6cb;
142
-
}
143
-
144
-
.result.error {
145
-
background: #f8d7da;
146
-
color: #721c24;
147
-
border: 1px solid #f5c6cb;
148
-
}
+1
-1
deno.json
+1
-1
deno.json
···
6
6
"exports": "./mod.ts",
7
7
"tasks": {
8
8
"dev": "deno run --watch --allow-net --allow-read client/main.ts",
9
+
"publish:client": "npx wrangler deploy",
9
10
"publish:lexicon": "deno run --env --allow-net --allow-read --allow-env scripts/publish_lexicon.ts"
10
11
},
11
12
"imports": {
12
13
"@atproto/api": "npm:@atproto/api"
13
14
},
14
-
"unstable": ["jsr"],
15
15
"compilerOptions": {
16
16
"lib": ["deno.ns", "deno.window", "deno.worker"]
17
17
},
+11
worker.js
+11
worker.js