npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@forge-kits/nuxt

v0.1.3

Published

Nuxt composables bridge for forge-kits (FastAPI)

Readme

@forge-kits/nuxt

Nuxt module that bridges forge-kits FastAPI backend with your Nuxt app.

Two auth contexts, one module:

  • useForgeAuth() / useForgeAuth('client') — regular users. Login, logout, current user.
  • useForgeAuth('guard') — RBAC context. Independent session, full RBAC via useForgePermissions.

Installation

npm install @forge-kits/nuxt
// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@forge-kits/nuxt'],
  forgeApi: {
    url: 'http://localhost:8000',
    prefix: '/api/v1',   // or false to omit
    strategy: 'cookie',  // 'cookie' | 'telegram'
    auth: {
      client: { autoFetch: true },
      guard: { autoFetch: false },
    },
  },
})

All options

| Option | Default | Description | |---|---|---| | url | http://localhost:8000 | Backend base URL | | prefix | /api/v1 | API prefix prepended to every request. Set to false to omit | | strategy | cookie | Auth strategy: cookie or telegram | | credentials | true | Send cookies with every request | | auth.client.autoFetch | false | Fetch client session on app start | | auth.client.login | /auth/login | Client login endpoint | | auth.client.logout | /auth/logout | Client logout endpoint | | auth.client.me | /auth/me | Client current-user endpoint | | auth.guard.autoFetch | false | Fetch guard session on app start | | auth.guard.login | /admin/auth/login | Guard login endpoint | | auth.guard.logout | /admin/auth/logout | Guard logout endpoint | | auth.guard.me | /admin/auth/me | Guard current-user endpoint |


Client auth — Cookie

The server sets a signed httpOnly session cookie. Every request sends credentials: 'include' automatically. On app start the module calls /auth/me and hydrates the user state.

Login page

<!-- pages/login.vue -->
<script setup lang="ts">
const { login, isAuthenticated } = useForgeAuth()
const { form, errors, serverError, loading, submit } = useForgeForm({
  email: '',
  password: '',
})

if (isAuthenticated.value) navigateTo('/')

async function handleLogin() {
  await submit(async (data) => {
    await login(data)
    navigateTo('/')
  })
}
</script>

<template>
  <form @submit.prevent="handleLogin">
    <div>
      <input v-model="form.email" type="email" placeholder="Email" />
      <span v-if="errors.email">{{ errors.email }}</span>
    </div>
    <div>
      <input v-model="form.password" type="password" placeholder="Password" />
      <span v-if="errors.password">{{ errors.password }}</span>
    </div>
    <p v-if="serverError">{{ serverError }}</p>
    <button :disabled="loading">{{ loading ? 'Signing in…' : 'Sign in' }}</button>
  </form>
</template>

Navbar / current user

<script setup lang="ts">
const { user, isAuthenticated, logout } = useForgeAuth()
</script>

<template>
  <nav>
    <template v-if="isAuthenticated">
      <span>{{ user?.email }}</span>
      <button @click="logout">Logout</button>
    </template>
    <NuxtLink v-else to="/login">Login</NuxtLink>
  </nav>
</template>

Client auth middleware

// middleware/auth.ts — manual
export default defineNuxtRouteMiddleware(() => {
  const { isAuthenticated } = useForgeAuth()
  if (!isAuthenticated.value) return navigateTo('/login')
})

// or via factory
definePageMeta({
  middleware: [ForgeAuthMiddleware({ redirect: '/login' })],
})

<ForgeAuth> component

Renders slot only when authenticated. Accepts role prop ('client' | 'guard', default 'client').

<ForgeAuth>
  <UserDashboard />
  <template #fallback>
    <NuxtLink to="/login">Login</NuxtLink>
  </template>
</ForgeAuth>

<!-- guard context -->
<ForgeAuth role="guard">
  <AdminPanel />
  <template #fallback>
    <NuxtLink to="/admin/login">Admin login</NuxtLink>
  </template>
</ForgeAuth>

Backend:

# app/controllers/auth_controller.py
class AuthController(Controller):
    prefix = "/auth"

    @route.post("/login")
    async def login(self, body: LoginRequest, response: Response):
        user = await User.get_or_none(email=body.email)
        if not user or not user.check_password(body.password):
            raise HTTPException(401, "Invalid credentials")
        auth.set_cookie(response, user.auth_claims())
        return {"ok": True}

    @route.post("/logout")
    async def logout(self, response: Response):
        auth.delete_cookie(response)
        return {"ok": True}

    @route.get("/me")
    async def me(self, auth_user: CurrentUser):
        user = await User.find_or_fail(int(auth_user.id))
        return {"id": user.id, "email": user.email}

Client auth — Telegram Mini App

Reads window.Telegram.WebApp.initData and sends it as X-Telegram-Init-Data on every request. No login/logout — the user is resolved via /auth/me on app start.

// nuxt.config.ts
forgeApi: { strategy: 'telegram' }
<script setup lang="ts">
const {
  user,              // backend user (server-verified)
  isAuthenticated,
  fetchUser,
  tgUser,            // from initDataUnsafe — display only, untrusted
  tgUserId,
  tgUsername,
  tgFullName,
  tgPhotoUrl,
  tgLanguageCode,
  tgIsPremium,
  tgAllowsWriteToPm,
  isWebApp,
  tgReady,           // call WebApp.ready()
  tgHaptic,          // 'light' | 'medium' | 'heavy' | 'rigid' | 'soft'
  tgHapticSuccess,
} = useForgeAuth()

onMounted(tgReady)
</script>

<template>
  <div v-if="isAuthenticated">
    <img v-if="tgPhotoUrl" :src="tgPhotoUrl" />
    <p>{{ tgFullName }}</p>
    <p v-if="tgIsPremium">⭐ Premium</p>
  </div>
</template>

Local development — paste a real initData string to .env:

VITE_TELEGRAM_INIT_DATA=user=%7B%22id%22%3A...&hash=abc123

<ForgeTg> component

Renders slot only inside a Telegram Mini App (isWebApp === true).

<ForgeTg>
  <TelegramSpecificUI />
  <template #fallback>
    <p>Open in Telegram to continue.</p>
  </template>
</ForgeTg>

Guard auth & RBAC

Guard has its own independent session (forge_guard state, separate endpoints). The guard's /me response must include permissions and roles arrays — the module uses them for all RBAC checks.

Guard login page

<!-- pages/admin/login.vue -->
<script setup lang="ts">
const { login, isAuthenticated } = useForgeAuth('guard')
const { form, errors, serverError, loading, submit } = useForgeForm({
  email: '',
  password: '',
})

if (isAuthenticated.value) navigateTo('/admin/dashboard')

async function handleLogin() {
  await submit(async (data) => {
    await login(data)
    navigateTo('/admin/dashboard')
  })
}
</script>

<template>
  <form @submit.prevent="handleLogin">
    <div>
      <input v-model="form.email" type="email" placeholder="Email" />
      <span v-if="errors.email">{{ errors.email }}</span>
    </div>
    <div>
      <input v-model="form.password" type="password" placeholder="Password" />
      <span v-if="errors.password">{{ errors.password }}</span>
    </div>
    <p v-if="serverError">{{ serverError }}</p>
    <button :disabled="loading">{{ loading ? 'Signing in…' : 'Admin sign in' }}</button>
  </form>
</template>

Guard auth middleware

// via factory (recommended)
definePageMeta({
  middleware: [ForgeAuthMiddleware({ role: 'guard', redirect: '/admin/login' })],
})

// or manual
export default defineNuxtRouteMiddleware(() => {
  const { isAuthenticated } = useForgeAuth('guard')
  if (!isAuthenticated.value) return navigateTo('/admin/login')
})

RBAC in templates — <ForgeCan> / <ForgeRole>

<!-- any permission from the list -->
<ForgeCan perm="edit:posts">
  <button>Edit</button>
</ForgeCan>

<!-- must have ALL permissions -->
<ForgeCan :perm="['edit:posts', 'publish:posts']" :all="true">
  <PublishPanel />
</ForgeCan>

<!-- role-based with fallback -->
<ForgeRole role="admin">
  <AdminPanel />
  <template #fallback>
    <p>Admins only.</p>
  </template>
</ForgeRole>

<!-- must have ALL roles -->
<ForgeRole :role="['admin', 'editor']" :all="true">
  <SuperPanel />
</ForgeRole>

| Prop | Type | Default | Description | |---|---|---|---| | perm / role | string \| string[] | — | Permission(s) or role(s) to check | | all | boolean | false | Require all (AND) instead of any (OR) |

RBAC in logic — useForgePermissions

<script setup lang="ts">
const { can, canAll, hasRole, hasAllRoles, permissions, roles } = useForgePermissions()
</script>

<template>
  <button v-if="can('edit:posts')">Edit</button>
  <button v-if="hasRole('admin')">Delete</button>
</template>

| Method | Returns true when | |---|---| | can(...perms) | guard has any of the permissions | | canAll(...perms) | guard has all permissions | | hasRole(...roles) | guard has any of the roles | | hasAllRoles(...roles) | guard has all roles |

RBAC route middleware

// pages/admin/posts/[id]/edit.vue
definePageMeta({
  middleware: [
    'admin-auth',
    PermissionMiddleware('edit:posts', { redirect: '/admin/403' }),
  ],
})

// require ALL permissions
definePageMeta({
  middleware: [PermissionAllMiddleware(['edit:posts', 'publish:posts'], { redirect: '/admin/403' })],
})

// role-based
definePageMeta({
  middleware: [RoleMiddleware('editor', { redirect: '/admin/403' })],
})

// require ALL roles
definePageMeta({
  middleware: [RoleAllMiddleware(['admin', 'editor'], { redirect: '/admin/403' })],
})

Without redirect option — throws 403 Forbidden. With redirect — calls navigateTo(redirect).

Backend:

@route.get("/me")
async def me(self, auth_user: CurrentUser):
    admin = await Admin.find_or_fail(int(auth_user.id))
    return {
        "id":          admin.id,
        "email":       admin.email,
        "permissions": await admin.get_all_permissions(),
        "roles":       await admin.get_role_names(),
    }

API calls — useForgeApi

Typed wrapper around $fetch. Automatically attaches baseURL, cookies/Telegram header, and credentials.

<script setup lang="ts">
interface Post { id: number; title: string; body: string }

const api = useForgeApi()

const posts = await api.get<Post[]>('/posts', { params: { page: 1, search: 'nuxt' } })
const post  = await api.post<Post>('/posts', { title: 'Hello', body: '...' })

await api.patch<Post>(`/posts/${post.id}`, { title: 'Updated' })
await api.put<Post>(`/posts/${post.id}`, { title: 'Replaced', body: '...' })
await api.delete(`/posts/${post.id}`)
</script>

Forms — useForgeForm

Automatically maps FastAPI 422 Pydantic validation errors to field-level errors. Non-field errors go to serverError. Re-throws unknown errors so you can handle them upstream.

<script setup lang="ts">
const api = useForgeApi()
const { form, errors, serverError, loading, clearErrors, submit } = useForgeForm({
  title: '',
  body: '',
  tags: '',
})

async function handleSubmit() {
  await submit(async (data) => {
    await api.post('/posts', data)
    navigateTo('/posts')
  })
}
</script>

<template>
  <form @submit.prevent="handleSubmit">
    <div>
      <input v-model="form.title" placeholder="Title" />
      <span v-if="errors.title">{{ errors.title }}</span>
    </div>
    <div>
      <textarea v-model="form.body" placeholder="Body" />
      <span v-if="errors.body">{{ errors.body }}</span>
    </div>
    <p v-if="serverError" class="error">{{ serverError }}</p>
    <button :disabled="loading">{{ loading ? 'Saving…' : 'Save' }}</button>
  </form>
</template>

| Return | Type | Description | |---|---|---| | form | reactive<T> | Two-way bound form data | | errors | Ref<Record<string, string>> | Field errors from Pydantic 422 | | serverError | Ref<string \| null> | Non-field error from detail string | | loading | Ref<boolean> | true while submit is in-flight | | clearErrors() | () => void | Reset all errors manually | | submit(fn) | (fn) => Promise<void> | Run fn, catch and map backend errors |


Pagination — useForgePagination

Fetches paginated data on mount. Backend must return the standard forge-kits envelope.

<script setup lang="ts">
interface Post { id: number; title: string }

const { data, meta, loading, error, page, nextPage, prevPage, goToPage } =
  useForgePagination<Post>('/posts', { perPage: 20 })
</script>

<template>
  <div v-if="loading">Loading…</div>
  <ul v-else>
    <li v-for="post in data" :key="post.id">{{ post.title }}</li>
  </ul>

  <div>
    <button :disabled="page <= 1" @click="prevPage">← Prev</button>
    <span>{{ page }} / {{ meta?.last_page }}</span>
    <button :disabled="!meta || page >= meta.last_page" @click="nextPage">Next →</button>
  </div>
</template>

| Option | Type | Default | Description | |---|---|---|---| | perPage | number | backend default | Items per page | | immediate | boolean | true | Fetch on mount |

PaginationMeta: { current_page, per_page, total, last_page, from, to }

Backend envelope: { data: T[], meta: PaginationMeta, links: { prev, next } }


File uploads — useForgeUpload

Uses XMLHttpRequest for real-time progress tracking. Cookies and Telegram header are sent automatically.

<script setup lang="ts">
const { progress, loading, error, result, upload, reset } = useForgeUpload('/files/upload')

async function handleFile(e: Event) {
  const file = (e.target as HTMLInputElement).files?.[0]
  if (!file) return
  const res = await upload(file)
  console.log('URL:', res.url)
}

// with extra form fields
async function handleAvatar(file: File) {
  await upload(file, { category: 'avatars' })
}
</script>

<template>
  <input type="file" @change="handleFile" />
  <div v-if="loading">
    <progress :value="progress" max="100" />
    <span>{{ progress }}%</span>
  </div>
  <p v-if="error">{{ error }}</p>
  <img v-if="result?.url" :src="result.url" />
  <button v-if="result" @click="reset">Upload another</button>
</template>

| Return | Type | Description | |---|---|---| | progress | Ref<number> | 0–100, updated in real time | | loading | Ref<boolean> | true while uploading | | error | Ref<string \| null> | Error message if upload failed | | result | Ref<{ url, path?, ...} \| null> | Server response | | upload(file, extra?) | Promise<UploadResponse> | Start upload | | reset() | () => void | Clear state |


API reference

Composables

| Composable | Signature | Returns | |---|---|---| | useForgeAuth | (role?: 'client' \| 'guard') | user, isAuthenticated, login, logout, fetchUser, initData, initDataUnsafe, tgUser, tgUserId, tgUsername, tgFullName, tgPhotoUrl, tgLanguageCode, tgIsPremium, tgAllowsWriteToPm, isWebApp, tgReady, tgHaptic, tgHapticSuccess | | useForgePermissions | () | permissions, roles, can, canAll, hasRole, hasAllRoles | | useForgeApi | () | get, post, patch, put, delete | | useForgeForm | <T>(initial: T) | form, errors, serverError, loading, clearErrors, submit | | useForgePagination | <T>(url, opts?) | data, meta, links, loading, error, page, perPage, fetch, nextPage, prevPage, goToPage | | useForgeUpload | (path: string) | progress, loading, error, result, upload, reset |

Components

| Component | Props | Description | |---|---|---| | <ForgeAuth> | role?: 'client' \| 'guard' | Renders slot if authenticated in the given context | | <ForgeTg> | — | Renders slot only inside a Telegram Mini App | | <ForgeCan> | perm: string\|string[], all?: boolean | Renders slot if guard has the permission(s) | | <ForgeRole> | role: string\|string[], all?: boolean | Renders slot if guard has the role(s) |

All components accept a #fallback slot rendered when access is denied.

Middleware factories

| Factory | Signature | Description | |---|---|---| | ForgeAuthMiddleware | (opts?) | Requires authenticated session | | PermissionMiddleware | (perm, opts?) | Any of the given permissions | | PermissionAllMiddleware | (perm, opts?) | All of the given permissions | | RoleMiddleware | (role, opts?) | Any of the given roles | | RoleAllMiddleware | (role, opts?) | All of the given roles |

opts for ForgeAuthMiddleware: { role?: 'client' | 'guard', redirect?: string }

opts for RBAC middleware: { redirect?: string }