[READ-ONLY] a fast, modern browser for the npm registry
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: atproto oauth (#273)

Co-authored-by: Bailey Townsend <baileytownsend2323@gmail.com>
Co-authored-by: Daniel Roe <daniel@roe.dev>

authored by

zeudev
Bailey Townsend
Daniel Roe
and committed by
GitHub
4e9cd163 6dd57506

+1274 -63
+2
.env.example
··· 1 + #secure password, can use openssl rand --hex 32 2 + NUXT_SESSION_PASSWORD=""
+1 -5
app/components/AppHeader.vue
··· 2 2 withDefaults( 3 3 defineProps<{ 4 4 showLogo?: boolean 5 - showConnector?: boolean 6 5 }>(), 7 6 { 8 7 showLogo: true, 9 - showConnector: true, 10 8 }, 11 9 ) 12 10 ··· 98 96 </kbd> 99 97 </NuxtLink> 100 98 101 - <div v-if="showConnector" class="hidden sm:block"> 102 - <ConnectorStatus /> 103 - </div> 99 + <HeaderAccountMenu /> 104 100 </div> 105 101 </nav> 106 102 </header>
+18
app/components/AuthButton.client.vue
··· 1 + <script setup lang="ts"> 2 + const showModal = ref(false) 3 + const { user } = useAtproto() 4 + </script> 5 + 6 + <template> 7 + <div class="relative"> 8 + <button 9 + type="button" 10 + class="relative font-mono text-sm flex items-center justify-center w-fit rounded-md transition-colors duration-200 hover:bg-bg-subtle focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50" 11 + @click="showModal = true" 12 + > 13 + {{ user?.handle || 'login' }} 14 + </button> 15 + 16 + <AuthModal v-model:open="showModal" /> 17 + </div> 18 + </template>
+10
app/components/AuthButton.server.vue
··· 1 + <template> 2 + <div class="relative"> 3 + <button 4 + type="button" 5 + class="relative font-mono text-sm flex items-center justify-center w-fit rounded-md transition-colors duration-200 hover:bg-bg-subtle focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50" 6 + > 7 + login 8 + </button> 9 + </div> 10 + </template>
+18
app/components/AuthButton.vue
··· 1 + <script setup lang="ts"> 2 + const showModal = ref(false) 3 + const { user } = useAtproto() 4 + </script> 5 + 6 + <template> 7 + <div class="relative"> 8 + <button 9 + type="button" 10 + class="relative font-mono text-sm flex items-center justify-center w-fit rounded-md transition-colors duration-200 hover:bg-bg-subtle focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50" 11 + @click="showModal = true" 12 + > 13 + {{ user?.handle || 'login' }} 14 + </button> 15 + 16 + <AuthModal v-model:open="showModal" /> 17 + </div> 18 + </template>
+198
app/components/AuthModal.vue
··· 1 + <script setup lang="ts"> 2 + const open = defineModel<boolean>('open', { default: false }) 3 + 4 + const handleInput = ref('') 5 + 6 + const { user, logout } = useAtproto() 7 + 8 + async function handleBlueskySignIn() { 9 + await navigateTo( 10 + { 11 + path: '/api/auth/atproto', 12 + query: { handle: 'https://bsky.social' }, 13 + }, 14 + { external: true }, 15 + ) 16 + } 17 + 18 + async function handleCreateAccount() { 19 + await navigateTo( 20 + { 21 + path: '/api/auth/atproto', 22 + query: { handle: 'https://npmx.social', create: 'true' }, 23 + }, 24 + { external: true }, 25 + ) 26 + } 27 + 28 + async function handleLogin() { 29 + if (handleInput.value) { 30 + await navigateTo( 31 + { 32 + path: '/api/auth/atproto', 33 + query: { handle: handleInput.value }, 34 + }, 35 + { external: true }, 36 + ) 37 + } 38 + } 39 + </script> 40 + 41 + <template> 42 + <Teleport to="body"> 43 + <Transition 44 + enter-active-class="transition-opacity duration-200" 45 + leave-active-class="transition-opacity duration-200" 46 + enter-from-class="opacity-0" 47 + leave-to-class="opacity-0" 48 + > 49 + <div v-if="open" class="fixed inset-0 z-50 flex items-center justify-center p-4"> 50 + <!-- Backdrop --> 51 + <button 52 + type="button" 53 + class="absolute inset-0 bg-black/60 cursor-default" 54 + :aria-label="$t('auth.modal.close')" 55 + @click="open = false" 56 + /> 57 + 58 + <!-- Modal --> 59 + <div 60 + class="relative w-full max-w-lg bg-bg border border-border rounded-lg shadow-xl max-h-[90vh] overflow-y-auto overscroll-contain" 61 + role="dialog" 62 + aria-modal="true" 63 + aria-labelledby="auth-modal-title" 64 + > 65 + <div class="p-6"> 66 + <div class="flex items-center justify-between mb-6"> 67 + <h2 id="auth-modal-title" class="font-mono text-lg font-medium"> 68 + {{ $t('auth.modal.title') }} 69 + </h2> 70 + <button 71 + type="button" 72 + class="text-fg-subtle hover:text-fg transition-colors duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50 rounded" 73 + :aria-label="$t('common.close')" 74 + @click="open = false" 75 + > 76 + <span class="i-carbon-close block w-5 h-5" aria-hidden="true" /> 77 + </button> 78 + </div> 79 + 80 + <div v-if="user?.handle" class="space-y-4"> 81 + <div class="flex items-center gap-3 p-4 bg-bg-subtle border border-border rounded-lg"> 82 + <span class="w-3 h-3 rounded-full bg-green-500" aria-hidden="true" /> 83 + <div> 84 + <p class="font-mono text-xs text-fg-muted"> 85 + {{ $t('auth.modal.connected_as', { handle: user.handle }) }} 86 + </p> 87 + </div> 88 + </div> 89 + <button 90 + class="w-full px-4 py-2 font-mono text-sm text-fg-muted bg-bg-subtle border border-border rounded-md transition-colors duration-200 hover:text-fg hover:border-border-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50" 91 + @click="logout" 92 + > 93 + {{ $t('auth.modal.disconnect') }} 94 + </button> 95 + </div> 96 + 97 + <!-- Disconnected state --> 98 + <form v-else class="space-y-4" @submit.prevent="handleLogin"> 99 + <p class="text-sm text-fg-muted">{{ $t('auth.modal.connect_prompt') }}</p> 100 + 101 + <div class="space-y-3"> 102 + <div> 103 + <label 104 + for="handle-input" 105 + class="block text-xs text-fg-subtle uppercase tracking-wider mb-1.5" 106 + > 107 + {{ $t('auth.modal.handle_label') }} 108 + </label> 109 + <input 110 + id="handle-input" 111 + v-model="handleInput" 112 + type="text" 113 + name="handle" 114 + :placeholder="$t('auth.modal.handle_placeholder')" 115 + autocomplete="off" 116 + spellcheck="false" 117 + class="w-full px-3 py-2 font-mono text-sm bg-bg-subtle border border-border rounded-md text-fg placeholder:text-fg-subtle transition-colors duration-200 focus:border-border-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50" 118 + /> 119 + </div> 120 + 121 + <details class="text-sm"> 122 + <summary 123 + class="text-fg-subtle cursor-pointer hover:text-fg-muted transition-colors duration-200" 124 + > 125 + {{ $t('auth.modal.what_is_atmosphere') }} 126 + </summary> 127 + <div class="mt-3"> 128 + <i18n-t keypath="auth.modal.atmosphere_explanation" tag="p"> 129 + <template #npmx> 130 + <span class="font-bold">npmx.dev</span> 131 + </template> 132 + <template #atproto> 133 + <a 134 + href="https://atproto.com" 135 + target="_blank" 136 + class="text-blue-400 hover:underline" 137 + > 138 + AT Protocol 139 + </a> 140 + </template> 141 + <template #bluesky> 142 + <a 143 + href="https://bsky.app" 144 + target="_blank" 145 + class="text-blue-400 hover:underline" 146 + > 147 + Bluesky 148 + </a> 149 + </template> 150 + <template #tangled> 151 + <a 152 + href="https://tangled.org" 153 + target="_blank" 154 + class="text-blue-400 hover:underline" 155 + > 156 + Tangled 157 + </a> 158 + </template> 159 + </i18n-t> 160 + </div> 161 + </details> 162 + </div> 163 + 164 + <button 165 + type="submit" 166 + :disabled="!handleInput.trim()" 167 + class="w-full px-4 py-2 font-mono text-sm text-bg bg-fg rounded-md transition-all duration-200 hover:bg-fg/90 disabled:opacity-50 disabled:cursor-not-allowed focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50 focus-visible:ring-offset-2 focus-visible:ring-offset-bg" 168 + > 169 + {{ $t('auth.modal.connect') }} 170 + </button> 171 + <button 172 + type="button" 173 + class="w-full px-4 py-2 font-mono text-sm text-bg bg-fg rounded-md transition-all duration-200 hover:bg-fg/90 disabled:opacity-50 disabled:cursor-not-allowed focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50 focus-visible:ring-offset-2 focus-visible:ring-offset-bg" 174 + @click="handleCreateAccount" 175 + > 176 + {{ $t('auth.modal.create_account') }} 177 + </button> 178 + <hr /> 179 + <button 180 + type="button" 181 + class="w-full px-4 py-2 font-mono text-sm text-bg bg-fg rounded-md transition-all duration-200 hover:bg-fg/90 disabled:opacity-50 disabled:cursor-not-allowed focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50 focus-visible:ring-offset-2 focus-visible:ring-offset-bg flex items-center justify-center gap-2" 182 + @click="handleBlueskySignIn" 183 + > 184 + {{ $t('auth.modal.connect_bluesky') }} 185 + <svg fill="none" viewBox="0 0 64 57" width="20" style="width: 20px"> 186 + <path 187 + fill="#0F73FF" 188 + d="M13.873 3.805C21.21 9.332 29.103 20.537 32 26.55v15.882c0-.338-.13.044-.41.867-1.512 4.456-7.418 21.847-20.923 7.944-7.111-7.32-3.819-14.64 9.125-16.85-7.405 1.264-15.73-.825-18.014-9.015C1.12 23.022 0 8.51 0 6.55 0-3.268 8.579-.182 13.873 3.805ZM50.127 3.805C42.79 9.332 34.897 20.537 32 26.55v15.882c0-.338.13.044.41.867 1.512 4.456 7.418 21.847 20.923 7.944 7.111-7.32 3.819-14.64-9.125-16.85 7.405 1.264 15.73-.825 18.014-9.015C62.88 23.022 64 8.51 64 6.55c0-9.818-8.578-6.732-13.873-2.745Z" 189 + ></path> 190 + </svg> 191 + </button> 192 + </form> 193 + </div> 194 + </div> 195 + </div> 196 + </Transition> 197 + </Teleport> 198 + </template>
+1 -1
app/components/ConnectorModal.vue
··· 93 93 <div> 94 94 <p class="font-mono text-sm text-fg">{{ $t('connector.modal.connected') }}</p> 95 95 <p v-if="npmUser" class="font-mono text-xs text-fg-muted"> 96 - {{ $t('connector.modal.logged_in_as', { user: npmUser }) }} 96 + {{ $t('connector.modal.connected_as_user', { user: npmUser }) }} 97 97 </p> 98 98 </div> 99 99 </div>
+251
app/components/HeaderAccountMenu.client.vue
··· 1 + <script setup lang="ts"> 2 + const { 3 + isConnected: isNpmConnected, 4 + isConnecting: isNpmConnecting, 5 + npmUser, 6 + avatar: npmAvatar, 7 + activeOperations, 8 + hasPendingOperations, 9 + } = useConnector() 10 + 11 + const { user: atprotoUser } = useAtproto() 12 + 13 + const isOpen = ref(false) 14 + const showConnectorModal = ref(false) 15 + const showAuthModal = ref(false) 16 + 17 + /** Check if connected to at least one service */ 18 + const hasAnyConnection = computed(() => isNpmConnected.value || !!atprotoUser.value) 19 + 20 + /** Check if connected to both services */ 21 + const hasBothConnections = computed(() => isNpmConnected.value && !!atprotoUser.value) 22 + 23 + /** Only show count of active (pending/approved/running) operations */ 24 + const operationCount = computed(() => activeOperations.value.length) 25 + 26 + function handleClickOutside(event: MouseEvent) { 27 + const target = event.target as HTMLElement 28 + if (!target.closest('.account-menu')) { 29 + isOpen.value = false 30 + } 31 + } 32 + 33 + function handleKeydown(event: KeyboardEvent) { 34 + if (event.key === 'Escape' && isOpen.value) { 35 + isOpen.value = false 36 + } 37 + } 38 + 39 + onMounted(() => { 40 + document.addEventListener('click', handleClickOutside) 41 + }) 42 + 43 + onUnmounted(() => { 44 + document.removeEventListener('click', handleClickOutside) 45 + }) 46 + 47 + function openConnectorModal() { 48 + isOpen.value = false 49 + showConnectorModal.value = true 50 + } 51 + 52 + function openAuthModal() { 53 + isOpen.value = false 54 + showAuthModal.value = true 55 + } 56 + </script> 57 + 58 + <template> 59 + <div class="account-menu relative" @keydown="handleKeydown"> 60 + <button 61 + type="button" 62 + class="relative flex items-center gap-2 px-2 py-1.5 rounded-md transition-colors duration-200 hover:bg-bg-subtle focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50" 63 + :aria-expanded="isOpen" 64 + aria-haspopup="true" 65 + @click="isOpen = !isOpen" 66 + > 67 + <!-- Stacked avatars when connected --> 68 + <div 69 + v-if="hasAnyConnection" 70 + class="flex items-center" 71 + :class="hasBothConnections ? '-space-x-2' : ''" 72 + > 73 + <!-- npm avatar (first/back) --> 74 + <img 75 + v-if="isNpmConnected && npmAvatar" 76 + :src="npmAvatar" 77 + :alt="npmUser || $t('account_menu.npm_cli')" 78 + width="24" 79 + height="24" 80 + class="w-6 h-6 rounded-full ring-2 ring-bg" 81 + /> 82 + <span 83 + v-else-if="isNpmConnected" 84 + class="w-6 h-6 rounded-full bg-bg-muted ring-2 ring-bg flex items-center justify-center" 85 + > 86 + <span class="i-carbon-terminal w-3 h-3 text-fg-muted" aria-hidden="true" /> 87 + </span> 88 + 89 + <!-- Atmosphere avatar (second/front, overlapping) --> 90 + <span 91 + v-if="atprotoUser" 92 + class="w-6 h-6 rounded-full bg-bg-muted ring-2 ring-bg flex items-center justify-center" 93 + :class="hasBothConnections ? 'relative z-10' : ''" 94 + > 95 + <span class="i-carbon-cloud w-3 h-3 text-fg-muted" aria-hidden="true" /> 96 + </span> 97 + </div> 98 + 99 + <!-- "connect" text when not connected --> 100 + <span v-if="!hasAnyConnection" class="font-mono text-sm"> 101 + {{ $t('account_menu.connect') }} 102 + </span> 103 + 104 + <!-- Chevron --> 105 + <span 106 + class="i-carbon-chevron-down w-3 h-3 transition-transform duration-200" 107 + :class="{ 'rotate-180': isOpen }" 108 + aria-hidden="true" 109 + /> 110 + 111 + <!-- Operation count badge (when npm connected with pending ops) --> 112 + <span 113 + v-if="isNpmConnected && operationCount > 0" 114 + class="absolute -top-1 -inset-ie-1 min-w-[1rem] h-4 px-1 flex items-center justify-center font-mono text-[10px] rounded-full" 115 + :class="hasPendingOperations ? 'bg-yellow-500 text-black' : 'bg-blue-500 text-white'" 116 + aria-hidden="true" 117 + > 118 + {{ operationCount }} 119 + </span> 120 + </button> 121 + 122 + <!-- Dropdown menu --> 123 + <Transition 124 + enter-active-class="transition-all duration-150" 125 + leave-active-class="transition-all duration-100" 126 + enter-from-class="opacity-0 translate-y-1" 127 + leave-to-class="opacity-0 translate-y-1" 128 + > 129 + <div v-if="isOpen" class="absolute inset-ie-0 top-full pt-2 w-72 z-50" role="menu"> 130 + <div class="bg-bg-elevated border border-border rounded-lg shadow-lg overflow-hidden"> 131 + <!-- Connected accounts section --> 132 + <div v-if="hasAnyConnection" class="py-1"> 133 + <!-- npm CLI connection --> 134 + <button 135 + v-if="isNpmConnected && npmUser" 136 + type="button" 137 + role="menuitem" 138 + class="w-full px-3 py-2.5 flex items-center gap-3 hover:bg-bg-subtle transition-colors text-start" 139 + @click="openConnectorModal" 140 + > 141 + <img 142 + v-if="npmAvatar" 143 + :src="npmAvatar" 144 + :alt="npmUser" 145 + width="32" 146 + height="32" 147 + class="w-8 h-8 rounded-full" 148 + /> 149 + <span 150 + v-else 151 + class="w-8 h-8 rounded-full bg-bg-muted flex items-center justify-center" 152 + > 153 + <span class="i-carbon-terminal w-4 h-4 text-fg-muted" aria-hidden="true" /> 154 + </span> 155 + <div class="flex-1 min-w-0"> 156 + <div class="font-mono text-sm text-fg truncate">~{{ npmUser }}</div> 157 + <div class="text-xs text-fg-subtle">{{ $t('account_menu.npm_cli') }}</div> 158 + </div> 159 + <span 160 + v-if="operationCount > 0" 161 + class="px-1.5 py-0.5 font-mono text-xs rounded" 162 + :class=" 163 + hasPendingOperations 164 + ? 'bg-yellow-500/20 text-yellow-600' 165 + : 'bg-blue-500/20 text-blue-500' 166 + " 167 + > 168 + {{ operationCount }} {{ $t('account_menu.ops') }} 169 + </span> 170 + </button> 171 + 172 + <!-- Atmosphere connection --> 173 + <button 174 + v-if="atprotoUser" 175 + type="button" 176 + role="menuitem" 177 + class="w-full px-3 py-2.5 flex items-center gap-3 hover:bg-bg-subtle transition-colors text-start" 178 + @click="openAuthModal" 179 + > 180 + <span class="w-8 h-8 rounded-full bg-bg-muted flex items-center justify-center"> 181 + <span class="i-carbon-cloud w-4 h-4 text-fg-muted" aria-hidden="true" /> 182 + </span> 183 + <div class="flex-1 min-w-0"> 184 + <div class="font-mono text-sm text-fg truncate">@{{ atprotoUser.handle }}</div> 185 + <div class="text-xs text-fg-subtle">{{ $t('account_menu.atmosphere') }}</div> 186 + </div> 187 + </button> 188 + </div> 189 + 190 + <!-- Divider (only if we have connections AND options to connect) --> 191 + <div 192 + v-if="hasAnyConnection && (!isNpmConnected || !atprotoUser)" 193 + class="border-t border-border" 194 + /> 195 + 196 + <!-- Connect options --> 197 + <div v-if="!isNpmConnected || !atprotoUser" class="py-1"> 198 + <button 199 + v-if="!isNpmConnected" 200 + type="button" 201 + role="menuitem" 202 + class="w-full px-3 py-2.5 flex items-center gap-3 hover:bg-bg-subtle transition-colors text-start" 203 + @click="openConnectorModal" 204 + > 205 + <span class="w-8 h-8 rounded-full bg-bg-muted flex items-center justify-center"> 206 + <span 207 + v-if="isNpmConnecting" 208 + class="i-carbon-circle-dash w-4 h-4 text-yellow-500 animate-spin" 209 + aria-hidden="true" 210 + /> 211 + <span v-else class="i-carbon-terminal w-4 h-4 text-fg-muted" aria-hidden="true" /> 212 + </span> 213 + <div class="flex-1 min-w-0"> 214 + <div class="font-mono text-sm text-fg"> 215 + {{ 216 + isNpmConnecting 217 + ? $t('account_menu.connecting') 218 + : $t('account_menu.connect_npm_cli') 219 + }} 220 + </div> 221 + <div class="text-xs text-fg-subtle">{{ $t('account_menu.npm_cli_desc') }}</div> 222 + </div> 223 + </button> 224 + 225 + <button 226 + v-if="!atprotoUser" 227 + type="button" 228 + role="menuitem" 229 + class="w-full px-3 py-2.5 flex items-center gap-3 hover:bg-bg-subtle transition-colors text-start" 230 + @click="openAuthModal" 231 + > 232 + <span class="w-8 h-8 rounded-full bg-bg-muted flex items-center justify-center"> 233 + <span class="i-carbon-cloud w-4 h-4 text-fg-muted" aria-hidden="true" /> 234 + </span> 235 + <div class="flex-1 min-w-0"> 236 + <div class="font-mono text-sm text-fg"> 237 + {{ $t('account_menu.connect_atmosphere') }} 238 + </div> 239 + <div class="text-xs text-fg-subtle">{{ $t('account_menu.atmosphere_desc') }}</div> 240 + </div> 241 + </button> 242 + </div> 243 + </div> 244 + </div> 245 + </Transition> 246 + 247 + <!-- Modals --> 248 + <ConnectorModal v-model:open="showConnectorModal" /> 249 + <AuthModal v-model:open="showAuthModal" /> 250 + </div> 251 + </template>
+5
app/components/HeaderAccountMenu.server.vue
··· 1 + <template> 2 + <div class="flex items-center gap-2 px-2 py-1.5"> 3 + <span class="font-mono text-sm text-fg-muted">{{ $t('account_menu.connect') }}</span> 4 + </div> 5 + </template>
+16
app/composables/useAtproto.ts
··· 1 + import type { UserSession } from '#shared/schemas/userSession' 2 + 3 + /** @public */ 4 + export function useAtproto() { 5 + const { data: user, pending, clear } = useFetch<UserSession | null>('/api/auth/session') 6 + 7 + async function logout() { 8 + await $fetch('/api/auth/session', { 9 + method: 'delete', 10 + }) 11 + 12 + clear() 13 + } 14 + 15 + return { user, pending, logout } 16 + }
+2 -2
app/composables/useConnector.ts
··· 71 71 lastExecutionTime: null, 72 72 })) 73 73 74 - const baseUrl = computed(() => `http://localhost:${config.value?.port ?? DEFAULT_PORT}`) 74 + const baseUrl = computed(() => `http://127.0.0.1:${config.value?.port ?? DEFAULT_PORT}`) 75 75 76 76 const route = useRoute() 77 77 const router = useRouter() ··· 109 109 state.value.error = null 110 110 111 111 try { 112 - const response = await $fetch<ConnectResponse>(`http://localhost:${port}/connect`, { 112 + const response = await $fetch<ConnectResponse>(`http://127.0.0.1:${port}/connect`, { 113 113 method: 'POST', 114 114 body: { token }, 115 115 timeout: 5000,
+1 -1
app/error.vue
··· 31 31 32 32 <template> 33 33 <div class="min-h-screen flex flex-col bg-bg text-fg"> 34 - <AppHeader :show-connector="false" /> 34 + <AppHeader /> 35 35 36 36 <main class="flex-1 container flex flex-col items-center justify-center py-20 text-center"> 37 37 <p class="font-mono text-8xl sm:text-9xl font-medium text-fg-subtle mb-4">
+1 -1
cli/src/cli.ts
··· 11 11 12 12 const DEFAULT_PORT = 31415 13 13 const DEFAULT_FRONTEND_URL = 'https://npmx.dev/' 14 - const DEV_FRONTEND_URL = 'http://localhost:3000/' 14 + const DEV_FRONTEND_URL = 'http://127.0.0.1:3000/' 15 15 16 16 async function runNpmLogin(): Promise<boolean> { 17 17 return new Promise(resolve => {
+1 -1
cli/src/server.ts
··· 53 53 } 54 54 55 55 const corsOptions: CorsOptions = { 56 - origin: ['https://npmx.dev', /^http:\/\/localhost:\d+$/], 56 + origin: ['https://npmx.dev', /^http:\/\/localhost:\d+$/, /^http:\/\/127.0.0.1:\d+$/], 57 57 methods: ['GET', 'POST', 'DELETE', 'OPTIONS'], 58 58 allowHeaders: ['Content-Type', 'Authorization'], 59 59 }
+31 -2
i18n/locales/en.json
··· 352 352 "connector": { 353 353 "status": { 354 354 "connecting": "connecting...", 355 - "connected_as": "connected as {'@'}{user}", 355 + "connected_as": "connected as ~{user}", 356 356 "connected": "connected", 357 357 "connect_cli": "connect local CLI", 358 358 "aria_connecting": "Connecting to local connector", ··· 364 364 "title": "Local Connector", 365 365 "close_modal": "Close modal", 366 366 "connected": "Connected", 367 - "logged_in_as": "Logged in as {'@'}{user}", 367 + "connected_as_user": "Connected as ~{user}", 368 368 "connected_hint": "You can now manage packages and organizations from the web UI.", 369 369 "disconnect": "Disconnect", 370 370 "run_hint": "Run the connector on your machine to enable admin features.", ··· 732 732 "description": "Find out the latest on npmx.", 733 733 "cta": "Follow on Bluesky" 734 734 } 735 + } 736 + }, 737 + "account_menu": { 738 + "connect": "connect", 739 + "account": "Account", 740 + "npm_cli": "npm CLI", 741 + "atmosphere": "Atmosphere", 742 + "npm_cli_desc": "Manage packages & orgs", 743 + "atmosphere_desc": "Social features & identity", 744 + "connect_npm_cli": "Connect to npm CLI", 745 + "connect_atmosphere": "Connect to Atmosphere", 746 + "connecting": "Connecting...", 747 + "ops": "ops", 748 + "disconnect": "Disconnect" 749 + }, 750 + "auth": { 751 + "modal": { 752 + "title": "Atmosphere", 753 + "close": "Close", 754 + "connected_as": "Connected as {'@'}{handle}", 755 + "disconnect": "Disconnect", 756 + "connect_prompt": "Connect with your Atmosphere account", 757 + "handle_label": "Handle", 758 + "handle_placeholder": "alice.bsky.social", 759 + "connect": "Connect", 760 + "create_account": "Create a new account", 761 + "connect_bluesky": "Connect with Bluesky", 762 + "what_is_atmosphere": "What is an Atmosphere account?", 763 + "atmosphere_explanation": "{npmx} uses the {atproto} to power many of its social features, allowing users to own their data and use one account for all compatible applications. Once you create an account, you can use other apps like {bluesky} and {tangled} with the same account." 735 764 } 736 765 }, 737 766 "header": {
+1
knip.json
··· 40 40 "@iconify-json/solar", 41 41 "@iconify-json/svg-spinners", 42 42 "@iconify-json/vscode-icons", 43 + "@vercel/kv", 43 44 "@voidzero-dev/vite-plus-core", 44 45 "h3", 45 46 "ohash",
+31 -2
lunaria/files/en-US.json
··· 352 352 "connector": { 353 353 "status": { 354 354 "connecting": "connecting...", 355 - "connected_as": "connected as {'@'}{user}", 355 + "connected_as": "connected as ~{user}", 356 356 "connected": "connected", 357 357 "connect_cli": "connect local CLI", 358 358 "aria_connecting": "Connecting to local connector", ··· 364 364 "title": "Local Connector", 365 365 "close_modal": "Close modal", 366 366 "connected": "Connected", 367 - "logged_in_as": "Logged in as {'@'}{user}", 367 + "connected_as_user": "Connected as ~{user}", 368 368 "connected_hint": "You can now manage packages and organizations from the web UI.", 369 369 "disconnect": "Disconnect", 370 370 "run_hint": "Run the connector on your machine to enable admin features.", ··· 732 732 "description": "Find out the latest on npmx.", 733 733 "cta": "Follow on Bluesky" 734 734 } 735 + } 736 + }, 737 + "account_menu": { 738 + "connect": "connect", 739 + "account": "Account", 740 + "npm_cli": "npm CLI", 741 + "atmosphere": "Atmosphere", 742 + "npm_cli_desc": "Manage packages & orgs", 743 + "atmosphere_desc": "Social features & identity", 744 + "connect_npm_cli": "Connect to npm CLI", 745 + "connect_atmosphere": "Connect to Atmosphere", 746 + "connecting": "Connecting...", 747 + "ops": "ops", 748 + "disconnect": "Disconnect" 749 + }, 750 + "auth": { 751 + "modal": { 752 + "title": "Atmosphere", 753 + "close": "Close", 754 + "connected_as": "Connected as {'@'}{handle}", 755 + "disconnect": "Disconnect", 756 + "connect_prompt": "Connect with your Atmosphere account", 757 + "handle_label": "Handle", 758 + "handle_placeholder": "alice.bsky.social", 759 + "connect": "Connect", 760 + "create_account": "Create a new account", 761 + "connect_bluesky": "Connect with Bluesky", 762 + "what_is_atmosphere": "What is an Atmosphere account?", 763 + "atmosphere_explanation": "{npmx} uses the {atproto} to power many of its social features, allowing users to own their data and use one account for all compatible applications. Once you create an account, you can use other apps like {bluesky} and {tangled} with the same account." 735 764 } 736 765 }, 737 766 "header": {
+10
modules/cache.ts
··· 27 27 ...nitroConfig.storage[FETCH_CACHE_STORAGE_BASE], 28 28 driver: 'vercel-runtime-cache', 29 29 } 30 + 31 + const env = process.env.VERCEL_ENV 32 + 33 + nitroConfig.storage['oauth-atproto-state'] = { 34 + driver: env === 'production' ? 'vercel-kv' : 'vercel-runtime-cache', 35 + } 36 + 37 + nitroConfig.storage['oauth-atproto-session'] = { 38 + driver: env === 'production' ? 'vercel-kv' : 'vercel-runtime-cache', 39 + } 30 40 }) 31 41 }, 32 42 })
+28
modules/dev.ts
··· 1 + import { defineNuxtModule, useNuxt } from 'nuxt/kit' 2 + import { join } from 'node:path' 3 + import { appendFileSync, existsSync, readFileSync } from 'node:fs' 4 + import { randomUUID } from 'node:crypto' 5 + 6 + export default defineNuxtModule({ 7 + meta: { 8 + name: 'dev', 9 + }, 10 + setup() { 11 + const nuxt = useNuxt() 12 + if (nuxt.options._prepare || process.env.NUXT_SESSION_PASSWORD) { 13 + return 14 + } 15 + 16 + const envPath = join(nuxt.options.rootDir, '.env') 17 + const hasPassword = 18 + existsSync(envPath) && /^NUXT_SESSION_PASSWORD=/m.test(readFileSync(envPath, 'utf-8')) 19 + 20 + if (!hasPassword) { 21 + console.info('Generating NUXT_SESSION_PASSWORD for development environment.') 22 + const password = randomUUID().replace(/-/g, '') 23 + 24 + nuxt.options.runtimeConfig.sessionPassword = password 25 + appendFileSync(envPath, `# generated by dev module\nNUXT_SESSION_PASSWORD=${password}\n`) 26 + } 27 + }, 28 + })
+24
nuxt.config.ts
··· 1 + import process from 'node:process' 1 2 import { currentLocales } from './config/i18n' 2 3 3 4 export default defineNuxtConfig({ ··· 45 46 46 47 css: ['~/assets/main.css', 'vue-data-ui/style.css'], 47 48 49 + runtimeConfig: { 50 + sessionPassword: '', 51 + // Upstash Redis for distributed OAuth token refresh locking in production 52 + upstash: { 53 + redisRestUrl: process.env.KV_REST_API_URL || '', 54 + redisRestToken: process.env.KV_REST_API_TOKEN || '', 55 + }, 56 + }, 57 + 48 58 devtools: { enabled: true }, 59 + 60 + devServer: { 61 + // Used with atproto oauth 62 + // https://atproto.com/specs/oauth#localhost-client-development 63 + host: '127.0.0.1', 64 + }, 49 65 50 66 app: { 51 67 head: { ··· 131 147 'fetch-cache': { 132 148 driver: 'fsLite', 133 149 base: './.cache/fetch', 150 + }, 151 + 'oauth-atproto-state': { 152 + driver: 'fsLite', 153 + base: './.cache/atproto-oauth/state', 154 + }, 155 + 'oauth-atproto-session': { 156 + driver: 'fsLite', 157 + base: './.cache/atproto-oauth/session', 134 158 }, 135 159 }, 136 160 },
+4
package.json
··· 33 33 "start:playwright:webserver": "NODE_ENV=test pnpm build && pnpm preview --port 5678" 34 34 }, 35 35 "dependencies": { 36 + "@atproto/api": "^0.18.17", 36 37 "@atproto/lex": "0.0.13", 38 + "@atproto/oauth-client-node": "^0.3.15", 37 39 "@deno/doc": "jsr:^0.189.1", 38 40 "@iconify-json/simple-icons": "1.2.68", 39 41 "@iconify-json/vscode-icons": "1.2.40", ··· 48 50 "@nuxtjs/i18n": "10.2.1", 49 51 "@shikijs/langs": "3.21.0", 50 52 "@shikijs/themes": "3.21.0", 53 + "@upstash/redis": "1.36.1", 54 + "@vercel/kv": "3.0.0", 51 55 "@vueuse/core": "14.1.0", 52 56 "@vueuse/nuxt": "14.1.0", 53 57 "module-replacements": "2.11.0",
+264 -48
pnpm-lock.yaml
··· 20 20 21 21 .: 22 22 dependencies: 23 + '@atproto/api': 24 + specifier: ^0.18.17 25 + version: 0.18.20 23 26 '@atproto/lex': 24 27 specifier: 0.0.13 25 28 version: 0.0.13 29 + '@atproto/oauth-client-node': 30 + specifier: ^0.3.15 31 + version: 0.3.16 26 32 '@deno/doc': 27 33 specifier: jsr:^0.189.1 28 34 version: '@jsr/deno__doc@0.189.1(patch_hash=24f326e123c822a07976329a5afe91a8713e82d53134b5586625b72431c87832)' ··· 46 52 version: 1.0.0-alpha.1(magicast@0.5.1)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2)) 47 53 '@nuxt/fonts': 48 54 specifier: 0.13.0 49 - version: 0.13.0(db0@0.3.4(better-sqlite3@12.6.2))(ioredis@5.9.2)(magicast@0.5.1)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2)) 55 + version: 0.13.0(@upstash/redis@1.36.1)(@vercel/kv@3.0.0)(db0@0.3.4(better-sqlite3@12.6.2))(ioredis@5.9.2)(magicast@0.5.1)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2)) 50 56 '@nuxt/scripts': 51 57 specifier: 0.13.2 52 - version: 0.13.2(@unhead/vue@2.1.2(vue@3.5.27(typescript@5.9.3)))(db0@0.3.4(better-sqlite3@12.6.2))(ioredis@5.9.2)(magicast@0.5.1)(typescript@5.9.3)(vue@3.5.27(typescript@5.9.3)) 58 + version: 0.13.2(@unhead/vue@2.1.2(vue@3.5.27(typescript@5.9.3)))(@upstash/redis@1.36.1)(@vercel/kv@3.0.0)(db0@0.3.4(better-sqlite3@12.6.2))(ioredis@5.9.2)(magicast@0.5.1)(typescript@5.9.3)(vue@3.5.27(typescript@5.9.3)) 53 59 '@nuxtjs/color-mode': 54 60 specifier: 4.0.0 55 61 version: 4.0.0(magicast@0.5.1) ··· 58 64 version: 2.1.0(@voidzero-dev/vite-plus-test@0.0.0-833c515fa25cef20905a7f9affb156dfa6f151ab(@types/node@24.10.9)(esbuild@0.27.2)(happy-dom@20.4.0)(jiti@2.6.1)(terser@5.46.0)(typescript@5.9.3)(yaml@2.8.2))(magicast@0.5.1) 59 65 '@nuxtjs/i18n': 60 66 specifier: 10.2.1 61 - version: 10.2.1(@vue/compiler-dom@3.5.27)(db0@0.3.4(better-sqlite3@12.6.2))(eslint@9.39.2(jiti@2.6.1))(ioredis@5.9.2)(magicast@0.5.1)(rollup@4.57.0)(vue@3.5.27(typescript@5.9.3)) 67 + version: 10.2.1(@upstash/redis@1.36.1)(@vercel/kv@3.0.0)(@vue/compiler-dom@3.5.27)(db0@0.3.4(better-sqlite3@12.6.2))(eslint@9.39.2(jiti@2.6.1))(ioredis@5.9.2)(magicast@0.5.1)(rollup@4.57.0)(vue@3.5.27(typescript@5.9.3)) 62 68 '@shikijs/langs': 63 69 specifier: 3.21.0 64 70 version: 3.21.0 65 71 '@shikijs/themes': 66 72 specifier: 3.21.0 67 73 version: 3.21.0 74 + '@upstash/redis': 75 + specifier: 1.36.1 76 + version: 1.36.1 77 + '@vercel/kv': 78 + specifier: 3.0.0 79 + version: 3.0.0 68 80 '@vueuse/core': 69 81 specifier: 14.1.0 70 82 version: 14.1.0(vue@3.5.27(typescript@5.9.3)) 71 83 '@vueuse/nuxt': 72 84 specifier: 14.1.0 73 - version: 14.1.0(magicast@0.5.1)(nuxt@4.3.0(@parcel/watcher@2.5.6)(@types/node@24.10.9)(@vue/compiler-sfc@3.5.27)(better-sqlite3@12.6.2)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.6.2))(eslint@9.39.2(jiti@2.6.1))(ioredis@5.9.2)(magicast@0.5.1)(optionator@0.9.4)(oxlint@1.42.0(oxlint-tsgolint@0.11.3))(rolldown@1.0.0-rc.1)(rollup@4.57.0)(terser@5.46.0)(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2))(vue-tsc@3.2.4(typescript@5.9.3))(yaml@2.8.2))(vue@3.5.27(typescript@5.9.3)) 85 + version: 14.1.0(magicast@0.5.1)(nuxt@4.3.0(@parcel/watcher@2.5.6)(@types/node@24.10.9)(@upstash/redis@1.36.1)(@vercel/kv@3.0.0)(@vue/compiler-sfc@3.5.27)(better-sqlite3@12.6.2)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.6.2))(eslint@9.39.2(jiti@2.6.1))(ioredis@5.9.2)(magicast@0.5.1)(optionator@0.9.4)(oxlint@1.42.0(oxlint-tsgolint@0.11.3))(rolldown@1.0.0-rc.1)(rollup@4.57.0)(terser@5.46.0)(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2))(vue-tsc@3.2.4(typescript@5.9.3))(yaml@2.8.2))(vue@3.5.27(typescript@5.9.3)) 74 86 module-replacements: 75 87 specifier: 2.11.0 76 88 version: 2.11.0 77 89 nuxt: 78 90 specifier: 4.3.0 79 - version: 4.3.0(@parcel/watcher@2.5.6)(@types/node@24.10.9)(@vue/compiler-sfc@3.5.27)(better-sqlite3@12.6.2)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.6.2))(eslint@9.39.2(jiti@2.6.1))(ioredis@5.9.2)(magicast@0.5.1)(optionator@0.9.4)(oxlint@1.42.0(oxlint-tsgolint@0.11.3))(rolldown@1.0.0-rc.1)(rollup@4.57.0)(terser@5.46.0)(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2))(vue-tsc@3.2.4(typescript@5.9.3))(yaml@2.8.2) 91 + version: 4.3.0(@parcel/watcher@2.5.6)(@types/node@24.10.9)(@upstash/redis@1.36.1)(@vercel/kv@3.0.0)(@vue/compiler-sfc@3.5.27)(better-sqlite3@12.6.2)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.6.2))(eslint@9.39.2(jiti@2.6.1))(ioredis@5.9.2)(magicast@0.5.1)(optionator@0.9.4)(oxlint@1.42.0(oxlint-tsgolint@0.11.3))(rolldown@1.0.0-rc.1)(rollup@4.57.0)(terser@5.46.0)(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2))(vue-tsc@3.2.4(typescript@5.9.3))(yaml@2.8.2) 80 92 nuxt-og-image: 81 93 specifier: 5.1.13 82 - version: 5.1.13(@unhead/vue@2.1.2(vue@3.5.27(typescript@5.9.3)))(magicast@0.5.1)(unstorage@1.17.4(db0@0.3.4(better-sqlite3@12.6.2))(ioredis@5.9.2))(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2))(vue@3.5.27(typescript@5.9.3)) 94 + version: 5.1.13(@unhead/vue@2.1.2(vue@3.5.27(typescript@5.9.3)))(magicast@0.5.1)(unstorage@1.17.4(@upstash/redis@1.36.1)(@vercel/kv@3.0.0)(db0@0.3.4(better-sqlite3@12.6.2))(ioredis@5.9.2))(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2))(vue@3.5.27(typescript@5.9.3)) 83 95 ohash: 84 96 specifier: 2.0.11 85 97 version: 2.0.11 ··· 261 273 version: 12.6.2 262 274 docus: 263 275 specifier: 5.4.4 264 - version: 5.4.4(29b67e4ea82958d3bb639aa676be121d) 276 + version: 5.4.4(f5d56481b2ddeca600f9d39a31d83017) 265 277 nuxt: 266 278 specifier: 4.3.0 267 - version: 4.3.0(@parcel/watcher@2.5.6)(@types/node@24.10.9)(@vue/compiler-sfc@3.5.27)(better-sqlite3@12.6.2)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.6.2))(eslint@9.39.2(jiti@2.6.1))(ioredis@5.9.2)(magicast@0.5.1)(optionator@0.9.4)(oxlint@1.42.0(oxlint-tsgolint@0.11.3))(rolldown@1.0.0-rc.1)(rollup@4.57.0)(terser@5.46.0)(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2))(vue-tsc@3.2.4(typescript@5.9.3))(yaml@2.8.2) 279 + version: 4.3.0(@parcel/watcher@2.5.6)(@types/node@24.10.9)(@upstash/redis@1.36.1)(@vercel/kv@3.0.0)(@vue/compiler-sfc@3.5.27)(better-sqlite3@12.6.2)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.6.2))(eslint@9.39.2(jiti@2.6.1))(ioredis@5.9.2)(magicast@0.5.1)(optionator@0.9.4)(oxlint@1.42.0(oxlint-tsgolint@0.11.3))(rolldown@1.0.0-rc.1)(rollup@4.57.0)(terser@5.46.0)(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2))(vue-tsc@3.2.4(typescript@5.9.3))(yaml@2.8.2) 268 280 269 281 packages: 270 282 ··· 288 300 '@atproto-labs/did-resolver@0.2.6': 289 301 resolution: {integrity: sha512-2K1bC04nI2fmgNcvof+yA28IhGlpWn2JKYlPa7To9JTKI45FINCGkQSGiL2nyXlyzDJJ34fZ1aq6/IRFIOIiqg==} 290 302 303 + '@atproto-labs/fetch-node@0.2.0': 304 + resolution: {integrity: sha512-Krq09nH/aeoiU2s9xdHA0FjTEFWG9B5FFenipv1iRixCcPc7V3DhTNDawxG9gI8Ny0k4dBVS9WTRN/IDzBx86Q==} 305 + engines: {node: '>=18.7.0'} 306 + 291 307 '@atproto-labs/fetch@0.2.3': 292 308 resolution: {integrity: sha512-NZtbJOCbxKUFRFKMpamT38PUQMY0hX0p7TG5AEYOPhZKZEP7dHZ1K2s1aB8MdVH0qxmqX7nQleNrrvLf09Zfdw==} 293 309 310 + '@atproto-labs/handle-resolver-node@0.1.25': 311 + resolution: {integrity: sha512-NY9WYM2VLd3IuMGRkkmvGBg8xqVEaK/fitv1vD8SMXqFTekdpjOLCCyv7EFtqVHouzmDcL83VOvWRfHVa8V9Yw==} 312 + engines: {node: '>=18.7.0'} 313 + 314 + '@atproto-labs/handle-resolver@0.3.6': 315 + resolution: {integrity: sha512-qnSTXvOBNj1EHhp2qTWSX8MS5q3AwYU5LKlt5fBvSbCjgmTr2j0URHCv+ydrwO55KvsojIkTMgeMOh4YuY4fCA==} 316 + 317 + '@atproto-labs/identity-resolver@0.3.6': 318 + resolution: {integrity: sha512-qoWqBDRobln0NR8L8dQjSp79E0chGkBhibEgxQa2f9WD+JbJdjQ0YvwwO5yeQn05pJoJmAwmI2wyJ45zjU7aWg==} 319 + 294 320 '@atproto-labs/pipe@0.1.1': 295 321 resolution: {integrity: sha512-hdNw2oUs2B6BN1lp+32pF7cp8EMKuIN5Qok2Vvv/aOpG/3tNSJ9YkvfI0k6Zd188LeDDYRUpYpxcoFIcGH/FNg==} 296 322 ··· 300 326 '@atproto-labs/simple-store@0.3.0': 301 327 resolution: {integrity: sha512-nOb6ONKBRJHRlukW1sVawUkBqReLlLx6hT35VS3imaNPwiXDxLnTK7lxw3Lrl9k5yugSBDQAkZAq3MPTEFSUBQ==} 302 328 329 + '@atproto/api@0.18.20': 330 + resolution: {integrity: sha512-BZYZkh2VJIFCXEnc/vzKwAwWjAQQTgbNJ8FBxpBK+z+KYh99O0uPCsRYKoCQsRrnkgrhzdU9+g2G+7zanTIGbw==} 331 + 303 332 '@atproto/common-web@0.4.14': 304 333 resolution: {integrity: sha512-rMU8Q+kpyPpirUS9OqT7aOD1hxKa+diem3vc7BA0lOkj0tU6wcAxegxmbPZ8JaOsR7SSYhP/jCt/5wbT4qqkuQ==} 334 + 335 + '@atproto/common-web@0.4.15': 336 + resolution: {integrity: sha512-A4l9gyqUNez8CjZp/Trypz/D3WIQsNj8dN05WR6+RoBbvwc9JhWjKPrm+WoVYc/F16RPdXHLkE3BEJlGIyYIiA==} 305 337 306 338 '@atproto/common@0.5.9': 307 339 resolution: {integrity: sha512-rzl8dB7ErpA/VUgCidahUtbxEph50J4g7j68bZmlwwrHlrtvTe8DjrwH5EUFEcegl9dadIhcVJ3qi0kPKEUr+g==} ··· 313 345 314 346 '@atproto/did@0.3.0': 315 347 resolution: {integrity: sha512-raUPzUGegtW/6OxwCmM8bhZvuIMzxG5t9oWsth6Tp91Kb5fTnHV2h/KKNF1C82doeA4BdXCErTyg7ISwLbQkzA==} 348 + 349 + '@atproto/jwk-jose@0.1.11': 350 + resolution: {integrity: sha512-i4Fnr2sTBYmMmHXl7NJh8GrCH+tDQEVWrcDMDnV5DjJfkgT17wIqvojIw9SNbSL4Uf0OtfEv6AgG0A+mgh8b5Q==} 351 + 352 + '@atproto/jwk-webcrypto@0.2.0': 353 + resolution: {integrity: sha512-UmgRrrEAkWvxwhlwe30UmDOdTEFidlIzBC7C3cCbeJMcBN1x8B3KH+crXrsTqfWQBG58mXgt8wgSK3Kxs2LhFg==} 354 + 355 + '@atproto/jwk@0.6.0': 356 + resolution: {integrity: sha512-bDoJPvt7TrQVi/rBfBrSSpGykhtIriKxeYCYQTiPRKFfyRhbgpElF0wPXADjIswnbzZdOwbY63az4E/CFVT3Tw==} 316 357 317 358 '@atproto/lex-builder@0.0.12': 318 359 resolution: {integrity: sha512-ObWnmsbkPwjKKIX/L0JyMptmggr3gvbZKPDcpr1eSBUWyWeqqX8OfIlHYLgm5veNuO776RV05CE7BdQFQUA+9Q==} ··· 323 364 '@atproto/lex-client@0.0.10': 324 365 resolution: {integrity: sha512-n3g9KoY5hw7W29mcR4TrjN5qOi6JiWty7r1heqLLfYiq5TxaRx9/QBa2hbN4h1p4xxICPZoDlNtuGq8YV4U8mg==} 325 366 367 + '@atproto/lex-data@0.0.10': 368 + resolution: {integrity: sha512-FDbcy8VIUVzS9Mi1F8SMxbkL/jOUmRRpqbeM1xB4A0fMxeZJTxf6naAbFt4gYF3quu/+TPJGmio6/7cav05FqQ==} 369 + 326 370 '@atproto/lex-data@0.0.9': 327 371 resolution: {integrity: sha512-1slwe4sG0cyWtsq16+rBoWIxNDqGPkkvN+PV6JuzA7dgUK9bjUmXBGQU4eZlUPSS43X1Nhmr/9VjgKmEzU9vDw==} 328 372 ··· 332 376 '@atproto/lex-installer@0.0.13': 333 377 resolution: {integrity: sha512-Uu9JsZBBTVel8qz+wgf/M46uimPMn4Ub3hToscngELa+C9+6amHAtcArVdJgv4UsDu13TneOj3I6bdkU0luLTw==} 334 378 379 + '@atproto/lex-json@0.0.10': 380 + resolution: {integrity: sha512-L6MyXU17C5ODMeob8myQ2F3xvgCTvJUtM0ew8qSApnN//iDasB/FDGgd7ty4UVNmx4NQ/rtvz8xV94YpG6kneQ==} 381 + 335 382 '@atproto/lex-json@0.0.9': 336 383 resolution: {integrity: sha512-Q2v1EVZcnd+ndyZj1r2UlGikA7q6It24CFPLbxokcf5Ba4RBupH8IkkQX7mqUDSRWPgQdmZYIdW9wUln+MKDqw==} 337 384 ··· 348 395 '@atproto/lexicon@0.6.1': 349 396 resolution: {integrity: sha512-/vI1kVlY50Si+5MXpvOucelnYwb0UJ6Qto5mCp+7Q5C+Jtp+SoSykAPVvjVtTnQUH2vrKOFOwpb3C375vSKzXw==} 350 397 398 + '@atproto/oauth-client-node@0.3.16': 399 + resolution: {integrity: sha512-2dooMzxAkiQ4MkOAZlEQ3iwbB9SEovrbIKMNuBbVCLQYORVNxe20tMdjs3lvhrzdpzvaHLlQnJJhw5dA9VELFw==} 400 + engines: {node: '>=18.7.0'} 401 + 402 + '@atproto/oauth-client@0.5.14': 403 + resolution: {integrity: sha512-sPH+vcdq9maTEAhJI0HzmFcFAMrkCS19np+RUssNkX6kS8Xr3OYr57tvYRCbkcnIyYTfYcxKQgpwHKx3RVEaYw==} 404 + 405 + '@atproto/oauth-types@0.6.2': 406 + resolution: {integrity: sha512-2cuboM4RQBCYR8NQC5uGRkW6KgCgKyq/B5/+tnMmWZYtZGVUQvsUWQHK/ZiMCnVXbcDNtc/RIEJQJDZ8FXMoxg==} 407 + 351 408 '@atproto/repo@0.8.12': 352 409 resolution: {integrity: sha512-QpVTVulgfz5PUiCTELlDBiRvnsnwrFWi+6CfY88VwXzrRHd9NE8GItK7sfxQ6U65vD/idH8ddCgFrlrsn1REPQ==} 353 410 engines: {node: '>=18.7.0'} 354 411 355 412 '@atproto/syntax@0.4.3': 356 413 resolution: {integrity: sha512-YoZUz40YAJr5nPwvCDWgodEOlt5IftZqPJvA0JDWjuZKD8yXddTwSzXSaKQAzGOpuM+/A3uXRtPzJJqlScc+iA==} 414 + 415 + '@atproto/xrpc@0.7.7': 416 + resolution: {integrity: sha512-K1ZyO/BU8JNtXX5dmPp7b5UrkLMMqpsIa/Lrj5D3Su+j1Xwq1m6QJ2XJ1AgjEjkI1v4Muzm7klianLE6XGxtmA==} 357 417 358 418 '@babel/code-frame@7.28.6': 359 419 resolution: {integrity: sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==} ··· 3995 4055 peerDependencies: 3996 4056 webpack: ^4 || ^5 3997 4057 4058 + '@upstash/redis@1.36.1': 4059 + resolution: {integrity: sha512-N6SjDcgXdOcTAF+7uNoY69o7hCspe9BcA7YjQdxVu5d25avljTwyLaHBW3krWjrP0FfocgMk94qyVtQbeDp39A==} 4060 + 4061 + '@vercel/kv@3.0.0': 4062 + resolution: {integrity: sha512-pKT8fRnfyYk2MgvyB6fn6ipJPCdfZwiKDdw7vB+HL50rjboEBHDVBEcnwfkEpVSp2AjNtoaOUH7zG+bVC/rvSg==} 4063 + engines: {node: '>=14.6'} 4064 + deprecated: 'Vercel KV is deprecated. If you had an existing KV store, it should have moved to Upstash Redis which you will see under Vercel Integrations. For new projects, install a Redis integration from Vercel Marketplace: https://vercel.com/marketplace?category=storage&search=redis' 4065 + 3998 4066 '@vercel/nft@1.3.0': 3999 4067 resolution: {integrity: sha512-i4EYGkCsIjzu4vorDUbqglZc5eFtQI2syHb++9ZUDm6TU4edVywGpVnYDein35x9sevONOn9/UabfQXuNXtuzQ==} 4000 4068 engines: {node: '>=20'} ··· 4572 4640 available-typed-arrays@1.0.7: 4573 4641 resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} 4574 4642 engines: {node: '>= 0.4'} 4643 + 4644 + await-lock@2.2.2: 4645 + resolution: {integrity: sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw==} 4575 4646 4576 4647 axe-core@4.11.1: 4577 4648 resolution: {integrity: sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A==} ··· 6053 6124 resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} 6054 6125 engines: {node: '>= 0.10'} 6055 6126 6127 + ipaddr.js@2.3.0: 6128 + resolution: {integrity: sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==} 6129 + engines: {node: '>= 10'} 6130 + 6056 6131 ipx@3.1.1: 6057 6132 resolution: {integrity: sha512-7Xnt54Dco7uYkfdAw0r2vCly3z0rSaVhEXMzPvl3FndsTVm5p26j+PO+gyinkYmcsEUvX2Rh7OGK7KzYWRu6BA==} 6058 6133 hasBin: true ··· 6326 6401 jiti@2.6.1: 6327 6402 resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} 6328 6403 hasBin: true 6404 + 6405 + jose@5.10.0: 6406 + resolution: {integrity: sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==} 6329 6407 6330 6408 jose@6.1.3: 6331 6409 resolution: {integrity: sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==} ··· 8453 8531 tinyrainbow@3.0.3: 8454 8532 resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==} 8455 8533 engines: {node: '>=14.0.0'} 8534 + 8535 + tlds@1.261.0: 8536 + resolution: {integrity: sha512-QXqwfEl9ddlGBaRFXIvNKK6OhipSiLXuRuLJX5DErz0o0Q0rYxulWLdFryTkV5PkdZct5iMInwYEGe/eR++1AA==} 8537 + hasBin: true 8456 8538 8457 8539 to-buffer@1.2.2: 8458 8540 resolution: {integrity: sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==} ··· 8610 8692 undici-types@7.16.0: 8611 8693 resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} 8612 8694 8695 + undici@6.23.0: 8696 + resolution: {integrity: sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==} 8697 + engines: {node: '>=18.17'} 8698 + 8613 8699 unenv@2.0.0-rc.24: 8614 8700 resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==} 8615 8701 ··· 9404 9490 '@atproto/did': 0.3.0 9405 9491 zod: 3.25.76 9406 9492 9493 + '@atproto-labs/fetch-node@0.2.0': 9494 + dependencies: 9495 + '@atproto-labs/fetch': 0.2.3 9496 + '@atproto-labs/pipe': 0.1.1 9497 + ipaddr.js: 2.3.0 9498 + undici: 6.23.0 9499 + 9407 9500 '@atproto-labs/fetch@0.2.3': 9408 9501 dependencies: 9409 9502 '@atproto-labs/pipe': 0.1.1 9410 9503 9504 + '@atproto-labs/handle-resolver-node@0.1.25': 9505 + dependencies: 9506 + '@atproto-labs/fetch-node': 0.2.0 9507 + '@atproto-labs/handle-resolver': 0.3.6 9508 + '@atproto/did': 0.3.0 9509 + 9510 + '@atproto-labs/handle-resolver@0.3.6': 9511 + dependencies: 9512 + '@atproto-labs/simple-store': 0.3.0 9513 + '@atproto-labs/simple-store-memory': 0.1.4 9514 + '@atproto/did': 0.3.0 9515 + zod: 3.25.76 9516 + 9517 + '@atproto-labs/identity-resolver@0.3.6': 9518 + dependencies: 9519 + '@atproto-labs/did-resolver': 0.2.6 9520 + '@atproto-labs/handle-resolver': 0.3.6 9521 + 9411 9522 '@atproto-labs/pipe@0.1.1': {} 9412 9523 9413 9524 '@atproto-labs/simple-store-memory@0.1.4': ··· 9417 9528 9418 9529 '@atproto-labs/simple-store@0.3.0': {} 9419 9530 9531 + '@atproto/api@0.18.20': 9532 + dependencies: 9533 + '@atproto/common-web': 0.4.15 9534 + '@atproto/lexicon': 0.6.1 9535 + '@atproto/syntax': 0.4.3 9536 + '@atproto/xrpc': 0.7.7 9537 + await-lock: 2.2.2 9538 + multiformats: 9.9.0 9539 + tlds: 1.261.0 9540 + zod: 3.25.76 9541 + 9420 9542 '@atproto/common-web@0.4.14': 9421 9543 dependencies: 9422 9544 '@atproto/lex-data': 0.0.9 9423 9545 '@atproto/lex-json': 0.0.9 9546 + '@atproto/syntax': 0.4.3 9547 + zod: 3.25.76 9548 + 9549 + '@atproto/common-web@0.4.15': 9550 + dependencies: 9551 + '@atproto/lex-data': 0.0.10 9552 + '@atproto/lex-json': 0.0.10 9424 9553 '@atproto/syntax': 0.4.3 9425 9554 zod: 3.25.76 9426 9555 ··· 9443 9572 dependencies: 9444 9573 zod: 3.25.76 9445 9574 9575 + '@atproto/jwk-jose@0.1.11': 9576 + dependencies: 9577 + '@atproto/jwk': 0.6.0 9578 + jose: 5.10.0 9579 + 9580 + '@atproto/jwk-webcrypto@0.2.0': 9581 + dependencies: 9582 + '@atproto/jwk': 0.6.0 9583 + '@atproto/jwk-jose': 0.1.11 9584 + zod: 3.25.76 9585 + 9586 + '@atproto/jwk@0.6.0': 9587 + dependencies: 9588 + multiformats: 9.9.0 9589 + zod: 3.25.76 9590 + 9446 9591 '@atproto/lex-builder@0.0.12': 9447 9592 dependencies: 9448 9593 '@atproto/lex-document': 0.0.11 ··· 9463 9608 '@atproto/lex-schema': 0.0.10 9464 9609 tslib: 2.8.1 9465 9610 9611 + '@atproto/lex-data@0.0.10': 9612 + dependencies: 9613 + multiformats: 9.9.0 9614 + tslib: 2.8.1 9615 + uint8arrays: 3.0.0 9616 + unicode-segmenter: 0.14.5 9617 + 9466 9618 '@atproto/lex-data@0.0.9': 9467 9619 dependencies: 9468 9620 multiformats: 9.9.0 ··· 9487 9639 '@atproto/syntax': 0.4.3 9488 9640 tslib: 2.8.1 9489 9641 9642 + '@atproto/lex-json@0.0.10': 9643 + dependencies: 9644 + '@atproto/lex-data': 0.0.10 9645 + tslib: 2.8.1 9646 + 9490 9647 '@atproto/lex-json@0.0.9': 9491 9648 dependencies: 9492 9649 '@atproto/lex-data': 0.0.9 ··· 9529 9686 multiformats: 9.9.0 9530 9687 zod: 3.25.76 9531 9688 9689 + '@atproto/oauth-client-node@0.3.16': 9690 + dependencies: 9691 + '@atproto-labs/did-resolver': 0.2.6 9692 + '@atproto-labs/handle-resolver-node': 0.1.25 9693 + '@atproto-labs/simple-store': 0.3.0 9694 + '@atproto/did': 0.3.0 9695 + '@atproto/jwk': 0.6.0 9696 + '@atproto/jwk-jose': 0.1.11 9697 + '@atproto/jwk-webcrypto': 0.2.0 9698 + '@atproto/oauth-client': 0.5.14 9699 + '@atproto/oauth-types': 0.6.2 9700 + 9701 + '@atproto/oauth-client@0.5.14': 9702 + dependencies: 9703 + '@atproto-labs/did-resolver': 0.2.6 9704 + '@atproto-labs/fetch': 0.2.3 9705 + '@atproto-labs/handle-resolver': 0.3.6 9706 + '@atproto-labs/identity-resolver': 0.3.6 9707 + '@atproto-labs/simple-store': 0.3.0 9708 + '@atproto-labs/simple-store-memory': 0.1.4 9709 + '@atproto/did': 0.3.0 9710 + '@atproto/jwk': 0.6.0 9711 + '@atproto/oauth-types': 0.6.2 9712 + '@atproto/xrpc': 0.7.7 9713 + core-js: 3.48.0 9714 + multiformats: 9.9.0 9715 + zod: 3.25.76 9716 + 9717 + '@atproto/oauth-types@0.6.2': 9718 + dependencies: 9719 + '@atproto/did': 0.3.0 9720 + '@atproto/jwk': 0.6.0 9721 + zod: 3.25.76 9722 + 9532 9723 '@atproto/repo@0.8.12': 9533 9724 dependencies: 9534 9725 '@atproto/common': 0.5.9 ··· 9544 9735 '@atproto/syntax@0.4.3': 9545 9736 dependencies: 9546 9737 tslib: 2.8.1 9738 + 9739 + '@atproto/xrpc@0.7.7': 9740 + dependencies: 9741 + '@atproto/lexicon': 0.6.1 9742 + zod: 3.25.76 9547 9743 9548 9744 '@babel/code-frame@7.28.6': 9549 9745 dependencies: ··· 11148 11344 - utf-8-validate 11149 11345 - vue 11150 11346 11151 - '@nuxt/fonts@0.12.1(db0@0.3.4(better-sqlite3@12.6.2))(ioredis@5.9.2)(magicast@0.5.1)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2))': 11347 + '@nuxt/fonts@0.12.1(@upstash/redis@1.36.1)(@vercel/kv@3.0.0)(db0@0.3.4(better-sqlite3@12.6.2))(ioredis@5.9.2)(magicast@0.5.1)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2))': 11152 11348 dependencies: 11153 11349 '@nuxt/devtools-kit': 3.1.1(magicast@0.5.1)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2)) 11154 11350 '@nuxt/kit': 4.3.0(magicast@0.5.1) ··· 11157 11353 defu: 6.1.4 11158 11354 esbuild: 0.25.12 11159 11355 fontaine: 0.7.0 11160 - fontless: 0.1.0(db0@0.3.4(better-sqlite3@12.6.2))(ioredis@5.9.2)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2)) 11356 + fontless: 0.1.0(@upstash/redis@1.36.1)(@vercel/kv@3.0.0)(db0@0.3.4(better-sqlite3@12.6.2))(ioredis@5.9.2)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2)) 11161 11357 h3: 1.15.5 11162 11358 jiti: 2.6.1 11163 11359 magic-regexp: 0.10.0 ··· 11170 11366 ufo: 1.6.3 11171 11367 unifont: 0.6.0 11172 11368 unplugin: 2.3.11 11173 - unstorage: 1.17.4(db0@0.3.4(better-sqlite3@12.6.2))(ioredis@5.9.2) 11369 + unstorage: 1.17.4(@upstash/redis@1.36.1)(@vercel/kv@3.0.0)(db0@0.3.4(better-sqlite3@12.6.2))(ioredis@5.9.2) 11174 11370 transitivePeerDependencies: 11175 11371 - '@azure/app-configuration' 11176 11372 - '@azure/cosmos' ··· 11194 11390 - uploadthing 11195 11391 - vite 11196 11392 11197 - '@nuxt/fonts@0.13.0(db0@0.3.4(better-sqlite3@12.6.2))(ioredis@5.9.2)(magicast@0.5.1)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2))': 11393 + '@nuxt/fonts@0.13.0(@upstash/redis@1.36.1)(@vercel/kv@3.0.0)(db0@0.3.4(better-sqlite3@12.6.2))(ioredis@5.9.2)(magicast@0.5.1)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2))': 11198 11394 dependencies: 11199 11395 '@nuxt/devtools-kit': 3.1.1(magicast@0.5.1)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2)) 11200 11396 '@nuxt/kit': 4.3.0(magicast@0.5.1) ··· 11203 11399 defu: 6.1.4 11204 11400 esbuild: 0.27.2 11205 11401 fontaine: 0.8.0 11206 - fontless: 0.1.0(db0@0.3.4(better-sqlite3@12.6.2))(ioredis@5.9.2)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2)) 11402 + fontless: 0.1.0(@upstash/redis@1.36.1)(@vercel/kv@3.0.0)(db0@0.3.4(better-sqlite3@12.6.2))(ioredis@5.9.2)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2)) 11207 11403 h3: 1.15.5 11208 11404 jiti: 2.6.1 11209 11405 magic-regexp: 0.10.0 ··· 11216 11412 ufo: 1.6.3 11217 11413 unifont: 0.6.0 11218 11414 unplugin: 2.3.11 11219 - unstorage: 1.17.4(db0@0.3.4(better-sqlite3@12.6.2))(ioredis@5.9.2) 11415 + unstorage: 1.17.4(@upstash/redis@1.36.1)(@vercel/kv@3.0.0)(db0@0.3.4(better-sqlite3@12.6.2))(ioredis@5.9.2) 11220 11416 transitivePeerDependencies: 11221 11417 - '@azure/app-configuration' 11222 11418 - '@azure/cosmos' ··· 11261 11457 - vite 11262 11458 - vue 11263 11459 11264 - '@nuxt/image@2.0.0(db0@0.3.4(better-sqlite3@12.6.2))(ioredis@5.9.2)(magicast@0.5.1)': 11460 + '@nuxt/image@2.0.0(@upstash/redis@1.36.1)(@vercel/kv@3.0.0)(db0@0.3.4(better-sqlite3@12.6.2))(ioredis@5.9.2)(magicast@0.5.1)': 11265 11461 dependencies: 11266 11462 '@nuxt/kit': 4.3.0(magicast@0.5.1) 11267 11463 consola: 3.4.2 ··· 11274 11470 std-env: 3.10.0 11275 11471 ufo: 1.6.3 11276 11472 optionalDependencies: 11277 - ipx: 3.1.1(db0@0.3.4(better-sqlite3@12.6.2))(ioredis@5.9.2) 11473 + ipx: 3.1.1(@upstash/redis@1.36.1)(@vercel/kv@3.0.0)(db0@0.3.4(better-sqlite3@12.6.2))(ioredis@5.9.2) 11278 11474 transitivePeerDependencies: 11279 11475 - '@azure/app-configuration' 11280 11476 - '@azure/cosmos' ··· 11348 11544 transitivePeerDependencies: 11349 11545 - magicast 11350 11546 11351 - '@nuxt/nitro-server@4.3.0(better-sqlite3@12.6.2)(db0@0.3.4(better-sqlite3@12.6.2))(ioredis@5.9.2)(magicast@0.5.1)(nuxt@4.3.0(@parcel/watcher@2.5.6)(@types/node@24.10.9)(@vue/compiler-sfc@3.5.27)(better-sqlite3@12.6.2)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.6.2))(eslint@9.39.2(jiti@2.6.1))(ioredis@5.9.2)(magicast@0.5.1)(optionator@0.9.4)(oxlint@1.42.0(oxlint-tsgolint@0.11.3))(rolldown@1.0.0-rc.1)(rollup@4.57.0)(terser@5.46.0)(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2))(vue-tsc@3.2.4(typescript@5.9.3))(yaml@2.8.2))(rolldown@1.0.0-rc.1)(typescript@5.9.3)': 11547 + '@nuxt/nitro-server@4.3.0(@upstash/redis@1.36.1)(@vercel/kv@3.0.0)(better-sqlite3@12.6.2)(db0@0.3.4(better-sqlite3@12.6.2))(ioredis@5.9.2)(magicast@0.5.1)(nuxt@4.3.0(@parcel/watcher@2.5.6)(@types/node@24.10.9)(@upstash/redis@1.36.1)(@vercel/kv@3.0.0)(@vue/compiler-sfc@3.5.27)(better-sqlite3@12.6.2)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.6.2))(eslint@9.39.2(jiti@2.6.1))(ioredis@5.9.2)(magicast@0.5.1)(optionator@0.9.4)(oxlint@1.42.0(oxlint-tsgolint@0.11.3))(rolldown@1.0.0-rc.1)(rollup@4.57.0)(terser@5.46.0)(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2))(vue-tsc@3.2.4(typescript@5.9.3))(yaml@2.8.2))(rolldown@1.0.0-rc.1)(typescript@5.9.3)': 11352 11548 dependencies: 11353 11549 '@nuxt/devalue': 2.0.2 11354 11550 '@nuxt/kit': 4.3.0(magicast@0.5.1) ··· 11365 11561 impound: 1.0.0 11366 11562 klona: 2.0.6 11367 11563 mocked-exports: 0.1.1 11368 - nitropack: 2.13.1(better-sqlite3@12.6.2)(rolldown@1.0.0-rc.1) 11369 - nuxt: 4.3.0(@parcel/watcher@2.5.6)(@types/node@24.10.9)(@vue/compiler-sfc@3.5.27)(better-sqlite3@12.6.2)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.6.2))(eslint@9.39.2(jiti@2.6.1))(ioredis@5.9.2)(magicast@0.5.1)(optionator@0.9.4)(oxlint@1.42.0(oxlint-tsgolint@0.11.3))(rolldown@1.0.0-rc.1)(rollup@4.57.0)(terser@5.46.0)(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2))(vue-tsc@3.2.4(typescript@5.9.3))(yaml@2.8.2) 11564 + nitropack: 2.13.1(@upstash/redis@1.36.1)(@vercel/kv@3.0.0)(better-sqlite3@12.6.2)(rolldown@1.0.0-rc.1) 11565 + nuxt: 4.3.0(@parcel/watcher@2.5.6)(@types/node@24.10.9)(@upstash/redis@1.36.1)(@vercel/kv@3.0.0)(@vue/compiler-sfc@3.5.27)(better-sqlite3@12.6.2)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.6.2))(eslint@9.39.2(jiti@2.6.1))(ioredis@5.9.2)(magicast@0.5.1)(optionator@0.9.4)(oxlint@1.42.0(oxlint-tsgolint@0.11.3))(rolldown@1.0.0-rc.1)(rollup@4.57.0)(terser@5.46.0)(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2))(vue-tsc@3.2.4(typescript@5.9.3))(yaml@2.8.2) 11370 11566 ohash: 2.0.11 11371 11567 pathe: 2.0.3 11372 11568 pkg-types: 2.3.0 ··· 11374 11570 std-env: 3.10.0 11375 11571 ufo: 1.6.3 11376 11572 unctx: 2.5.0 11377 - unstorage: 1.17.4(db0@0.3.4(better-sqlite3@12.6.2))(ioredis@5.9.2) 11573 + unstorage: 1.17.4(@upstash/redis@1.36.1)(@vercel/kv@3.0.0)(db0@0.3.4(better-sqlite3@12.6.2))(ioredis@5.9.2) 11378 11574 vue: 3.5.27(typescript@5.9.3) 11379 11575 vue-bundle-renderer: 2.2.0 11380 11576 vue-devtools-stub: 0.1.0 ··· 11421 11617 pkg-types: 2.3.0 11422 11618 std-env: 3.10.0 11423 11619 11424 - '@nuxt/scripts@0.13.2(@unhead/vue@2.1.2(vue@3.5.27(typescript@5.9.3)))(db0@0.3.4(better-sqlite3@12.6.2))(ioredis@5.9.2)(magicast@0.5.1)(typescript@5.9.3)(vue@3.5.27(typescript@5.9.3))': 11620 + '@nuxt/scripts@0.13.2(@unhead/vue@2.1.2(vue@3.5.27(typescript@5.9.3)))(@upstash/redis@1.36.1)(@vercel/kv@3.0.0)(db0@0.3.4(better-sqlite3@12.6.2))(ioredis@5.9.2)(magicast@0.5.1)(typescript@5.9.3)(vue@3.5.27(typescript@5.9.3))': 11425 11621 dependencies: 11426 11622 '@nuxt/kit': 4.3.0(magicast@0.5.1) 11427 11623 '@unhead/vue': 2.1.2(vue@3.5.27(typescript@5.9.3)) ··· 11439 11635 std-env: 3.10.0 11440 11636 ufo: 1.6.3 11441 11637 unplugin: 2.3.11 11442 - unstorage: 1.17.4(db0@0.3.4(better-sqlite3@12.6.2))(ioredis@5.9.2) 11638 + unstorage: 1.17.4(@upstash/redis@1.36.1)(@vercel/kv@3.0.0)(db0@0.3.4(better-sqlite3@12.6.2))(ioredis@5.9.2) 11443 11639 valibot: 1.2.0(typescript@5.9.3) 11444 11640 transitivePeerDependencies: 11445 11641 - '@azure/app-configuration' ··· 11564 11760 - magicast 11565 11761 - typescript 11566 11762 11567 - '@nuxt/ui@4.4.0(@nuxt/content@3.11.0(better-sqlite3@12.6.2)(magicast@0.5.1)(valibot@1.2.0(typescript@5.9.3)))(@tiptap/extensions@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1))(@tiptap/y-tiptap@3.0.2(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.5)(y-protocols@1.0.7(yjs@13.6.29))(yjs@13.6.29))(db0@0.3.4(better-sqlite3@12.6.2))(embla-carousel@8.6.0)(ioredis@5.9.2)(magicast@0.5.1)(tailwindcss@4.1.18)(typescript@5.9.3)(valibot@1.2.0(typescript@5.9.3))(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2))(vue-router@4.6.4(vue@3.5.27(typescript@5.9.3)))(vue@3.5.27(typescript@5.9.3))(yjs@13.6.29)(zod@4.3.6)': 11763 + '@nuxt/ui@4.4.0(@nuxt/content@3.11.0(better-sqlite3@12.6.2)(magicast@0.5.1)(valibot@1.2.0(typescript@5.9.3)))(@tiptap/extensions@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1))(@tiptap/y-tiptap@3.0.2(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.5)(y-protocols@1.0.7(yjs@13.6.29))(yjs@13.6.29))(@upstash/redis@1.36.1)(@vercel/kv@3.0.0)(db0@0.3.4(better-sqlite3@12.6.2))(embla-carousel@8.6.0)(ioredis@5.9.2)(magicast@0.5.1)(tailwindcss@4.1.18)(typescript@5.9.3)(valibot@1.2.0(typescript@5.9.3))(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2))(vue-router@4.6.4(vue@3.5.27(typescript@5.9.3)))(vue@3.5.27(typescript@5.9.3))(yjs@13.6.29)(zod@4.3.6)': 11568 11764 dependencies: 11569 11765 '@floating-ui/dom': 1.7.5 11570 11766 '@iconify/vue': 5.0.0(vue@3.5.27(typescript@5.9.3)) 11571 11767 '@internationalized/date': 3.10.1 11572 11768 '@internationalized/number': 3.6.5 11573 - '@nuxt/fonts': 0.12.1(db0@0.3.4(better-sqlite3@12.6.2))(ioredis@5.9.2)(magicast@0.5.1)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2)) 11769 + '@nuxt/fonts': 0.12.1(@upstash/redis@1.36.1)(@vercel/kv@3.0.0)(db0@0.3.4(better-sqlite3@12.6.2))(ioredis@5.9.2)(magicast@0.5.1)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2)) 11574 11770 '@nuxt/icon': 2.2.1(magicast@0.5.1)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2))(vue@3.5.27(typescript@5.9.3)) 11575 11771 '@nuxt/kit': 4.3.0(magicast@0.5.1) 11576 11772 '@nuxt/schema': 4.3.0 ··· 11679 11875 - vue 11680 11876 - yjs 11681 11877 11682 - '@nuxt/vite-builder@4.3.0(@types/node@24.10.9)(eslint@9.39.2(jiti@2.6.1))(magicast@0.5.1)(nuxt@4.3.0(@parcel/watcher@2.5.6)(@types/node@24.10.9)(@vue/compiler-sfc@3.5.27)(better-sqlite3@12.6.2)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.6.2))(eslint@9.39.2(jiti@2.6.1))(ioredis@5.9.2)(magicast@0.5.1)(optionator@0.9.4)(oxlint@1.42.0(oxlint-tsgolint@0.11.3))(rolldown@1.0.0-rc.1)(rollup@4.57.0)(terser@5.46.0)(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2))(vue-tsc@3.2.4(typescript@5.9.3))(yaml@2.8.2))(optionator@0.9.4)(oxlint@1.42.0(oxlint-tsgolint@0.11.3))(rolldown@1.0.0-rc.1)(rollup@4.57.0)(terser@5.46.0)(typescript@5.9.3)(vue-tsc@3.2.4(typescript@5.9.3))(vue@3.5.27(typescript@5.9.3))(yaml@2.8.2)': 11878 + '@nuxt/vite-builder@4.3.0(@types/node@24.10.9)(eslint@9.39.2(jiti@2.6.1))(magicast@0.5.1)(nuxt@4.3.0(@parcel/watcher@2.5.6)(@types/node@24.10.9)(@upstash/redis@1.36.1)(@vercel/kv@3.0.0)(@vue/compiler-sfc@3.5.27)(better-sqlite3@12.6.2)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.6.2))(eslint@9.39.2(jiti@2.6.1))(ioredis@5.9.2)(magicast@0.5.1)(optionator@0.9.4)(oxlint@1.42.0(oxlint-tsgolint@0.11.3))(rolldown@1.0.0-rc.1)(rollup@4.57.0)(terser@5.46.0)(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2))(vue-tsc@3.2.4(typescript@5.9.3))(yaml@2.8.2))(optionator@0.9.4)(oxlint@1.42.0(oxlint-tsgolint@0.11.3))(rolldown@1.0.0-rc.1)(rollup@4.57.0)(terser@5.46.0)(typescript@5.9.3)(vue-tsc@3.2.4(typescript@5.9.3))(vue@3.5.27(typescript@5.9.3))(yaml@2.8.2)': 11683 11879 dependencies: 11684 11880 '@nuxt/kit': 4.3.0(magicast@0.5.1) 11685 11881 '@rollup/plugin-replace': 6.0.3(rollup@4.57.0) ··· 11698 11894 magic-string: 0.30.21 11699 11895 mlly: 1.8.0 11700 11896 mocked-exports: 0.1.1 11701 - nuxt: 4.3.0(@parcel/watcher@2.5.6)(@types/node@24.10.9)(@vue/compiler-sfc@3.5.27)(better-sqlite3@12.6.2)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.6.2))(eslint@9.39.2(jiti@2.6.1))(ioredis@5.9.2)(magicast@0.5.1)(optionator@0.9.4)(oxlint@1.42.0(oxlint-tsgolint@0.11.3))(rolldown@1.0.0-rc.1)(rollup@4.57.0)(terser@5.46.0)(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2))(vue-tsc@3.2.4(typescript@5.9.3))(yaml@2.8.2) 11897 + nuxt: 4.3.0(@parcel/watcher@2.5.6)(@types/node@24.10.9)(@upstash/redis@1.36.1)(@vercel/kv@3.0.0)(@vue/compiler-sfc@3.5.27)(better-sqlite3@12.6.2)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.6.2))(eslint@9.39.2(jiti@2.6.1))(ioredis@5.9.2)(magicast@0.5.1)(optionator@0.9.4)(oxlint@1.42.0(oxlint-tsgolint@0.11.3))(rolldown@1.0.0-rc.1)(rollup@4.57.0)(terser@5.46.0)(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2))(vue-tsc@3.2.4(typescript@5.9.3))(yaml@2.8.2) 11702 11898 pathe: 2.0.3 11703 11899 pkg-types: 2.3.0 11704 11900 postcss: 8.5.6 ··· 11778 11974 - magicast 11779 11975 - vitest 11780 11976 11781 - '@nuxtjs/i18n@10.2.1(@vue/compiler-dom@3.5.27)(db0@0.3.4(better-sqlite3@12.6.2))(eslint@9.39.2(jiti@2.6.1))(ioredis@5.9.2)(magicast@0.5.1)(rollup@4.57.0)(vue@3.5.27(typescript@5.9.3))': 11977 + '@nuxtjs/i18n@10.2.1(@upstash/redis@1.36.1)(@vercel/kv@3.0.0)(@vue/compiler-dom@3.5.27)(db0@0.3.4(better-sqlite3@12.6.2))(eslint@9.39.2(jiti@2.6.1))(ioredis@5.9.2)(magicast@0.5.1)(rollup@4.57.0)(vue@3.5.27(typescript@5.9.3))': 11782 11978 dependencies: 11783 11979 '@intlify/core': 11.2.8 11784 11980 '@intlify/h3': 0.7.4 ··· 11805 12001 ufo: 1.6.3 11806 12002 unplugin: 2.3.11 11807 12003 unplugin-vue-router: 0.16.2(@vue/compiler-sfc@3.5.27)(vue-router@4.6.4(vue@3.5.27(typescript@5.9.3)))(vue@3.5.27(typescript@5.9.3)) 11808 - unstorage: 1.17.4(db0@0.3.4(better-sqlite3@12.6.2))(ioredis@5.9.2) 12004 + unstorage: 1.17.4(@upstash/redis@1.36.1)(@vercel/kv@3.0.0)(db0@0.3.4(better-sqlite3@12.6.2))(ioredis@5.9.2) 11809 12005 vue-i18n: 11.2.8(vue@3.5.27(typescript@5.9.3)) 11810 12006 vue-router: 4.6.4(vue@3.5.27(typescript@5.9.3)) 11811 12007 transitivePeerDependencies: ··· 13451 13647 webpack: 5.104.1(esbuild@0.27.2) 13452 13648 webpack-sources: 3.3.3 13453 13649 13650 + '@upstash/redis@1.36.1': 13651 + dependencies: 13652 + uncrypto: 0.1.3 13653 + 13654 + '@vercel/kv@3.0.0': 13655 + dependencies: 13656 + '@upstash/redis': 1.36.1 13657 + 13454 13658 '@vercel/nft@1.3.0(rollup@4.57.0)': 13455 13659 dependencies: 13456 13660 '@mapbox/node-pre-gyp': 2.0.3 ··· 13833 14037 13834 14038 '@vueuse/metadata@14.1.0': {} 13835 14039 13836 - '@vueuse/nuxt@14.1.0(magicast@0.5.1)(nuxt@4.3.0(@parcel/watcher@2.5.6)(@types/node@24.10.9)(@vue/compiler-sfc@3.5.27)(better-sqlite3@12.6.2)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.6.2))(eslint@9.39.2(jiti@2.6.1))(ioredis@5.9.2)(magicast@0.5.1)(optionator@0.9.4)(oxlint@1.42.0(oxlint-tsgolint@0.11.3))(rolldown@1.0.0-rc.1)(rollup@4.57.0)(terser@5.46.0)(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2))(vue-tsc@3.2.4(typescript@5.9.3))(yaml@2.8.2))(vue@3.5.27(typescript@5.9.3))': 14040 + '@vueuse/nuxt@14.1.0(magicast@0.5.1)(nuxt@4.3.0(@parcel/watcher@2.5.6)(@types/node@24.10.9)(@upstash/redis@1.36.1)(@vercel/kv@3.0.0)(@vue/compiler-sfc@3.5.27)(better-sqlite3@12.6.2)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.6.2))(eslint@9.39.2(jiti@2.6.1))(ioredis@5.9.2)(magicast@0.5.1)(optionator@0.9.4)(oxlint@1.42.0(oxlint-tsgolint@0.11.3))(rolldown@1.0.0-rc.1)(rollup@4.57.0)(terser@5.46.0)(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2))(vue-tsc@3.2.4(typescript@5.9.3))(yaml@2.8.2))(vue@3.5.27(typescript@5.9.3))': 13837 14041 dependencies: 13838 14042 '@nuxt/kit': 4.3.0(magicast@0.5.1) 13839 14043 '@vueuse/core': 14.1.0(vue@3.5.27(typescript@5.9.3)) 13840 14044 '@vueuse/metadata': 14.1.0 13841 14045 local-pkg: 1.1.2 13842 - nuxt: 4.3.0(@parcel/watcher@2.5.6)(@types/node@24.10.9)(@vue/compiler-sfc@3.5.27)(better-sqlite3@12.6.2)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.6.2))(eslint@9.39.2(jiti@2.6.1))(ioredis@5.9.2)(magicast@0.5.1)(optionator@0.9.4)(oxlint@1.42.0(oxlint-tsgolint@0.11.3))(rolldown@1.0.0-rc.1)(rollup@4.57.0)(terser@5.46.0)(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2))(vue-tsc@3.2.4(typescript@5.9.3))(yaml@2.8.2) 14046 + nuxt: 4.3.0(@parcel/watcher@2.5.6)(@types/node@24.10.9)(@upstash/redis@1.36.1)(@vercel/kv@3.0.0)(@vue/compiler-sfc@3.5.27)(better-sqlite3@12.6.2)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.6.2))(eslint@9.39.2(jiti@2.6.1))(ioredis@5.9.2)(magicast@0.5.1)(optionator@0.9.4)(oxlint@1.42.0(oxlint-tsgolint@0.11.3))(rolldown@1.0.0-rc.1)(rollup@4.57.0)(terser@5.46.0)(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2))(vue-tsc@3.2.4(typescript@5.9.3))(yaml@2.8.2) 13843 14047 vue: 3.5.27(typescript@5.9.3) 13844 14048 transitivePeerDependencies: 13845 14049 - magicast ··· 14137 14341 dependencies: 14138 14342 possible-typed-array-names: 1.1.0 14139 14343 14344 + await-lock@2.2.2: {} 14345 + 14140 14346 axe-core@4.11.1: {} 14141 14347 14142 14348 b4a@1.7.3: {} ··· 14689 14895 14690 14896 diff@8.0.3: {} 14691 14897 14692 - docus@5.4.4(29b67e4ea82958d3bb639aa676be121d): 14898 + docus@5.4.4(f5d56481b2ddeca600f9d39a31d83017): 14693 14899 dependencies: 14694 14900 '@iconify-json/lucide': 1.2.87 14695 14901 '@iconify-json/simple-icons': 1.2.68 14696 14902 '@iconify-json/vscode-icons': 1.2.40 14697 14903 '@nuxt/content': 3.11.0(better-sqlite3@12.6.2)(magicast@0.5.1)(valibot@1.2.0(typescript@5.9.3)) 14698 - '@nuxt/image': 2.0.0(db0@0.3.4(better-sqlite3@12.6.2))(ioredis@5.9.2)(magicast@0.5.1) 14904 + '@nuxt/image': 2.0.0(@upstash/redis@1.36.1)(@vercel/kv@3.0.0)(db0@0.3.4(better-sqlite3@12.6.2))(ioredis@5.9.2)(magicast@0.5.1) 14699 14905 '@nuxt/kit': 4.3.0(magicast@0.5.1) 14700 - '@nuxt/ui': 4.4.0(@nuxt/content@3.11.0(better-sqlite3@12.6.2)(magicast@0.5.1)(valibot@1.2.0(typescript@5.9.3)))(@tiptap/extensions@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1))(@tiptap/y-tiptap@3.0.2(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.5)(y-protocols@1.0.7(yjs@13.6.29))(yjs@13.6.29))(db0@0.3.4(better-sqlite3@12.6.2))(embla-carousel@8.6.0)(ioredis@5.9.2)(magicast@0.5.1)(tailwindcss@4.1.18)(typescript@5.9.3)(valibot@1.2.0(typescript@5.9.3))(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2))(vue-router@4.6.4(vue@3.5.27(typescript@5.9.3)))(vue@3.5.27(typescript@5.9.3))(yjs@13.6.29)(zod@4.3.6) 14701 - '@nuxtjs/i18n': 10.2.1(@vue/compiler-dom@3.5.27)(db0@0.3.4(better-sqlite3@12.6.2))(eslint@9.39.2(jiti@2.6.1))(ioredis@5.9.2)(magicast@0.5.1)(rollup@4.57.0)(vue@3.5.27(typescript@5.9.3)) 14906 + '@nuxt/ui': 4.4.0(@nuxt/content@3.11.0(better-sqlite3@12.6.2)(magicast@0.5.1)(valibot@1.2.0(typescript@5.9.3)))(@tiptap/extensions@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1))(@tiptap/y-tiptap@3.0.2(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.5)(y-protocols@1.0.7(yjs@13.6.29))(yjs@13.6.29))(@upstash/redis@1.36.1)(@vercel/kv@3.0.0)(db0@0.3.4(better-sqlite3@12.6.2))(embla-carousel@8.6.0)(ioredis@5.9.2)(magicast@0.5.1)(tailwindcss@4.1.18)(typescript@5.9.3)(valibot@1.2.0(typescript@5.9.3))(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2))(vue-router@4.6.4(vue@3.5.27(typescript@5.9.3)))(vue@3.5.27(typescript@5.9.3))(yjs@13.6.29)(zod@4.3.6) 14907 + '@nuxtjs/i18n': 10.2.1(@upstash/redis@1.36.1)(@vercel/kv@3.0.0)(@vue/compiler-dom@3.5.27)(db0@0.3.4(better-sqlite3@12.6.2))(eslint@9.39.2(jiti@2.6.1))(ioredis@5.9.2)(magicast@0.5.1)(rollup@4.57.0)(vue@3.5.27(typescript@5.9.3)) 14702 14908 '@nuxtjs/mcp-toolkit': 0.6.2(hono@4.11.7)(magicast@0.5.1)(zod@4.3.6) 14703 14909 '@nuxtjs/mdc': 0.20.0(magicast@0.5.1) 14704 14910 '@nuxtjs/robots': 5.6.7(magicast@0.5.1)(vue@3.5.27(typescript@5.9.3))(zod@4.3.6) ··· 14709 14915 git-url-parse: 16.1.0 14710 14916 minimark: 0.2.0 14711 14917 motion-v: 1.10.2(@vueuse/core@14.1.0(vue@3.5.27(typescript@5.9.3)))(vue@3.5.27(typescript@5.9.3)) 14712 - nuxt: 4.3.0(@parcel/watcher@2.5.6)(@types/node@24.10.9)(@vue/compiler-sfc@3.5.27)(better-sqlite3@12.6.2)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.6.2))(eslint@9.39.2(jiti@2.6.1))(ioredis@5.9.2)(magicast@0.5.1)(optionator@0.9.4)(oxlint@1.42.0(oxlint-tsgolint@0.11.3))(rolldown@1.0.0-rc.1)(rollup@4.57.0)(terser@5.46.0)(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2))(vue-tsc@3.2.4(typescript@5.9.3))(yaml@2.8.2) 14918 + nuxt: 4.3.0(@parcel/watcher@2.5.6)(@types/node@24.10.9)(@upstash/redis@1.36.1)(@vercel/kv@3.0.0)(@vue/compiler-sfc@3.5.27)(better-sqlite3@12.6.2)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.6.2))(eslint@9.39.2(jiti@2.6.1))(ioredis@5.9.2)(magicast@0.5.1)(optionator@0.9.4)(oxlint@1.42.0(oxlint-tsgolint@0.11.3))(rolldown@1.0.0-rc.1)(rollup@4.57.0)(terser@5.46.0)(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2))(vue-tsc@3.2.4(typescript@5.9.3))(yaml@2.8.2) 14713 14919 nuxt-llms: 0.2.0(magicast@0.5.1) 14714 - nuxt-og-image: 5.1.13(@unhead/vue@2.1.2(vue@3.5.27(typescript@5.9.3)))(magicast@0.5.1)(unstorage@1.17.4(db0@0.3.4(better-sqlite3@12.6.2))(ioredis@5.9.2))(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2))(vue@3.5.27(typescript@5.9.3)) 14920 + nuxt-og-image: 5.1.13(@unhead/vue@2.1.2(vue@3.5.27(typescript@5.9.3)))(magicast@0.5.1)(unstorage@1.17.4(@upstash/redis@1.36.1)(@vercel/kv@3.0.0)(db0@0.3.4(better-sqlite3@12.6.2))(ioredis@5.9.2))(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2))(vue@3.5.27(typescript@5.9.3)) 14715 14921 pkg-types: 2.3.0 14716 14922 scule: 1.3.0 14717 14923 tailwindcss: 4.1.18 ··· 15384 15590 dependencies: 15385 15591 tiny-inflate: 1.0.3 15386 15592 15387 - fontless@0.1.0(db0@0.3.4(better-sqlite3@12.6.2))(ioredis@5.9.2)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2)): 15593 + fontless@0.1.0(@upstash/redis@1.36.1)(@vercel/kv@3.0.0)(db0@0.3.4(better-sqlite3@12.6.2))(ioredis@5.9.2)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2)): 15388 15594 dependencies: 15389 15595 consola: 3.4.2 15390 15596 css-tree: 3.1.0 ··· 15398 15604 pathe: 2.0.3 15399 15605 ufo: 1.6.3 15400 15606 unifont: 0.6.0 15401 - unstorage: 1.17.4(db0@0.3.4(better-sqlite3@12.6.2))(ioredis@5.9.2) 15607 + unstorage: 1.17.4(@upstash/redis@1.36.1)(@vercel/kv@3.0.0)(db0@0.3.4(better-sqlite3@12.6.2))(ioredis@5.9.2) 15402 15608 optionalDependencies: 15403 15609 vite: 7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2) 15404 15610 transitivePeerDependencies: ··· 15934 16140 15935 16141 ipaddr.js@1.9.1: {} 15936 16142 15937 - ipx@3.1.1(db0@0.3.4(better-sqlite3@12.6.2))(ioredis@5.9.2): 16143 + ipaddr.js@2.3.0: {} 16144 + 16145 + ipx@3.1.1(@upstash/redis@1.36.1)(@vercel/kv@3.0.0)(db0@0.3.4(better-sqlite3@12.6.2))(ioredis@5.9.2): 15938 16146 dependencies: 15939 16147 '@fastify/accept-negotiator': 2.0.1 15940 16148 citty: 0.1.6 ··· 15950 16158 sharp: 0.34.5 15951 16159 svgo: 4.0.0 15952 16160 ufo: 1.6.3 15953 - unstorage: 1.17.4(db0@0.3.4(better-sqlite3@12.6.2))(ioredis@5.9.2) 16161 + unstorage: 1.17.4(@upstash/redis@1.36.1)(@vercel/kv@3.0.0)(db0@0.3.4(better-sqlite3@12.6.2))(ioredis@5.9.2) 15954 16162 xss: 1.0.15 15955 16163 transitivePeerDependencies: 15956 16164 - '@azure/app-configuration' ··· 16224 16432 jiti@2.3.3: {} 16225 16433 16226 16434 jiti@2.6.1: {} 16435 + 16436 + jose@5.10.0: {} 16227 16437 16228 16438 jose@6.1.3: {} 16229 16439 ··· 17039 17249 17040 17250 neotraverse@0.6.18: {} 17041 17251 17042 - nitropack@2.13.1(better-sqlite3@12.6.2)(rolldown@1.0.0-rc.1): 17252 + nitropack@2.13.1(@upstash/redis@1.36.1)(@vercel/kv@3.0.0)(better-sqlite3@12.6.2)(rolldown@1.0.0-rc.1): 17043 17253 dependencies: 17044 17254 '@cloudflare/kv-asset-handler': 0.4.2 17045 17255 '@rollup/plugin-alias': 6.0.0(rollup@4.57.0) ··· 17106 17316 unenv: 2.0.0-rc.24 17107 17317 unimport: 5.6.0 17108 17318 unplugin-utils: 0.3.1 17109 - unstorage: 1.17.4(db0@0.3.4(better-sqlite3@12.6.2))(ioredis@5.9.2) 17319 + unstorage: 1.17.4(@upstash/redis@1.36.1)(@vercel/kv@3.0.0)(db0@0.3.4(better-sqlite3@12.6.2))(ioredis@5.9.2) 17110 17320 untyped: 2.0.0 17111 17321 unwasm: 0.5.3 17112 17322 youch: 4.1.0-beta.13 ··· 17212 17422 transitivePeerDependencies: 17213 17423 - magicast 17214 17424 17215 - nuxt-og-image@5.1.13(@unhead/vue@2.1.2(vue@3.5.27(typescript@5.9.3)))(magicast@0.5.1)(unstorage@1.17.4(db0@0.3.4(better-sqlite3@12.6.2))(ioredis@5.9.2))(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2))(vue@3.5.27(typescript@5.9.3)): 17425 + nuxt-og-image@5.1.13(@unhead/vue@2.1.2(vue@3.5.27(typescript@5.9.3)))(magicast@0.5.1)(unstorage@1.17.4(@upstash/redis@1.36.1)(@vercel/kv@3.0.0)(db0@0.3.4(better-sqlite3@12.6.2))(ioredis@5.9.2))(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2))(vue@3.5.27(typescript@5.9.3)): 17216 17426 dependencies: 17217 17427 '@nuxt/devtools-kit': 3.1.1(magicast@0.5.1)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2)) 17218 17428 '@nuxt/kit': 4.3.0(magicast@0.5.1) ··· 17243 17453 strip-literal: 3.1.0 17244 17454 ufo: 1.6.3 17245 17455 unplugin: 2.3.11 17246 - unstorage: 1.17.4(db0@0.3.4(better-sqlite3@12.6.2))(ioredis@5.9.2) 17456 + unstorage: 1.17.4(@upstash/redis@1.36.1)(@vercel/kv@3.0.0)(db0@0.3.4(better-sqlite3@12.6.2))(ioredis@5.9.2) 17247 17457 unwasm: 0.5.3 17248 17458 yoga-wasm-web: 0.3.3 17249 17459 transitivePeerDependencies: ··· 17277 17487 - magicast 17278 17488 - vue 17279 17489 17280 - nuxt@4.3.0(@parcel/watcher@2.5.6)(@types/node@24.10.9)(@vue/compiler-sfc@3.5.27)(better-sqlite3@12.6.2)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.6.2))(eslint@9.39.2(jiti@2.6.1))(ioredis@5.9.2)(magicast@0.5.1)(optionator@0.9.4)(oxlint@1.42.0(oxlint-tsgolint@0.11.3))(rolldown@1.0.0-rc.1)(rollup@4.57.0)(terser@5.46.0)(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2))(vue-tsc@3.2.4(typescript@5.9.3))(yaml@2.8.2): 17490 + nuxt@4.3.0(@parcel/watcher@2.5.6)(@types/node@24.10.9)(@upstash/redis@1.36.1)(@vercel/kv@3.0.0)(@vue/compiler-sfc@3.5.27)(better-sqlite3@12.6.2)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.6.2))(eslint@9.39.2(jiti@2.6.1))(ioredis@5.9.2)(magicast@0.5.1)(optionator@0.9.4)(oxlint@1.42.0(oxlint-tsgolint@0.11.3))(rolldown@1.0.0-rc.1)(rollup@4.57.0)(terser@5.46.0)(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2))(vue-tsc@3.2.4(typescript@5.9.3))(yaml@2.8.2): 17281 17491 dependencies: 17282 17492 '@dxup/nuxt': 0.3.2(magicast@0.5.1) 17283 17493 '@nuxt/cli': 3.32.0(cac@6.7.14)(magicast@0.5.1) 17284 17494 '@nuxt/devtools': 3.1.1(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2))(vue@3.5.27(typescript@5.9.3)) 17285 17495 '@nuxt/kit': 4.3.0(magicast@0.5.1) 17286 - '@nuxt/nitro-server': 4.3.0(better-sqlite3@12.6.2)(db0@0.3.4(better-sqlite3@12.6.2))(ioredis@5.9.2)(magicast@0.5.1)(nuxt@4.3.0(@parcel/watcher@2.5.6)(@types/node@24.10.9)(@vue/compiler-sfc@3.5.27)(better-sqlite3@12.6.2)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.6.2))(eslint@9.39.2(jiti@2.6.1))(ioredis@5.9.2)(magicast@0.5.1)(optionator@0.9.4)(oxlint@1.42.0(oxlint-tsgolint@0.11.3))(rolldown@1.0.0-rc.1)(rollup@4.57.0)(terser@5.46.0)(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2))(vue-tsc@3.2.4(typescript@5.9.3))(yaml@2.8.2))(rolldown@1.0.0-rc.1)(typescript@5.9.3) 17496 + '@nuxt/nitro-server': 4.3.0(@upstash/redis@1.36.1)(@vercel/kv@3.0.0)(better-sqlite3@12.6.2)(db0@0.3.4(better-sqlite3@12.6.2))(ioredis@5.9.2)(magicast@0.5.1)(nuxt@4.3.0(@parcel/watcher@2.5.6)(@types/node@24.10.9)(@upstash/redis@1.36.1)(@vercel/kv@3.0.0)(@vue/compiler-sfc@3.5.27)(better-sqlite3@12.6.2)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.6.2))(eslint@9.39.2(jiti@2.6.1))(ioredis@5.9.2)(magicast@0.5.1)(optionator@0.9.4)(oxlint@1.42.0(oxlint-tsgolint@0.11.3))(rolldown@1.0.0-rc.1)(rollup@4.57.0)(terser@5.46.0)(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2))(vue-tsc@3.2.4(typescript@5.9.3))(yaml@2.8.2))(rolldown@1.0.0-rc.1)(typescript@5.9.3) 17287 17497 '@nuxt/schema': 4.3.0 17288 17498 '@nuxt/telemetry': 2.6.6(magicast@0.5.1) 17289 - '@nuxt/vite-builder': 4.3.0(@types/node@24.10.9)(eslint@9.39.2(jiti@2.6.1))(magicast@0.5.1)(nuxt@4.3.0(@parcel/watcher@2.5.6)(@types/node@24.10.9)(@vue/compiler-sfc@3.5.27)(better-sqlite3@12.6.2)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.6.2))(eslint@9.39.2(jiti@2.6.1))(ioredis@5.9.2)(magicast@0.5.1)(optionator@0.9.4)(oxlint@1.42.0(oxlint-tsgolint@0.11.3))(rolldown@1.0.0-rc.1)(rollup@4.57.0)(terser@5.46.0)(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2))(vue-tsc@3.2.4(typescript@5.9.3))(yaml@2.8.2))(optionator@0.9.4)(oxlint@1.42.0(oxlint-tsgolint@0.11.3))(rolldown@1.0.0-rc.1)(rollup@4.57.0)(terser@5.46.0)(typescript@5.9.3)(vue-tsc@3.2.4(typescript@5.9.3))(vue@3.5.27(typescript@5.9.3))(yaml@2.8.2) 17499 + '@nuxt/vite-builder': 4.3.0(@types/node@24.10.9)(eslint@9.39.2(jiti@2.6.1))(magicast@0.5.1)(nuxt@4.3.0(@parcel/watcher@2.5.6)(@types/node@24.10.9)(@upstash/redis@1.36.1)(@vercel/kv@3.0.0)(@vue/compiler-sfc@3.5.27)(better-sqlite3@12.6.2)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.6.2))(eslint@9.39.2(jiti@2.6.1))(ioredis@5.9.2)(magicast@0.5.1)(optionator@0.9.4)(oxlint@1.42.0(oxlint-tsgolint@0.11.3))(rolldown@1.0.0-rc.1)(rollup@4.57.0)(terser@5.46.0)(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2))(vue-tsc@3.2.4(typescript@5.9.3))(yaml@2.8.2))(optionator@0.9.4)(oxlint@1.42.0(oxlint-tsgolint@0.11.3))(rolldown@1.0.0-rc.1)(rollup@4.57.0)(terser@5.46.0)(typescript@5.9.3)(vue-tsc@3.2.4(typescript@5.9.3))(vue@3.5.27(typescript@5.9.3))(yaml@2.8.2) 17290 17500 '@unhead/vue': 2.1.2(vue@3.5.27(typescript@5.9.3)) 17291 17501 '@vue/shared': 3.5.27 17292 17502 c12: 3.3.3(magicast@0.5.1) ··· 19119 19329 19120 19330 tinyrainbow@3.0.3: {} 19121 19331 19332 + tlds@1.261.0: {} 19333 + 19122 19334 to-buffer@1.2.2: 19123 19335 dependencies: 19124 19336 isarray: 2.0.5 ··· 19288 19500 unplugin: 2.3.11 19289 19501 19290 19502 undici-types@7.16.0: {} 19503 + 19504 + undici@6.23.0: {} 19291 19505 19292 19506 unenv@2.0.0-rc.24: 19293 19507 dependencies: ··· 19525 19739 dependencies: 19526 19740 rolldown: 1.0.0-rc.1 19527 19741 19528 - unstorage@1.17.4(db0@0.3.4(better-sqlite3@12.6.2))(ioredis@5.9.2): 19742 + unstorage@1.17.4(@upstash/redis@1.36.1)(@vercel/kv@3.0.0)(db0@0.3.4(better-sqlite3@12.6.2))(ioredis@5.9.2): 19529 19743 dependencies: 19530 19744 anymatch: 3.1.3 19531 19745 chokidar: 5.0.0 ··· 19536 19750 ofetch: 1.5.1 19537 19751 ufo: 1.6.3 19538 19752 optionalDependencies: 19753 + '@upstash/redis': 1.36.1 19754 + '@vercel/kv': 3.0.0 19539 19755 db0: 0.3.4(better-sqlite3@12.6.2) 19540 19756 ioredis: 5.9.2 19541 19757
+64
server/api/auth/atproto.get.ts
··· 1 + import { Agent } from '@atproto/api' 2 + import { NodeOAuthClient } from '@atproto/oauth-client-node' 3 + import { createError, getQuery, sendRedirect } from 'h3' 4 + import { useOAuthStorage } from '#server/utils/atproto/storage' 5 + import { SLINGSHOT_ENDPOINT } from '#shared/utils/constants' 6 + import type { UserSession } from '#shared/schemas/userSession' 7 + 8 + export default defineEventHandler(async event => { 9 + const config = useRuntimeConfig(event) 10 + if (!config.sessionPassword) { 11 + throw createError({ 12 + status: 500, 13 + message: 'NUXT_SESSION_PASSWORD not set', 14 + }) 15 + } 16 + 17 + const query = getQuery(event) 18 + const clientMetadata = getOauthClientMetadata() 19 + const { stateStore, sessionStore } = useOAuthStorage(event) 20 + 21 + const atclient = new NodeOAuthClient({ 22 + stateStore, 23 + sessionStore, 24 + clientMetadata, 25 + }) 26 + 27 + if (!query.code) { 28 + const handle = query.handle?.toString() 29 + const create = query.create?.toString() 30 + 31 + if (!handle) { 32 + throw createError({ 33 + status: 400, 34 + message: 'Handle not provided in query', 35 + }) 36 + } 37 + 38 + const redirectUrl = await atclient.authorize(handle, { 39 + scope, 40 + prompt: create ? 'create' : undefined, 41 + }) 42 + return sendRedirect(event, redirectUrl.toString()) 43 + } 44 + 45 + const { session: authSession } = await atclient.callback( 46 + new URLSearchParams(query as Record<string, string>), 47 + ) 48 + const agent = new Agent(authSession) 49 + event.context.agent = agent 50 + 51 + const session = await useSession(event, { 52 + password: config.sessionPassword, 53 + }) 54 + 55 + const response = await fetch( 56 + `${SLINGSHOT_ENDPOINT}/xrpc/com.bad-example.identity.resolveMiniDoc?identifier=${agent.did}`, 57 + { headers: { 'User-Agent': 'npmx' } }, 58 + ) 59 + const miniDoc = (await response.json()) as UserSession 60 + 61 + await session.update(miniDoc) 62 + 63 + return sendRedirect(event, '/') 64 + })
+6
server/api/auth/session.delete.ts
··· 1 + export default eventHandlerWithOAuthSession(async (event, oAuthSession, serverSession) => { 2 + await oAuthSession?.signOut() 3 + await serverSession.clear() 4 + 5 + return 'Session cleared' 6 + })
+11
server/api/auth/session.get.ts
··· 1 + import { UserSessionSchema } from '#shared/schemas/userSession' 2 + import { safeParse } from 'valibot' 3 + 4 + export default eventHandlerWithOAuthSession(async (event, oAuthSession, serverSession) => { 5 + const result = safeParse(UserSessionSchema, serverSession.data) 6 + if (!result.success) { 7 + return null 8 + } 9 + 10 + return result.output 11 + })
+3
server/routes/oauth-client-metadata.json.get.ts
··· 1 + export default defineEventHandler(() => { 2 + return getOauthClientMetadata() 3 + })
+68
server/utils/atproto/lock.ts
··· 1 + import type { RuntimeLock } from '@atproto/oauth-client-node' 2 + import { requestLocalLock } from '@atproto/oauth-client-node' 3 + import { Redis } from '@upstash/redis' 4 + 5 + type Awaitable<T> = T | PromiseLike<T> 6 + 7 + /** 8 + * Creates a distributed lock using Upstash Redis. 9 + * Falls back gracefully if the lock cannot be acquired. 10 + */ 11 + function createUpstashLock(redis: Redis): RuntimeLock { 12 + return async <T>(key: string, fn: () => Awaitable<T>): Promise<T> => { 13 + const lockKey = `oauth:lock:${key}` 14 + const lockValue = crypto.randomUUID() 15 + const lockTTL = 30 // seconds 16 + 17 + // Try to acquire lock with NX (only set if not exists) and EX (expire) 18 + const acquired = await redis.set(lockKey, lockValue, { 19 + nx: true, 20 + ex: lockTTL, 21 + }) 22 + 23 + if (!acquired) { 24 + // Another instance holds the lock, wait briefly and retry once 25 + await new Promise(resolve => setTimeout(resolve, 100)) 26 + const retryAcquired = await redis.set(lockKey, lockValue, { 27 + nx: true, 28 + ex: lockTTL, 29 + }) 30 + if (!retryAcquired) { 31 + // Still can't acquire, proceed without lock (better than failing) 32 + // The worst case is a token refresh race, which will just require re-auth 33 + return await fn() 34 + } 35 + } 36 + 37 + try { 38 + return await fn() 39 + } finally { 40 + // Release lock only if we still own it (compare-and-delete) 41 + const currentValue = await redis.get(lockKey) 42 + if (currentValue === lockValue) { 43 + await redis.del(lockKey) 44 + } 45 + } 46 + } 47 + } 48 + 49 + /** 50 + * Returns the appropriate lock mechanism based on environment: 51 + * - Production with Upstash config: distributed Redis lock 52 + * - Otherwise: in-memory lock (sufficient for single instance) 53 + */ 54 + export function getOAuthLock(): RuntimeLock { 55 + const config = useRuntimeConfig() 56 + 57 + // Use distributed lock in production if Upstash is configured 58 + if (!import.meta.dev && config.upstash?.redisRestUrl && config.upstash?.redisRestToken) { 59 + const redis = new Redis({ 60 + url: config.upstash.redisRestUrl, 61 + token: config.upstash.redisRestToken, 62 + }) 63 + return createUpstashLock(redis) 64 + } 65 + 66 + // Fall back to in-memory lock for dev/preview or when Redis isn't configured 67 + return requestLocalLock 68 + }
+82
server/utils/atproto/oauth.ts
··· 1 + import type { OAuthClientMetadataInput, OAuthSession } from '@atproto/oauth-client-node' 2 + import type { EventHandlerRequest, H3Event, SessionManager } from 'h3' 3 + import { NodeOAuthClient } from '@atproto/oauth-client-node' 4 + import { parse } from 'valibot' 5 + import { getOAuthLock } from '#server/utils/atproto/lock' 6 + import { useOAuthStorage } from '#server/utils/atproto/storage' 7 + import { UNSET_NUXT_SESSION_PASSWORD } from '#shared/utils/constants' 8 + import { OAuthMetadataSchema } from '#shared/schemas/oauth' 9 + // TODO: limit scope as features gets added. atproto just allows login so no scary login screen till we have scopes 10 + export const scope = 'atproto' 11 + 12 + export function getOauthClientMetadata() { 13 + const dev = import.meta.dev 14 + 15 + // on dev, match in nuxt.config.ts devServer: { host: "127.0.0.1" } 16 + const client_uri = dev ? `http://127.0.0.1:3000` : 'https://npmx.dev' 17 + const redirect_uri = `${client_uri}/api/auth/atproto` 18 + 19 + const client_id = dev 20 + ? `http://localhost?redirect_uri=${encodeURIComponent(redirect_uri)}&scope=${encodeURIComponent(scope)}` 21 + : `${client_uri}/oauth-client-metadata.json` 22 + 23 + // If anything changes here, please make sure to also update /shared/schemas/oauth.ts to match 24 + return parse(OAuthMetadataSchema, { 25 + client_name: 'npmx.dev', 26 + client_id, 27 + client_uri, 28 + scope, 29 + redirect_uris: [redirect_uri] as [string, ...string[]], 30 + grant_types: ['authorization_code', 'refresh_token'], 31 + application_type: 'web', 32 + token_endpoint_auth_method: 'none', 33 + dpop_bound_access_tokens: true, 34 + }) as OAuthClientMetadataInput 35 + } 36 + 37 + type EventHandlerWithOAuthSession<T extends EventHandlerRequest, D> = ( 38 + event: H3Event<T>, 39 + session: OAuthSession | undefined, 40 + serverSession: SessionManager, 41 + ) => Promise<D> 42 + 43 + async function getOAuthSession(event: H3Event): Promise<OAuthSession | undefined> { 44 + const clientMetadata = getOauthClientMetadata() 45 + const { stateStore, sessionStore } = useOAuthStorage(event) 46 + 47 + const client = new NodeOAuthClient({ 48 + stateStore, 49 + sessionStore, 50 + clientMetadata, 51 + requestLock: getOAuthLock(), 52 + }) 53 + 54 + const currentSession = await sessionStore.get() 55 + if (!currentSession) return undefined 56 + 57 + // restore using the subject 58 + return await client.restore(currentSession.tokenSet.sub) 59 + } 60 + 61 + /** @public */ 62 + export function eventHandlerWithOAuthSession<T extends EventHandlerRequest, D>( 63 + handler: EventHandlerWithOAuthSession<T, D>, 64 + ) { 65 + return defineEventHandler(async event => { 66 + const config = useRuntimeConfig(event) 67 + 68 + if (!config.sessionPassword) { 69 + throw createError({ 70 + status: 500, 71 + message: UNSET_NUXT_SESSION_PASSWORD, 72 + }) 73 + } 74 + 75 + const serverSession = await useSession(event, { 76 + password: config.sessionPassword, 77 + }) 78 + 79 + const oAuthSession = await getOAuthSession(event) 80 + return await handler(event, oAuthSession, serverSession) 81 + }) 82 + }
+89
server/utils/atproto/storage.ts
··· 1 + import type { 2 + NodeSavedSession, 3 + NodeSavedSessionStore, 4 + NodeSavedState, 5 + NodeSavedStateStore, 6 + } from '@atproto/oauth-client-node' 7 + import type { H3Event } from 'h3' 8 + 9 + /** 10 + * Storage key prefix for oauth state storage. 11 + */ 12 + export const OAUTH_STATE_CACHE_STORAGE_BASE = 'oauth-atproto-state' 13 + 14 + export class OAuthStateStore implements NodeSavedStateStore { 15 + private readonly cookieKey = 'oauth:atproto:state' 16 + private readonly storage = useStorage(OAUTH_STATE_CACHE_STORAGE_BASE) 17 + 18 + constructor(private event: H3Event) {} 19 + 20 + async get(): Promise<NodeSavedState | undefined> { 21 + const stateKey = getCookie(this.event, this.cookieKey) 22 + if (!stateKey) return 23 + const result = await this.storage.getItem<NodeSavedState>(stateKey) 24 + if (!result) return 25 + return result 26 + } 27 + 28 + async set(key: string, val: NodeSavedState) { 29 + setCookie(this.event, this.cookieKey, key, { 30 + httpOnly: true, 31 + secure: !import.meta.dev, 32 + sameSite: 'lax', 33 + }) 34 + await this.storage.setItem<NodeSavedState>(key, val) 35 + } 36 + 37 + async del() { 38 + const stateKey = getCookie(this.event, this.cookieKey) 39 + deleteCookie(this.event, this.cookieKey) 40 + if (stateKey) { 41 + await this.storage.del(stateKey) 42 + } 43 + } 44 + } 45 + 46 + /** 47 + * Storage key prefix for oauth session storage. 48 + */ 49 + export const OAUTH_SESSION_CACHE_STORAGE_BASE = 'oauth-atproto-session' 50 + 51 + export class OAuthSessionStore implements NodeSavedSessionStore { 52 + // TODO: not sure if we will support multi accounts, but if we do in the future will need to change this around 53 + private readonly cookieKey = 'oauth:atproto:session' 54 + private readonly storage = useStorage(OAUTH_SESSION_CACHE_STORAGE_BASE) 55 + 56 + constructor(private event: H3Event) {} 57 + 58 + async get(): Promise<NodeSavedSession | undefined> { 59 + const sessionKey = getCookie(this.event, this.cookieKey) 60 + if (!sessionKey) return 61 + const result = await this.storage.getItem<NodeSavedSession>(sessionKey) 62 + if (!result) return 63 + return result 64 + } 65 + 66 + async set(key: string, val: NodeSavedSession) { 67 + setCookie(this.event, this.cookieKey, key, { 68 + httpOnly: true, 69 + secure: !import.meta.dev, 70 + sameSite: 'lax', 71 + }) 72 + await this.storage.setItem<NodeSavedSession>(key, val) 73 + } 74 + 75 + async del() { 76 + const sessionKey = getCookie(this.event, this.cookieKey) 77 + if (sessionKey) { 78 + await this.storage.del(sessionKey) 79 + } 80 + deleteCookie(this.event, this.cookieKey) 81 + } 82 + } 83 + 84 + export const useOAuthStorage = (event: H3Event) => { 85 + return { 86 + stateStore: new OAuthStateStore(event), 87 + sessionStore: new OAuthSessionStore(event), 88 + } 89 + }
+13
shared/schemas/oauth.ts
··· 1 + import { object, string, pipe, url, array, minLength, boolean } from 'valibot' 2 + 3 + export const OAuthMetadataSchema = object({ 4 + client_id: pipe(string(), url()), 5 + client_name: string(), 6 + client_uri: pipe(string(), url()), 7 + redirect_uris: pipe(array(string()), minLength(1)), 8 + scope: string(), 9 + grant_types: array(string()), 10 + application_type: string(), 11 + token_endpoint_auth_method: string(), 12 + dpop_bound_access_tokens: boolean(), 13 + })
+10
shared/schemas/userSession.ts
··· 1 + import { object, string, pipe, url } from 'valibot' 2 + import type { InferOutput } from 'valibot' 3 + 4 + export const UserSessionSchema = object({ 5 + did: string(), 6 + handle: string(), 7 + pds: pipe(string(), url()), 8 + }) 9 + 10 + export type UserSession = InferOutput<typeof UserSessionSchema>
+5
shared/utils/constants.ts
··· 16 16 export const NPM_MISSING_README_SENTINEL = 'ERROR: No README data found!' 17 17 export const ERROR_JSR_FETCH_FAILED = 'Failed to fetch package from JSR registry.' 18 18 export const ERROR_NPM_FETCH_FAILED = 'Failed to fetch package from npm registry.' 19 + export const UNSET_NUXT_SESSION_PASSWORD = 'NUXT_SESSION_PASSWORD not set' 19 20 /** @public */ 20 21 export const ERROR_SUGGESTIONS_FETCH_FAILED = 'Failed to fetch suggestions.' 22 + 23 + // microcosm services 24 + export const CONSTELLATION_ENDPOINT = 'https://constellation.microcosm.blue' 25 + export const SLINGSHOT_ENDPOINT = 'https://slingshot.microcosm.blue' 21 26 22 27 // Theming 23 28 export const ACCENT_COLORS = {
+5
shared/utils/fetch-cache-config.ts
··· 5 5 * using Nitro's storage layer (backed by Vercel's runtime cache in production). 6 6 */ 7 7 8 + import { CONSTELLATION_ENDPOINT, SLINGSHOT_ENDPOINT } from './constants' 9 + 8 10 /** 9 11 * Domains that should have their fetch responses cached. 10 12 * Only requests to these domains will be intercepted and cached. ··· 24 26 'api.bitbucket.org', // Bitbucket API 25 27 'codeberg.org', // Codeberg (Gitea-based) 26 28 'gitee.com', // Gitee API 29 + // microcosm endpoints for atproto data 30 + CONSTELLATION_ENDPOINT, 31 + SLINGSHOT_ENDPOINT, 27 32 ] as const 28 33 29 34 /**