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

@lenne.tech/nuxt-extensions

v1.18.1

Published

Reusable Nuxt 4 composables, components, and Better-Auth integration for lenne.tech projects

Readme

@lenne.tech/nuxt-extensions

npm version npm downloads License Nuxt

Reusable Nuxt 4 composables, components, and Better-Auth integration for lenne.tech projects.

Quick Start

Want to start a new fullstack project?

Use the nuxt-base-starter (Frontend) together with the nest-server-starter (Backend) to initialize a complete fullstack project with this package pre-configured.

Both starters are designed to work seamlessly together and serve as reference implementations showing how to use all features in a real application.

Quick initialization:

npx @lenne.tech/cli fullstack init my-project

Requirements

| Dependency | Required version | Notes | |---|---|---| | Node | >= 22 | | | Nuxt | ^4.0.0 | | | better-auth | >=1.7.1 <1.8.0 | required peer — pin it, see below | | @better-auth/passkey | >=1.7.1 <1.8.0 | optional peer, passkey/WebAuthn only | | tus-js-client | >=4.0.0 | optional peer, resumable uploads only | | @lenne.tech/nest-server | >= 11.37.0 | only if you use the backend — see the lock-step rule |

Why better-auth is pinned to one minor line, and why you must not widen it: better-auth is one protocol with two ends. This module is the client end and @lenne.tech/nest-server is the server end, and they must resolve the same better-auth minor. better-auth breaks in minor releases — 1.7 removed subpath exports and changed the 2FA response shape — so a caret (^1.7.1) would re-open the hole at 1.8. If the api and the app drift apart, 2FA activation fails at runtime with a generic client error while both projects' checks stay green: each is internally consistent, and only the assembled workspace has both halves.

Raising better-auth therefore means raising it in @lenne.tech/nest-server and @lenne.tech/nuxt-extensions in one step.

Installation

npm install @lenne.tech/nuxt-extensions [email protected]
# Optional: For passkey support
npm install @better-auth/[email protected]
# Optional: For TUS file uploads
npm install tus-js-client

In a fullstack monorepo, pin it workspace-wide so the api and the app cannot drift:

# pnpm-workspace.yaml
overrides:
  better-auth: 1.7.1
  '@better-auth/passkey': 1.7.1

Features

  • Better-Auth Integration - Login, 2FA, Passkey/WebAuthn support
  • Cookie/JWT Dual-Mode - Automatic fallback from cookies to JWT
  • TUS File Upload - Resumable uploads with pause/resume
  • Transition Components - Ready-to-use Vue transition wrappers
  • i18n Support - English and German translations (works without i18n too)
  • Auto-imports - All composables and components are auto-imported
  • Pre-hydration input preservation - text typed before hydration is kept, not erased
  • Form label repair - restores <label for> associations that hydration breaks

Environment Variables

| Variable | Context | Description | |----------|---------|-------------| | NUXT_PUBLIC_API_URL | Client + Server | Public API URL. Primary way to configure the API endpoint. Used for client-side requests and as SSR fallback. | | NUXT_API_URL | Server only | Internal API URL for SSR requests. Use when the backend has a private network address that should not be exposed to the client. | | NUXT_PUBLIC_API_PROXY | Client | Set to true to enable the Vite dev proxy. Routes client requests through /api/ for same-origin cookies. Only for local development. |

How URL Resolution Works

All environment variables are resolved at runtime (not build time). This means you can build a Docker image once and deploy it to different environments by changing env vars — no rebuild needed.

SSR fallback chain: NUXT_API_URLNUXT_PUBLIC_API_URLauth.baseURL (from nuxt.config.ts) → (unset)

Client fallback chain (no proxy): NUXT_PUBLIC_API_URLauth.baseURL (from nuxt.config.ts) → (unset)

Client with proxy (NUXT_PUBLIC_API_PROXY=true): All requests go to /api/{path} — the Vite dev proxy forwards them to the backend.

No implicit localhost default. If none of the sources above is set, there is no built-in fallback URL: API paths stay relative (e.g. /iam/token) and resolve against the app origin, so auth and setup calls 404 unless the app is served behind a same-origin reverse proxy. The module logs a one-time warning ([LtExtensions] No API URL configured…) at app init and on the first API call. Always set NUXT_PUBLIC_API_URL (and optionally NUXT_API_URL for SSR).

Security: NUXT_API_URL is never exposed to the client bundle. It stays in runtimeConfig.apiUrl (server only). This is important when using internal network addresses like http://api.svc.cluster.local.

Deployment Scenarios

Local development — Frontend and backend on different ports, proxy ensures same-origin cookies:

NUXT_PUBLIC_API_URL=http://localhost:3000
NUXT_PUBLIC_API_PROXY=true

Production (simple) — Backend reachable via public URL from both SSR and client:

NUXT_PUBLIC_API_URL=https://api.example.com

Production (internal network) — SSR uses fast internal route, client uses public URL:

NUXT_PUBLIC_API_URL=https://api.example.com
NUXT_API_URL=http://api-internal:3000

Legacy (nuxt.config.ts only) — Works but env vars are preferred for runtime flexibility:

// nuxt.config.ts
ltExtensions: {
  auth: {
    baseURL: 'https://api.example.com',
  },
}

Configuration

// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@lenne.tech/nuxt-extensions'],

  ltExtensions: {
    // Auth configuration
    auth: {
      enabled: true,                // Enable auth features
      baseURL: '',                  // API base URL (empty = use env vars)
      basePath: '/iam',             // Better-Auth endpoint prefix
      loginPath: '/auth/login',     // Login redirect path
      twoFactorRedirectPath: '/auth/2fa',  // 2FA redirect path

      // Plugin options
      enableAdmin: true,            // Admin plugin
      enableTwoFactor: true,        // 2FA plugin
      enablePasskey: true,          // Passkey/WebAuthn plugin

      // Interceptor options
      interceptor: {
        enabled: true,              // 401 auto-handler
        publicPaths: ['/auth/login', '/auth/register'],
      },

      // System setup (first admin user creation)
      systemSetup: {
        enabled: false,             // Enable setup flow
        setupPath: '/auth/setup',   // Setup page path
      },
    },

    // Error translation configuration
    errorTranslation: {
      enabled: true,                // Translate backend error codes
      defaultLocale: 'de',          // Fallback locale
    },

    // TUS upload configuration
    tus: {
      defaultEndpoint: '/files/upload',
      defaultChunkSize: 5 * 1024 * 1024,  // 5MB
    },

    // i18n configuration (optional)
    i18n: {
      autoMerge: true,              // Auto-merge locales with @nuxtjs/i18n
    },

    // AI module configuration
    ai: {
      enabled: true,                // Enable AI composables / auto-imports
      basePath: '/ai',              // Must match the nest-server AI controller
    },
  },
});

Form Label Repair

Restores <label for> associations that hydration breaks. Active by default.

The problem

Nuxt UI's FormField derives the label's for and the control's id from a single useId() call, so they cannot disagree — unless useId() itself returns different values on server and client, which happens when the two walk a different number of async boundaries.

Measured against @nuxt/ui 4.11.x:

| | label for | control id | |---|---|---| | SSR payload | v-0-4-2 | v-0-4-2 | | after hydration | v-0-4-2 | v-0-1-2 |

Vue 3.5.39 did not cause this, it exposed it (vuejs/core#9083 force-patches an element's dynamic props on hydration). The control's id is such a prop; the label's for, passed through reka-ui's Label component, is not — so only the label keeps the stale value.

What it costs when it breaks

The control loses its programmatic label. Without a placeholder it has no accessible name at all and a screen reader announces "edit text, blank"; with a placeholder that becomes the name instead, so the visible label is no longer part of it and speech input stops working. Clicking the label focuses nothing either — which on a checkbox or radio is the primary hit target, not a convenience. Tests using getByRole('textbox', { name }) stop finding fields.

What the repair does

After mount, and for a bounded window afterwards, three associations are repaired inside each Nuxt UI field whose label no longer resolves:

| Association | Repaired how | Why it needs its own path | |---|---|---| | <label for> → control | re-pointed at the field's control | for carries both the accessible name and click-to-focus | | aria-labelledby on a group | caption id minted, group pointed at it | A radio group's id sits on a <div role="radiogroup">, and for only resolves against labelable elements — there it is inert however carefully it is chosen | | aria-describedby → error / help | dangling tokens re-pointed, token-wise | The error container's id is force-patched on hydration; the reference to it is not. So the message is visible and never announced (WCAG 3.3.1 / 3.3.3) |

It refuses to guess. A repair that fires where it should not is worse than the defect: a dangling label is inert and visible, a mis-pointed one is confidently wrong and silent, and on a radio or checkbox a click on the caption would change a value the user never chose. So the for repair only fires when the field contains exactly one eligible control. Skipped deliberately:

| Case | Why | |------|-----| | Radio / checkbox groups | One field root, many controls — binding every caption to one item would submit a value nobody picked. Named via aria-labelledby instead | | Reka's submit proxy ([data-hidden] with no id) | Not the user's control. The missing id is what identifies it — Nuxt UI's UFileUpload renders the real file input the same way, but stamps the field id on it | | Anything aria-hidden, hidden, type="hidden" or display:none | Outside the accessibility tree, so a label pointing there would carry no name — the repair would report success and change nothing | | A control that already has a working label | Multiple <label>s concatenate into one accessible name, so a second one produces a name matching no visible text | | An id that is not unique in the document | for resolves through getElementById, which returns the first match — the label's click would go to the other element | | Labels your application wrote | Only Nuxt UI's own field labels (data-slot="label") are touched |

disabled and readonly fields ARE repaired. They are rendered, stay in the accessibility tree and are announced, so WCAG 1.3.1 / 4.1.2 apply unchanged — and WCAG's only carve-out for inactive components is contrast (1.4.3 / 1.4.11); there is no naming exemption. For a disabled field the repair restores the name only, since a disabled control prevents click dispatch and is not focusable. On a read-only form that is the whole value: it is a page people read rather than operate. Excluding disabled was tried in 1.13.0 and reproduced the very defect this plugin repairs on every read-only form.

This is a repair, not a cure. The real fix is for the id not to diverge; this exists because the divergence sits in the framework stack rather than in any one application.

Opting out

export default defineNuxtConfig({
  ltExtensions: {
    formLabelAssociation: {
      enabled: false,
      // Or keep it on and tune the two knobs:
      // maxRepairMs: 3000,        // window for deferred subtrees; clamped to [0, 30000]
      // observeDeferred: false,   // drop the MutationObserver for hydrate-on-visible content
    },
  },
});

Two real reasons to turn it off: your application assigns for itself and depends on those exact values, or your tests assert on literal for / id strings rather than on the accessible name. Fix the second reason instead — assert on the accessible name, which is what a user perceives.

Debugging it in a consuming project

  • A for, aria-labelledby or aria-describedby that changes after mount is this plugin. In development it logs once per page, naming the fields it repaired.
  • Repaired labels carry data-lt-label-repaired, so an E2E suite can assert the repair count is zero once the upstream divergence is fixed — and delete the plugin at that point.

Pre-Hydration Input Preservation

Text typed before a page has hydrated is kept instead of being silently thrown away.

The problem

Until Vue hydrates a server-rendered <input>, the element carries no framework listener. Text typed in that window is written to the DOM node, the input event reaches nothing, and v-model's mounted hook then writes the model value back over it. The entry is erased, not delayed — no error, no toast, nothing persisted.

It is load-dependent, so it hides in development and shows up on a cold cache, a slow device or a throttled CPU — most often on the login screen, where people type on sight.

What Vue already does, and where it stops

Vue fixed this in 3.5.41 (vuejs/core#14411): on hydration it compares the field's live value against what the server rendered and, when they differ, adopts the typed value into the model instead of overwriting the node.

That adoption is gated on type="text" and textarea. Every other type still loses the entry — and those are exactly the fields a sign-in form uses:

| Input type | Typed before hydration | |------------|------------------------| | text, textarea | kept by Vue itself | | email, password, tel, url, search, number | erased |

Widening the gate is tracked upstream as vuejs/core#15210 — open, labelled p2-edge-case, no milestone.

What this module adds

A small client plugin that closes the remaining gap. Just before hydration it reads what is in each field; just after, it writes back anything that was overwritten and dispatches a synthetic input event so v-model adopts it.

A field counts as edited when its value differs from defaultValue — the same test Vue uses, so values the server pre-filled are never mistaken for user input. A browser autofill that lands before hydration is recovered the same way.

Fields stay ordinary editable fields throughout. Nothing is made readonly, so autofill, screen-reader semantics and the mobile on-screen keyboard are untouched, and the user never faces a field that looks usable and silently refuses.

This is a stopgap. When Vue covers the remaining types, delete it — test/pre-hydration-input.test.ts pins the current gate and will fail on the types Vue takes over, which is the signal.

Opting out

export default defineNuxtConfig({
  ltExtensions: {
    preHydrationInput: {
      enabled: false,
      // or keep it on and only change how long deferred subtrees are waited for:
      // maxRestoreMs: 3000,
    },
  },
});

Usage

Authentication

<script setup>
// Auth composable (auto-imported)
const {
  user,
  isAuthenticated,
  isAdmin,
  isLoading,
  signIn,
  signOut,
  authenticateWithPasskey,
  registerPasskey,
  twoFactor,
  changePassword,
  requestPasswordReset,
  resetPassword,
} = useLtAuth();

// Login with email/password
async function handleLogin(email: string, password: string) {
  const result = await signIn.email({ email, password });
  if (result.requiresTwoFactor) {
    navigateTo('/auth/2fa');
  }
}

// Passkey login
async function handlePasskeyLogin() {
  const result = await authenticateWithPasskey();
  if (result.success) {
    navigateTo('/dashboard');
  }
}
</script>

<template>
  <div v-if="isAuthenticated">
    Welcome, {{ user?.name }}!
    <button @click="signOut()">Logout</button>
  </div>
</template>

Admin detection (role vs roles)

isAdmin accepts both user shapes, so the same frontend works against either backend:

| Backend | User shape | isAdmin is true when | |---------|------------|-------------------------| | @lenne.tech/nest-server | roles: ['admin'] — core Better-Auth additionalField (type: 'string[]'), no singular role | roles contains 'admin' | | Better-Auth admin plugin | role: 'admin' — single role | role === 'admin' |

<script setup>
const { isAdmin } = useLtAuth();
</script>

<template>
  <NuxtLink v-if="isAdmin" to="/admin">Admin</NuxtLink>
</template>

isAdmin is a UX gate, not an authorization boundary. It reads the client-side lt-auth-state cookie cache, which is not httpOnly and therefore user-writable. Use it to decide what to render; always enforce admin rights server-side (@Restricted(RoleEnum.ADMIN) in nest-server).

The auth.enableAdmin option toggles the Better-Auth admin client plugin (user-management calls such as admin.listUsers()). It does not control isAdmin — role detection works regardless of that flag.

For roles other than admin, use hasRole(role) / hasAnyRole(...roles). They apply the exact same both-shapes union and Array.isArray guard as isAdmin (which is itself just hasRole('admin')), so you never need the unguarded user.value?.roles?.includes(...):

<script setup>
const { hasRole, hasAnyRole } = useLtAuth();
</script>

<template>
  <button v-if="hasRole('editor')">Edit</button>
  <NuxtLink v-if="hasAnyRole('admin', 'editor')" to="/manage">Manage</NuxtLink>
</template>

Password Change and Reset

All three methods hash the password with SHA256 before it leaves the browser, because @lenne.tech/nest-server verifies that shape. Use them rather than calling the endpoints yourself — a hand-rolled reset that skips the hashing stores a credential the login can never match, and the user is locked out of the account they just set a password for. That is not hypothetical; it is why these two methods were added in 1.16.0.

<script setup lang="ts">
const { changePassword, requestPasswordReset, resetPassword } = useLtAuth();
const config = useRuntimeConfig();

// Signed-in user changes their own password.
// `revokeOtherSessions` ends every OTHER session — set it after a suspected compromise.
await changePassword({ currentPassword, newPassword, revokeOtherSessions: true });

// Step 1: send the reset mail.
await requestPasswordReset({
  email,
  redirectTo: new URL('/auth/reset-password', config.public.siteUrl).toString(),
});

// Step 2: on the reset page, with the token from the query string.
await resetPassword({ newPassword, token: route.query.token as string });
</script>

redirectTo must be an absolute URL. Better Auth resolves it against the API origin, so a relative value lands on the API host where the route does not exist — a 403 INVALID_REDIRECT_URL, no mail sent, and nothing visible in the browser. In an lt starter project the auto-imported appUrl() helper builds one and throws rather than returning something relative; that helper lives in the starter (app/utils/app-origin.ts), not in this package, so outside a starter use new URL(path, siteUrl).toString() as above.

Never build redirectTo from user input (route.query, a referrer, a form field). The link carries a live single-use reset token, so an attacker-chosen destination is account takeover rather than a phishing redirect. The server's trustedOrigins allowlist is what stops it.

Enforce a minimum password length in your own form. better-auth checks minPasswordLength against the value it receives, and because this library hashes first, that value is always a 64-character hex string — the check passes on every path. The form is the only place the rule can live.

Custom Better Auth Plugins

You can extend the auth client with additional Better Auth plugins:

Option 1: Plugin Registration (recommended)

Create a Nuxt plugin to register plugins before the auth client is initialized:

// plugins/auth-plugins.client.ts
import { registerLtAuthPlugins } from '@lenne.tech/nuxt-extensions/lib';
import { organizationClient, magicLinkClient } from 'better-auth/client/plugins';

export default defineNuxtPlugin(() => {
  registerLtAuthPlugins([
    organizationClient(),
    magicLinkClient(),
  ]);
});

Note: In Vue components, registerLtAuthPlugins is auto-imported. In .ts files (like Nuxt plugins), import from @lenne.tech/nuxt-extensions/lib.

Option 2: Direct Factory Usage

For full control, create the auth client directly with your plugins:

import { createLtAuthClient } from '@lenne.tech/nuxt-extensions/lib';
import { organizationClient } from 'better-auth/client/plugins';

const authClient = createLtAuthClient({
  plugins: [organizationClient()],
});

// Use authClient.organization.* methods

Available Better Auth Plugins:

  • organizationClient - Organization/team management
  • magicLinkClient - Passwordless email login
  • oneTapClient - Google One Tap login
  • anonymousClient - Anonymous/guest sessions
  • See Better Auth Plugins for full list

TUS File Upload

<script setup>
const {
  addFiles,
  uploads,
  totalProgress,
  isUploading,
  pauseUpload,
  resumeUpload,
  cancelUpload,
} = useLtTusUpload({
  endpoint: '/api/files/upload',
  onSuccess: (item) => console.log('Uploaded:', item.url),
  onError: (item, error) => console.error('Failed:', error),
});

const { formatFileSize } = useLtFile();

function handleFileSelect(event: Event) {
  const input = event.target as HTMLInputElement;
  if (input.files) {
    addFiles(Array.from(input.files));
  }
}
</script>

<template>
  <div>
    <input type="file" multiple @change="handleFileSelect" />

    <div v-for="upload in uploads" :key="upload.id">
      <span>{{ upload.file.name }}</span>
      <span>{{ formatFileSize(upload.progress.bytesUploaded) }} / {{ formatFileSize(upload.progress.bytesTotal) }}</span>
      <span>{{ upload.progress.percentage }}%</span>

      <button v-if="upload.status === 'uploading'" @click="pauseUpload(upload.id)">
        Pause
      </button>
      <button v-if="upload.status === 'paused'" @click="resumeUpload(upload.id)">
        Resume
      </button>
    </div>

    <div v-if="isUploading">
      Total Progress: {{ totalProgress.percentage }}%
    </div>
  </div>
</template>

Transition Components

<template>
  <!-- Fade transition -->
  <LtTransitionFade>
    <div v-if="show">Content with fade</div>
  </LtTransitionFade>

  <!-- Fade with scale -->
  <LtTransitionFadeScale :start-duration="200" :leave-duration="150">
    <div v-if="show">Content with fade and scale</div>
  </LtTransitionFadeScale>

  <!-- Slide from right -->
  <LtTransitionSlide>
    <div v-if="show">Content slides from right</div>
  </LtTransitionSlide>

  <!-- Slide from bottom -->
  <LtTransitionSlideBottom>
    <div v-if="show">Content slides from bottom</div>
  </LtTransitionSlideBottom>

  <!-- Slide from left -->
  <LtTransitionSlideRevert>
    <div v-if="show">Content slides from left</div>
  </LtTransitionSlideRevert>
</template>

Web Share API

<script setup>
const { share } = useLtShare();

async function handleShare() {
  await share('Check this out!', 'Amazing content');
  // Uses native share on mobile, clipboard fallback on desktop
}
</script>

Utilities

// Tailwind TypeScript helper
const buttonClasses = tw`bg-blue-500 hover:bg-blue-700 text-white`;

// Crypto utilities (for WebAuthn)
const hash = await ltSha256('password');
const base64 = ltArrayBufferToBase64Url(buffer);
const uint8 = ltBase64UrlToUint8Array(base64String);

AI Assistant

Headless, provider-agnostic composables for the @lenne.tech/nest-server AI module (REST + SSE). All requests use the same auth-aware ltAuthFetch (Cookie/JWT) and URL resolution as the rest of the library. Configure via ltExtensions.ai:

// nuxt.config.ts
ltExtensions: {
  ai: { enabled: true, basePath: '/ai' }, // basePath must match the nest-server AI controller
}

Chat (streaming, multi-turn)

<script setup lang="ts">
const { budget, confirm, messages, requiresConfirmation, send, streaming } = useLtAiChat();
</script>

useLtAiChat() streams the answer token-by-token, tracks the conversationId, exposes the per-response budget summary, and drives the confirmation flow for mutating/ destructive actions (requiresConfirmationconfirm()).

User-facing prompts ("Vorlagen")

<script setup lang="ts">
const { create, error, load, loading, prompts, remove, update } = useLtAiPrompts();
await load();
// create({ name: 'Summary', content: 'Summarize {{topic}}', scope: 'user' });
</script>

useLtAiPrompts() lets any signed-in user manage re-usable prompt snippets. scope: 'user' is private to the owner; scope: 'tenant' is shared across the owner's tenant. The list returned by load() is already server-filtered to what the user is allowed to see.

Lower-level + supporting composables

| Composable | Purpose | |------------|---------| | useLtAi() | One-shot prompt(input) and streaming promptStream(input, handlers) (POST /ai/stream) | | useLtAiChat() | Multi-turn chat state, streaming, budget, confirmation, stop()/clear(), optional maxMessages cap, auto-stop on component unmount | | useLtAiConnections() | User self-service: available connections + select() (selected/locked) | | useLtAiUsage() | Full token/prompt usage breakdown (GET /ai/usage) | | useLtAiPrompts() | User-facing CRUD for re-usable prompt snippets ("Vorlagen", scope: 'user' / 'tenant') | | useLtAiPlaceholders() | Loads {{placeholder}} registry from the backend so slot / prompt editors render a dynamic helper sidebar | | useLtAiAdmin() | Admin CRUD: connections (+ detectCapabilities), preferences, budget-limits, slots (incl. listEffectiveSlots/resetSlot), prompt hints, interactions |

Helpers: buildLtAiUrl(path), ltAiRequest(method, path, body?), parseLtAiSseStream(response, onEvent, options?). All AI DTOs are exported as LtAi* types. The streaming endpoint is consumed via a fetch + ReadableStream SSE reader (not EventSource, since it is a POST with auth).

Security note: useLtAiAdmin() is exposed as a regular composable; admin gating is enforced server-side (@Restricted(ADMIN)). Render admin UI behind a frontend route guard for UX, but trust the backend for authorization.

i18n Support

The package works with or without @nuxtjs/i18n:

| Setup | Language | Text Source | |-------|----------|-------------| | Without i18n | German | Hardcoded fallback texts | | With i18n, Locale: de | German | From de.json | | With i18n, Locale: en | English | From en.json | | With i18n, other Locale | English | Fallback to en.json |

API Reference

Composables

| Composable | Description | |------------|-------------| | useLtAuth() | Better-Auth integration with session, passkey, 2FA, password change + reset | | useLtAuthClient() | Direct access to the Better-Auth client singleton (twoFactor.*, admin.*, passkey) | | useLtErrorTranslation() | Translate backend error codes to user-friendly messages | | useLtTusUpload() | TUS protocol file uploads with pause/resume | | useLtFile() | File utilities (size formatting, URLs) | | useLtShare() | Web Share API with clipboard fallback | | useSystemSetup() | System setup flow for initial admin user creation | | useLtAi() | One-shot prompt() + streaming promptStream() for the nest-server AI module | | useLtAiChat() | Multi-turn chat state, streaming, budget summary, confirmation gate, maxMessages cap | | useLtAiConnections() | User self-service connection list + select() | | useLtAiUsage() | Token/prompt usage breakdown per user / tenant | | useLtAiPrompts() | User-facing prompt snippet CRUD ('user' / 'tenant' scope) | | useLtAiPlaceholders() | Loads the backend's {{placeholder}} registry | | useLtAiAdmin() | Admin CRUD for connections, preferences, budget limits, slots, prompt hints, interactions |

Components

| Component | Description | |-----------|-------------| | <LtTransitionFade> | Opacity fade transition | | <LtTransitionFadeScale> | Fade with scale transition | | <LtTransitionSlide> | Slide from right transition | | <LtTransitionSlideBottom> | Slide from bottom transition | | <LtTransitionSlideRevert> | Slide from left transition |

Utilities

| Utility | Description | |---------|-------------| | tw | Tailwind TypeScript helper | | ltSha256() | SHA256 hash function | | ltArrayBufferToBase64Url() | ArrayBuffer to base64url conversion | | ltBase64UrlToUint8Array() | Base64url to Uint8Array conversion | | createLtAuthClient() | Auth client factory for custom configuration | | registerLtAuthPlugins() | Register custom Better Auth plugins |

Related Projects

| Project | Description | |---------|-------------| | nuxt-base-starter | Frontend starter template (uses this package) | | nest-server-starter | Backend starter template (Better-Auth backend) | | @lenne.tech/nest-server | Backend framework with Better-Auth support | | @lenne.tech/cli | CLI tool for fullstack project initialization |

Fullstack Architecture

+-------------------------------------------------------------+
|                     Your Fullstack App                       |
+-----------------------------+-------------------------------+
|         Frontend            |           Backend              |
|    (nuxt-base-starter)      |    (nest-server-starter)       |
+-----------------------------+-------------------------------+
|  @lenne.tech/nuxt-extensions|   @lenne.tech/nest-server     |
|  - useLtAuth()              |   - CoreBetterAuthModule      |
|  - useLtTusUpload()         |   - CoreFileModule            |
|  - <LtTransition*>          |   - CoreUserModule            |
+-----------------------------+-------------------------------+

Development

# Install dependencies
npm install

# Generate type stubs
npm run dev:prepare

# Develop with playground
npm run dev

# Build the module
npm run build

# Run tests
npm run test

License

MIT