Highly ambitious ATProtocol AppView service and sdks

actually fix all deno lint and check errors

+119 -442
+2
deno.lock
··· 3 "specifiers": { 4 "jsr:@shikijs/shiki@*": "3.7.0", 5 "jsr:@slices/client@~0.1.0-alpha.3": "0.1.0-alpha.3", 6 "jsr:@std/assert@^1.0.14": "1.0.14", 7 "jsr:@std/cli@^1.0.21": "1.0.22", 8 "jsr:@std/cli@^1.0.22": "1.0.22", ··· 42 "dependencies": [ 43 "npm:@shikijs/core", 44 "npm:@shikijs/engine-oniguruma", 45 "npm:shiki" 46 ] 47 },
··· 3 "specifiers": { 4 "jsr:@shikijs/shiki@*": "3.7.0", 5 "jsr:@slices/client@~0.1.0-alpha.3": "0.1.0-alpha.3", 6 + "jsr:@std/assert@*": "1.0.14", 7 "jsr:@std/assert@^1.0.14": "1.0.14", 8 "jsr:@std/cli@^1.0.21": "1.0.22", 9 "jsr:@std/cli@^1.0.22": "1.0.22", ··· 43 "dependencies": [ 44 "npm:@shikijs/core", 45 "npm:@shikijs/engine-oniguruma", 46 + "npm:@shikijs/types", 47 "npm:shiki" 48 ] 49 },
-390
frontend/src/client.test.ts
··· 1 - /** 2 - * OAuth Token Refresh Test 3 - * 4 - * Tests various OAuth token scenarios to understand what happens 5 - * when tokens expire and need to be refreshed. 6 - * 7 - * Run with: deno test client.test.ts --allow-all 8 - */ 9 - 10 - import { assertRejects } from "@std/assert"; 11 - import { AtProtoClient } from "./client.ts"; 12 - 13 - // Store original fetch to restore later 14 - const originalFetch = globalThis.fetch; 15 - 16 - // Mock OAuth client to simulate different token states 17 - class MockOAuthClient { 18 - private tokenState: "valid" | "expired" | "refresh_fails" | "no_tokens"; 19 - private callCount = 0; 20 - 21 - constructor( 22 - initialState: "valid" | "expired" | "refresh_fails" | "no_tokens", 23 - ) { 24 - this.tokenState = initialState; 25 - } 26 - 27 - ensureValidToken() { 28 - this.callCount++; 29 - console.log( 30 - `MockOAuth.ensureValidToken() called (attempt ${this.callCount})`, 31 - ); 32 - 33 - switch (this.tokenState) { 34 - case "valid": 35 - console.log("Returning valid tokens"); 36 - return Promise.resolve({ 37 - accessToken: "valid_access_token_12345", 38 - tokenType: "Bearer", 39 - }); 40 - 41 - case "expired": 42 - if (this.callCount === 1) { 43 - console.log("First call - tokens expired, attempting refresh..."); 44 - // Simulate successful refresh on first call 45 - this.tokenState = "valid"; 46 - return Promise.resolve({ 47 - accessToken: "refreshed_access_token_67890", 48 - tokenType: "Bearer", 49 - }); 50 - } else { 51 - console.log("Subsequent call - returning refreshed tokens"); 52 - return Promise.resolve({ 53 - accessToken: "refreshed_access_token_67890", 54 - tokenType: "Bearer", 55 - }); 56 - } 57 - 58 - case "refresh_fails": 59 - console.log("Token refresh failed"); 60 - throw new Error("OAuth token refresh failed: invalid_grant"); 61 - 62 - case "no_tokens": 63 - console.log("No tokens available"); 64 - throw new Error("No OAuth tokens found"); 65 - 66 - default: 67 - throw new Error("Unknown token state"); 68 - } 69 - } 70 - 71 - isAuthenticated() { 72 - return Promise.resolve(this.tokenState === "valid"); 73 - } 74 - } 75 - 76 - // Mock fetch function that simulates API responses 77 - function createMockFetch() { 78 - return ( 79 - url: string | URL | Request, 80 - init?: RequestInit, 81 - ): Promise<Response> => { 82 - const requestUrl = url.toString(); 83 - const method = init?.method || "GET"; 84 - const headers = (init?.headers as Record<string, string>) || {}; 85 - 86 - console.log(`Mock fetch: ${method} ${requestUrl}`); 87 - console.log(`Authorization header: ${headers["Authorization"] || "none"}`); 88 - 89 - // Check if request has valid authorization 90 - const hasAuth = headers["Authorization"]?.startsWith("Bearer valid") || 91 - headers["Authorization"]?.startsWith("Bearer refreshed"); 92 - 93 - if (method === "GET") { 94 - // GET requests always succeed (read-only operations) 95 - return new Response( 96 - JSON.stringify({ 97 - records: [ 98 - { 99 - uri: "test://uri", 100 - cid: "test-cid", 101 - value: { name: "Test Record" }, 102 - }, 103 - ], 104 - }), 105 - { 106 - status: 200, 107 - headers: { "Content-Type": "application/json" }, 108 - }, 109 - ); 110 - } else { 111 - // POST/PUT/DELETE require valid auth 112 - if (hasAuth) { 113 - return new Response( 114 - JSON.stringify({ 115 - uri: "test://created-uri", 116 - cid: "test-created-cid", 117 - }), 118 - { 119 - status: 200, 120 - headers: { "Content-Type": "application/json" }, 121 - }, 122 - ); 123 - } else { 124 - return new Response( 125 - JSON.stringify({ 126 - error: "Unauthorized", 127 - }), 128 - { 129 - status: 401, 130 - headers: { "Content-Type": "application/json" }, 131 - }, 132 - ); 133 - } 134 - } 135 - }; 136 - } 137 - 138 - function createTestClient( 139 - tokenState: "valid" | "expired" | "refresh_fails" | "no_tokens", 140 - ) { 141 - const mockOAuth = new MockOAuthClient(tokenState); 142 - return new AtProtoClient( 143 - "https://test-api.example.com", 144 - "at://did:plc:test/network.slices.slice/test", 145 - mockOAuth as unknown as Parameters<typeof AtProtoClient>[2], 146 - ); 147 - } 148 - 149 - Deno.test("Valid tokens - read operation should succeed", async () => { 150 - // Setup mock fetch 151 - globalThis.fetch = createMockFetch(); 152 - 153 - try { 154 - const client = createTestClient("valid"); 155 - await client.network.slices.slice.getRecords(); 156 - console.log("Read operation succeeded as expected"); 157 - } catch (error) { 158 - throw new Error(`Read operation should have succeeded: ${error}`); 159 - } finally { 160 - // Restore original fetch 161 - globalThis.fetch = originalFetch; 162 - } 163 - }); 164 - 165 - Deno.test("Valid tokens - write operation should succeed", async () => { 166 - // Setup mock fetch 167 - globalThis.fetch = createMockFetch(); 168 - 169 - try { 170 - const client = createTestClient("valid"); 171 - await client.network.slices.slice.updateRecord("test", { 172 - name: "Test Slice", 173 - domain: "network.slices", 174 - createdAt: new Date().toISOString(), 175 - }); 176 - console.log("Write operation succeeded as expected"); 177 - } catch (error) { 178 - throw new Error(`Write operation should have succeeded: ${error}`); 179 - } finally { 180 - // Restore original fetch 181 - globalThis.fetch = originalFetch; 182 - } 183 - }); 184 - 185 - Deno.test( 186 - "Expired tokens (refreshable) - read operation should succeed", 187 - async () => { 188 - // Setup mock fetch 189 - globalThis.fetch = createMockFetch(); 190 - 191 - try { 192 - const client = createTestClient("expired"); 193 - await client.network.slices.slice.getRecords(); 194 - console.log("Read operation succeeded after token refresh"); 195 - } catch (error) { 196 - throw new Error( 197 - `Read operation should have succeeded after refresh: ${error}`, 198 - ); 199 - } finally { 200 - // Restore original fetch 201 - globalThis.fetch = originalFetch; 202 - } 203 - }, 204 - ); 205 - 206 - Deno.test( 207 - "Expired tokens (refreshable) - write operation should succeed", 208 - async () => { 209 - // Setup mock fetch 210 - globalThis.fetch = createMockFetch(); 211 - 212 - try { 213 - const client = createTestClient("expired"); 214 - await client.network.slices.slice.updateRecord("test", { 215 - name: "Test Slice", 216 - domain: "network.slices", 217 - createdAt: new Date().toISOString(), 218 - }); 219 - console.log("Write operation succeeded after token refresh"); 220 - } catch (error) { 221 - throw new Error( 222 - `Write operation should have succeeded after refresh: ${error}`, 223 - ); 224 - } finally { 225 - // Restore original fetch 226 - globalThis.fetch = originalFetch; 227 - } 228 - }, 229 - ); 230 - 231 - Deno.test("Token refresh fails - read operation should succeed", async () => { 232 - // Setup mock fetch 233 - globalThis.fetch = createMockFetch(); 234 - 235 - try { 236 - const client = createTestClient("refresh_fails"); 237 - await client.network.slices.slice.getRecords(); 238 - console.log("Read operation succeeded without auth (as expected)"); 239 - } catch (error) { 240 - throw new Error( 241 - `Read operation should succeed even without auth: ${error}`, 242 - ); 243 - } finally { 244 - // Restore original fetch 245 - globalThis.fetch = originalFetch; 246 - } 247 - }); 248 - 249 - Deno.test( 250 - "Token refresh fails - write operation should fail with auth error", 251 - async () => { 252 - // Setup mock fetch 253 - globalThis.fetch = createMockFetch(); 254 - 255 - try { 256 - const client = createTestClient("refresh_fails"); 257 - await assertRejects( 258 - async () => { 259 - await client.network.slices.slice.updateRecord("test", { 260 - name: "Test Slice", 261 - domain: "network.slices", 262 - createdAt: new Date().toISOString(), 263 - }); 264 - }, 265 - Error, 266 - "Authentication required", 267 - "Write operation should fail with authentication error when tokens can't be refreshed", 268 - ); 269 - } finally { 270 - // Restore original fetch 271 - globalThis.fetch = originalFetch; 272 - } 273 - }, 274 - ); 275 - 276 - Deno.test("No tokens - read operation should succeed", async () => { 277 - // Setup mock fetch 278 - globalThis.fetch = createMockFetch(); 279 - 280 - try { 281 - const client = createTestClient("no_tokens"); 282 - await client.network.slices.slice.getRecords(); 283 - console.log("Read operation succeeded without tokens (as expected)"); 284 - } catch (error) { 285 - throw new Error( 286 - `Read operation should succeed even without tokens: ${error}`, 287 - ); 288 - } finally { 289 - // Restore original fetch 290 - globalThis.fetch = originalFetch; 291 - } 292 - }); 293 - 294 - Deno.test( 295 - "No tokens - write operation should fail with auth error", 296 - async () => { 297 - // Setup mock fetch 298 - globalThis.fetch = createMockFetch(); 299 - 300 - try { 301 - const client = createTestClient("no_tokens"); 302 - await assertRejects( 303 - async () => { 304 - await client.network.slices.slice.updateRecord("test", { 305 - name: "Test Slice", 306 - domain: "network.slices", 307 - createdAt: new Date().toISOString(), 308 - }); 309 - }, 310 - Error, 311 - "Authentication required", 312 - "Write operation should fail with authentication error when no tokens available", 313 - ); 314 - } finally { 315 - // Restore original fetch 316 - globalThis.fetch = originalFetch; 317 - } 318 - }, 319 - ); 320 - 321 - Deno.test("401 response triggers token refresh and retry", async () => { 322 - // Create a fetch that returns 401 first time, 200 second time 323 - let callCount = 0; 324 - const mockFetch = ( 325 - url: string | URL | Request, 326 - init?: RequestInit, 327 - ): Promise<Response> => { 328 - callCount++; 329 - const requestUrl = url.toString(); 330 - const method = init?.method || "GET"; 331 - const headers = (init?.headers as Record<string, string>) || {}; 332 - 333 - console.log(`Mock fetch call ${callCount}: ${method} ${requestUrl}`); 334 - console.log(`Authorization header: ${headers["Authorization"] || "none"}`); 335 - 336 - if (method === "POST") { 337 - if (callCount === 1) { 338 - // First call returns 401 339 - console.log("First call - returning 401 Unauthorized"); 340 - return Promise.resolve( 341 - new Response(JSON.stringify({ error: "Unauthorized" }), { 342 - status: 401, 343 - headers: { "Content-Type": "application/json" }, 344 - }), 345 - ); 346 - } else { 347 - // Second call should succeed (with refreshed token) 348 - console.log("Second call - returning success"); 349 - return Promise.resolve( 350 - new Response( 351 - JSON.stringify({ 352 - uri: "test://created-uri", 353 - cid: "test-created-cid", 354 - }), 355 - { 356 - status: 200, 357 - headers: { "Content-Type": "application/json" }, 358 - }, 359 - ), 360 - ); 361 - } 362 - } 363 - 364 - return Promise.resolve(new Response("Not found", { status: 404 })); 365 - }; 366 - 367 - globalThis.fetch = mockFetch; 368 - 369 - try { 370 - const client = createTestClient("valid"); 371 - await client.network.slices.slice.updateRecord("test", { 372 - name: "Test Slice", 373 - domain: "network.slices", 374 - createdAt: new Date().toISOString(), 375 - }); 376 - 377 - // Should have made exactly 2 calls 378 - console.log(`Total fetch calls made: ${callCount}`); 379 - if (callCount !== 2) { 380 - throw new Error(`Expected 2 fetch calls but got ${callCount}`); 381 - } 382 - 383 - console.log( 384 - "401 retry test passed - request was retried after token refresh", 385 - ); 386 - } finally { 387 - // Restore original fetch 388 - globalThis.fetch = originalFetch; 389 - } 390 - });
···
+8 -3
frontend/src/client.ts
··· 1 // Generated TypeScript client for AT Protocol records 2 - // Generated at: 2025-09-24 17:45:47 UTC 3 // Lexicons: 25 4 5 /** ··· 50 */ 51 52 import { 53 type BlobRef, 54 type CountRecordsResponse, 55 type GetActorsParams, ··· 2419 export class AtProtoClient extends SlicesClient { 2420 readonly app: AppClient; 2421 readonly network: NetworkClient; 2422 - readonly oauth?: OAuthClient; 2423 2424 - constructor(baseUrl: string, sliceUri: string, oauthClient?: OAuthClient) { 2425 super(baseUrl, sliceUri, oauthClient); 2426 this.app = new AppClient(this); 2427 this.network = new NetworkClient(this);
··· 1 // Generated TypeScript client for AT Protocol records 2 + // Generated at: 2025-09-24 17:50:48 UTC 3 // Lexicons: 25 4 5 /** ··· 50 */ 51 52 import { 53 + type AuthProvider, 54 type BlobRef, 55 type CountRecordsResponse, 56 type GetActorsParams, ··· 2420 export class AtProtoClient extends SlicesClient { 2421 readonly app: AppClient; 2422 readonly network: NetworkClient; 2423 + readonly oauth?: OAuthClient | AuthProvider; 2424 2425 + constructor( 2426 + baseUrl: string, 2427 + sliceUri: string, 2428 + oauthClient?: OAuthClient | AuthProvider, 2429 + ) { 2430 super(baseUrl, sliceUri, oauthClient); 2431 this.app = new AppClient(this); 2432 this.network = new NetworkClient(this);
+1
frontend/src/features/dashboard/templates/fragments/CreateSliceDialog.tsx
··· 62 <Button 63 type="button" 64 variant="secondary" 65 _="on click remove #create-slice-modal" 66 > 67 Cancel
··· 62 <Button 63 type="button" 64 variant="secondary" 65 + /* @ts-ignore - Hyperscript attribute */ 66 _="on click remove #create-slice-modal" 67 > 68 Cancel
+1
frontend/src/features/docs/templates/DocsPage.tsx
··· 34 id="docs-nav" 35 className="block w-full px-3 py-2 text-base border border-zinc-300 dark:border-zinc-600 bg-white dark:bg-zinc-800 text-zinc-900 dark:text-white rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-transparent" 36 value={currentSlug} 37 _="on change set window.location to `/docs/${me.value}`" 38 > 39 {docs.map((doc) => (
··· 34 id="docs-nav" 35 className="block w-full px-3 py-2 text-base border border-zinc-300 dark:border-zinc-600 bg-white dark:bg-zinc-800 text-zinc-900 dark:text-white rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-transparent" 36 value={currentSlug} 37 + /* @ts-ignore - Hyperscript attribute */ 38 _="on change set window.location to `/docs/${me.value}`" 39 > 40 {docs.map((doc) => (
+1
frontend/src/features/landing/templates/fragments/WaitlistFormModal.tsx
··· 28 <div class="flex justify-end space-x-3"> 29 <button 30 type="button" 31 _="on click call #waitlist-modal.close()" 32 class="font-mono text-sm px-4 py-2 text-gray-700 hover:text-gray-900" 33 >
··· 28 <div class="flex justify-end space-x-3"> 29 <button 30 type="button" 31 + /* @ts-ignore - Hyperscript attribute */ 32 _="on click call #waitlist-modal.close()" 33 class="font-mono text-sm px-4 py-2 text-gray-700 hover:text-gray-900" 34 >
+2
frontend/src/features/landing/templates/fragments/WaitlistSuccessModal.tsx
··· 12 <dialog 13 id="waitlist-success-modal" 14 class="p-0 rounded-lg shadow-xl max-w-md m-auto backdrop:bg-black/50" 15 _={show ? "on load call me.showModal()" : ""} 16 > 17 <div class="p-6"> ··· 29 </p> 30 <button 31 type="button" 32 _="on click call #waitlist-success-modal.close() then call window.history.replaceState({}, document.title, '/')" 33 class="font-mono text-sm bg-gray-800 text-white px-6 py-2 hover:bg-gray-700 transition-colors" 34 >
··· 12 <dialog 13 id="waitlist-success-modal" 14 class="p-0 rounded-lg shadow-xl max-w-md m-auto backdrop:bg-black/50" 15 + /* @ts-ignore - Hyperscript attribute */ 16 _={show ? "on load call me.showModal()" : ""} 17 > 18 <div class="p-6"> ··· 30 </p> 31 <button 32 type="button" 33 + /* @ts-ignore - Hyperscript attribute */ 34 _="on click call #waitlist-success-modal.close() then call window.history.replaceState({}, document.title, '/')" 35 class="font-mono text-sm bg-gray-800 text-white px-6 py-2 hover:bg-gray-700 transition-colors" 36 >
+17 -17
frontend/src/features/settings/handlers.tsx
··· 4 import { requireAuth, withAuth } from "../../routes/middleware.ts"; 5 import { createSessionClient, publicClient } from "../../config.ts"; 6 import { buildAtUri } from "../../utils/at-uri.ts"; 7 - import type { SocialSlicesActorProfile } from "../../client.ts"; 8 import { SettingsPage } from "./templates/SettingsPage.tsx"; 9 import { recordBlobToCdnUrl } from "@slices/client"; 10 11 async function handleSettingsPage(req: Request): Promise<Response> { 12 const context = await withAuth(req); ··· 21 22 let profile: 23 | { 24 - displayName?: string; 25 - description?: string; 26 - avatar?: string; 27 - } 28 | undefined; 29 30 try { 31 - const profileRecord = await publicClient.network.slices.actor.profile 32 - .getRecord({ 33 uri: buildAtUri({ 34 did: context.currentUser.sub!, 35 collection: "network.slices.actor.profile", ··· 42 description: profileRecord.value.description, 43 avatar: profileRecord.value.avatar 44 ? recordBlobToCdnUrl( 45 - profileRecord, 46 - profileRecord.value.avatar, 47 - "avatar", 48 - ) 49 : undefined, 50 }; 51 } ··· 59 currentUser={context.currentUser} 60 updated={updated === "true"} 61 error={error} 62 - />, 63 ); 64 } 65 ··· 74 const description = formData.get("description") as string; 75 const avatarFile = formData.get("avatar") as File; 76 77 - const profileData: Partial<SocialSlicesActorProfile> = { 78 displayName: displayName?.trim() || undefined, 79 description: description?.trim() || undefined, 80 createdAt: new Date().toISOString(), ··· 103 throw new Error("User DID (sub) is required for profile operations"); 104 } 105 106 - const existingProfile = await publicClient.network.slices.actor.profile 107 - .getRecord({ 108 uri: buildAtUri({ 109 did: context.currentUser.sub, 110 collection: "network.slices.actor.profile", ··· 126 127 await sessionClient.network.slices.actor.profile.updateRecord( 128 "self", 129 - updatedProfile, 130 ); 131 } else { 132 await sessionClient.network.slices.actor.profile.createRecord( 133 profileData, 134 - true, 135 ); 136 } 137
··· 4 import { requireAuth, withAuth } from "../../routes/middleware.ts"; 5 import { createSessionClient, publicClient } from "../../config.ts"; 6 import { buildAtUri } from "../../utils/at-uri.ts"; 7 import { SettingsPage } from "./templates/SettingsPage.tsx"; 8 import { recordBlobToCdnUrl } from "@slices/client"; 9 + import { NetworkSlicesActorProfile } from "../../client.ts"; 10 11 async function handleSettingsPage(req: Request): Promise<Response> { 12 const context = await withAuth(req); ··· 21 22 let profile: 23 | { 24 + displayName?: string; 25 + description?: string; 26 + avatar?: string; 27 + } 28 | undefined; 29 30 try { 31 + const profileRecord = 32 + await publicClient.network.slices.actor.profile.getRecord({ 33 uri: buildAtUri({ 34 did: context.currentUser.sub!, 35 collection: "network.slices.actor.profile", ··· 42 description: profileRecord.value.description, 43 avatar: profileRecord.value.avatar 44 ? recordBlobToCdnUrl( 45 + profileRecord, 46 + profileRecord.value.avatar, 47 + "avatar" 48 + ) 49 : undefined, 50 }; 51 } ··· 59 currentUser={context.currentUser} 60 updated={updated === "true"} 61 error={error} 62 + /> 63 ); 64 } 65 ··· 74 const description = formData.get("description") as string; 75 const avatarFile = formData.get("avatar") as File; 76 77 + const profileData: Partial<NetworkSlicesActorProfile> = { 78 displayName: displayName?.trim() || undefined, 79 description: description?.trim() || undefined, 80 createdAt: new Date().toISOString(), ··· 103 throw new Error("User DID (sub) is required for profile operations"); 104 } 105 106 + const existingProfile = 107 + await publicClient.network.slices.actor.profile.getRecord({ 108 uri: buildAtUri({ 109 did: context.currentUser.sub, 110 collection: "network.slices.actor.profile", ··· 126 127 await sessionClient.network.slices.actor.profile.updateRecord( 128 "self", 129 + updatedProfile 130 ); 131 } else { 132 await sessionClient.network.slices.actor.profile.createRecord( 133 profileData, 134 + true 135 ); 136 } 137
+1
frontend/src/features/settings/templates/fragments/SettingsResult.tsx
··· 26 <button 27 type="button" 28 className="mt-2 text-sm underline hover:no-underline" 29 _="on click call window.location.reload()" 30 > 31 Refresh page to see changes
··· 26 <button 27 type="button" 28 className="mt-2 text-sm underline hover:no-underline" 29 + /* @ts-ignore - Hyperscript attribute */ 30 _="on click call window.location.reload()" 31 > 32 Refresh page to see changes
+1
frontend/src/features/slices/codegen/templates/SliceCodegenPage.tsx
··· 52 <Button 53 variant="success" 54 size="md" 55 _={`on click js navigator.clipboard.writeText(${JSON.stringify( 56 generatedCode 57 )}).then(() => { me.textContent = 'Copied!'; setTimeout(() => me.textContent = 'Copy to Clipboard', 2000); }) end`}
··· 52 <Button 53 variant="success" 54 size="md" 55 + /* @ts-ignore - Hyperscript attribute */ 56 _={`on click js navigator.clipboard.writeText(${JSON.stringify( 57 generatedCode 58 )}).then(() => { me.textContent = 'Copied!'; setTimeout(() => me.textContent = 'Copy to Clipboard', 2000); }) end`}
+2
frontend/src/features/slices/lexicon/templates/LexiconDetailPage.tsx
··· 80 <div className="flex-shrink-0"> 81 <Button 82 variant="secondary" 83 _={`on click call navigator.clipboard.writeText(${JSON.stringify( 84 lexiconJson 85 )})`} ··· 124 <input 125 type="checkbox" 126 checked={excludedFromSync === true} 127 _="on change trigger submit on closest <form/>" 128 className="sr-only peer" 129 />
··· 80 <div className="flex-shrink-0"> 81 <Button 82 variant="secondary" 83 + /* @ts-ignore - Hyperscript attribute */ 84 _={`on click call navigator.clipboard.writeText(${JSON.stringify( 85 lexiconJson 86 )})`} ··· 125 <input 126 type="checkbox" 127 checked={excludedFromSync === true} 128 + /* @ts-ignore - Hyperscript attribute */ 129 _="on change trigger submit on closest <form/>" 130 className="sr-only peer" 131 />
+1
frontend/src/features/slices/lexicon/templates/fragments/LexiconFormModal.tsx
··· 63 <Button 64 type="button" 65 variant="secondary" 66 _="on click set #modal-container's innerHTML to ''" 67 > 68 Cancel
··· 63 <Button 64 type="button" 65 variant="secondary" 66 + /* @ts-ignore - Hyperscript attribute */ 67 _="on click set #modal-container's innerHTML to ''" 68 > 69 Cancel
+1
frontend/src/features/slices/lexicon/templates/fragments/LexiconsList.tsx
··· 54 id="select-all" 55 className="accent-blue-600 dark:accent-white" 56 style="color-scheme: light dark;" 57 _="on change 58 set checkboxes to document.querySelectorAll('input[name=lexicon_rkey]') 59 for cb in checkboxes
··· 54 id="select-all" 55 className="accent-blue-600 dark:accent-white" 56 style="color-scheme: light dark;" 57 + /* @ts-ignore - Hyperscript attribute */ 58 _="on change 59 set checkboxes to document.querySelectorAll('input[name=lexicon_rkey]') 60 for cb in checkboxes
+2
frontend/src/features/slices/oauth/handlers.tsx
··· 45 </p> 46 <button 47 type="button" 48 _="on click set #modal-container's innerHTML to ''" 49 className="bg-gray-500 text-white px-4 py-2 rounded-lg hover:bg-gray-600 transition" 50 > ··· 212 </p> 213 <button 214 type="button" 215 _="on click set #modal-container's innerHTML to ''" 216 className="bg-gray-500 text-white px-4 py-2 rounded-lg hover:bg-gray-600 transition" 217 >
··· 45 </p> 46 <button 47 type="button" 48 + /* @ts-ignore - Hyperscript attribute */ 49 _="on click set #modal-container's innerHTML to ''" 50 className="bg-gray-500 text-white px-4 py-2 rounded-lg hover:bg-gray-600 transition" 51 > ··· 213 </p> 214 <button 215 type="button" 216 + /* @ts-ignore - Hyperscript attribute */ 217 _="on click set #modal-container's innerHTML to ''" 218 className="bg-gray-500 text-white px-4 py-2 rounded-lg hover:bg-gray-600 transition" 219 >
+2
frontend/src/features/slices/oauth/templates/fragments/OAuthClientModal.tsx
··· 121 <Button 122 type="button" 123 variant="secondary" 124 _="on click set #modal-container's innerHTML to ''" 125 > 126 Cancel ··· 212 <Button 213 type="button" 214 variant="secondary" 215 _="on click set #modal-container's innerHTML to ''" 216 > 217 Cancel
··· 121 <Button 122 type="button" 123 variant="secondary" 124 + /* @ts-ignore - Hyperscript attribute */ 125 _="on click set #modal-container's innerHTML to ''" 126 > 127 Cancel ··· 213 <Button 214 type="button" 215 variant="secondary" 216 + /* @ts-ignore - Hyperscript attribute */ 217 _="on click set #modal-container's innerHTML to ''" 218 > 219 Cancel
+1
frontend/src/features/slices/oauth/templates/fragments/OAuthRegistrationResult.tsx
··· 62 <Button 63 type="button" 64 variant="secondary" 65 _="on click set #modal-container's innerHTML to ''" 66 > 67 Close
··· 62 <Button 63 type="button" 64 variant="secondary" 65 + /* @ts-ignore - Hyperscript attribute */ 66 _="on click set #modal-container's innerHTML to ''" 67 > 68 Close
+17 -9
frontend/src/features/slices/records/handlers.tsx
··· 8 withSliceAccess, 9 } from "../../../routes/slice-middleware.ts"; 10 import { extractSliceParams } from "../../../utils/slice-params.ts"; 11 - import type { IndexedRecord } from "../../../client.ts"; 12 import { RecordsList } from "./templates/fragments/RecordsList.tsx"; 13 import { Card } from "../../../shared/fragments/Card.tsx"; 14 import { EmptyState } from "../../../shared/fragments/EmptyState.tsx"; ··· 17 18 async function handleSliceRecordsPage( 19 req: Request, 20 - params?: URLPatternResult, 21 ): Promise<Response> { 22 const authContext = await withAuth(req); 23 const sliceParams = extractSliceParams(params); ··· 29 const context = await withSliceAccess( 30 authContext, 31 sliceParams.handle, 32 - sliceParams.sliceId, 33 ); 34 35 // Check if slice exists (records page is public) ··· 52 // Fetch real records if a collection is selected 53 let records: Array<IndexedRecord & { pretty_value: string }> = []; 54 55 - if ((selectedCollection || (searchQuery && searchQuery.trim() !== "")) && collections.length > 0) { 56 try { 57 - const sliceClient = getSliceClient(authContext, sliceParams.sliceId, context.sliceContext.profileDid); 58 - const recordsResult = await sliceClient.network.slices.slice 59 - .getSliceRecords({ 60 where: { 61 ...(selectedCollection && { 62 collection: { eq: selectedCollection }, 63 }), 64 - ...(searchQuery && searchQuery.trim() !== "" && { json: { contains: searchQuery } }), 65 ...(selectedAuthor && { did: { eq: selectedAuthor } }), 66 }, 67 limit: 20, ··· 135 availableCollections={collections} 136 currentUser={authContext.currentUser} 137 hasSliceAccess={context.sliceContext?.hasAccess} 138 - />, 139 ); 140 } 141
··· 8 withSliceAccess, 9 } from "../../../routes/slice-middleware.ts"; 10 import { extractSliceParams } from "../../../utils/slice-params.ts"; 11 + import type { IndexedRecord } from "@slices/client"; 12 import { RecordsList } from "./templates/fragments/RecordsList.tsx"; 13 import { Card } from "../../../shared/fragments/Card.tsx"; 14 import { EmptyState } from "../../../shared/fragments/EmptyState.tsx"; ··· 17 18 async function handleSliceRecordsPage( 19 req: Request, 20 + params?: URLPatternResult 21 ): Promise<Response> { 22 const authContext = await withAuth(req); 23 const sliceParams = extractSliceParams(params); ··· 29 const context = await withSliceAccess( 30 authContext, 31 sliceParams.handle, 32 + sliceParams.sliceId 33 ); 34 35 // Check if slice exists (records page is public) ··· 52 // Fetch real records if a collection is selected 53 let records: Array<IndexedRecord & { pretty_value: string }> = []; 54 55 + if ( 56 + (selectedCollection || (searchQuery && searchQuery.trim() !== "")) && 57 + collections.length > 0 58 + ) { 59 try { 60 + const sliceClient = getSliceClient( 61 + authContext, 62 + sliceParams.sliceId, 63 + context.sliceContext.profileDid 64 + ); 65 + const recordsResult = 66 + await sliceClient.network.slices.slice.getSliceRecords({ 67 where: { 68 ...(selectedCollection && { 69 collection: { eq: selectedCollection }, 70 }), 71 + ...(searchQuery && 72 + searchQuery.trim() !== "" && { json: { contains: searchQuery } }), 73 ...(selectedAuthor && { did: { eq: selectedAuthor } }), 74 }, 75 limit: 20, ··· 143 availableCollections={collections} 144 currentUser={authContext.currentUser} 145 hasSliceAccess={context.sliceContext?.hasAccess} 146 + /> 147 ); 148 } 149
+2
frontend/src/features/slices/sync/templates/fragments/SyncFormModal.tsx
··· 71 <Button 72 type="button" 73 variant="secondary" 74 _="on click set #modal-container's innerHTML to ''" 75 > 76 Cancel ··· 83 <i 84 data-lucide="loader-2" 85 className="htmx-indicator animate-spin mr-2 h-4 w-4" 86 _="on load js lucide.createIcons() end" 87 > 88 </i>
··· 71 <Button 72 type="button" 73 variant="secondary" 74 + /* @ts-ignore - Hyperscript attribute */ 75 _="on click set #modal-container's innerHTML to ''" 76 > 77 Cancel ··· 84 <i 85 data-lucide="loader-2" 86 className="htmx-indicator animate-spin mr-2 h-4 w-4" 87 + /* @ts-ignore - Hyperscript attribute */ 88 _="on load js lucide.createIcons() end" 89 > 90 </i>
+1
frontend/src/features/slices/waitlist/templates/fragments/CreateInviteModal.tsx
··· 59 <Button 60 type="button" 61 variant="outline" 62 _="on click set #modal-container's innerHTML to ''" 63 > 64 Cancel
··· 59 <Button 60 type="button" 61 variant="outline" 62 + /* @ts-ignore - Hyperscript attribute */ 63 _="on click set #modal-container's innerHTML to ''" 64 > 65 Cancel
+1
frontend/src/shared/fragments/AvatarInput.tsx
··· 67 name="avatar" 68 accept="image/*" 69 className="hidden" 70 _="on change 71 if my.files.length > 0 72 set file to my.files[0]
··· 67 name="avatar" 68 accept="image/*" 69 className="hidden" 70 + /* @ts-ignore - Hyperscript attribute */ 71 _="on change 72 if my.files.length > 0 73 set file to my.files[0]
+14 -8
frontend/src/shared/fragments/Button.tsx
··· 1 import type { JSX } from "preact"; 2 import { cn } from "../../utils/cn.ts"; 3 4 type ButtonVariant = 5 | "primary" // zinc-900 background ··· 12 type ButtonSize = "sm" | "md" | "lg"; 13 14 export interface ButtonProps 15 - extends JSX.ButtonHTMLAttributes<HTMLButtonElement> { 16 variant?: ButtonVariant; 17 size?: ButtonSize; 18 - children: JSX.Element | JSX.Element[] | string; 19 href?: string; // If provided, renders as a link styled like a button 20 } 21 22 const variantClasses = { 23 - primary: "bg-zinc-50 hover:bg-zinc-50/90 dark:bg-zinc-600 dark:hover:bg-zinc-600/90 text-zinc-900 dark:text-zinc-100 border border-zinc-200 dark:border-zinc-500/70", 24 - secondary: "bg-zinc-100 hover:bg-zinc-100/90 dark:bg-zinc-700 dark:hover:bg-zinc-700/90 text-zinc-900 dark:text-zinc-100 border border-zinc-200/70 dark:border-zinc-600/70", 25 - outline: "bg-transparent border border-zinc-300/70 hover:bg-zinc-50 dark:border-zinc-600/70 dark:hover:bg-zinc-600/10 text-zinc-900 dark:text-zinc-100", 26 - success: "bg-green-600 hover:bg-green-600/90 dark:bg-green-600 dark:hover:bg-green-600/90 text-white border border-green-700 dark:border-green-500", 27 - danger: "bg-red-600 hover:bg-red-600/90 dark:bg-red-600 dark:hover:bg-red-600/90 text-white border border-red-700 dark:border-red-500", 28 blue: "bg-blue-600 hover:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-700 text-white border border-blue-700 dark:border-blue-500", 29 }; 30 ··· 48 "rounded transition-colors inline-flex items-center font-medium", 49 variantClasses[variant], 50 sizeClasses[size], 51 - className, 52 ); 53 54 if (href) {
··· 1 import type { JSX } from "preact"; 2 import { cn } from "../../utils/cn.ts"; 3 + import { ComponentChildren } from "preact"; 4 5 type ButtonVariant = 6 | "primary" // zinc-900 background ··· 13 type ButtonSize = "sm" | "md" | "lg"; 14 15 export interface ButtonProps 16 + extends Omit<JSX.IntrinsicElements["button"], "size"> { 17 variant?: ButtonVariant; 18 size?: ButtonSize; 19 + children: ComponentChildren; 20 href?: string; // If provided, renders as a link styled like a button 21 } 22 23 const variantClasses = { 24 + primary: 25 + "bg-zinc-50 hover:bg-zinc-50/90 dark:bg-zinc-600 dark:hover:bg-zinc-600/90 text-zinc-900 dark:text-zinc-100 border border-zinc-200 dark:border-zinc-500/70", 26 + secondary: 27 + "bg-zinc-100 hover:bg-zinc-100/90 dark:bg-zinc-700 dark:hover:bg-zinc-700/90 text-zinc-900 dark:text-zinc-100 border border-zinc-200/70 dark:border-zinc-600/70", 28 + outline: 29 + "bg-transparent border border-zinc-300/70 hover:bg-zinc-50 dark:border-zinc-600/70 dark:hover:bg-zinc-600/10 text-zinc-900 dark:text-zinc-100", 30 + success: 31 + "bg-green-600 hover:bg-green-600/90 dark:bg-green-600 dark:hover:bg-green-600/90 text-white border border-green-700 dark:border-green-500", 32 + danger: 33 + "bg-red-600 hover:bg-red-600/90 dark:bg-red-600 dark:hover:bg-red-600/90 text-white border border-red-700 dark:border-red-500", 34 blue: "bg-blue-600 hover:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-700 text-white border border-blue-700 dark:border-blue-500", 35 }; 36 ··· 54 "rounded transition-colors inline-flex items-center font-medium", 55 variantClasses[variant], 56 sizeClasses[size], 57 + className 58 ); 59 60 if (href) {
+1 -1
frontend/src/shared/fragments/Input.tsx
··· 3 4 type InputSize = "sm" | "md" | "lg"; 5 6 - export interface InputProps extends JSX.InputHTMLAttributes<HTMLInputElement> { 7 label?: string; 8 error?: string; 9 size?: InputSize;
··· 3 4 type InputSize = "sm" | "md" | "lg"; 5 6 + export interface InputProps extends Omit<JSX.IntrinsicElements['input'], 'size'> { 7 label?: string; 8 error?: string; 9 size?: InputSize;
+1
frontend/src/shared/fragments/LogViewer.tsx
··· 66 <details className="mt-2"> 67 <summary 68 className="cursor-pointer hover:text-zinc-700 dark:hover:text-zinc-300" 69 _="on click toggle .hidden on next <pre/>" 70 > 71 <Text as="span" size="xs" variant="muted">View metadata</Text>
··· 66 <details className="mt-2"> 67 <summary 68 className="cursor-pointer hover:text-zinc-700 dark:hover:text-zinc-300" 69 + /* @ts-ignore - Hyperscript attribute */ 70 _="on click toggle .hidden on next <pre/>" 71 > 72 <Text as="span" size="xs" variant="muted">View metadata</Text>
+2
frontend/src/shared/fragments/Modal.tsx
··· 29 return ( 30 <div 31 className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50" 32 _={`on click if event.target === me then ${ 33 onClose.replace("on click ", "") 34 }`} ··· 41 </div> 42 <button 43 type="button" 44 _={onClose} 45 className="text-gray-400 dark:text-zinc-500 hover:text-gray-600 dark:hover:text-zinc-400 text-2xl leading-none" 46 >
··· 29 return ( 30 <div 31 className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50" 32 + /* @ts-ignore - Hyperscript attribute */ 33 _={`on click if event.target === me then ${ 34 onClose.replace("on click ", "") 35 }`} ··· 42 </div> 43 <button 44 type="button" 45 + /* @ts-ignore - Hyperscript attribute */ 46 _={onClose} 47 className="text-gray-400 dark:text-zinc-500 hover:text-gray-600 dark:hover:text-zinc-400 text-2xl leading-none" 48 >
+1
frontend/src/shared/fragments/Navigation.tsx
··· 41 <button 42 type="button" 43 className="flex items-center p-1 rounded-full hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-colors" 44 _="on click toggle .hidden on #avatar-dropdown 45 on click from document 46 if not me.contains(event.target) and not #avatar-dropdown.contains(event.target)
··· 41 <button 42 type="button" 43 className="flex items-center p-1 rounded-full hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-colors" 44 + /* @ts-ignore - Hyperscript attribute */ 45 _="on click toggle .hidden on #avatar-dropdown 46 on click from document 47 if not me.contains(event.target) and not #avatar-dropdown.contains(event.target)
+5 -3
frontend/src/shared/fragments/PageHeader.tsx
··· 1 - import type { JSX } from "preact"; 2 3 interface PageHeaderProps { 4 title: string; 5 - children?: JSX.Element | JSX.Element[]; 6 } 7 8 export function PageHeader({ title, children }: PageHeaderProps) { 9 return ( 10 <div className="flex items-center justify-between mb-8"> 11 - <h1 className="text-3xl font-bold text-zinc-900 dark:text-white">{title}</h1> 12 {children && <div className="flex items-center gap-4">{children}</div>} 13 </div> 14 );
··· 1 + import { ComponentChildren } from "preact"; 2 3 interface PageHeaderProps { 4 title: string; 5 + children?: ComponentChildren; 6 } 7 8 export function PageHeader({ title, children }: PageHeaderProps) { 9 return ( 10 <div className="flex items-center justify-between mb-8"> 11 + <h1 className="text-3xl font-bold text-zinc-900 dark:text-white"> 12 + {title} 13 + </h1> 14 {children && <div className="flex items-center gap-4">{children}</div>} 15 </div> 16 );
+1 -1
frontend/src/shared/fragments/Select.tsx
··· 6 type SelectSize = "sm" | "md" | "lg"; 7 8 export interface SelectProps 9 - extends JSX.SelectHTMLAttributes<HTMLSelectElement> { 10 label?: string; 11 error?: string; 12 size?: SelectSize;
··· 6 type SelectSize = "sm" | "md" | "lg"; 7 8 export interface SelectProps 9 + extends Omit<JSX.IntrinsicElements['select'], 'size'> { 10 label?: string; 11 error?: string; 12 size?: SelectSize;
+17 -1
frontend/src/shared/fragments/Text.tsx
··· 13 14 type TextSize = "xs" | "sm" | "base" | "lg" | "xl" | "2xl" | "3xl"; 15 16 type TextElement = 17 | "p" 18 | "span" ··· 31 interface TextProps { 32 variant?: TextVariant; 33 size?: TextSize; 34 as?: TextElement; 35 className?: string; 36 children: ComponentChildren; ··· 57 "3xl": "text-3xl", 58 }; 59 60 export function Text({ 61 variant = "primary", 62 size = "base", 63 as = "span", 64 className, 65 children, 66 }: TextProps): JSX.Element { 67 const Component = as; 68 69 - const classes = cn(textColors[variant], textSizes[size], className); 70 71 return ( 72 <Component className={classes}>
··· 13 14 type TextSize = "xs" | "sm" | "base" | "lg" | "xl" | "2xl" | "3xl"; 15 16 + type TextWeight = "normal" | "medium" | "semibold" | "bold"; 17 + 18 type TextElement = 19 | "p" 20 | "span" ··· 33 interface TextProps { 34 variant?: TextVariant; 35 size?: TextSize; 36 + weight?: TextWeight; 37 as?: TextElement; 38 className?: string; 39 children: ComponentChildren; ··· 60 "3xl": "text-3xl", 61 }; 62 63 + const textWeights = { 64 + normal: "font-normal", 65 + medium: "font-medium", 66 + semibold: "font-semibold", 67 + bold: "font-bold", 68 + }; 69 + 70 export function Text({ 71 variant = "primary", 72 size = "base", 73 + weight, 74 as = "span", 75 className, 76 children, 77 }: TextProps): JSX.Element { 78 const Component = as; 79 80 + const classes = cn( 81 + textColors[variant], 82 + textSizes[size], 83 + weight && textWeights[weight], 84 + className 85 + ); 86 87 return ( 88 <Component className={classes}>
+1 -1
frontend/src/shared/fragments/Textarea.tsx
··· 4 type TextareaSize = "sm" | "md" | "lg"; 5 6 export interface TextareaProps 7 - extends JSX.TextareaHTMLAttributes<HTMLTextAreaElement> { 8 label?: string; 9 error?: string; 10 size?: TextareaSize;
··· 4 type TextareaSize = "sm" | "md" | "lg"; 5 6 export interface TextareaProps 7 + extends Omit<JSX.IntrinsicElements['textarea'], 'size'> { 8 label?: string; 9 error?: string; 10 size?: TextareaSize;
+8 -3
packages/cli/src/generated_client.ts
··· 1 // Generated TypeScript client for AT Protocol records 2 - // Generated at: 2025-09-24 17:45:39 UTC 3 // Lexicons: 25 4 5 /** ··· 50 */ 51 52 import { 53 type BlobRef, 54 type CountRecordsResponse, 55 type GetActorsParams, ··· 2419 export class AtProtoClient extends SlicesClient { 2420 readonly app: AppClient; 2421 readonly network: NetworkClient; 2422 - readonly oauth?: OAuthClient; 2423 2424 - constructor(baseUrl: string, sliceUri: string, oauthClient?: OAuthClient) { 2425 super(baseUrl, sliceUri, oauthClient); 2426 this.app = new AppClient(this); 2427 this.network = new NetworkClient(this);
··· 1 // Generated TypeScript client for AT Protocol records 2 + // Generated at: 2025-09-24 17:50:27 UTC 3 // Lexicons: 25 4 5 /** ··· 50 */ 51 52 import { 53 + type AuthProvider, 54 type BlobRef, 55 type CountRecordsResponse, 56 type GetActorsParams, ··· 2420 export class AtProtoClient extends SlicesClient { 2421 readonly app: AppClient; 2422 readonly network: NetworkClient; 2423 + readonly oauth?: OAuthClient | AuthProvider; 2424 2425 + constructor( 2426 + baseUrl: string, 2427 + sliceUri: string, 2428 + oauthClient?: OAuthClient | AuthProvider, 2429 + ) { 2430 super(baseUrl, sliceUri, oauthClient); 2431 this.app = new AppClient(this); 2432 this.network = new NetworkClient(this);
+2 -2
packages/codegen/src/client.ts
··· 162 ? [ 163 { 164 name: "oauth", 165 - type: "OAuthClient", 166 isReadonly: true, 167 hasQuestionToken: true, 168 }, ··· 188 { name: "sliceUri", type: "string" }, 189 { 190 name: "oauthClient", 191 - type: "OAuthClient", 192 hasQuestionToken: true, 193 }, 194 ]
··· 162 ? [ 163 { 164 name: "oauth", 165 + type: "OAuthClient | AuthProvider", 166 isReadonly: true, 167 hasQuestionToken: true, 168 }, ··· 188 { name: "sliceUri", type: "string" }, 189 { 190 name: "oauthClient", 191 + type: "OAuthClient | AuthProvider", 192 hasQuestionToken: true, 193 }, 194 ]
+2 -3
packages/codegen/src/mod.ts
··· 279 280 ${ 281 excludeSlicesClient 282 - ? 'import { SlicesClient, type RecordResponse, type GetRecordsResponse, type CountRecordsResponse, type GetRecordParams, type WhereCondition, type IndexedRecordFields, type SortField, type BlobRef } from "@slices/client";' 283 - : 'import { SlicesClient, type RecordResponse, type GetRecordsResponse, type CountRecordsResponse, type GetRecordParams, type WhereCondition, type IndexedRecordFields, type SortField, type GetActorsParams, type GetActorsResponse, type BlobRef, type SliceLevelRecordsParams, type SliceRecordsOutput } from "@slices/client";' 284 } 285 - import type { OAuthClient } from "@slices/oauth"; 286 287 `; 288 }
··· 279 280 ${ 281 excludeSlicesClient 282 + ? 'import { SlicesClient, type RecordResponse, type GetRecordsResponse, type CountRecordsResponse, type GetRecordParams, type WhereCondition, type IndexedRecordFields, type SortField, type BlobRef, type AuthProvider } from "@slices/client";\nimport type { OAuthClient } from "@slices/oauth";' 283 + : 'import { SlicesClient, type RecordResponse, type GetRecordsResponse, type CountRecordsResponse, type GetRecordParams, type WhereCondition, type IndexedRecordFields, type SortField, type GetActorsParams, type GetActorsResponse, type BlobRef, type SliceLevelRecordsParams, type SliceRecordsOutput, type AuthProvider } from "@slices/client";\nimport type { OAuthClient } from "@slices/oauth";' 284 } 285 286 `; 287 }