a web app for declaring if an atproto account is automated + package for the lexicon jsr.io/@voyager/autonomy-lexicon
atprotocol lexicon

Compare changes

Choose any two refs to compare.

+3
.gitignore
··· 1 1 # Deno 2 2 .deno/ 3 3 4 + # Cloudflare 5 + .wrangler/* 6 + 4 7 # NPM build output 5 8 npm/ 6 9
+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
··· 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.:&#10;Letta&#10;Railway&#10;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
··· 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
··· 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
··· 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
··· 1 + import htmlContent from './client/index.html'; 2 + 3 + export default { 4 + async fetch(request) { 5 + return new Response(htmlContent, { 6 + headers: { 7 + 'content-type': 'text/html;charset=UTF-8', 8 + }, 9 + }); 10 + }, 11 + };
+8
wrangler.toml
··· 1 + name = "autonomy-declaration" 2 + main = "worker.js" 3 + compatibility_date = "2024-01-01" 4 + 5 + [[rules]] 6 + type = "Text" 7 + globs = ["**/*.html"] 8 + fallthrough = false