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

Add Stylebase CSS and update Worker config

+381 -309
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 - <script type="importmap"> 8 - { 9 - "imports": { 10 - "@atproto/api": "https://esm.sh/@atproto/api@0.13.11" 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 + <script type="importmap"> 9 + { 10 + "imports": { 11 + "@atproto/api": "https://esm.sh/@atproto/api@0.13.11" 12 + } 11 13 } 12 - } 13 - </script> 14 - </head> 15 - <body> 16 - <header> 17 - <h1>Autonomy Declaration</h1> 18 - <p>Declare automation and AI usage for transparency and accountability on Bluesky</p> 19 - </header> 14 + </script> 15 + </head> 16 + <body> 17 + <header class="l:root"> 18 + <h1>Autonomy Declaration</h1> 19 + <p> 20 + Declare automation and AI usage for transparency and accountability on 21 + Bluesky 22 + </p> 23 + </header> 20 24 21 - <main> 22 - <form id="declaration-form" novalidate> 23 - <section> 24 - <h2>Authentication</h2> 25 + <main class="l:root"> 26 + <form id="declaration-form" class="l:waterfall" novalidate> 27 + <section> 28 + <h2>Authentication</h2> 25 29 26 - <div> 27 - <label for="handle">Bluesky Handle (required)</label> 28 - <input 29 - type="text" 30 - id="handle" 31 - name="handle" 32 - placeholder="username.bsky.social" 33 - required 34 - autocomplete="username" 35 - /> 36 - <small>Your Bluesky handle or DID</small> 37 - <p id="handle-error" role="alert" hidden></p> 38 - </div> 30 + <div> 31 + <label for="handle">Bluesky Handle (required)</label> 32 + <input 33 + type="text" 34 + id="handle" 35 + name="handle" 36 + placeholder="username.bsky.social" 37 + required 38 + autocomplete="username" 39 + /> 40 + <small>Your Bluesky handle or DID</small> 41 + <p id="handle-error" role="alert" hidden></p> 42 + </div> 39 43 40 - <div> 41 - <label for="password">App Password (required)</label> 42 - <input 43 - type="password" 44 - id="password" 45 - name="password" 46 - placeholder="xxxx-xxxx-xxxx-xxxx" 47 - required 48 - autocomplete="current-password" 49 - pattern=".{19,19}" 50 - /> 51 - <small>19 characters with 3 dashes (generate in Bluesky settings)</small> 52 - <p id="password-error" role="alert" hidden></p> 53 - </div> 54 - </section> 44 + <div> 45 + <label for="password">App Password (required)</label> 46 + <input 47 + type="password" 48 + id="password" 49 + name="password" 50 + placeholder="xxxx-xxxx-xxxx-xxxx" 51 + required 52 + autocomplete="current-password" 53 + pattern=".{19,19}" 54 + /> 55 + <small 56 + >19 characters with 3 dashes (generate in Bluesky settings)</small 57 + > 58 + <p id="password-error" role="alert" hidden></p> 59 + </div> 60 + </section> 55 61 56 - <section> 57 - <h2>Declaration Details</h2> 62 + <section> 63 + <h2>Declaration Details</h2> 58 64 59 - <div> 60 - <label for="automationLevel">Automation Level</label> 61 - <select id="automationLevel" name="automationLevel"> 62 - <option value="">Select level (optional)</option> 63 - <option value="human">Human - No automation</option> 64 - <option value="assisted">Assisted - Tools help but human decides</option> 65 - <option value="collaborative">Collaborative - Human and AI work together</option> 66 - <option value="automated">Automated - Primarily AI-driven</option> 67 - </select> 68 - <small>Level of automation in account management and content creation</small> 69 - </div> 65 + <div> 66 + <label for="automationLevel">Automation Level</label> 67 + <select id="automationLevel" name="automationLevel"> 68 + <option value="">Select level (optional)</option> 69 + <option value="human">Human - No automation</option> 70 + <option value="assisted"> 71 + Assisted - Tools help but human decides 72 + </option> 73 + <option value="collaborative"> 74 + Collaborative - Human and AI work together 75 + </option> 76 + <option value="automated">Automated - Primarily AI-driven</option> 77 + </select> 78 + <small 79 + >Level of automation in account management and content 80 + creation</small 81 + > 82 + </div> 70 83 71 - <div> 72 - <label> 73 - <input type="checkbox" id="usesGenerativeAI" name="usesGenerativeAI" /> 74 - Uses Generative AI 75 - </label> 76 - <small>Check if this account uses LLMs, image generation, etc.</small> 77 - </div> 84 + <div> 85 + <label> 86 + <input 87 + type="checkbox" 88 + id="usesGenerativeAI" 89 + name="usesGenerativeAI" 90 + /> 91 + Uses Generative AI 92 + </label> 93 + <small 94 + >Check if this account uses LLMs, image generation, etc.</small 95 + > 96 + </div> 78 97 79 - <div> 80 - <label for="description">Description</label> 81 - <textarea 82 - id="description" 83 - name="description" 84 - rows="4" 85 - maxlength="300" 86 - placeholder="Explain how this account is automated and what it does..." 87 - ></textarea> 88 - <small>Plain language explanation (max 300 characters, optional)</small> 89 - <p id="description-error" role="alert" hidden></p> 90 - </div> 91 - </section> 98 + <div> 99 + <label for="description">Description</label> 100 + <textarea 101 + id="description" 102 + name="description" 103 + rows="4" 104 + maxlength="300" 105 + placeholder="Explain how this account is automated and what it does..." 106 + ></textarea> 107 + <small 108 + >Plain language explanation (max 300 characters, optional)</small 109 + > 110 + <p id="description-error" role="alert" hidden></p> 111 + </div> 112 + </section> 92 113 93 - <fieldset> 94 - <legend>Responsible Party (optional)</legend> 114 + <fieldset> 115 + <legend>Responsible Party (optional)</legend> 95 116 96 - <div> 97 - <label for="responsibleType">Type</label> 98 - <select id="responsibleType" name="responsibleType"> 99 - <option value="">Select type (optional)</option> 100 - <option value="person">Person</option> 101 - <option value="organization">Organization</option> 102 - </select> 103 - </div> 104 - 105 - <div> 106 - <label for="responsibleName">Name</label> 107 - <input 108 - type="text" 109 - id="responsibleName" 110 - name="responsibleName" 111 - maxlength="100" 112 - placeholder="Name of person or organization" 113 - /> 114 - <p id="responsibleName-error" role="alert" hidden></p> 115 - </div> 117 + <div> 118 + <label for="responsibleType">Type</label> 119 + <select id="responsibleType" name="responsibleType"> 120 + <option value="">Select type (optional)</option> 121 + <option value="person">Person</option> 122 + <option value="organization">Organization</option> 123 + </select> 124 + </div> 116 125 117 - <div> 118 - <label for="responsibleContact">Contact</label> 119 - <input 120 - type="text" 121 - id="responsibleContact" 122 - name="responsibleContact" 123 - maxlength="300" 124 - placeholder="Email, URL, handle, or DID" 125 - /> 126 - <p id="responsibleContact-error" role="alert" hidden></p> 127 - </div> 126 + <div> 127 + <label for="responsibleName">Name</label> 128 + <input 129 + type="text" 130 + id="responsibleName" 131 + name="responsibleName" 132 + maxlength="100" 133 + placeholder="Name of person or organization" 134 + /> 135 + <p id="responsibleName-error" role="alert" hidden></p> 136 + </div> 128 137 129 - <div> 130 - <label for="responsibleDid">DID</label> 131 - <input 132 - type="text" 133 - id="responsibleDid" 134 - name="responsibleDid" 135 - placeholder="did:plc:..." 136 - /> 137 - <small>ATProto DID if they have an identity</small> 138 - <p id="responsibleDid-error" role="alert" hidden></p> 139 - </div> 140 - </fieldset> 138 + <div> 139 + <label for="responsibleContact">Contact</label> 140 + <input 141 + type="text" 142 + id="responsibleContact" 143 + name="responsibleContact" 144 + maxlength="300" 145 + placeholder="Email, URL, handle, or DID" 146 + /> 147 + <p id="responsibleContact-error" role="alert" hidden></p> 148 + </div> 141 149 142 - <section> 143 - <h2>Additional Information</h2> 150 + <div> 151 + <label for="responsibleDid">DID</label> 152 + <input 153 + type="text" 154 + id="responsibleDid" 155 + name="responsibleDid" 156 + placeholder="did:plc:..." 157 + /> 158 + <small>ATProto DID if they have an identity</small> 159 + <p id="responsibleDid-error" role="alert" hidden></p> 160 + </div> 161 + </fieldset> 144 162 145 - <div> 146 - <label for="disclosureUrl">Disclosure URL</label> 147 - <input 148 - type="url" 149 - id="disclosureUrl" 150 - name="disclosureUrl" 151 - placeholder="https://..." 152 - /> 153 - <small>URL with additional information about automation (optional)</small> 154 - <p id="disclosureUrl-error" role="alert" hidden></p> 155 - </div> 163 + <section> 164 + <h2>Additional Information</h2> 156 165 157 - <div> 158 - <label for="externalServices">External Services</label> 159 - <textarea 160 - id="externalServices" 161 - name="externalServices" 162 - rows="5" 163 - placeholder="One service per line, e.g.:&#10;Letta&#10;Railway&#10;Google Gemini 2.5-pro" 164 - ></textarea> 165 - <small>External tools and services used (one per line, max 20 services, 200 chars each)</small> 166 - <p id="externalServices-error" role="alert" hidden></p> 167 - </div> 168 - </section> 166 + <div> 167 + <label for="disclosureUrl">Disclosure URL</label> 168 + <input 169 + type="url" 170 + id="disclosureUrl" 171 + name="disclosureUrl" 172 + placeholder="https://..." 173 + /> 174 + <small 175 + >URL with additional information about automation 176 + (optional)</small 177 + > 178 + <p id="disclosureUrl-error" role="alert" hidden></p> 179 + </div> 169 180 170 - <button type="submit" id="submit-btn">Submit Declaration</button> 171 - </form> 181 + <div> 182 + <label for="externalServices">External Services</label> 183 + <textarea 184 + id="externalServices" 185 + name="externalServices" 186 + rows="5" 187 + placeholder="One service per line, e.g.:&#10;Letta&#10;Railway&#10;Google Gemini 2.5-pro" 188 + ></textarea> 189 + <small 190 + >External tools and services used (one per line, max 20 services, 191 + 200 chars each)</small 192 + > 193 + <p id="externalServices-error" role="alert" hidden></p> 194 + </div> 195 + </section> 172 196 173 - <div id="result" role="status" aria-live="polite" hidden></div> 174 - </main> 197 + <button type="submit" id="submit-btn">Submit Declaration</button> 198 + </form> 175 199 176 - <footer> 177 - <p> 178 - This form creates a record in your Personal Data Server (PDS) using the 179 - <code>studio.voyager.account.autonomy</code> lexicon. 180 - </p> 181 - </footer> 200 + <div id="result" role="status" aria-live="polite" hidden></div> 201 + </main> 182 202 183 - <script type="module"> 184 - import { BskyAgent } from '@atproto/api'; 203 + <footer class="l:root"> 204 + <p> 205 + This form creates a record in your Personal Data Server (PDS) using the 206 + <code>studio.voyager.account.autonomy</code> lexicon. 207 + </p> 208 + </footer> 185 209 186 - const form = document.getElementById('declaration-form'); 187 - const submitBtn = document.getElementById('submit-btn'); 188 - const resultDiv = document.getElementById('result'); 210 + <script type="module"> 211 + import { BskyAgent } from "@atproto/api"; 189 212 190 - const validators = { 191 - handle: (value) => { 192 - if (!value) return 'Handle is required'; 193 - if (!value.includes('.') && !value.startsWith('did:')) { 194 - return 'Handle must be a valid Bluesky handle (e.g., username.bsky.social) or DID'; 195 - } 196 - return null; 197 - }, 198 - password: (value) => { 199 - if (!value) return 'App password is required'; 200 - if (value.length !== 19) return 'App password must be exactly 19 characters'; 201 - const dashCount = (value.match(/-/g) || []).length; 202 - if (dashCount !== 3) return 'App password must contain exactly 3 dashes'; 203 - return null; 204 - }, 205 - description: (value) => value && value.length > 300 ? 'Description must be 300 characters or less' : null, 206 - responsibleName: (value) => value && value.length > 100 ? 'Name must be 100 characters or less' : null, 207 - responsibleContact: (value) => value && value.length > 300 ? 'Contact must be 300 characters or less' : null, 208 - responsibleDid: (value) => value && !value.startsWith('did:') ? 'DID must start with "did:"' : null, 209 - disclosureUrl: (value) => { 210 - if (value) { 211 - try { new URL(value); } catch { return 'Must be a valid URL'; } 212 - } 213 - return null; 214 - }, 215 - externalServices: (value) => { 216 - if (value) { 217 - const services = value.split('\n').map(s => s.trim()).filter(s => s.length > 0); 218 - if (services.length > 20) return 'Maximum 20 external services allowed'; 219 - for (const service of services) { 220 - if (service.length > 200) return `Service name too long (max 200 chars): ${service.substring(0, 50)}...`; 221 - } 222 - } 223 - return null; 224 - } 225 - }; 213 + const form = document.getElementById("declaration-form"); 214 + const submitBtn = document.getElementById("submit-btn"); 215 + const resultDiv = document.getElementById("result"); 226 216 227 - function showError(fieldId, message) { 228 - const errorElement = document.getElementById(`${fieldId}-error`); 229 - if (errorElement) { 230 - errorElement.textContent = message; 231 - errorElement.hidden = false; 232 - } 233 - } 217 + const validators = { 218 + handle: (value) => { 219 + if (!value) return "Handle is required"; 220 + if (!value.includes(".") && !value.startsWith("did:")) { 221 + return "Handle must be a valid Bluesky handle (e.g., username.bsky.social) or DID"; 222 + } 223 + return null; 224 + }, 225 + password: (value) => { 226 + if (!value) return "App password is required"; 227 + if (value.length !== 19) 228 + return "App password must be exactly 19 characters"; 229 + const dashCount = (value.match(/-/g) || []).length; 230 + if (dashCount !== 3) 231 + return "App password must contain exactly 3 dashes"; 232 + return null; 233 + }, 234 + description: (value) => 235 + value && value.length > 300 236 + ? "Description must be 300 characters or less" 237 + : null, 238 + responsibleName: (value) => 239 + value && value.length > 100 240 + ? "Name must be 100 characters or less" 241 + : null, 242 + responsibleContact: (value) => 243 + value && value.length > 300 244 + ? "Contact must be 300 characters or less" 245 + : null, 246 + responsibleDid: (value) => 247 + value && !value.startsWith("did:") 248 + ? 'DID must start with "did:"' 249 + : null, 250 + disclosureUrl: (value) => { 251 + if (value) { 252 + try { 253 + new URL(value); 254 + } catch { 255 + return "Must be a valid URL"; 256 + } 257 + } 258 + return null; 259 + }, 260 + externalServices: (value) => { 261 + if (value) { 262 + const services = value 263 + .split("\n") 264 + .map((s) => s.trim()) 265 + .filter((s) => s.length > 0); 266 + if (services.length > 20) 267 + return "Maximum 20 external services allowed"; 268 + for (const service of services) { 269 + if (service.length > 200) 270 + return `Service name too long (max 200 chars): ${service.substring(0, 50)}...`; 271 + } 272 + } 273 + return null; 274 + }, 275 + }; 234 276 235 - function clearError(fieldId) { 236 - const errorElement = document.getElementById(`${fieldId}-error`); 237 - if (errorElement) { 238 - errorElement.textContent = ''; 239 - errorElement.hidden = true; 240 - } 241 - } 277 + function showError(fieldId, message) { 278 + const errorElement = document.getElementById(`${fieldId}-error`); 279 + if (errorElement) { 280 + errorElement.textContent = message; 281 + errorElement.hidden = false; 282 + } 283 + } 242 284 243 - function clearAllErrors() { 244 - ['handle', 'password', 'description', 'responsibleName', 'responsibleContact', 'responsibleDid', 'disclosureUrl', 'externalServices'].forEach(clearError); 245 - } 285 + function clearError(fieldId) { 286 + const errorElement = document.getElementById(`${fieldId}-error`); 287 + if (errorElement) { 288 + errorElement.textContent = ""; 289 + errorElement.hidden = true; 290 + } 291 + } 246 292 247 - function validateForm(formData) { 248 - clearAllErrors(); 249 - let isValid = true; 250 - for (const [fieldId, validator] of Object.entries(validators)) { 251 - const value = formData.get(fieldId) || ''; 252 - const error = validator(value); 253 - if (error) { 254 - console.log('Validation error for', fieldId, ':', error); 255 - showError(fieldId, error); 256 - isValid = false; 257 - } 258 - } 259 - return isValid; 260 - } 293 + function clearAllErrors() { 294 + [ 295 + "handle", 296 + "password", 297 + "description", 298 + "responsibleName", 299 + "responsibleContact", 300 + "responsibleDid", 301 + "disclosureUrl", 302 + "externalServices", 303 + ].forEach(clearError); 304 + } 261 305 262 - form.addEventListener('submit', async (e) => { 263 - e.preventDefault(); 264 - const formData = new FormData(form); 306 + function validateForm(formData) { 307 + clearAllErrors(); 308 + let isValid = true; 309 + for (const [fieldId, validator] of Object.entries(validators)) { 310 + const value = formData.get(fieldId) || ""; 311 + const error = validator(value); 312 + if (error) { 313 + console.log("Validation error for", fieldId, ":", error); 314 + showError(fieldId, error); 315 + isValid = false; 316 + } 317 + } 318 + return isValid; 319 + } 265 320 266 - if (!validateForm(formData)) { 267 - resultDiv.textContent = 'Please fix the errors above before submitting.'; 268 - resultDiv.hidden = false; 269 - return; 270 - } 321 + form.addEventListener("submit", async (e) => { 322 + e.preventDefault(); 323 + const formData = new FormData(form); 271 324 272 - submitBtn.disabled = true; 273 - submitBtn.textContent = 'Submitting...'; 274 - resultDiv.hidden = true; 325 + if (!validateForm(formData)) { 326 + resultDiv.textContent = 327 + "Please fix the errors above before submitting."; 328 + resultDiv.hidden = false; 329 + return; 330 + } 275 331 276 - try { 277 - const agent = new BskyAgent({ service: 'https://bsky.social' }); 278 - await agent.login({ 279 - identifier: formData.get('handle').trim(), 280 - password: formData.get('password') 281 - }); 332 + submitBtn.disabled = true; 333 + submitBtn.textContent = "Submitting..."; 334 + resultDiv.hidden = true; 282 335 283 - const record = { 284 - $type: 'studio.voyager.account.autonomy', 285 - createdAt: new Date().toISOString() 286 - }; 336 + try { 337 + const agent = new BskyAgent({ service: "https://bsky.social" }); 338 + await agent.login({ 339 + identifier: formData.get("handle").trim(), 340 + password: formData.get("password"), 341 + }); 287 342 288 - const automationLevel = formData.get('automationLevel'); 289 - const usesGenerativeAI = formData.get('usesGenerativeAI') === 'on'; 290 - const description = formData.get('description').trim(); 291 - const responsibleType = formData.get('responsibleType'); 292 - const responsibleName = formData.get('responsibleName').trim(); 293 - const responsibleContact = formData.get('responsibleContact').trim(); 294 - const responsibleDid = formData.get('responsibleDid').trim(); 295 - const disclosureUrl = formData.get('disclosureUrl').trim(); 296 - const externalServicesText = formData.get('externalServices').trim(); 343 + const record = { 344 + $type: "studio.voyager.account.autonomy", 345 + createdAt: new Date().toISOString(), 346 + }; 297 347 298 - if (automationLevel) record.automationLevel = automationLevel; 299 - if (usesGenerativeAI) record.usesGenerativeAI = true; 300 - if (description) record.description = description; 348 + const automationLevel = formData.get("automationLevel"); 349 + const usesGenerativeAI = formData.get("usesGenerativeAI") === "on"; 350 + const description = formData.get("description").trim(); 351 + const responsibleType = formData.get("responsibleType"); 352 + const responsibleName = formData.get("responsibleName").trim(); 353 + const responsibleContact = formData.get("responsibleContact").trim(); 354 + const responsibleDid = formData.get("responsibleDid").trim(); 355 + const disclosureUrl = formData.get("disclosureUrl").trim(); 356 + const externalServicesText = formData.get("externalServices").trim(); 301 357 302 - if (responsibleType || responsibleName || responsibleContact || responsibleDid) { 303 - record.responsibleParty = {}; 304 - if (responsibleType) record.responsibleParty.type = responsibleType; 305 - if (responsibleName) record.responsibleParty.name = responsibleName; 306 - if (responsibleContact) record.responsibleParty.contact = responsibleContact; 307 - if (responsibleDid) record.responsibleParty.did = responsibleDid; 308 - } 358 + if (automationLevel) record.automationLevel = automationLevel; 359 + if (usesGenerativeAI) record.usesGenerativeAI = true; 360 + if (description) record.description = description; 309 361 310 - if (disclosureUrl) record.disclosureUrl = disclosureUrl; 362 + if ( 363 + responsibleType || 364 + responsibleName || 365 + responsibleContact || 366 + responsibleDid 367 + ) { 368 + record.responsibleParty = {}; 369 + if (responsibleType) record.responsibleParty.type = responsibleType; 370 + if (responsibleName) record.responsibleParty.name = responsibleName; 371 + if (responsibleContact) 372 + record.responsibleParty.contact = responsibleContact; 373 + if (responsibleDid) record.responsibleParty.did = responsibleDid; 374 + } 311 375 312 - if (externalServicesText) { 313 - const services = externalServicesText.split('\n').map(s => s.trim()).filter(s => s.length > 0).slice(0, 20); 314 - if (services.length > 0) record.externalServices = services; 315 - } 376 + if (disclosureUrl) record.disclosureUrl = disclosureUrl; 316 377 317 - const response = await agent.api.com.atproto.repo.putRecord({ 318 - repo: agent.session?.did || '', 319 - collection: 'studio.voyager.account.autonomy', 320 - rkey: 'self', 321 - record: record 322 - }); 378 + if (externalServicesText) { 379 + const services = externalServicesText 380 + .split("\n") 381 + .map((s) => s.trim()) 382 + .filter((s) => s.length > 0) 383 + .slice(0, 20); 384 + if (services.length > 0) record.externalServices = services; 385 + } 323 386 324 - resultDiv.textContent = `Declaration submitted successfully! Record URI: ${response.data.uri}`; 325 - resultDiv.hidden = false; 326 - form.reset(); 387 + const response = await agent.api.com.atproto.repo.putRecord({ 388 + repo: agent.session?.did || "", 389 + collection: "studio.voyager.account.autonomy", 390 + rkey: "self", 391 + record: record, 392 + }); 327 393 328 - } catch (error) { 329 - let errorMessage = 'Error: '; 330 - if (error.message.includes('Invalid identifier or password')) { 331 - errorMessage += 'Invalid handle or app password. Please check your credentials.'; 332 - } else if (error.message.includes('Network')) { 333 - errorMessage += 'Network error. Please check your connection and try again.'; 334 - } else if (error.message.includes('AuthenticationRequired')) { 335 - errorMessage += 'Authentication failed. Please verify your app password.'; 336 - } else { 337 - errorMessage += error.message; 338 - } 339 - resultDiv.textContent = errorMessage; 340 - resultDiv.hidden = false; 341 - console.error('Submission error:', error); 342 - } finally { 343 - submitBtn.disabled = false; 344 - submitBtn.textContent = 'Submit Declaration'; 345 - } 346 - }); 347 - </script> 348 - </body> 394 + resultDiv.textContent = `Declaration submitted successfully! Record URI: ${response.data.uri}`; 395 + resultDiv.hidden = false; 396 + form.reset(); 397 + } catch (error) { 398 + let errorMessage = "Error: "; 399 + if (error.message.includes("Invalid identifier or password")) { 400 + errorMessage += 401 + "Invalid handle or app password. Please check your credentials."; 402 + } else if (error.message.includes("Network")) { 403 + errorMessage += 404 + "Network error. Please check your connection and try again."; 405 + } else if (error.message.includes("AuthenticationRequired")) { 406 + errorMessage += 407 + "Authentication failed. Please verify your app password."; 408 + } else { 409 + errorMessage += error.message; 410 + } 411 + resultDiv.textContent = errorMessage; 412 + resultDiv.hidden = false; 413 + console.error("Submission error:", error); 414 + } finally { 415 + submitBtn.disabled = false; 416 + submitBtn.textContent = "Submit Declaration"; 417 + } 418 + }); 419 + </script> 420 + </body> 349 421 </html>
+1
client/styles.css
··· 1 + @import "https://unpkg.com/@taurean/stylebase@0.11.0/dist/stylebase.min.css";
+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": {
+11
worker.js
··· 1 1 import htmlContent from './client/index.html'; 2 + import cssContent from './client/styles.css'; 2 3 3 4 export default { 4 5 async fetch(request) { 6 + const url = new URL(request.url); 7 + 8 + if (url.pathname === '/styles.css') { 9 + return new Response(cssContent, { 10 + headers: { 11 + 'content-type': 'text/css;charset=UTF-8', 12 + }, 13 + }); 14 + } 15 + 5 16 return new Response(htmlContent, { 6 17 headers: { 7 18 'content-type': 'text/html;charset=UTF-8',
+5
wrangler.toml
··· 5 5 [[rules]] 6 6 type = "Text" 7 7 globs = ["**/*.html"] 8 + fallthrough = true 9 + 10 + [[rules]] 11 + type = "Text" 12 + globs = ["**/*.css"] 8 13 fallthrough = false