From cceda46b7ef218e885781155e900ce87d4a7b4ff Mon Sep 17 00:00:00 2001 From: thecodearcher Date: Sat, 20 Jun 2026 17:15:41 +0100 Subject: [PATCH 01/17] feat: add TypeScript client SDK --- clients/go.mod | 3 + clients/typescript/.changeset/README.md | 8 + clients/typescript/.changeset/config.json | 11 + clients/typescript/.gitignore | 11 + clients/typescript/.prettierrc.json | 7 + clients/typescript/LICENSE | 21 + clients/typescript/README.md | 21 + clients/typescript/eslint.config.js | 32 + clients/typescript/package.json | 29 + clients/typescript/packages/client/README.md | 72 + clients/typescript/packages/client/banner.svg | 45 + .../typescript/packages/client/package.json | 123 + .../packages/client/src/broadcast-channel.ts | 79 + .../packages/client/src/build-tree.ts | 106 + .../typescript/packages/client/src/client.ts | 92 + .../packages/client/src/constants.ts | 17 + .../typescript/packages/client/src/context.ts | 33 + .../packages/client/src/define-plugin.ts | 75 + .../packages/client/src/envelope.ts | 48 + .../typescript/packages/client/src/errors.ts | 47 + .../typescript/packages/client/src/fetcher.ts | 179 + .../typescript/packages/client/src/helpers.ts | 69 + .../typescript/packages/client/src/hooks.ts | 60 + .../typescript/packages/client/src/index.ts | 48 + .../typescript/packages/client/src/infer.ts | 69 + .../packages/client/src/json-deep-equal.ts | 53 + .../packages/client/src/normalize.ts | 24 + .../typescript/packages/client/src/path.ts | 50 + .../packages/client/src/pipeline.ts | 96 + .../typescript/packages/client/src/plugin.ts | 87 + .../client/src/plugins/bearer/index.ts | 60 + .../client/src/plugins/bearer/storage.ts | 48 + .../client/src/plugins/bearer/types.ts | 34 + .../client/src/plugins/credential/index.ts | 76 + .../client/src/plugins/credential/types.ts | 44 + .../packages/client/src/plugins/index.ts | 6 + .../client/src/plugins/magic-link/index.ts | 26 + .../client/src/plugins/magic-link/types.ts | 11 + .../client/src/plugins/oauth/index.ts | 69 + .../client/src/plugins/oauth/types.ts | 42 + .../src/plugins/session-jwt/constants.ts | 2 + .../client/src/plugins/session-jwt/index.ts | 134 + .../client/src/plugins/session-jwt/jwt.ts | 44 + .../client/src/plugins/session-jwt/types.ts | 10 + .../client/src/plugins/two-factor/index.ts | 87 + .../client/src/plugins/two-factor/types.ts | 31 + .../packages/client/src/react/index.ts | 35 + .../packages/client/src/react/react-store.ts | 29 + .../typescript/packages/client/src/route.ts | 113 + .../typescript/packages/client/src/routes.ts | 89 + .../packages/client/src/serialize.ts | 31 + .../packages/client/src/session-store.ts | 124 + .../packages/client/src/session-sync.ts | 82 + .../packages/client/src/solid/index.ts | 36 + .../packages/client/src/solid/solid-store.ts | 24 + .../packages/client/src/svelte/index.ts | 39 + .../packages/client/src/type-utils.ts | 26 + .../typescript/packages/client/src/types.ts | 111 + .../packages/client/src/vue/index.ts | 37 + .../packages/client/src/vue/vue-store.ts | 32 + .../client/test/broadcast-channel.test.ts | 154 + .../client/test/client-integration.test.ts | 232 ++ .../client/test/json-deep-equal.test.ts | 33 + .../packages/client/test/path.test.ts | 49 + .../client/test/session-store.test.ts | 134 + .../packages/client/test/session-sync.test.ts | 277 ++ .../typescript/packages/client/tsconfig.json | 17 + .../packages/client/tsdown.config.ts | 19 + .../packages/client/vitest.config.ts | 8 + clients/typescript/pnpm-lock.yaml | 3201 +++++++++++++++++ clients/typescript/pnpm-workspace.yaml | 2 + clients/typescript/tsconfig.base.json | 22 + 72 files changed, 7295 insertions(+) create mode 100644 clients/go.mod create mode 100644 clients/typescript/.changeset/README.md create mode 100644 clients/typescript/.changeset/config.json create mode 100644 clients/typescript/.gitignore create mode 100644 clients/typescript/.prettierrc.json create mode 100644 clients/typescript/LICENSE create mode 100644 clients/typescript/README.md create mode 100644 clients/typescript/eslint.config.js create mode 100644 clients/typescript/package.json create mode 100644 clients/typescript/packages/client/README.md create mode 100644 clients/typescript/packages/client/banner.svg create mode 100644 clients/typescript/packages/client/package.json create mode 100644 clients/typescript/packages/client/src/broadcast-channel.ts create mode 100644 clients/typescript/packages/client/src/build-tree.ts create mode 100644 clients/typescript/packages/client/src/client.ts create mode 100644 clients/typescript/packages/client/src/constants.ts create mode 100644 clients/typescript/packages/client/src/context.ts create mode 100644 clients/typescript/packages/client/src/define-plugin.ts create mode 100644 clients/typescript/packages/client/src/envelope.ts create mode 100644 clients/typescript/packages/client/src/errors.ts create mode 100644 clients/typescript/packages/client/src/fetcher.ts create mode 100644 clients/typescript/packages/client/src/helpers.ts create mode 100644 clients/typescript/packages/client/src/hooks.ts create mode 100644 clients/typescript/packages/client/src/index.ts create mode 100644 clients/typescript/packages/client/src/infer.ts create mode 100644 clients/typescript/packages/client/src/json-deep-equal.ts create mode 100644 clients/typescript/packages/client/src/normalize.ts create mode 100644 clients/typescript/packages/client/src/path.ts create mode 100644 clients/typescript/packages/client/src/pipeline.ts create mode 100644 clients/typescript/packages/client/src/plugin.ts create mode 100644 clients/typescript/packages/client/src/plugins/bearer/index.ts create mode 100644 clients/typescript/packages/client/src/plugins/bearer/storage.ts create mode 100644 clients/typescript/packages/client/src/plugins/bearer/types.ts create mode 100644 clients/typescript/packages/client/src/plugins/credential/index.ts create mode 100644 clients/typescript/packages/client/src/plugins/credential/types.ts create mode 100644 clients/typescript/packages/client/src/plugins/index.ts create mode 100644 clients/typescript/packages/client/src/plugins/magic-link/index.ts create mode 100644 clients/typescript/packages/client/src/plugins/magic-link/types.ts create mode 100644 clients/typescript/packages/client/src/plugins/oauth/index.ts create mode 100644 clients/typescript/packages/client/src/plugins/oauth/types.ts create mode 100644 clients/typescript/packages/client/src/plugins/session-jwt/constants.ts create mode 100644 clients/typescript/packages/client/src/plugins/session-jwt/index.ts create mode 100644 clients/typescript/packages/client/src/plugins/session-jwt/jwt.ts create mode 100644 clients/typescript/packages/client/src/plugins/session-jwt/types.ts create mode 100644 clients/typescript/packages/client/src/plugins/two-factor/index.ts create mode 100644 clients/typescript/packages/client/src/plugins/two-factor/types.ts create mode 100644 clients/typescript/packages/client/src/react/index.ts create mode 100644 clients/typescript/packages/client/src/react/react-store.ts create mode 100644 clients/typescript/packages/client/src/route.ts create mode 100644 clients/typescript/packages/client/src/routes.ts create mode 100644 clients/typescript/packages/client/src/serialize.ts create mode 100644 clients/typescript/packages/client/src/session-store.ts create mode 100644 clients/typescript/packages/client/src/session-sync.ts create mode 100644 clients/typescript/packages/client/src/solid/index.ts create mode 100644 clients/typescript/packages/client/src/solid/solid-store.ts create mode 100644 clients/typescript/packages/client/src/svelte/index.ts create mode 100644 clients/typescript/packages/client/src/type-utils.ts create mode 100644 clients/typescript/packages/client/src/types.ts create mode 100644 clients/typescript/packages/client/src/vue/index.ts create mode 100644 clients/typescript/packages/client/src/vue/vue-store.ts create mode 100644 clients/typescript/packages/client/test/broadcast-channel.test.ts create mode 100644 clients/typescript/packages/client/test/client-integration.test.ts create mode 100644 clients/typescript/packages/client/test/json-deep-equal.test.ts create mode 100644 clients/typescript/packages/client/test/path.test.ts create mode 100644 clients/typescript/packages/client/test/session-store.test.ts create mode 100644 clients/typescript/packages/client/test/session-sync.test.ts create mode 100644 clients/typescript/packages/client/tsconfig.json create mode 100644 clients/typescript/packages/client/tsdown.config.ts create mode 100644 clients/typescript/packages/client/vitest.config.ts create mode 100644 clients/typescript/pnpm-lock.yaml create mode 100644 clients/typescript/pnpm-workspace.yaml create mode 100644 clients/typescript/tsconfig.base.json diff --git a/clients/go.mod b/clients/go.mod new file mode 100644 index 0000000..777962e --- /dev/null +++ b/clients/go.mod @@ -0,0 +1,3 @@ +module github.com/thecodearcher/limen/clients + +go 1.25.0 diff --git a/clients/typescript/.changeset/README.md b/clients/typescript/.changeset/README.md new file mode 100644 index 0000000..654c6d4 --- /dev/null +++ b/clients/typescript/.changeset/README.md @@ -0,0 +1,8 @@ +# Changesets + +Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works +with multi-package repos, or single-package repos to help you version and publish your code. You can +find the full documentation for it [in our repository](https://github.com/changesets/changesets). + +We have a quick list of common questions to get you started engaging with this project in +[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md). diff --git a/clients/typescript/.changeset/config.json b/clients/typescript/.changeset/config.json new file mode 100644 index 0000000..5c58ec9 --- /dev/null +++ b/clients/typescript/.changeset/config.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://unpkg.com/@changesets/config@3.1.4/schema.json", + "changelog": "@changesets/cli/changelog", + "commit": false, + "fixed": [], + "linked": [], + "access": "restricted", + "baseBranch": "main", + "updateInternalDependencies": "patch", + "ignore": [] +} diff --git a/clients/typescript/.gitignore b/clients/typescript/.gitignore new file mode 100644 index 0000000..fc10e9c --- /dev/null +++ b/clients/typescript/.gitignore @@ -0,0 +1,11 @@ +node_modules/ +dist/ +coverage/ +*.tsbuildinfo +.DS_Store +.env +.env.local +.idea/ +.vscode/ +.cursor/ +.claude/ \ No newline at end of file diff --git a/clients/typescript/.prettierrc.json b/clients/typescript/.prettierrc.json new file mode 100644 index 0000000..d261151 --- /dev/null +++ b/clients/typescript/.prettierrc.json @@ -0,0 +1,7 @@ +{ + "semi": true, + "singleQuote": false, + "trailingComma": "all", + "printWidth": 120, + "arrowParens": "always" +} \ No newline at end of file diff --git a/clients/typescript/LICENSE b/clients/typescript/LICENSE new file mode 100644 index 0000000..1520332 --- /dev/null +++ b/clients/typescript/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Brian Iyoha + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/clients/typescript/README.md b/clients/typescript/README.md new file mode 100644 index 0000000..1ff6d84 --- /dev/null +++ b/clients/typescript/README.md @@ -0,0 +1,21 @@ +# limen-ts + +TypeScript client SDK for [Limen](https://github.com/thecodearcher/limen) — a modern, composable authentication library for Go. + +## Packages + +- [`limen-auth`](./packages/client) — framework-agnostic core +- [`limen-auth/react`](./packages/client/src/react) — React hooks adapter (subpath of `limen-auth`) +- [`limen-auth/vue`](./packages/client/src/vue) — Vue composables adapter (subpath of `limen-auth`) +- [`limen-auth/svelte`](./packages/client/src/svelte) — Svelte stores adapter (subpath of `limen-auth`) +- [`limen-auth/solid`](./packages/client/src/solid) — Solid primitives adapter (subpath of `limen-auth`) + +## Development + +```bash +pnpm install +pnpm typecheck +pnpm lint +pnpm test +pnpm build +``` diff --git a/clients/typescript/eslint.config.js b/clients/typescript/eslint.config.js new file mode 100644 index 0000000..b82dac7 --- /dev/null +++ b/clients/typescript/eslint.config.js @@ -0,0 +1,32 @@ +// @ts-check +import js from "@eslint/js"; +import { defineConfig } from "eslint/config"; +import globals from "globals"; +import tseslint from "typescript-eslint"; + +export default defineConfig([ + { + ignores: [ + "**/dist/**", + "**/node_modules/**", + "**/*.config.{ts,js,mjs,cjs}", + ], + }, + js.configs.recommended, + tseslint.configs.recommended, + { + languageOptions: { + ecmaVersion: 2022, + sourceType: "module", + globals: { ...globals.browser, ...globals.node }, + }, + rules: { + "@typescript-eslint/consistent-type-imports": "error", + "curly": ["error", "all"], + "@typescript-eslint/no-unused-vars": [ + "error", + { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }, + ], + }, + }, +]); diff --git a/clients/typescript/package.json b/clients/typescript/package.json new file mode 100644 index 0000000..b51f66b --- /dev/null +++ b/clients/typescript/package.json @@ -0,0 +1,29 @@ +{ + "name": "@limenauth/root", + "private": true, + "type": "module", + "engines": { + "node": ">=20", + "pnpm": ">=10" + }, + "packageManager": "pnpm@10.15.0", + "scripts": { + "build": "pnpm -r --filter './packages/*' run build", + "typecheck": "pnpm -r --filter './packages/*' run typecheck", + "lint": "eslint packages", + "format": "prettier --write \"packages/**/*.{ts,tsx,md}\"", + "format:check": "prettier --check \"packages/**/*.{ts,tsx,md}\"", + "test": "pnpm -r --filter './packages/*' run test" + }, + "devDependencies": { + "@changesets/cli": "^2.31.0", + "@eslint/js": "^10.0.1", + "@types/node": "^25.9.1", + "eslint": "^10.4.0", + "globals": "^17.6.0", + "prettier": "^3.8.3", + "tsdown": "^0.22.2", + "typescript": "^6.0.3", + "typescript-eslint": "^8.59.4" + } +} \ No newline at end of file diff --git a/clients/typescript/packages/client/README.md b/clients/typescript/packages/client/README.md new file mode 100644 index 0000000..2fb6c4c --- /dev/null +++ b/clients/typescript/packages/client/README.md @@ -0,0 +1,72 @@ +

+ + Limen + +

+ +# limen-auth + +Official TypeScript client SDK for **[Limen](https://github.com/thecodearcher/limen)** — a modern, composable authentication library for Go. Framework-agnostic core with first-class **React, Vue, Svelte, and Solid** adapters. + +> 📖 Full guides and API reference: **[limenauth.dev](https://limenauth.dev)** + +## Install + +```bash +npm install limen-auth +``` + +Works with any framework — or none at all. If you're on React, Vue, Svelte, or Solid, just have that framework installed; there's nothing else to add. + +## Quick start + +```ts +import { createAuthClient } from "limen-auth"; +import { credentialPasswordPlugin } from "limen-auth/plugins/credential"; + +export const auth = createAuthClient({ + baseURL: "http://localhost:8080", // your Limen server origin + plugins: [credentialPasswordPlugin()], +}); + +await auth.signIn.credential({ credential: "ada@example.com", password: "secret" }); +const session = await auth.getSession(); // Session | null +await auth.signout(); +``` + +`auth.$session` is a reactive store for the current user. It loads on its own, keeps your open tabs in sync, and updates as you sign in and out — so the UI always reflects the real session. + +## Framework adapters + +Import `createAuthClient` from your framework's entry point and you get a `useSession()` wired to it: + +```tsx +import { createAuthClient } from "limen-auth/react"; +import { credentialPasswordPlugin } from "limen-auth/plugins/credential"; + +export const auth = createAuthClient({ baseURL: "...", plugins: [credentialPasswordPlugin()] }); + +function Profile() { + const { data, isPending } = auth.useSession(); + if (isPending) return

Loading…

; + return data ?

Hi {data.user.email}

:

Signed out

; +} +``` + +Also available from `limen-auth/vue`, `limen-auth/svelte`, and `limen-auth/solid`. + +## Plugins + +Add the sign-in flows you need as plugins (each lives under `limen-auth/plugins/`): + +- `credentialPasswordPlugin` — email/username + password +- `oauthClientPlugin` — social / OAuth providers +- `magicLinkPlugin` — passwordless email links +- `twoFactorPlugin` — TOTP, OTP, and backup codes +- `bearerPlugin` / `sessionJwtPlugin` — token-based sessions + +See the plugin and full API reference at **[limenauth.dev](https://limenauth.dev)**. + +## License + +MIT © Brian Iyoha diff --git a/clients/typescript/packages/client/banner.svg b/clients/typescript/packages/client/banner.svg new file mode 100644 index 0000000..a6b6b83 --- /dev/null +++ b/clients/typescript/packages/client/banner.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/clients/typescript/packages/client/package.json b/clients/typescript/packages/client/package.json new file mode 100644 index 0000000..32edffa --- /dev/null +++ b/clients/typescript/packages/client/package.json @@ -0,0 +1,123 @@ +{ + "name": "limen-auth", + "version": "0.0.0", + "description": "TypeScript authentication client for Limen — framework-agnostic core with React, Vue, Svelte, and Solid adapters.", + "keywords": [ + "limen", + "auth", + "authentication", + "session", + "oauth", + "two-factor", + "magic-link", + "jwt", + "react", + "vue", + "svelte", + "solid", + "typescript", + "sdk" + ], + "homepage": "https://github.com/thecodearcher/limen#readme", + "bugs": "https://github.com/thecodearcher/limen/issues", + "repository": { + "type": "git", + "url": "git+https://github.com/thecodearcher/limen.git", + "directory": "clients/typescript/packages/client" + }, + "license": "MIT", + "author": "Brian Iyoha ", + "type": "module", + "module": "./dist/index.mjs", + "publishConfig": { + "access": "public" + }, + "exports": { + ".": { + "import": { + "types": "./dist/index.d.mts", + "default": "./dist/index.mjs" + } + }, + "./react": { + "import": { + "types": "./dist/react/index.d.mts", + "default": "./dist/react/index.mjs" + } + }, + "./vue": { + "import": { + "types": "./dist/vue/index.d.mts", + "default": "./dist/vue/index.mjs" + } + }, + "./svelte": { + "import": { + "types": "./dist/svelte/index.d.mts", + "default": "./dist/svelte/index.mjs" + } + }, + "./solid": { + "import": { + "types": "./dist/solid/index.d.mts", + "default": "./dist/solid/index.mjs" + } + }, + "./plugins": { + "import": { + "types": "./dist/plugins/index.d.mts", + "default": "./dist/plugins/index.mjs" + } + }, + "./plugins/*": { + "import": { + "types": "./dist/plugins/*/index.d.mts", + "default": "./dist/plugins/*/index.mjs" + } + } + }, + "files": [ + "dist", + "README.md" + ], + "sideEffects": false, + "scripts": { + "build": "tsdown", + "typecheck": "tsc --noEmit", + "test": "vitest run" + }, + "dependencies": { + "nanostores": "^1.3.0" + }, + "peerDependencies": { + "react": ">=18", + "solid-js": ">=1", + "svelte": ">=4", + "vue": ">=3" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "solid-js": { + "optional": true + }, + "svelte": { + "optional": true + }, + "vue": { + "optional": true + } + }, + "devDependencies": { + "@testing-library/react": "^16.1.0", + "@types/react": "^19.0.0", + "happy-dom": "^20.10.5", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "solid-js": "^1.9.0", + "svelte": "^5.0.0", + "vitest": "^4.1.9", + "vue": "^3.5.0" + } +} \ No newline at end of file diff --git a/clients/typescript/packages/client/src/broadcast-channel.ts b/clients/typescript/packages/client/src/broadcast-channel.ts new file mode 100644 index 0000000..0dda120 --- /dev/null +++ b/clients/typescript/packages/client/src/broadcast-channel.ts @@ -0,0 +1,79 @@ +/** + * Cross-tab message transport: callers post and + * subscribe to typed messages. Uses `BroadcastChannel` where available, falling + * back to `localStorage` `storage` events. A no-op in non-browser environments. + */ +type BroadcastPort = { + post(message: T): void; + subscribe(listener: (message: T) => void): () => void; + close(): void; +}; + +const NOOP_PORT: BroadcastPort = { + post() {}, + subscribe: () => () => {}, + close() {}, +}; + +export function createBroadcastChannel(name: string): BroadcastPort { + if (typeof window === "undefined") { + return NOOP_PORT as BroadcastPort; + } + + const storageKey = `${name}-sync`; + const listeners = new Set<(message: T) => void>(); + const emit = (message: T): void => { + for (const listener of listeners) { + listener(message); + } + }; + + const channel = typeof BroadcastChannel !== "undefined" ? new BroadcastChannel(name) : null; + if (channel) { + channel.onmessage = (event: MessageEvent) => { + if (event.data != null) { + emit(event.data); + } + }; + } + + const onStorage = (event: StorageEvent): void => { + if (event.key !== storageKey || !event.newValue) { + return; + } + + emit(JSON.parse(event.newValue) as T); + }; + + if (!channel) { + window.addEventListener("storage", onStorage); + } + + return { + post(message) { + if (channel) { + channel.postMessage(message); + return; + } + + if (typeof globalThis.localStorage !== "undefined") { + globalThis.localStorage.setItem(storageKey, JSON.stringify(message)); + globalThis.localStorage.removeItem(storageKey); + } + }, + subscribe(listener) { + listeners.add(listener); + return () => { + listeners.delete(listener); + }; + }, + close() { + listeners.clear(); + if (channel) { + channel.close(); + return; + } + window.removeEventListener("storage", onStorage); + }, + }; +} diff --git a/clients/typescript/packages/client/src/build-tree.ts b/clients/typescript/packages/client/src/build-tree.ts new file mode 100644 index 0000000..fcf71ae --- /dev/null +++ b/clients/typescript/packages/client/src/build-tree.ts @@ -0,0 +1,106 @@ +import type { AnyRouteContext, RouteContext } from "./context"; +import type { AnyClientPlugin, RunRoute } from "./define-plugin"; +import type { Fetcher } from "./fetcher"; +import { ensureLeadingSlash, kebabToCamel, normalizeBasePath } from "./helpers"; +import { chainFromDotted, pathToChain } from "./path"; +import { runRoute } from "./pipeline"; +import type { FetchInit } from "./plugin"; +import type { AnyRoute, AnyRouteDescriptor, RouteCallOptions } from "./route"; + +export type ClientOverrides = Record | undefined; + +/** Scope a context's `fetch` to one plugin's base path (after client `overrides`). */ +function scopeContext( + ctx: AnyRouteContext, + fetcher: Fetcher, + plugin: AnyClientPlugin, + overrides: ClientOverrides, +): RouteContext { + const defaultBase = normalizeBasePath(plugin.basePath ?? ""); + const overrideBase = overrides?.[kebabToCamel(plugin.id)]?.basePath; + const resolvedBase = normalizeBasePath(overrideBase ?? plugin.basePath ?? ""); + return { + ...ctx, + fetch: (path: string, init?: FetchInit) => { + const absolute = init?.absolute === true; + const requestPath = (absolute ? "" : resolvedBase) + ensureLeadingSlash(path); + const routePath = (absolute ? "" : defaultBase) + ensureLeadingSlash(path); + return fetcher.fetch(requestPath, init, routePath); + }, + }; +} + +function chainFor(plugin: AnyClientPlugin, def: AnyRoute): string[] { + if (typeof def.as === "string") { + return chainFromDotted(def.as); + } + return [...pathToChain(plugin.basePath ?? ""), ...pathToChain(def.path)]; +} + +function mountAtChain(target: Record, pathSegments: string[], callable: unknown): void { + let current = target; + for (let i = 0; i < pathSegments.length - 1; i += 1) { + const segment = pathSegments[i] as string; + const child = current[segment]; + if (child === undefined) { + const namespace: Record = {}; + current[segment] = namespace; + current = namespace; + } else { + current = child as Record; + } + } + const finalSegment = pathSegments[pathSegments.length - 1] as string; + current[finalSegment] = callable; +} + +function isNamespace(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value) && typeof value !== "function"; +} + +function mergeInto(target: Record, source: Record): void { + for (const [key, value] of Object.entries(source)) { + const existing = target[key]; + if (isNamespace(existing) && isNamespace(value)) { + mergeInto(existing, value); + continue; + } + target[key] = value; + } +} + +type BuildClientTreeArgs = { + plugins: readonly AnyClientPlugin[]; + ctx: AnyRouteContext; + fetcher: Fetcher; + overrides: ClientOverrides; +}; + +/** + * Build the public API object from plugin routes and actions. + */ +export function buildClientTree({ plugins, ctx, fetcher, overrides }: BuildClientTreeArgs): Record { + const api: Record = {}; + + for (const plugin of plugins) { + const scopedCtx = scopeContext(ctx, fetcher, plugin, overrides); + const contribution: Record = {}; + + for (const def of plugin.routes as readonly AnyRoute[]) { + if (def.expose === false) { + continue; + } + const call = (input?: unknown, opts?: RouteCallOptions) => runRoute(scopedCtx, def, input, opts); + mountAtChain(contribution, chainFor(plugin, def), call); + } + + if (plugin.actions !== undefined) { + const run: RunRoute = (route, input) => runRoute(scopedCtx, route as AnyRouteDescriptor, input) as Promise; + mergeInto(contribution, plugin.actions(scopedCtx, run) as Record); + } + + mergeInto(api, contribution); + } + + return api; +} diff --git a/clients/typescript/packages/client/src/client.ts b/clients/typescript/packages/client/src/client.ts new file mode 100644 index 0000000..8959ef3 --- /dev/null +++ b/clients/typescript/packages/client/src/client.ts @@ -0,0 +1,92 @@ +import type { ClientOverrides } from "./build-tree"; +import { buildClientTree } from "./build-tree"; +import { DEFAULT_ENVELOPE_CONFIG } from "./constants"; +import type { RouteContext } from "./context"; +import type { AnyClientPlugin } from "./define-plugin"; +import { Fetcher } from "./fetcher"; +import { normalizeBasePath, stripTrailingSlash } from "./helpers"; +import { HookRunner } from "./hooks"; +import { defaultSessionParse } from "./normalize"; +import type { FetchInit } from "./plugin"; +import { coreClientPlugin, createSessionHydrator } from "./routes"; +import { createSessionStore } from "./session-store"; +import type { AuthClient, ClientFetchOptions, CreateAuthClientOptions, EnvelopeConfig, RedirectFn } from "./types"; + +export function createAuthClient( + opts: CreateAuthClientOptions, +): AuthClient { + const baseURL = stripTrailingSlash(opts.baseURL); + const basePath = normalizeBasePath(opts.basePath ?? "/auth"); + + const userPlugins = (opts.plugins ?? []) as readonly AnyClientPlugin[]; + const plugins: readonly AnyClientPlugin[] = [coreClientPlugin(), ...userPlugins]; + + const hooks = new HookRunner(plugins); + const envelope = { ...DEFAULT_ENVELOPE_CONFIG, ...opts.envelope } satisfies EnvelopeConfig; + const fetcher = buildFetcher(baseURL, basePath, envelope, hooks, opts.fetchOptions ?? {}); + + const parseSession = opts.parseSession ?? defaultSessionParse; + const redirect = resolveRedirect(opts.redirectFn); + const baseFetch = (path: string, init?: FetchInit) => fetcher.fetch(path, init); + + const store = createSessionStore({ + hydrator: createSessionHydrator({ fetch: baseFetch, parseSession }), + crossTabSync: opts.crossTabSync !== false, + refetchOnWindowFocus: opts.refetchOnWindowFocus !== false, + ...(opts.initialSession !== undefined ? { initialSession: opts.initialSession } : {}), + }); + + const ctx: RouteContext = { + fetch: baseFetch, + redirect, + parseSession, + setSession: (session) => store.setData(session), + refetchSession: () => store.refetch(), + store, + }; + + const api = buildClientTree({ + plugins, + ctx, + fetcher, + overrides: opts.overrides as ClientOverrides, + }); + + const client: Record = { + baseURL, + basePath, + ...api, + $session: store.$session, + }; + + return client as AuthClient; +} + +function buildFetcher( + baseURL: string, + basePath: string, + envelope: EnvelopeConfig, + hooks: HookRunner, + fetchOptions: ClientFetchOptions, +): Fetcher { + return new Fetcher({ + baseURL, + basePath, + envelope, + hooks, + fetchOptions, + }); +} + +function resolveRedirect(redirect: RedirectFn | undefined): RedirectFn { + return (url: string) => { + if (redirect !== undefined) { + return redirect(url); + } + if (typeof window !== "undefined" && typeof window.location !== "undefined") { + window.location.href = url; + return true; + } + return false; + }; +} diff --git a/clients/typescript/packages/client/src/constants.ts b/clients/typescript/packages/client/src/constants.ts new file mode 100644 index 0000000..eb7abfc --- /dev/null +++ b/clients/typescript/packages/client/src/constants.ts @@ -0,0 +1,17 @@ +import type { EnvelopeConfig, EnvelopeFields } from "./types"; + +export const DEFAULT_ENVELOPE_CONFIG: EnvelopeConfig = { + mode: "off", +}; + +export const DEFAULT_ENVELOPE_FIELDS: EnvelopeFields = { + data: "data", + message: "message", +}; + +/** Response headers the server emits for the JWT and rotated refresh token. */ +export const SET_AUTH_TOKEN_HEADER = "Set-Auth-Token"; +export const SET_REFRESH_TOKEN_HEADER = "Set-Refresh-Token"; + +/** Default `localStorage` key for the persisted tokens. */ +export const DEFAULT_TOKEN_STORAGE_KEY = "limen.tokens"; diff --git a/clients/typescript/packages/client/src/context.ts b/clients/typescript/packages/client/src/context.ts new file mode 100644 index 0000000..761ac07 --- /dev/null +++ b/clients/typescript/packages/client/src/context.ts @@ -0,0 +1,33 @@ +import type { FetchInit } from "./plugin"; +import type { SessionStore } from "./session-store"; +import type { ParseSession, RedirectFn, Session } from "./types"; + +/** + * Context passed to route handlers and plugin actions. + */ +export type RouteContext = { + /** + * Fetch a path relative to the plugin base path. Pass + * `init.absolute = true` to resolve from the client base path instead. + */ + fetch: (path: string, init?: FetchInit) => Promise; + /** + * Navigate to an absolute URL. Returns whether navigation happened. + */ + readonly redirect: RedirectFn; + /** + * Parse a session-bearing response. + */ + readonly parseSession: ParseSession; + /** + * Write a session into the reactive store, or `null` to clear it. + */ + setSession: (session: Session | null) => void; + /** Revalidate the current session. */ + refetchSession: () => Promise; + /** Reactive session store. */ + readonly store: SessionStore; +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any -- TFields erased at boundaries +export type AnyRouteContext = RouteContext; diff --git a/clients/typescript/packages/client/src/define-plugin.ts b/clients/typescript/packages/client/src/define-plugin.ts new file mode 100644 index 0000000..ad0610f --- /dev/null +++ b/clients/typescript/packages/client/src/define-plugin.ts @@ -0,0 +1,75 @@ +import type { RouteContext } from "./context"; +import type { InferRoutes, PathSegments } from "./infer"; +import type { PluginHooks } from "./plugin"; +import type { AnyRouteDescriptor, RouteDescriptor } from "./route"; +import type { IsAny, UnionToIntersection } from "./type-utils"; + +/** Invoke one of the plugin's routes from `actions`. */ +export type RunRoute = (route: RouteDescriptor, input: I) => Promise; + +/** + * Client plugin contract + */ +export type ClientPlugin< + Id extends string, + BasePath extends string, + Routes extends readonly AnyRouteDescriptor[], + Actions, +> = { + readonly id: Id; + /** Default mount path relative to the client `basePath`, e.g. `"/magic-link"`; omit for the root (`""`). */ + readonly basePath?: BasePath; + readonly routes: Routes; + readonly hooks?: PluginHooks; + readonly actions?: (ctx: RouteContext, run: RunRoute) => Actions; +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any -- match any plugin shape +export type AnyClientPlugin = ClientPlugin; + +/** + * Register a plugin's routes while preserving each route's input/output types + * for the generated client API. + */ +export function defineRoutes(...routes: Routes): Routes { + return routes; +} + +/** + * Define a client plugin for `createAuthClient`. + */ +export function defineClientPlugin< + const Id extends string, + const Routes extends readonly AnyRouteDescriptor[], + Actions = Record, + const BasePath extends string = "", +>(def: ClientPlugin): ClientPlugin { + return { + id: def.id, + basePath: (def.basePath ?? "") as BasePath, + routes: def.routes, + ...(def.hooks !== undefined ? { hooks: def.hooks } : {}), + ...(def.actions !== undefined ? { actions: def.actions } : {}), + }; +} + +/** + * The client-only methods a plugin contributes. Widened `any` contributes + * nothing so the client does not become permissive. + */ +type ActionsOf

= P extends { actions?: (ctx: never, run: never) => infer A } + ? IsAny extends true + ? unknown + : A extends object + ? A + : unknown + : unknown; + +export type InferPluginContribution

= + P extends ClientPlugin + ? InferRoutes> & ActionsOf

+ : unknown; + +export type CombinedClientContributions = UnionToIntersection< + { [K in keyof Plugins]: InferPluginContribution }[number] +>; diff --git a/clients/typescript/packages/client/src/envelope.ts b/clients/typescript/packages/client/src/envelope.ts new file mode 100644 index 0000000..613d42d --- /dev/null +++ b/clients/typescript/packages/client/src/envelope.ts @@ -0,0 +1,48 @@ +import { DEFAULT_ENVELOPE_FIELDS } from "./constants"; +import { type EnvelopeConfig } from "./types"; + +/** + * Unwrap a parsed JSON success body according to envelope config. + * + * - `mode: "off"`: return as-is. + * - `mode: "wrap-success" | "always"`: extract `body[fields.data]` if present; + * if the key is missing (server didn't wrap this particular response), fall + * back to the raw body so we don't lose data. + * + * Returning `unknown` is intentional: the caller knows the expected shape and + * narrows / validates it. Trying to be clever here would invite false typing. + */ +export function unwrapPayload(body: unknown, envelope: EnvelopeConfig): unknown { + if (envelope.mode === "off") { + return body; + } + if (body === null || typeof body !== "object") { + return body; + } + const fields = envelope.fields ?? DEFAULT_ENVELOPE_FIELDS; + const record = body as Record; + if (fields.data in record) { + return record[fields.data]; + } + return body; +} + +/** + * Pull the human-readable error message out of a non-2xx body. + * + * - `mode: "off" | "wrap-success"`: error bodies look like `{ message }` + * - `mode: "always"`: error bodies use the configured `fields.message` key. + * + * Returns `undefined` when no message can be located; the caller substitutes + * a status-based fallback (e.g. HTTP status text). + */ +export function unwrapErrorMessage(body: unknown, envelope: EnvelopeConfig): string | undefined { + if (body === null || typeof body !== "object") { + return undefined; + } + const record = body as Record; + const fields = envelope.fields ?? DEFAULT_ENVELOPE_FIELDS; + const key = envelope.mode === "always" ? fields.message : "message"; + const value = record[key]; + return typeof value === "string" ? value : undefined; +} diff --git a/clients/typescript/packages/client/src/errors.ts b/clients/typescript/packages/client/src/errors.ts new file mode 100644 index 0000000..24fd996 --- /dev/null +++ b/clients/typescript/packages/client/src/errors.ts @@ -0,0 +1,47 @@ +export type LimenErrorCode = + | "unauthorized" + | "forbidden" + | "not_found" + | "rate_limited" + | "validation_error" + | "conflict" + | "server_error" + | "unknown"; + +/** Map HTTP status → typed code. Anything unmapped becomes `"unknown"`. */ +// prettier-ignore +export function deriveErrorCode(status: number): LimenErrorCode { + if (status === 401) {return "unauthorized";} + if (status === 403) {return "forbidden";} + if (status === 404) {return "not_found";} + if (status === 409) {return "conflict";} + if (status === 422 || status === 400) {return "validation_error";} + if (status === 429) {return "rate_limited";} + if (status >= 500 && status < 600) {return "server_error";} + return "unknown"; +} + +/** + * The single error type every SDK call throws on non-2xx. Carries the raw + * server message, the HTTP status, and a derived typed code. Subclass-free so + * `instanceof LimenError` is the only check consumers need. + */ +export class LimenError extends Error { + override readonly name = "LimenError"; + readonly status: number; + readonly code: LimenErrorCode; + + constructor(message: string, status: number, code?: LimenErrorCode) { + super(message); + this.status = status; + this.code = code ?? deriveErrorCode(status); + } + + get isUnauthorized(): boolean { + return this.code === "unauthorized"; + } + + get isRateLimited(): boolean { + return this.code === "rate_limited"; + } +} diff --git a/clients/typescript/packages/client/src/fetcher.ts b/clients/typescript/packages/client/src/fetcher.ts new file mode 100644 index 0000000..9add952 --- /dev/null +++ b/clients/typescript/packages/client/src/fetcher.ts @@ -0,0 +1,179 @@ +import { unwrapErrorMessage, unwrapPayload } from "./envelope"; +import { LimenError, deriveErrorCode } from "./errors"; +import { ensureLeadingSlash, joinURL, stripTrailingSlash } from "./helpers"; +import type { HookRunner } from "./hooks"; +import type { FetchInit, RequestContext, ResponseContext } from "./plugin"; +import type { EnvelopeConfig, HTTPMethod } from "./types"; + +type ClientFetchCallbackContext = ResponseContext & {}; + +export type FetcherFetchOptions = { + /** Whether to send credentials (cookies). Defaults to `"include"`. */ + credentials?: RequestCredentials; + /** Custom fetch impl. Defaults to `globalThis.fetch`. */ + impl?: typeof fetch; + /** Default headers merged into every request. Per-request headers override these. */ + headers?: HeadersInit; + /** Callback function to be called when the request is successful. */ + onSuccess?: (context: ClientFetchCallbackContext & { response: Response }) => void; + /** Callback function to be called when the request fails. */ + onError?: (context: ClientFetchCallbackContext & { error: Error }) => void; +}; + +type FetcherOptions = { + baseURL: string; + basePath: string; + fetchOptions: FetcherFetchOptions; + hooks: HookRunner; + envelope: EnvelopeConfig; +}; + +export class Fetcher { + private readonly fetchImpl: typeof fetch; + private readonly credentials: RequestCredentials; + + constructor(private readonly opts: FetcherOptions) { + this.fetchImpl = opts.fetchOptions.impl ?? globalThis.fetch.bind(globalThis); + this.credentials = opts.fetchOptions.credentials ?? "include"; + } + + async fetch(path: string, init?: FetchInit, routePath: string = path): Promise { + const method: HTTPMethod = init?.method ?? (init?.body !== undefined ? "POST" : "GET"); + return this.run({ + method, + path, + routePath, + body: init?.body, + headers: init?.headers, + query: init?.query, + }); + } + + /** + * Builds the request context, runs hooks, does + * the fetch, parses the response, runs hooks again, throws on non-2xx. + */ + private async run(args: { + method: HTTPMethod; + path: string; + routePath: string; + body: unknown; + headers: HeadersInit | undefined; + query: Record | undefined; + }): Promise { + const fullPath = this.normalizeRelativePath(this.opts.basePath, args.path, args.query); + const url = joinURL(this.opts.baseURL, fullPath); + + const headers = new Headers({ ...args.headers, ...(this.opts.fetchOptions.headers ?? {}) }); + if (!headers.has("Content-Type")) { + headers.set("Content-Type", "application/json"); + } + if (!headers.has("Accept")) { + headers.set("Accept", "application/json"); + } + + let reqCtx: RequestContext = { + method: args.method, + fullPath: fullPath, + path: args.path, + routePath: args.routePath, + url, + headers, + body: args.body, + }; + + reqCtx = await this.opts.hooks.runBeforeRequest(reqCtx); + + const payload = reqCtx.body !== undefined && reqCtx.body !== null ? JSON.stringify(reqCtx.body) : undefined; + + const requestInit: RequestInit = { + method: reqCtx.method, + headers: reqCtx.headers, + credentials: this.credentials, + }; + + if (payload !== undefined) { + requestInit.body = payload; + } + + let response: Response; + try { + response = await this.fetchImpl(reqCtx.url, requestInit); + } catch (err) { + this.opts.fetchOptions.onError?.({ + ...reqCtx, + status: 0, + ok: false, + error: err as Error, + }); + throw new LimenError(err instanceof Error ? err.message : "Network request failed", 0, "unknown"); + } + + const parsedBody = await this.parseResponseBody(response); + const unwrapped = + response.ok && parsedBody !== undefined ? unwrapPayload(parsedBody, this.opts.envelope) : parsedBody; + + let resCtx: ResponseContext = { + method: args.method, + fullPath: fullPath, + path: args.path, + routePath: args.routePath, + status: response.status, + ok: response.ok, + headers: response.headers, + body: unwrapped, + }; + + resCtx = await this.opts.hooks.runAfterResponse(resCtx); + + if (resCtx.ok) { + this.opts.fetchOptions.onSuccess?.({ + ...resCtx, + response, + }); + return resCtx.body as T; + } + + const message = + unwrapErrorMessage(resCtx.body, this.opts.envelope) ?? + response.statusText ?? + `Request failed with status ${response.status}`; + + const error = new LimenError(message, response.status, deriveErrorCode(response.status)); + this.opts.fetchOptions.onError?.({ + ...reqCtx, + status: response.status, + ok: false, + error, + }); + throw error; + } + + private async parseResponseBody(response: Response): Promise { + if (response.status === 204) { + return undefined; + } + const text = await response.text(); + if (text.length === 0) { + return undefined; + } + return JSON.parse(text) as unknown; + } + + private normalizeRelativePath( + basePath: string, + relativePath: string, + query: Record | undefined, + ): string { + const base = basePath === "" || basePath === "/" ? "" : ensureLeadingSlash(stripTrailingSlash(basePath)); + let path = base + ensureLeadingSlash(relativePath); + if (query !== undefined) { + const params = new URLSearchParams(query); + const qs = params.toString(); + if (qs.length > 0) { + path = `${path}?${qs}`; + } + } + return path; + } +} diff --git a/clients/typescript/packages/client/src/helpers.ts b/clients/typescript/packages/client/src/helpers.ts new file mode 100644 index 0000000..99fbd42 --- /dev/null +++ b/clients/typescript/packages/client/src/helpers.ts @@ -0,0 +1,69 @@ +export function stripTrailingSlash(s: string): string { + return s.endsWith("/") ? s.slice(0, -1) : s; +} + +/** + * Ensure a leading slash while preserving `""` as "no path". + */ +export function ensureLeadingSlash(s: string): string { + if (s === "" || s === "/") { + return s; + } + return s.startsWith("/") ? s : `/${s}`; +} + +/** + * Normalize a base path to `""` or a leading-slash path without a trailing + * slash. + */ +export function normalizeBasePath(p: string): string { + if (p === "" || p === "/") { + return ""; + } + const withLeading = p.startsWith("/") ? p : `/${p}`; + return withLeading.endsWith("/") ? withLeading.slice(0, -1) : withLeading; +} + +export function joinURL(baseURL: string, path: string): string { + return stripTrailingSlash(baseURL) + ensureLeadingSlash(path); +} + +export function toCamelCaseKey(key: string): string { + return key.replace(/_([a-z])/g, (_, c: string) => c.toUpperCase()); +} + +export function kebabToCamel(key: string): string { + return key.replace(/-([a-z0-9])/g, (_, c: string) => c.toUpperCase()); +} + +/** + * Shallow-convert an object's keys from snake_case to camelCase. Non-object + * inputs and arrays pass through untouched. + */ +export function camelizeKeys>(raw: unknown): T { + if (raw === null || typeof raw !== "object" || Array.isArray(raw)) { + return raw as T; + } + const out: Record = {}; + for (const [key, value] of Object.entries(raw)) { + out[toCamelCaseKey(key)] = value; + } + return out as T; +} + +export function camelizeEach>(raw: unknown): T[] { + if (!Array.isArray(raw)) { + return []; + } + return raw.map((item) => camelizeKeys(item)); +} + +/** + * Convert camelCase to snake_case, leaving already-snake-cased keys unchanged. + */ +export function camelToSnake(key: string): string { + if (key.includes("_")) { + return key; + } + return key.replace(/[A-Z]/g, (m) => `_${m.toLowerCase()}`); +} diff --git a/clients/typescript/packages/client/src/hooks.ts b/clients/typescript/packages/client/src/hooks.ts new file mode 100644 index 0000000..c64876e --- /dev/null +++ b/clients/typescript/packages/client/src/hooks.ts @@ -0,0 +1,60 @@ +import type { + AfterResponseHook, + BeforeRequestHook, + PluginHooks, + RequestContext, + ResponseContext, + RouteMatcher, +} from "./plugin"; + +type HookProvider = { + hooks?: PluginHooks; +}; + +function matchesRoute(matcher: RouteMatcher | undefined, routePath: string): boolean { + if (matcher === undefined) { + return true; + } + if (typeof matcher === "string") { + return routePath === matcher; + } + if (typeof matcher === "function") { + return matcher({ path: routePath }); + } + return routePath !== undefined && matcher.includes(routePath); +} + +export class HookRunner { + private readonly before: BeforeRequestHook[]; + private readonly after: AfterResponseHook[]; + + constructor(plugins: readonly HookProvider[]) { + this.before = plugins.flatMap((p) => p.hooks?.beforeRequest ?? []); + this.after = plugins.flatMap((p) => p.hooks?.afterResponse ?? []); + } + + async runBeforeRequest(initial: RequestContext): Promise { + let ctx = initial; + for (const hook of this.before) { + if (!matchesRoute(hook.match, ctx.routePath)) { + continue; + } + ctx = await hook.run(ctx); + } + return ctx; + } + + async runAfterResponse(initial: ResponseContext): Promise { + let ctx = initial; + for (const hook of this.after) { + if (!matchesRoute(hook.match, ctx.routePath)) { + continue; + } + if (!hook.allowOnFailure && ctx.status >= 400) { + continue; + } + ctx = await hook.run(ctx); + } + return ctx; + } +} diff --git a/clients/typescript/packages/client/src/index.ts b/clients/typescript/packages/client/src/index.ts new file mode 100644 index 0000000..8b72124 --- /dev/null +++ b/clients/typescript/packages/client/src/index.ts @@ -0,0 +1,48 @@ +export const VERSION = "0.0.0"; + +export { createAuthClient } from "./client"; + +export { LimenError, deriveErrorCode } from "./errors"; +export type { LimenErrorCode } from "./errors"; + +export { camelizeEach, camelizeKeys } from "./helpers"; +export { defaultSessionParse, normalizeUser } from "./normalize"; + +export type { SessionState, SessionStore } from "./session-store"; + +export type { + AfterResponseHook, + BeforeRequestHook, + FetchInit, + PluginClientOverride, + PluginIdOf, + PluginOverrides, + RequestContext, + ResponseContext, + RouteMatcher, +} from "./plugin"; + +export { defineClientPlugin, defineRoutes } from "./define-plugin"; +export { route } from "./route"; +export { defaultSerialize } from "./serialize"; + +export type { AnyRouteContext, RouteContext } from "./context"; +export type { RouteCallOptions, RouteHandler } from "./route"; +export type { RunRoute } from "./define-plugin"; + +export { coreClientPlugin } from "./routes"; +export type { ActiveSession, CoreContribution, VerifyEmailInput } from "./routes"; + +export type { + AuthClient, + ClientFetchOptions, + CreateAuthClientOptions, + EnvelopeConfig, + EnvelopeFields, + EnvelopeMode, + HTTPMethod, + ParseSession, + RedirectFn, + Session, + User, +} from "./types"; diff --git a/clients/typescript/packages/client/src/infer.ts b/clients/typescript/packages/client/src/infer.ts new file mode 100644 index 0000000..bc7d70b --- /dev/null +++ b/clients/typescript/packages/client/src/infer.ts @@ -0,0 +1,69 @@ +import type { InputOf, OutputOf, RouteCallOptions } from "./route"; +import type { KebabToCamel, Split, UnionToIntersection } from "./type-utils"; + +type IsParam = S extends `:${string}` ? true : false; + +/** + * Turn a route path literal into its camelCased client-chain segments, dropping + * leading slashes and `:param` segments. `"/otp/send"` -> `["otp", "send"]`. + */ +export type PathSegments

= P extends `/${infer Rest}` + ? PathSegments + : P extends `${infer Head}/${infer Tail}` + ? Head extends "" + ? PathSegments + : IsParam extends true + ? PathSegments + : [KebabToCamel, ...PathSegments] + : P extends "" + ? [] + : IsParam

extends true + ? [] + : [KebabToCamel

]; + +type PathOf = R extends { path: infer P extends string } ? P : never; + +/** + * The resolved chain for one route: an absolute `as` wins, otherwise it is the + * plugin's base-path segments followed by the route-path segments. + */ +type ChainSegments = R extends { as: infer A extends string } + ? Split + : [...BasePrefix, ...PathSegments>]; + +type Nest = Segs extends readonly [ + infer Head extends string, + ...infer Rest extends readonly string[], +] + ? // Avoid emitting an index signature when route paths widen to `string`. + string extends Head + ? unknown + : { [K in Head]: Nest } + : Fn; + +/** + * A no-input route (`I` is `void`) takes no input — only the optional, trailing + * call options; everything else takes `input` followed by the call options. + * Options are always the trailing argument so the runtime can place them + * unambiguously (a lone argument is always the route input). + */ +type InferRouteFn = [I] extends [void] + ? (input?: void, opts?: RouteCallOptions) => Promise + : (input: I, opts?: RouteCallOptions) => Promise; + +type IsExposed = R extends { expose: false } ? false : true; + +type RouteFn = InferRouteFn, OutputOf>; + +type InferOneRoute = + IsExposed extends false ? unknown : Nest, RouteFn>; + +/** + * Infer the public API tree for a list of route descriptors, given the owning + * plugin's base-path segments. Each route mounts a callable at its resolved + * chain; sibling keys merge via intersection, so `signin.credential` and + * `signup.credential` coexist and both autocomplete. + */ +export type InferRoutes = UnionToIntersection< + { [K in keyof Routes]: InferOneRoute }[number] +>; diff --git a/clients/typescript/packages/client/src/json-deep-equal.ts b/clients/typescript/packages/client/src/json-deep-equal.ts new file mode 100644 index 0000000..e47be93 --- /dev/null +++ b/clients/typescript/packages/client/src/json-deep-equal.ts @@ -0,0 +1,53 @@ +function isPlainObject(value: unknown): value is Record { + if (typeof value !== "object" || value === null) { + return false; + } + const proto = Object.getPrototypeOf(value); + return proto === Object.prototype || proto === null; +} + +function equalArrays(a: unknown[], b: unknown[]): boolean { + if (a.length !== b.length) { + return false; + } + + for (let i = 0; i < a.length; i += 1) { + if (!deepJsonEqual(a[i], b[i])) { + return false; + } + } + return true; +} + +function equalObjects(a: Record, b: Record): boolean { + const aKeys = Object.keys(a); + const bKeys = Object.keys(b); + if (aKeys.length !== bKeys.length) { + return false; + } + + for (const key of aKeys) { + if (!Object.hasOwn(b, key) || !deepJsonEqual(a[key], b[key])) { + return false; + } + } + return true; +} + +/** Structural equality for JSON-shaped values. */ +export function deepJsonEqual(a: unknown, b: unknown): boolean { + if (Object.is(a, b)) { + return true; + } + + const aIsArray = Array.isArray(a); + const bIsArray = Array.isArray(b); + if (aIsArray && bIsArray) { + return equalArrays(a, b); + } + + if (!isPlainObject(a) || !isPlainObject(b)) { + return false; + } + return equalObjects(a, b); +} diff --git a/clients/typescript/packages/client/src/normalize.ts b/clients/typescript/packages/client/src/normalize.ts new file mode 100644 index 0000000..18cc578 --- /dev/null +++ b/clients/typescript/packages/client/src/normalize.ts @@ -0,0 +1,24 @@ +import { toCamelCaseKey } from "./helpers"; +import type { Session, User } from "./types"; + +/** + * Convert snake_case User keys from server payloads to camelCase. Unknown + * extension fields are converted with the same rule so consumers consistently + * read camelCase in SDK responses. + */ +export function normalizeUser(raw: Record): User { + const out: Record = {}; + for (const [key, value] of Object.entries(raw)) { + out[toCamelCaseKey(key)] = value; + } + return out as User; +} + +export function defaultSessionParse(raw: unknown): Session { + if (!raw || typeof raw !== "object") { + throw new TypeError(`Expected session response to be an object, got ${raw === null ? "null" : typeof raw}`); + } + const obj = raw as Record; + const userRaw = (obj["user"] ?? obj) as Record; + return { user: normalizeUser(userRaw) }; +} diff --git a/clients/typescript/packages/client/src/path.ts b/clients/typescript/packages/client/src/path.ts new file mode 100644 index 0000000..e009758 --- /dev/null +++ b/clients/typescript/packages/client/src/path.ts @@ -0,0 +1,50 @@ +import { kebabToCamel } from "./helpers"; + +/** + * Derive the public client chain from a route path. Mirrors the type-level + * `PathSegments` in `infer.ts` exactly, so runtime materialization and compile + * time inference can never drift. + * + * "/otp/send" -> ["otp", "send"] + * "/revoke-sessions" -> ["revokeSessions"] + * "/:provider/authorize" -> ["authorize"] (param segments are dropped) + */ +export function pathToChain(path: string): string[] { + return path + .split("/") + .filter((seg) => seg.length > 0 && !seg.startsWith(":")) + .map(kebabToCamel); +} + +export function chainFromDotted(chain: string): string[] { + return chain.split(".").filter((seg) => seg.length > 0); +} + +type ResolvedPath = { + /** Path with `:param` segments substituted. */ + path: string; + /** Input with declared param keys removed. */ + rest: unknown; +}; + +/** + * Substitute declared path params from `input` into `path` and strip them from + * the payload. + */ +export function resolvePath(path: string, params: readonly string[] | undefined, input: unknown): ResolvedPath { + if (!params || params.length === 0) { + return { path, rest: input }; + } + const source = (input ?? {}) as Record; + const rest: Record = { ...source }; + let resolved = path; + for (const param of params) { + const value = rest[param]; + if (value === undefined) { + throw new Error(`Missing required path param "${param}" for route "${path}"`); + } + resolved = resolved.replace(`:${param}`, encodeURIComponent(value as string)); + delete rest[param]; + } + return { path: resolved, rest }; +} diff --git a/clients/typescript/packages/client/src/pipeline.ts b/clients/typescript/packages/client/src/pipeline.ts new file mode 100644 index 0000000..de6c8fd --- /dev/null +++ b/clients/typescript/packages/client/src/pipeline.ts @@ -0,0 +1,96 @@ +import type { AnyRouteContext } from "./context"; +import type { LimenError } from "./errors"; +import { camelizeEach } from "./helpers"; +import { resolvePath } from "./path"; +import type { FetchInit } from "./plugin"; +import type { AnyRoute, HttpRunner, RouteCallOptions } from "./route"; +import { defaultSerialize } from "./serialize"; +import type { Session } from "./types"; + +/** + * Run the default HTTP steps for a route — merge defaults, resolve path params, + * serialize, dispatch, parse — without applying session effects. + */ +async function runHttp(ctx: AnyRouteContext, def: AnyRoute, input: unknown): Promise { + let merged = input; + if (def.defaults !== undefined) { + merged = { ...(def.defaults as Record), ...((input ?? {}) as Record) }; + } + + const { path, rest } = resolvePath(def.path, def.params, merged); + const payload = def.serialize !== undefined ? def.serialize(rest) : defaultSerialize(rest); + + const init: FetchInit = { method: def.method, absolute: def.absolute ?? false }; + if (def.method === "GET" && payload !== undefined) { + init.query = payload as Record; + } else { + init.body = payload; + } + + const raw = await ctx.fetch(path, init); + + if (def.parseSession === true) { + return ctx.parseSession(raw); + } + + if (def.parse !== undefined) { + return def.parse(raw); + } + return Array.isArray(raw) ? camelizeEach(raw) : raw; +} + +async function applyEffects(ctx: AnyRouteContext, def: AnyRoute, result: unknown): Promise { + if (def.clearSession === true) { + ctx.store.setData(null); + } + + if (def.parseSession === true && def.skipStore !== true) { + if (result !== null && typeof result === "object" && "user" in result) { + ctx.store.setData(result as Session); + } + } + + if (def.refetchSession === true) { + await ctx.store.refetch(); + } +} + +function makeHttpRunner(ctx: AnyRouteContext, def: AnyRoute, boundInput: unknown): HttpRunner { + const run = (override?: unknown): Promise => + runHttp(ctx, def, override === undefined ? boundInput : override); + return run as HttpRunner; +} + +/** + * Execute a route's behaviour: delegate to its `handler` when present (handler + * owns all behaviour, including any effects), otherwise run the default + * pipeline and apply declarative effects once at the top level. + */ +async function dispatchRoute(ctx: AnyRouteContext, def: AnyRoute, input: unknown): Promise { + if (def.handler !== undefined) { + return def.handler(ctx, input, makeHttpRunner(ctx, def, input)); + } + const result = await runHttp(ctx, def, input); + await applyEffects(ctx, def, result); + return result; +} + +/** + * Run a route as a public client call, firing the per-call `onSuccess` / + * `onError` hooks around the resolved value or thrown error. + */ +export async function runRoute( + ctx: AnyRouteContext, + def: AnyRoute, + input: unknown, + opts?: RouteCallOptions, +): Promise { + try { + const result = await dispatchRoute(ctx, def, input); + opts?.onSuccess?.(result); + return result; + } catch (error) { + opts?.onError?.(error as LimenError); + throw error; + } +} diff --git a/clients/typescript/packages/client/src/plugin.ts b/clients/typescript/packages/client/src/plugin.ts new file mode 100644 index 0000000..364c901 --- /dev/null +++ b/clients/typescript/packages/client/src/plugin.ts @@ -0,0 +1,87 @@ +import type { KebabToCamel } from "./type-utils"; +import type { HTTPMethod } from "./types"; + +/** + * Route filter for hooks. Matches the stable route path, so client overrides do + * not change which hooks run. + */ +export type RouteMatcher = string | readonly string[] | ((ctx: { path: string }) => boolean); + +/** + * Mutable request context passed to `beforeRequest` hooks. Return the updated + * context, or throw to abort the request. + */ +export type RequestContext = { + method: HTTPMethod; + /** Path relative to `baseURL` (already includes the configured `basePath`). */ + fullPath: string; + /** Path relative to the client `basePath`. */ + path: string; + /** Stable route identifier, unaffected by client overrides. */ + routePath: string; + /** Full request URL. */ + url: string; + headers: Headers; + body: unknown; +}; + +/** + * Mutable response context passed to `afterResponse` hooks. The body has + * already been envelope-unwrapped. + */ +export type ResponseContext = Omit & { + /** Status code. */ + status: number; + /** Whether the response is successful. */ + ok: boolean; +}; + +export type BeforeRequestHook = { + /** Optional route filter. Omit to run for every request. */ + match?: RouteMatcher; + run: (req: RequestContext) => RequestContext | Promise; +}; + +export type AfterResponseHook = { + match?: RouteMatcher; + /** Run even for failed responses. */ + allowOnFailure?: boolean; + run: (res: ResponseContext) => ResponseContext | Promise; +}; + +/** + * Request and response hooks contributed by a plugin. + */ +export type PluginHooks = { + beforeRequest?: BeforeRequestHook[]; + afterResponse?: AfterResponseHook[]; +}; + +/** Options accepted by `ctx.fetch(path, init)`. */ +export type FetchInit = { + method?: HTTPMethod; + /** JSON body. The fetcher stringifies. */ + body?: unknown; + /** Appended as `?k=v` query string. */ + query?: Record; + /** Extra headers to merge with the defaults (`Content-Type`, `Accept`). */ + headers?: HeadersInit; + /** + * Resolve `path` from the client base path instead of the plugin base path. + */ + absolute?: boolean; +}; + +export type PluginIdOf

= P extends { readonly id: infer Id extends string } ? Id : never; + +export type PluginClientOverride = { + /** Replace the plugin's default base path (relative to the client `basePath`). */ + basePath?: string; +}; + +/** + * Per-plugin client overrides, keyed by camelCased plugin id. + */ +export type PluginOverrides = Partial< + Record>, PluginClientOverride> +>; diff --git a/clients/typescript/packages/client/src/plugins/bearer/index.ts b/clients/typescript/packages/client/src/plugins/bearer/index.ts new file mode 100644 index 0000000..5f91ed9 --- /dev/null +++ b/clients/typescript/packages/client/src/plugins/bearer/index.ts @@ -0,0 +1,60 @@ +import { DEFAULT_TOKEN_STORAGE_KEY, SET_AUTH_TOKEN_HEADER, SET_REFRESH_TOKEN_HEADER } from "../../constants"; +import { defineClientPlugin, defineRoutes } from "../../define-plugin"; +import { resolveDefaultStorage } from "./storage"; +import type { BearerPluginConfig, BearerTokens } from "./types"; + +export function bearerPlugin(config: BearerPluginConfig = {}) { + const store = config.storage ?? resolveDefaultStorage(config.storageKey ?? DEFAULT_TOKEN_STORAGE_KEY); + + return defineClientPlugin({ + id: "bearer", + routes: defineRoutes(), + actions: () => ({ + bearer: { + getTokens: () => store.get(), + setTokens: (tokens: BearerTokens) => store.set(tokens), + clear: () => store.clear(), + }, + }), + hooks: { + beforeRequest: [ + { + run: (req) => { + const tokens = store.get(); + if (tokens?.accessToken && !req.headers.has("Authorization")) { + req.headers.set("Authorization", `Bearer ${tokens.accessToken}`); + } + return req; + }, + }, + ], + afterResponse: [ + { + allowOnFailure: true, + run: (res) => { + const accessToken = res.headers.get(SET_AUTH_TOKEN_HEADER); + if (accessToken) { + const tokens: BearerTokens = { accessToken }; + const refreshToken = res.headers.get(SET_REFRESH_TOKEN_HEADER); + if (refreshToken) { + tokens.refreshToken = refreshToken; + } + store.set(tokens); + } + return res; + }, + }, + { + match: ["/signout", "/revoke-sessions"], + run: (res) => { + store.clear(); + return res; + }, + }, + ], + }, + }); +} + +export { localStorageBearerStorage, memoryBearerStorage, resolveDefaultStorage } from "./storage"; +export type { BearerPluginConfig, BearerPlugin as BearerPublic, BearerStorage, BearerTokens } from "./types"; diff --git a/clients/typescript/packages/client/src/plugins/bearer/storage.ts b/clients/typescript/packages/client/src/plugins/bearer/storage.ts new file mode 100644 index 0000000..706cec7 --- /dev/null +++ b/clients/typescript/packages/client/src/plugins/bearer/storage.ts @@ -0,0 +1,48 @@ +import { DEFAULT_TOKEN_STORAGE_KEY } from "../../constants"; +import type { BearerStorage, BearerTokens } from "./types"; + +export function memoryBearerStorage(): BearerStorage { + let tokens: BearerTokens | null = null; + return { + get: () => tokens, + set: (next) => { + tokens = next; + }, + clear: () => { + tokens = null; + }, + }; +} + +export function localStorageBearerStorage(key: string = DEFAULT_TOKEN_STORAGE_KEY): BearerStorage { + return { + get: () => { + try { + const raw = globalThis.localStorage.getItem(key); + if (!raw) { + return null; + } + const parsed = JSON.parse(raw) as Partial; + if (typeof parsed?.accessToken !== "string") { + return null; + } + return parsed as BearerTokens; + } catch { + return null; + } + }, + set: (tokens) => { + globalThis.localStorage.setItem(key, JSON.stringify(tokens)); + }, + clear: () => { + globalThis.localStorage.removeItem(key); + }, + }; +} + +export function resolveDefaultStorage(key: string): BearerStorage { + if (typeof globalThis.localStorage !== "undefined") { + return localStorageBearerStorage(key); + } + return memoryBearerStorage(); +} diff --git a/clients/typescript/packages/client/src/plugins/bearer/types.ts b/clients/typescript/packages/client/src/plugins/bearer/types.ts new file mode 100644 index 0000000..56efd1f --- /dev/null +++ b/clients/typescript/packages/client/src/plugins/bearer/types.ts @@ -0,0 +1,34 @@ +export type BearerPlugin = { + bearer: { + /** Currently stored tokens, or `null` if signed out / never set. */ + getTokens: () => BearerTokens | null; + /** Manually persist tokens. */ + setTokens: (tokens: BearerTokens) => void; + /** Discard stored tokens. Also runs automatically on `signOut`. */ + clear: () => void; + }; +}; + +export type BearerTokens = { + accessToken: string; + refreshToken?: string; +}; + +export interface BearerStorage { + get(): BearerTokens | null; + set(tokens: BearerTokens): void; + clear(): void; +} + +export type BearerPluginConfig = { + /** + * Where to persist tokens. Defaults to `localStorage` when available, + * otherwise an in-memory store (SSR / non-browser). + */ + storage?: BearerStorage; + /** + * Key used by the default `localStorage` adapter. Ignored when a custom + * `storage` is supplied. Defaults to `"limen.tokens"`. + */ + storageKey?: string; +}; diff --git a/clients/typescript/packages/client/src/plugins/credential/index.ts b/clients/typescript/packages/client/src/plugins/credential/index.ts new file mode 100644 index 0000000..5968f5c --- /dev/null +++ b/clients/typescript/packages/client/src/plugins/credential/index.ts @@ -0,0 +1,76 @@ +import { defineClientPlugin, defineRoutes } from "../../define-plugin"; +import { route } from "../../route"; +import type { Session } from "../../types"; +import type { + ChangePasswordInput, + CheckUsernameInput, + RequestPasswordResetInput, + ResetPasswordInput, + SetPasswordInput, + SignInCredentialInput, + SignUpCredentialInput, +} from "./types"; + +export function credentialPasswordPlugin() { + const routes = defineRoutes( + route>()({ + method: "POST", + path: "/signin/credential", + parseSession: true, + as: "signIn.credential", + defaults: { rememberMe: true }, + }), + route>()({ + method: "POST", + path: "/signup/credential", + parseSession: true, + as: "signUp.credential", + }), + route()({ + method: "POST", + path: "/passwords/request-reset", + as: "password.requestReset", + }), + route()({ + method: "POST", + path: "/passwords/reset", + as: "password.reset", + }), + route>()({ + method: "POST", + path: "/passwords/change", + parseSession: true, + defaults: { revokeOtherSessions: true }, + as: "password.change", + }), + route>()({ + method: "PUT", + path: "/passwords", + as: "password.set", + parseSession: true, + defaults: { revokeOtherSessions: true }, + }), + route()({ + method: "POST", + path: "/usernames/check", + parse: (raw) => (raw as { available: boolean }).available, + as: "username.checkAvailability", + }), + ); + + return defineClientPlugin({ + id: "credential-password", + basePath: "/", + routes, + }); +} + +export type { + ChangePasswordInput, + CheckUsernameInput, + RequestPasswordResetInput, + ResetPasswordInput, + SetPasswordInput, + SignInCredentialInput, + SignUpCredentialInput, +} from "./types"; diff --git a/clients/typescript/packages/client/src/plugins/credential/types.ts b/clients/typescript/packages/client/src/plugins/credential/types.ts new file mode 100644 index 0000000..d3e288d --- /dev/null +++ b/clients/typescript/packages/client/src/plugins/credential/types.ts @@ -0,0 +1,44 @@ +export type SignInCredentialInput = { + /** Email or username, depending on what the server enables. */ + credential: string; + password: string; + /** When true (default), issues a long-lived session. */ + rememberMe?: boolean; +}; + +export type SignUpCredentialInput = { + email: string; + password: string; + /** Required when username-on-signup is enabled. */ + username?: string; + /** Optional profile fields the server records on the user. */ + firstname?: string; + lastname?: string; + /** Extra fields recorded on the user at registration. */ + additionalFields?: Record; +}; + +export type ChangePasswordInput = { + currentPassword: string; + newPassword: string; + /** Invalidate other sessions on success. Defaults to true. */ + revokeOtherSessions?: boolean; +}; + +export type SetPasswordInput = { + newPassword: string; + revokeOtherSessions?: boolean; +}; + +export type RequestPasswordResetInput = { + email: string; +}; + +export type ResetPasswordInput = { + token: string; + newPassword: string; +}; + +export type CheckUsernameInput = { + username: string; +}; diff --git a/clients/typescript/packages/client/src/plugins/index.ts b/clients/typescript/packages/client/src/plugins/index.ts new file mode 100644 index 0000000..7e93685 --- /dev/null +++ b/clients/typescript/packages/client/src/plugins/index.ts @@ -0,0 +1,6 @@ +export * from "./bearer"; +export * from "./credential"; +export * from "./magic-link"; +export * from "./oauth"; +export * from "./session-jwt"; +export * from "./two-factor"; diff --git a/clients/typescript/packages/client/src/plugins/magic-link/index.ts b/clients/typescript/packages/client/src/plugins/magic-link/index.ts new file mode 100644 index 0000000..bb0d31f --- /dev/null +++ b/clients/typescript/packages/client/src/plugins/magic-link/index.ts @@ -0,0 +1,26 @@ +import { defineClientPlugin, defineRoutes } from "../../define-plugin"; +import { route } from "../../route"; +import type { Session } from "../../types"; +import type { RequestMagicLinkInput, VerifyMagicLinkInput } from "./types"; + +export function magicLinkPlugin() { + const routes = defineRoutes( + route()({ + method: "POST", + path: "/signin", + }), + route>()({ + method: "GET", + path: "/verify", + parseSession: true, + }), + ); + + return defineClientPlugin({ + id: "magic-link", + basePath: "/magic-link", + routes, + }); +} + +export type { RequestMagicLinkInput, VerifyMagicLinkInput } from "./types"; diff --git a/clients/typescript/packages/client/src/plugins/magic-link/types.ts b/clients/typescript/packages/client/src/plugins/magic-link/types.ts new file mode 100644 index 0000000..e5758b2 --- /dev/null +++ b/clients/typescript/packages/client/src/plugins/magic-link/types.ts @@ -0,0 +1,11 @@ +export type RequestMagicLinkInput = { + email: string; + redirectUri?: string; + newUserRedirectUri?: string; + errorRedirectUri?: string; + meta?: Record; +}; + +export type VerifyMagicLinkInput = { + token: string; +}; diff --git a/clients/typescript/packages/client/src/plugins/oauth/index.ts b/clients/typescript/packages/client/src/plugins/oauth/index.ts new file mode 100644 index 0000000..e87bacc --- /dev/null +++ b/clients/typescript/packages/client/src/plugins/oauth/index.ts @@ -0,0 +1,69 @@ +import { defineClientPlugin, defineRoutes } from "../../define-plugin"; +import { camelizeKeys } from "../../helpers"; +import { route, type RouteHandler } from "../../route"; +import type { LinkOAuthInput, OAuthAccount, OAuthAuthorizeResult, OAuthTokens, SignInOAuthInput } from "./types"; + +const fetchThenRedirect: RouteHandler = async (ctx, input, http) => { + const { disableRedirect, ...rest } = input; + const { url } = await http<{ url: string }>(rest); + return { url, redirect: disableRedirect === true ? false : ctx.redirect(url) }; +}; + +export function oauthClientPlugin() { + const routes = defineRoutes( + route()({ + method: "GET", + path: "/:provider/authorize", + as: "signIn.social", + params: ["provider"], + handler: fetchThenRedirect, + }), + route()({ + method: "GET", + path: "/:provider/link", + as: "social.link", + params: ["provider"], + handler: fetchThenRedirect, + }), + route<{ provider: string }, void>()({ + method: "DELETE", + path: "/:provider/unlink", + as: "social.unlink", + params: ["provider"], + }), + route()({ + method: "GET", + path: "/accounts", + as: "social.accounts", + }), + route<{ provider: string }, OAuthTokens>()({ + method: "GET", + path: "/:provider/tokens", + as: "social.tokens", + params: ["provider"], + parse: camelizeKeys, + }), + route<{ provider: string }, OAuthTokens>()({ + method: "POST", + path: "/:provider/tokens/refresh", + as: "social.refreshTokens", + params: ["provider"], + parse: camelizeKeys, + }), + ); + + return defineClientPlugin({ + id: "oauth", + basePath: "/oauth", + routes, + }); +} + +export type { + LinkOAuthInput, + OAuthAccount, + OAuthAuthorizeQuery, + OAuthAuthorizeResult, + OAuthTokens, + SignInOAuthInput, +} from "./types"; diff --git a/clients/typescript/packages/client/src/plugins/oauth/types.ts b/clients/typescript/packages/client/src/plugins/oauth/types.ts new file mode 100644 index 0000000..89f82bf --- /dev/null +++ b/clients/typescript/packages/client/src/plugins/oauth/types.ts @@ -0,0 +1,42 @@ +export type OAuthAuthorizeQuery = { + /** Where the server redirects the browser after a successful callback. */ + redirectUri?: string; + /** Where the server redirects on a failed callback. Falls back to `redirectUri`. */ + errorRedirectUri?: string; +}; + +export type SignInOAuthInput = OAuthAuthorizeQuery & { + /** Provider id, e.g. `"google"` or `"github"`. */ + provider: string; + /** + * When true, skip auto-navigation and only resolve with the authorization + * URL — the caller is responsible for navigating the browser. + */ + disableRedirect?: boolean; +}; + +export type LinkOAuthInput = SignInOAuthInput; + +export type OAuthAuthorizeResult = { + /** Provider authorization URL. */ + url: string; + /** Whether the SDK navigated to `url`. */ + redirect: boolean; +}; + +export type OAuthAccount = { + provider: string; + providerAccountId: string; + scopes: string[]; + accessTokenExpiresAt?: string; + createdAt: string; + updatedAt: string; +}; + +export type OAuthTokens = { + accessToken: string; + refreshToken?: string; + idToken?: string; + accessTokenExpiresAt?: string; + scope?: string; +}; diff --git a/clients/typescript/packages/client/src/plugins/session-jwt/constants.ts b/clients/typescript/packages/client/src/plugins/session-jwt/constants.ts new file mode 100644 index 0000000..b012e1a --- /dev/null +++ b/clients/typescript/packages/client/src/plugins/session-jwt/constants.ts @@ -0,0 +1,2 @@ +/** Refresh once the access token is within this many seconds of its `exp`. */ +export const DEFAULT_EXPIRY_SKEW_SECONDS = 30; diff --git a/clients/typescript/packages/client/src/plugins/session-jwt/index.ts b/clients/typescript/packages/client/src/plugins/session-jwt/index.ts new file mode 100644 index 0000000..55b9df1 --- /dev/null +++ b/clients/typescript/packages/client/src/plugins/session-jwt/index.ts @@ -0,0 +1,134 @@ +import { DEFAULT_TOKEN_STORAGE_KEY } from "../../constants"; +import type { AnyRouteContext } from "../../context"; +import { defineClientPlugin, defineRoutes, type RunRoute } from "../../define-plugin"; +import { LimenError } from "../../errors"; +import { route } from "../../route"; +import type { Session } from "../../types"; +import { resolveDefaultStorage } from "../bearer"; +import { DEFAULT_EXPIRY_SKEW_SECONDS } from "./constants"; +import { isExpiring, tokensFromHeaders } from "./jwt"; +import type { RefreshInput, SessionJwtPluginConfig, SessionJwtTokens } from "./types"; + +export function sessionJwtPlugin(config: SessionJwtPluginConfig = {}) { + const store = config.storage ?? resolveDefaultStorage(config.storageKey ?? DEFAULT_TOKEN_STORAGE_KEY); + const skewMs = (config.expirySkewSeconds ?? DEFAULT_EXPIRY_SKEW_SECONDS) * 1000; + + const refreshRoute = route>()({ + method: "POST", + path: "/refresh", + parseSession: true, + expose: false, + }); + + let ctx!: AnyRouteContext; + let run!: RunRoute; + let inFlight: Promise | null = null; + + const runRefresh = async (): Promise => { + const current = store.get(); + if (!current?.refreshToken) { + throw new LimenError("No refresh token found", 401, "unauthorized"); + } + try { + await run(refreshRoute, { refreshToken: current.refreshToken }); + const tokens = store.get(); + if (!tokens?.accessToken) { + throw new LimenError("Refresh did not return a valid access token", 500, "unknown"); + } + return tokens; + } catch (err) { + store.clear(); + ctx.setSession(null); + throw err; + } + }; + + const refresh = (): Promise => { + if (!inFlight) { + inFlight = runRefresh().finally(() => { + inFlight = null; + }); + } + return inFlight; + }; + + const getAccessToken = async (): Promise => { + const current = store.get(); + if (!current?.accessToken) { + return null; + } + + if (!isExpiring(current.accessToken, skewMs)) { + return current.accessToken; + } + + if (!current.refreshToken) { + return null; + } + return (await refresh()).accessToken; + }; + + const applyAuthHeader = async (req: { headers: Headers }): Promise => { + if (req.headers.has("Authorization")) { + return; + } + const token = await getAccessToken().catch(() => null); + if (token) { + req.headers.set("Authorization", `Bearer ${token}`); + } + }; + + return defineClientPlugin({ + id: "session-jwt", + routes: defineRoutes(refreshRoute), + actions: (pluginCtx, pluginRun) => { + ctx = pluginCtx; + run = pluginRun; + return { + sessionJwt: { + /** + * Get the current access token. If the token is expiring, refresh it. + * @returns The current access token or null if no token is found. + */ + getAccessToken, + refresh, + getTokens: () => store.get(), + clear: () => store.clear(), + }, + }; + }, + hooks: { + beforeRequest: [ + { + match: (route) => route.path !== "/refresh", + run: async (req) => { + await applyAuthHeader(req); + return req; + }, + }, + ], + afterResponse: [ + { + allowOnFailure: true, + run: (res) => { + const tokens = tokensFromHeaders(res.headers); + if (tokens) { + store.set(tokens); + } + return res; + }, + }, + { + match: ["/signout", "/revoke-sessions"], + allowOnFailure: true, + run: (res) => { + store.clear(); + return res; + }, + }, + ], + }, + }); +} + +export type { SessionJwtPluginConfig, SessionJwtTokens } from "./types"; diff --git a/clients/typescript/packages/client/src/plugins/session-jwt/jwt.ts b/clients/typescript/packages/client/src/plugins/session-jwt/jwt.ts new file mode 100644 index 0000000..befbbfc --- /dev/null +++ b/clients/typescript/packages/client/src/plugins/session-jwt/jwt.ts @@ -0,0 +1,44 @@ +import { SET_AUTH_TOKEN_HEADER, SET_REFRESH_TOKEN_HEADER } from "../../constants"; +import type { SessionJwtTokens } from "./types"; + +/** + * Read the `exp` (seconds since epoch) from a JWT without verifying its + * signature — the server is the source of truth; the client only needs expiry + * to decide when to refresh. Returns `null` for anything malformed. + */ +export function decodeJwtExp(token: string): number | null { + const payload = token.split(".")[1]; + if (!payload) { + return null; + } + try { + const b64 = payload.replace(/-/g, "+").replace(/_/g, "/"); + const padded = b64 + "=".repeat((4 - (b64.length % 4)) % 4); + const claims = JSON.parse(atob(padded)) as { exp?: unknown }; + return typeof claims.exp === "number" ? claims.exp : null; + } catch { + return null; + } +} + +export function isExpiring(token: string, skewMs: number): boolean { + const exp = decodeJwtExp(token); + if (exp === null) { + return true; + } + return exp * 1000 - skewMs <= Date.now(); +} + +export function tokensFromHeaders(headers: Headers): SessionJwtTokens | null { + const accessToken = headers.get(SET_AUTH_TOKEN_HEADER); + if (!accessToken) { + return null; + } + + const tokens: SessionJwtTokens = { accessToken }; + const refreshToken = headers.get(SET_REFRESH_TOKEN_HEADER); + if (refreshToken) { + tokens.refreshToken = refreshToken; + } + return tokens; +} diff --git a/clients/typescript/packages/client/src/plugins/session-jwt/types.ts b/clients/typescript/packages/client/src/plugins/session-jwt/types.ts new file mode 100644 index 0000000..2da76e3 --- /dev/null +++ b/clients/typescript/packages/client/src/plugins/session-jwt/types.ts @@ -0,0 +1,10 @@ +import type { BearerPluginConfig, BearerTokens } from "../bearer"; + +export type SessionJwtTokens = BearerTokens; + +export type SessionJwtPluginConfig = BearerPluginConfig & { + /** Refresh once the access token is within this many seconds of expiry. Default 30. */ + expirySkewSeconds?: number; +}; + +export type RefreshInput = { refreshToken: string }; diff --git a/clients/typescript/packages/client/src/plugins/two-factor/index.ts b/clients/typescript/packages/client/src/plugins/two-factor/index.ts new file mode 100644 index 0000000..5d7711a --- /dev/null +++ b/clients/typescript/packages/client/src/plugins/two-factor/index.ts @@ -0,0 +1,87 @@ +import { defineClientPlugin, defineRoutes } from "../../define-plugin"; +import { route } from "../../route"; +import type { Session } from "../../types"; +import type { + DisableTwoFactorInput, + FinalizeSetupInput, + InitiateSetupInput, + SendOTPInput, + TwoFactorConfig, + TwoFactorSetupURI, + VerifyInput, +} from "./types"; + +export function twoFactorPlugin(config: TwoFactorConfig) { + const routes = defineRoutes( + route()({ + method: "POST", + path: "/initiate-setup", + }), + route>()({ + method: "POST", + path: "/finalize-setup", + parseSession: true, + }), + route>()({ + method: "POST", + path: "/disable", + parseSession: true, + }), + route>()({ + method: "POST", + path: "/verify", + parseSession: true, + }), + route()({ + method: "GET", + path: "/totp/uri", + as: "twoFactor.getTotpUri", + }), + route()({ + method: "GET", + path: "/backup-codes", + as: "twoFactor.getBackupCodes", + }), + route()({ + method: "PUT", + path: "/backup-codes", + as: "twoFactor.regenerateBackupCodes", + }), + route()({ + method: "POST", + path: "/otp/send", + as: "twoFactor.sendOTP", + }), + ); + + return defineClientPlugin({ + id: "two-factor", + basePath: "/two-factor", + routes, + hooks: { + afterResponse: [ + { + match: "/signin/credential", + run: (ctx) => { + const body = ctx.body as Record; + if (body["two_factor_required"] === true) { + config.onTwoFactorRedirect(); + } + return ctx; + }, + }, + ], + }, + }); +} + +export type { + DisableTwoFactorInput, + FinalizeSetupInput, + InitiateSetupInput, + SendOTPInput, + TwoFactorConfig, + TwoFactorMethod, + TwoFactorSetupURI, + VerifyInput, +} from "./types"; diff --git a/clients/typescript/packages/client/src/plugins/two-factor/types.ts b/clients/typescript/packages/client/src/plugins/two-factor/types.ts new file mode 100644 index 0000000..b29eaed --- /dev/null +++ b/clients/typescript/packages/client/src/plugins/two-factor/types.ts @@ -0,0 +1,31 @@ +export type TwoFactorConfig = { + /** Called when sign-in requires a two-factor challenge. */ + onTwoFactorRedirect: () => void; +}; + +export type VerifyInput = { + code: string; + method?: TwoFactorMethod; +}; + +export type InitiateSetupInput = { + password: string; +}; + +export type FinalizeSetupInput = { + code: string; +}; + +export type DisableTwoFactorInput = { + password: string; +}; + +export type SendOTPInput = { + email: string; +}; + +export type TwoFactorSetupURI = { + uri: string; +}; + +export type TwoFactorMethod = "totp" | "otp"; diff --git a/clients/typescript/packages/client/src/react/index.ts b/clients/typescript/packages/client/src/react/index.ts new file mode 100644 index 0000000..abbeff3 --- /dev/null +++ b/clients/typescript/packages/client/src/react/index.ts @@ -0,0 +1,35 @@ +import { createAuthClient as createCoreClient } from "../client"; +import type { AnyClientPlugin } from "../define-plugin"; +import type { SessionState } from "../session-store"; +import type { Prettify } from "../type-utils"; +import type { AuthClient, CreateAuthClientOptions } from "../types"; +import { useStore } from "./react-store"; + +/** + * An {@link AuthClient} augmented with React hooks. + */ +export type ReactAuthClient = Prettify< + AuthClient & { + /** + * Reactively read the session store. Re-renders the component whenever + * `{ data, isPending, error }` changes. + */ + useSession: () => SessionState; + } +>; + +/** + * Create a Limen auth client with React hooks attached. + */ +export function createAuthClient( + opts: CreateAuthClientOptions, +): ReactAuthClient { + const client = createCoreClient(opts); + const useSession = (): SessionState => useStore(client.$session); + + return Object.assign(client, { useSession }) as ReactAuthClient; +} + +export type { SessionState, SessionStore } from "../session-store"; +export type { AuthClient, CreateAuthClientOptions, Session, User } from "../types"; +export { useStore } from "./react-store"; diff --git a/clients/typescript/packages/client/src/react/react-store.ts b/clients/typescript/packages/client/src/react/react-store.ts new file mode 100644 index 0000000..832abe2 --- /dev/null +++ b/clients/typescript/packages/client/src/react/react-store.ts @@ -0,0 +1,29 @@ +import type { Store, StoreValue } from "nanostores"; +import { useCallback, useRef, useSyncExternalStore } from "react"; + +export function useStore(store: SomeStore): StoreValue { + type Value = StoreValue; + + const snapshotRef = useRef(store.get()); + + const subscribe = useCallback( + (onChange: () => void) => { + const emitValue = (value: Value): void => { + if (snapshotRef.current === value) { + return; + } + snapshotRef.current = value; + onChange(); + }; + // Resync before listening: the value may have changed between render and + // this effect, and `listen` does not fire for the current value. + emitValue(store.value); + return store.listen(emitValue); + }, + [store], + ); + + const get = (): Value => snapshotRef.current as Value; + + return useSyncExternalStore(subscribe, get, get); +} diff --git a/clients/typescript/packages/client/src/route.ts b/clients/typescript/packages/client/src/route.ts new file mode 100644 index 0000000..e2543fd --- /dev/null +++ b/clients/typescript/packages/client/src/route.ts @@ -0,0 +1,113 @@ +import type { RouteContext } from "./context"; +import type { LimenError } from "./errors"; +import type { HTTPMethod } from "./types"; + +declare const INPUT: unique symbol; +declare const OUTPUT: unique symbol; + +/** + * Runs the route's default HTTP request and parser without session effects. + * Pass an input override when a handler needs to omit client-only fields. + */ +export type HttpRunner = (input?: I) => Promise; + +/** + * Custom route behavior for flows that need more than the default request + * pipeline. + */ +export type RouteHandler = ( + ctx: RouteContext, + input: I, + http: HttpRunner, +) => Promise; + +/** + * Per-call options accepted as the final argument of every generated route + * method. + */ +export type RouteCallOptions = { + /** Invoked with the resolved value after the call succeeds. */ + onSuccess?: (data: O) => void; + /** Invoked with the error just before it is re-thrown. */ + onError?: (error: LimenError) => void; +}; + +/** + * Declarative client route definition. The public client chain is derived from + * `path` unless `as` is provided. + */ +export type RouteDef = { + method: HTTPMethod; + path: `/${string}`; + + /** Dotted client chain override, e.g. `"twoFactor.getTotpUri"`. */ + as?: string; + + /** Merged under the caller's input before serialization. */ + defaults?: Partial; + /** SDK input → wire body/query. Defaults to shallow camelCase → snake_case. */ + serialize?: (input: I) => unknown; + /** Raw response → typed output. Ignored when `parseSession` is set. */ + parse?: (raw: unknown) => O; + /** + * Parse the response as a session and store it when it contains a `user`. + * Set `skipStore` to return the parsed session without writing it. + */ + parseSession?: boolean; + /** Resolve `path` from the client base path instead of the plugin base path. */ + absolute?: boolean; + + /** Input keys used as `:param` path values. */ + params?: readonly (keyof I & string)[]; + + /** For `parseSession` routes, skip the session-store write. */ + skipStore?: boolean; + /** Clear the session store on success. */ + clearSession?: boolean; + /** Revalidate the session after success. */ + refetchSession?: boolean; + + /** Set `false` to keep the route out of the public client API. */ + expose?: boolean; + + /** Override the default route call behavior. */ + handler?: RouteHandler; +}; + +/** + * A route definition carrying its input and output types for inference. + */ +export type RouteDescriptor = RouteDef> = D & { + readonly [INPUT]: I; + readonly [OUTPUT]: O; +}; + +/** + * Loose route-descriptor constraint for route tuples and plugin definitions. + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any -- accepts any route descriptor +export type AnyRouteDescriptor = RouteDescriptor; + +/** + * Route definition with erased input/output types but typed fields. + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any -- erases route I/O only +export type AnyRoute = RouteDef; + +export type InputOf = R extends { readonly [INPUT]: infer I } ? I : never; +export type OutputOf = R extends { readonly [OUTPUT]: infer O } ? O : never; + +/** + * Define an HTTP-backed client method for a plugin. The input/output types + * become the generated method's argument and resolved value. + * + * @example + * route>()({ + * method: "POST", + * path: "/verify", + * parseSession: true, + * }) + */ +export function route() { + return >(def: D): RouteDescriptor => def as RouteDescriptor; +} diff --git a/clients/typescript/packages/client/src/routes.ts b/clients/typescript/packages/client/src/routes.ts new file mode 100644 index 0000000..e90a6b8 --- /dev/null +++ b/clients/typescript/packages/client/src/routes.ts @@ -0,0 +1,89 @@ +import type { RouteContext } from "./context"; +import type { InferPluginContribution } from "./define-plugin"; +import { defineClientPlugin, defineRoutes } from "./define-plugin"; +import { route } from "./route"; +import type { Session } from "./types"; + +export type VerifyEmailInput = { + token: string; +}; + +export type ActiveSession = { + id: string | number; + token: string; + userId: unknown; + createdAt: string; + expiresAt: string; + lastAccess: string; + metadata?: Record; +}; + +/** + * Core routes available on every client. + */ +export function coreClientPlugin() { + const routes = defineRoutes( + route()({ + method: "GET", + path: "/sessions", + }), + route()({ + method: "POST", + path: "/signout", + clearSession: true, + }), + route()({ + method: "POST", + path: "/revoke-sessions", + clearSession: true, + }), + route()({ + method: "POST", + path: "/verify-email", + refetchSession: true, + }), + route()({ + method: "POST", + path: "/email-verifications", + as: "requestEmailVerification", + }), + ); + + return defineClientPlugin({ + id: "core", + basePath: "/", + routes, + actions: (ctx) => ({ + /** + * Revalidate session state with `GET /me`, update `$session`, and return the + * resolved value (`null` when signed out). + * + * Prefer subscribing to `$session` for reactive UI state (`data`, `isPending`, + * `error`). Use `getSession()` when you need an awaited server re-check, such + * as route guards or SSR revalidation after `initialSession`. + */ + getSession: async (): Promise | null> => { + await ctx.refetchSession(); + const state = ctx.store.$session.get(); + if (state.error) { + throw state.error; + } + return state.data as Session | null; + }, + }), + }); +} + +export type CoreContribution = InferPluginContribution>>; + +/** + * Fetch and parse the current session for the reactive store. + */ +export function createSessionHydrator( + ctx: Pick, "fetch" | "parseSession">, +): () => Promise> { + return async () => { + const raw = await ctx.fetch("/me", { method: "GET" }); + return ctx.parseSession(raw); + }; +} diff --git a/clients/typescript/packages/client/src/serialize.ts b/clients/typescript/packages/client/src/serialize.ts new file mode 100644 index 0000000..422cdaf --- /dev/null +++ b/clients/typescript/packages/client/src/serialize.ts @@ -0,0 +1,31 @@ +import { camelToSnake } from "./helpers"; + +/** + * Default request serializer: shallow camelCase → snake_case, drops `undefined`, + * leaves non-objects unchanged. + * + * `additionalFields` entries are merged into the top-level body verbatim. + * Known route fields win on key collisions. + */ +export function defaultSerialize(input: unknown): unknown { + if (input === null || input === undefined) { + return input; + } + if (typeof input !== "object" || Array.isArray(input)) { + return input; + } + const { additionalFields, ...rest } = input as Record; + const out: Record = {}; + + if (additionalFields && typeof additionalFields === "object" && !Array.isArray(additionalFields)) { + Object.assign(out, additionalFields); + } + + for (const [key, value] of Object.entries(rest)) { + if (value === undefined) { + continue; + } + out[camelToSnake(key)] = value; + } + return out; +} diff --git a/clients/typescript/packages/client/src/session-store.ts b/clients/typescript/packages/client/src/session-store.ts new file mode 100644 index 0000000..11524f2 --- /dev/null +++ b/clients/typescript/packages/client/src/session-store.ts @@ -0,0 +1,124 @@ +import { atom, onMount, type ReadableAtom } from "nanostores"; +import { LimenError } from "./errors"; +import { createSessionSync } from "./session-sync"; +import type { Session } from "./types"; + +export type SessionState = { + /** The current session, or `null` when signed out. */ + data: Session | null; + /** True while a `/me` fetch is in flight. */ + isPending: boolean; + /** + * The last non-401 failure (network error, 5xx, etc.). A 401 is not an error + * — it resolves to `data: null`. `error` is cleared on the next successful or + * 401 outcome. + */ + error: LimenError | null; +}; + +type RefetchOptions = { + /** Skip the fetch when the last hydration ran within this many milliseconds. */ + maxAgeMs?: number; + /** Skip the fetch when the session is signed out. */ + skipSignedOut?: boolean; +}; + +export type SessionStore = { + readonly $session: ReadableAtom>; + setData(session: Session | null): void; + /** + * Re-validate the session from the server. + */ + refetch(options?: RefetchOptions): Promise; +}; + +type CreateSessionStoreArgs = { + hydrator: () => Promise>; + initialSession?: Session | null; + /** Mirror session changes to other same-origin tabs. */ + crossTabSync?: boolean; + /** Re-validate against `/me` when the tab returns to the foreground. */ + refetchOnWindowFocus?: boolean; +}; + +export function createSessionStore(options: CreateSessionStoreArgs): SessionStore { + const $session = atom>({ + data: options.initialSession ?? null, + isPending: false, + error: null, + }); + + let inFlightHydration: Promise | null = null; + // Bumped on every write so older async refresh results cannot overwrite newer state. + let writeVersion = 0; + // Timestamp the most recent session refetched. + let lastRefreshedAt = 0; + const isStale = (requestVersion: number): boolean => requestVersion !== writeVersion; + + const fetchSessionFromServer = async (): Promise => { + const requestVersion = ++writeVersion; + $session.set({ data: $session.get().data, isPending: true, error: null }); + try { + const session = await options.hydrator(); + + if (isStale(requestVersion)) { + return; + } + + $session.set({ data: session, isPending: false, error: null }); + } catch (err) { + if (isStale(requestVersion)) { + return; + } + + if (err instanceof LimenError && err.isUnauthorized) { + // Not an error — the user is simply signed out. + $session.set({ data: null, isPending: false, error: null }); + return; + } + + const error = + err instanceof LimenError + ? err + : new LimenError(err instanceof Error ? err.message : "Failed to load session", 0, "unknown"); + // Preserve the last known session; surface the failure via `error`. + $session.set({ data: $session.get().data, isPending: false, error }); + } + }; + + const refetch = (options?: RefetchOptions): Promise => { + const { skipSignedOut, maxAgeMs } = options ?? {}; + if (skipSignedOut && $session.get().data === null) { + return Promise.resolve(); + } + + if (maxAgeMs !== undefined && Date.now() - lastRefreshedAt < maxAgeMs) { + return Promise.resolve(); + } + + if (!inFlightHydration) { + inFlightHydration = fetchSessionFromServer().finally(() => { + inFlightHydration = null; + lastRefreshedAt = Date.now(); + }); + } + return inFlightHydration; + }; + + const setData = (session: Session | null): void => { + writeVersion++; // supersede any in-flight hydrate so it can't overwrite this + $session.set({ data: session, isPending: false, error: null }); + }; + + const store: SessionStore = { $session, setData, refetch }; + + onMount($session, () => + createSessionSync(store, { + fetchOnMount: options.initialSession === undefined, + crossTabSync: options.crossTabSync ?? false, + refetchOnWindowFocus: options.refetchOnWindowFocus ?? false, + }), + ); + + return store; +} diff --git a/clients/typescript/packages/client/src/session-sync.ts b/clients/typescript/packages/client/src/session-sync.ts new file mode 100644 index 0000000..19701e9 --- /dev/null +++ b/clients/typescript/packages/client/src/session-sync.ts @@ -0,0 +1,82 @@ +import { onNotify } from "nanostores"; +import { createBroadcastChannel } from "./broadcast-channel"; +import { deepJsonEqual } from "./json-deep-equal"; +import type { SessionStore } from "./session-store"; +import type { Session } from "./types"; + +const CHANNEL_NAME = "limen.session"; +const FOCUS_REFETCH_THROTTLE_MS = 5_000; + +type SyncMessage = { data: Session | null }; + +type SessionSyncOptions = { + /** Fetch the session on mount. */ + fetchOnMount: boolean; + /** Mirror session changes to other same-origin tabs. */ + crossTabSync: boolean; + /** Re-validate against `/me` when the tab returns to the foreground. */ + refetchOnWindowFocus: boolean; +}; + +export function createSessionSync( + store: SessionStore, + options: SessionSyncOptions, +): () => void { + if (options.fetchOnMount) { + void store.refetch(); + } + + const teardowns: Array<() => void> = []; + if (options.crossTabSync) { + teardowns.push(syncAcrossTabs(store)); + } + + if (options.refetchOnWindowFocus) { + teardowns.push(refetchOnFocus(store)); + } + + return () => { + for (const teardown of teardowns) { + teardown(); + } + }; +} + +function syncAcrossTabs(store: SessionStore): () => void { + const port = createBroadcastChannel>(CHANNEL_NAME); + let lastData = store.$session.get().data; + + const unsubscribe = port.subscribe((message) => { + // Mark remote updates as seen before applying them to avoid echoing them. + lastData = message.data; + store.setData(message.data); + }); + + const unbindNotify = onNotify(store.$session, () => { + const data = store.$session.get().data; + if (deepJsonEqual(data, lastData)) { + return; + } + lastData = data; + port.post({ data: data }); + }); + + return () => { + unbindNotify(); + unsubscribe(); + port.close(); + }; +} + +function refetchOnFocus(store: SessionStore): () => void { + if (typeof document === "undefined") { + return () => {}; + } + const onVisibilityChange = (): void => { + if (document.visibilityState === "visible") { + void store.refetch({ maxAgeMs: FOCUS_REFETCH_THROTTLE_MS, skipSignedOut: true }); + } + }; + document.addEventListener("visibilitychange", onVisibilityChange); + return () => document.removeEventListener("visibilitychange", onVisibilityChange); +} diff --git a/clients/typescript/packages/client/src/solid/index.ts b/clients/typescript/packages/client/src/solid/index.ts new file mode 100644 index 0000000..ad41c1d --- /dev/null +++ b/clients/typescript/packages/client/src/solid/index.ts @@ -0,0 +1,36 @@ +import type { Accessor } from "solid-js"; +import { createAuthClient as createCoreClient } from "../client"; +import type { AnyClientPlugin } from "../define-plugin"; +import type { SessionState } from "../session-store"; +import type { Prettify } from "../type-utils"; +import type { AuthClient, CreateAuthClientOptions } from "../types"; +import { useStore } from "./solid-store"; + +/** + * An {@link AuthClient} augmented with Solid primitives. + */ +export type SolidAuthClient = Prettify< + AuthClient & { + /** + * Reactively read the session store as a Solid accessor. Updates whenever + * `{ data, isPending, error }` changes. + */ + useSession: () => Accessor>; + } +>; + +/** + * Create a Limen auth client with Solid primitives attached. + */ +export function createAuthClient( + opts: CreateAuthClientOptions, +): SolidAuthClient { + const client = createCoreClient(opts); + const useSession = (): Accessor> => useStore(client.$session); + + return Object.assign(client, { useSession }) as SolidAuthClient; +} + +export type { SessionState, SessionStore } from "../session-store"; +export type { AuthClient, CreateAuthClientOptions, Session, User } from "../types"; +export { useStore } from "./solid-store"; diff --git a/clients/typescript/packages/client/src/solid/solid-store.ts b/clients/typescript/packages/client/src/solid/solid-store.ts new file mode 100644 index 0000000..68acc78 --- /dev/null +++ b/clients/typescript/packages/client/src/solid/solid-store.ts @@ -0,0 +1,24 @@ +import type { Store, StoreValue } from "nanostores"; +import type { Accessor } from "solid-js"; +import { onCleanup } from "solid-js"; +import { createStore, reconcile } from "solid-js/store"; + +export function useStore>( + store: SomeStore, +): Accessor { + // Activate the store so `get()` is populated, then hand off to the real + // subscriber and unbind — avoids a dangling activation (https://github.com/nanostores/solid/issues/19). + const unbindActivation = store.listen(() => {}); + + const [state, setState] = createStore({ value: store.get() as Value }); + + // `reconcile` diffs each update so only changed paths re-trigger (fine-grained). + const unsubscribe = store.subscribe((value) => { + setState("value", reconcile(value as Value)); + }); + + onCleanup(() => unsubscribe()); + unbindActivation(); + + return () => state.value; +} diff --git a/clients/typescript/packages/client/src/svelte/index.ts b/clients/typescript/packages/client/src/svelte/index.ts new file mode 100644 index 0000000..2c06be2 --- /dev/null +++ b/clients/typescript/packages/client/src/svelte/index.ts @@ -0,0 +1,39 @@ +import type { Readable } from "svelte/store"; +import { createAuthClient as createCoreClient } from "../client"; +import type { AnyClientPlugin } from "../define-plugin"; +import type { SessionState } from "../session-store"; +import type { Prettify } from "../type-utils"; +import type { AuthClient, CreateAuthClientOptions } from "../types"; + +/** + * An {@link AuthClient} augmented with Svelte stores. + */ +export type SvelteAuthClient = Prettify< + AuthClient & { + /** + * The reactive session as a Svelte readable store. Use it with `$`: + * `$session.data`, `$session.isPending`, `$session.error`. + */ + useSession: () => Readable>; + } +>; + +/** + * Create a Limen auth client with Svelte stores attached. + * + * nanostores stores already satisfy Svelte's store contract (`subscribe(run)` + * fires immediately and returns an unsubscribe), so `useSession` returns the + * reactive `$session` store directly — no wrapper. + */ +export function createAuthClient( + opts: CreateAuthClientOptions, +): SvelteAuthClient { + const client = createCoreClient(opts); + const useSession = (): Readable> => + client.$session as unknown as Readable>; + + return Object.assign(client, { useSession }) as SvelteAuthClient; +} + +export type { SessionState, SessionStore } from "../session-store"; +export type { AuthClient, CreateAuthClientOptions, Session, User } from "../types"; diff --git a/clients/typescript/packages/client/src/type-utils.ts b/clients/typescript/packages/client/src/type-utils.ts new file mode 100644 index 0000000..ec8462d --- /dev/null +++ b/clients/typescript/packages/client/src/type-utils.ts @@ -0,0 +1,26 @@ +/** Collapse an intersection into a single readable object type on hover. */ +export type Prettify = { [K in keyof T]: T[K] } & {}; + +/** Turn a union `A | B | C` into the intersection `A & B & C`. */ +export type UnionToIntersection = (U extends unknown ? (x: U) => void : never) extends (x: infer I) => void + ? I + : never; + +/** + * Convert a kebab-case literal to camelCase: `"magic-link"` -> `"magicLink"`. + */ +export type KebabToCamel = S extends `${infer Head}-${infer Tail}` + ? `${Head}${Capitalize>}` + : S; + +/** Split a string literal into a tuple on a delimiter, dropping empty segments. */ +export type Split = S extends `${infer Head}${D}${infer Tail}` + ? Head extends "" + ? Split + : [Head, ...Split] + : S extends "" + ? [] + : [S]; + +/** Check if a type is `any`. */ +export type IsAny = 0 extends 1 & T ? true : false; diff --git a/clients/typescript/packages/client/src/types.ts b/clients/typescript/packages/client/src/types.ts new file mode 100644 index 0000000..029818b --- /dev/null +++ b/clients/typescript/packages/client/src/types.ts @@ -0,0 +1,111 @@ +import type { ReadableAtom } from "nanostores"; +import type { AnyClientPlugin, CombinedClientContributions } from "./define-plugin"; +import type { FetcherFetchOptions } from "./fetcher"; +import type { PluginOverrides } from "./plugin"; +import type { CoreContribution } from "./routes"; +import type { SessionState } from "./session-store"; +import type { Prettify } from "./type-utils"; + +export type ClientFetchOptions = Partial; + +export type CreateAuthClientOptions = { + /** Server origin, e.g. `"http://localhost:8080"`. Trailing slash is stripped. */ + baseURL: string; + /** Path where the Limen handler is mounted. Defaults to `"/auth"`. */ + basePath?: string; + /** Options that modify how the SDK performs HTTP requests. */ + fetchOptions?: ClientFetchOptions; + /** + * Optional transformer for non-default session payloads. + * + * Provide this when your server returns custom user/session fields. It must + * map the raw response into `Session`. + */ + parseSession?: ParseSession; + /** + * How the SDK navigates the browser when a flow hands control to an external + * page (e.g. an OAuth provider's authorization URL). Defaults to + * `window.location.href = url` when a `window` is available; a no-op in + * non-browser environments. Provide a custom function to integrate with a + * client-side router. + */ + redirectFn?: RedirectFn; + /** Plugins to register. */ + plugins?: Plugins; + /** + * Response-envelope config. Set this when the server wraps successful or all + * responses. + */ + envelope?: EnvelopeConfig; + /** + * Per-plugin route overrides. Keys are camelCased plugin ids, e.g. + * `{ magicLink: { basePath: "/passwordless" } }`. + */ + overrides?: PluginOverrides; + /** + * SSR seed for the session store. Provide the session resolved server-side to + * avoid a hydration flash. When provided, lazy hydration is skipped until you + * call `getSession()` or the store revalidates. + */ + initialSession?: Session | null; + /** + * Keep session state in sync across browser tabs. + * Enabled by default in browsers. Set `false` to disable. + */ + crossTabSync?: boolean; + /** + * Refresh session state when the tab becomes active again. + * Enabled by default in browsers. Set `false` to disable. + */ + refetchOnWindowFocus?: boolean; +}; + +export type AuthClient = Prettify< + { + readonly baseURL: string; + readonly basePath: string; + /** + * Reactive session store holding `{ data, isPending, error }`. Read it with + * `.get()` / `.listen()` or a framework `useStore`. + */ + readonly $session: ReadableAtom>; + } & CoreContribution & + CombinedClientContributions +>; + +/** + * The user object returned from `/me` and any session-bearing response. + * + * `TFields` lets consumers extend the shape with custom user fields. The + * fields listed here are always present. + * + * @example + * type AppUser = User<{ firstName: string; orgId: string }>; + */ +export type User = { + id: string; + email: string; + emailVerifiedAt: string | null; +} & TFields; + +export type Session = { + user: User; +}; + +export type EnvelopeMode = "off" | "wrap-success" | "always"; + +export type EnvelopeFields = { + data: string; + message: string; +}; + +export type EnvelopeConfig = { + mode: EnvelopeMode; + fields?: EnvelopeFields; +}; + +export type ParseSession = (raw: unknown) => Session; + +export type HTTPMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "HEAD" | "OPTIONS"; + +export type RedirectFn = (url: string) => boolean; diff --git a/clients/typescript/packages/client/src/vue/index.ts b/clients/typescript/packages/client/src/vue/index.ts new file mode 100644 index 0000000..0cd78a6 --- /dev/null +++ b/clients/typescript/packages/client/src/vue/index.ts @@ -0,0 +1,37 @@ +import { createAuthClient as createCoreClient } from "../client"; +import type { AnyClientPlugin } from "../define-plugin"; +import type { SessionState } from "../session-store"; +import type { Prettify } from "../type-utils"; +import type { AuthClient, CreateAuthClientOptions } from "../types"; +import type { ReactiveValue } from "./vue-store"; +import { useStore } from "./vue-store"; + +/** + * An {@link AuthClient} augmented with Vue composables. + */ +export type VueAuthClient = Prettify< + AuthClient & { + /** + * Reactively read the session store as a readonly ref. Updates whenever + * `{ data, isPending, error }` changes. + */ + useSession: () => ReactiveValue>; + } +>; + +/** + * Create a Limen auth client with Vue composables attached. + */ +export function createAuthClient( + opts: CreateAuthClientOptions, +): VueAuthClient { + const client = createCoreClient(opts); + const useSession = (): ReactiveValue> => useStore(client.$session); + + return Object.assign(client, { useSession }) as VueAuthClient; +} + +export type { SessionState, SessionStore } from "../session-store"; +export type { AuthClient, CreateAuthClientOptions, Session, User } from "../types"; +export { useStore } from "./vue-store"; +export type { ReactiveValue } from "./vue-store"; diff --git a/clients/typescript/packages/client/src/vue/vue-store.ts b/clients/typescript/packages/client/src/vue/vue-store.ts new file mode 100644 index 0000000..6f377dd --- /dev/null +++ b/clients/typescript/packages/client/src/vue/vue-store.ts @@ -0,0 +1,32 @@ +import type { Store, StoreValue } from "nanostores"; +import type { DeepReadonly, ShallowRef, UnwrapNestedRefs } from "vue"; +import { getCurrentScope, onScopeDispose, readonly, shallowRef } from "vue"; + +/** What {@link useStore} returns: a readonly reactive ref of the store value. */ +export type ReactiveValue = DeepReadonly>>; + +/** + * Subscribe a Vue component to a nanostores store. + * + * Modeled on `@nanostores/vue`'s `useStore`: a `shallowRef` mirrors the store + * value, `subscribe` keeps it in sync (firing once immediately), and the + * listener is torn down on scope dispose. On the server (no `window`) it reads + * once without subscribing, so a store seeded with `initialSession` renders + * without leaving a subscription behind. + */ +export function useStore(store: SomeStore): ReactiveValue> { + type Value = StoreValue; + + const state = shallowRef(store.get()); + + if (typeof window !== "undefined") { + const unsubscribe = store.subscribe((value) => { + state.value = value; + }); + if (getCurrentScope()) { + onScopeDispose(unsubscribe); + } + } + + return readonly(state); +} diff --git a/clients/typescript/packages/client/test/broadcast-channel.test.ts b/clients/typescript/packages/client/test/broadcast-channel.test.ts new file mode 100644 index 0000000..6313fe0 --- /dev/null +++ b/clients/typescript/packages/client/test/broadcast-channel.test.ts @@ -0,0 +1,154 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { createBroadcastChannel } from "../src/broadcast-channel"; + +class FakeBroadcastChannel { + static peers = new Map>(); + static reset(): void { + FakeBroadcastChannel.peers.clear(); + } + + onmessage: ((event: MessageEvent) => void) | null = null; + private closed = false; + + constructor(public readonly name: string) { + const set = FakeBroadcastChannel.peers.get(name) ?? new Set(); + set.add(this); + FakeBroadcastChannel.peers.set(name, set); + } + + postMessage(data: unknown): void { + for (const peer of FakeBroadcastChannel.peers.get(this.name) ?? []) { + if (peer === this || peer.closed) { + continue; + } + peer.onmessage?.({ data: structuredClone(data) } as MessageEvent); + } + } + + close(): void { + this.closed = true; + FakeBroadcastChannel.peers.get(this.name)?.delete(this); + } +} + +describe("createBroadcastChannel over BroadcastChannel", () => { + beforeEach(() => { + FakeBroadcastChannel.reset(); + vi.stubGlobal("BroadcastChannel", FakeBroadcastChannel); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it("delivers a posted message to other ports but not the sender", () => { + const a = createBroadcastChannel<{ n: number }>("limen.session"); + const b = createBroadcastChannel<{ n: number }>("limen.session"); + const aSeen = vi.fn(); + const bSeen = vi.fn(); + a.subscribe(aSeen); + b.subscribe(bSeen); + + a.post({ n: 1 }); + + expect(bSeen).toHaveBeenCalledWith({ n: 1 }); + expect(aSeen).not.toHaveBeenCalled(); + + a.close(); + b.close(); + }); + + it("stops delivering to a closed port", () => { + const a = createBroadcastChannel<{ n: number }>("limen.session"); + const b = createBroadcastChannel<{ n: number }>("limen.session"); + const bSeen = vi.fn(); + b.subscribe(bSeen); + b.close(); + + a.post({ n: 1 }); + + expect(bSeen).not.toHaveBeenCalled(); + a.close(); + }); + + it("removes a listener when its unsubscribe is called", () => { + const a = createBroadcastChannel<{ n: number }>("limen.session"); + const b = createBroadcastChannel<{ n: number }>("limen.session"); + const bSeen = vi.fn(); + const off = b.subscribe(bSeen); + off(); + + a.post({ n: 1 }); + + expect(bSeen).not.toHaveBeenCalled(); + a.close(); + b.close(); + }); +}); + +describe("createBroadcastChannel storage fallback", () => { + beforeEach(() => { + vi.stubGlobal("BroadcastChannel", undefined); + globalThis.localStorage.clear(); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + globalThis.localStorage.clear(); + }); + + it("posts via localStorage then clears it", () => { + const port = createBroadcastChannel<{ n: number }>("limen.session"); + const setItem = vi.spyOn(globalThis.localStorage, "setItem"); + + port.post({ n: 1 }); + + expect(setItem).toHaveBeenCalledWith("limen.session-sync", JSON.stringify({ n: 1 })); + expect(globalThis.localStorage.getItem("limen.session-sync")).toBeNull(); + + setItem.mockRestore(); + port.close(); + }); + + it("emits on a storage event for its key", () => { + const port = createBroadcastChannel<{ n: number }>("limen.session"); + const seen = vi.fn(); + port.subscribe(seen); + + window.dispatchEvent( + new StorageEvent("storage", { key: "limen.session-sync", newValue: JSON.stringify({ n: 1 }) }), + ); + + expect(seen).toHaveBeenCalledWith({ n: 1 }); + port.close(); + }); + + it("ignores storage events for other keys", () => { + const port = createBroadcastChannel("limen.session"); + const seen = vi.fn(); + port.subscribe(seen); + + window.dispatchEvent(new StorageEvent("storage", { key: "limen.tokens", newValue: "{}" })); + + expect(seen).not.toHaveBeenCalled(); + port.close(); + }); +}); + +describe("createBroadcastChannel in non-browser environments", () => { + it("is a no-op when window is undefined", () => { + vi.stubGlobal("window", undefined); + try { + const port = createBroadcastChannel<{ n: number }>("limen.session"); + const seen = vi.fn(); + const off = port.subscribe(seen); + + expect(() => port.post({ n: 1 })).not.toThrow(); + expect(seen).not.toHaveBeenCalled(); + expect(() => off()).not.toThrow(); + expect(() => port.close()).not.toThrow(); + } finally { + vi.unstubAllGlobals(); + } + }); +}); diff --git a/clients/typescript/packages/client/test/client-integration.test.ts b/clients/typescript/packages/client/test/client-integration.test.ts new file mode 100644 index 0000000..a0899e4 --- /dev/null +++ b/clients/typescript/packages/client/test/client-integration.test.ts @@ -0,0 +1,232 @@ +import { describe, expect, it, vi } from "vitest"; +import { createAuthClient, LimenError } from "../src"; +import { bearerPlugin } from "../src/plugins/bearer"; +import { credentialPasswordPlugin } from "../src/plugins/credential"; +import { magicLinkPlugin } from "../src/plugins/magic-link"; +import { oauthClientPlugin } from "../src/plugins/oauth"; +import { twoFactorPlugin } from "../src/plugins/two-factor"; + +type Recorded = { url: string; method: string; body: unknown; headers: Headers }; +type MockReply = { status?: number; body?: unknown; headers?: Record }; + +function mockFetch(reply: (req: Recorded) => MockReply) { + const calls: Recorded[] = []; + const impl = (async (input: RequestInfo | URL, init?: RequestInit) => { + const url = String(input); + const rawBody = init?.body; + const rec: Recorded = { + url, + method: init?.method ?? "GET", + body: typeof rawBody === "string" && rawBody.length > 0 ? JSON.parse(rawBody) : undefined, + headers: new Headers(init?.headers), + }; + calls.push(rec); + const out = reply(rec); + const payload = out.body === undefined ? "" : JSON.stringify(out.body); + const responseInit: ResponseInit = { status: out.status ?? 200 }; + if (out.headers) { + responseInit.headers = out.headers; + } + return new Response(payload, responseInit); + }) as typeof fetch; + return { impl, calls }; +} + +const userBody = { user: { id: "u1", email: "ada@example.com", email_verified_at: null, first_name: "Ada" } }; + +function setup(reply: (req: Recorded) => MockReply, redirectFn?: (url: string) => boolean) { + const { impl, calls } = mockFetch(reply); + const auth = createAuthClient({ + baseURL: "http://localhost:8080", + plugins: [ + credentialPasswordPlugin(), + magicLinkPlugin(), + oauthClientPlugin(), + twoFactorPlugin({ onTwoFactorRedirect: () => {} }), + bearerPlugin(), + ], + fetchOptions: { impl }, + crossTabSync: false, + refetchOnWindowFocus: false, + ...(redirectFn ? { redirectFn } : {}), + }); + return { auth, calls }; +} + +describe("createAuthClient — request shaping", () => { + it("signIn.credential: POST under basePath, camel→snake body, rememberMe default, persists session", async () => { + const { auth, calls } = setup(() => ({ body: userBody })); + + const session = await auth.signIn.credential({ credential: "ada@example.com", password: "pw" }); + + expect(calls[0]?.method).toBe("POST"); + expect(calls[0]?.url).toBe("http://localhost:8080/auth/signin/credential"); + expect(calls[0]?.body).toEqual({ credential: "ada@example.com", password: "pw", remember_me: true }); + expect(session.user.id).toBe("u1"); + expect((session.user as { firstName?: string }).firstName).toBe("Ada"); + expect(auth.$session.get().data?.user.id).toBe("u1"); + }); + + it("getSession(): GET /me, reconciles the store, returns the session", async () => { + const { auth, calls } = setup(() => ({ body: userBody })); + const session = await auth.getSession(); + expect(calls[0]?.method).toBe("GET"); + expect(calls[0]?.url).toBe("http://localhost:8080/auth/me"); + expect(session?.user.id).toBe("u1"); + expect(auth.$session.get().data?.user.id).toBe("u1"); + }); + + it("signout(): POST /signout then clears the session", async () => { + const { auth, calls } = setup(() => ({ body: userBody })); + await auth.signIn.credential({ credential: "ada@example.com", password: "pw" }); + expect(auth.$session.get().data).not.toBeNull(); + + await auth.signout(); + expect(calls.at(-1)?.method).toBe("POST"); + expect(calls.at(-1)?.url).toBe("http://localhost:8080/auth/signout"); + expect(auth.$session.get().data).toBeNull(); + }); + + it("verifyEmail(): POST /verify-email then refetches /me", async () => { + const { auth, calls } = setup((req) => (req.url.endsWith("/verify-email") ? { body: "ok" } : { body: userBody })); + const message = await auth.verifyEmail({ token: "tok" }); + expect(message).toBe("ok"); + expect(calls[0]?.url).toBe("http://localhost:8080/auth/verify-email"); + expect(calls[0]?.body).toEqual({ token: "tok" }); + expect(calls[1]?.method).toBe("GET"); + expect(calls[1]?.url).toBe("http://localhost:8080/auth/me"); + }); + + it("username.checkAvailability(): unwraps `{ available }` to a boolean", async () => { + const { auth, calls } = setup(() => ({ body: { available: false } })); + const available = await auth.username.checkAvailability({ username: "ada" }); + expect(available).toBe(false); + expect(calls[0]?.url).toBe("http://localhost:8080/auth/usernames/check"); + expect(calls[0]?.body).toEqual({ username: "ada" }); + }); +}); + +describe("createAuthClient — oauth path params + redirect handler", () => { + it("signIn.social(): substitutes :provider, strips provider + disableRedirect, no auto-nav when disabled", async () => { + const { auth, calls } = setup(() => ({ body: { url: "https://accounts.google.com/o/oauth2/auth?x=1" } })); + + const result = await auth.signIn.social({ + provider: "google", + redirectUri: "https://app/cb", + disableRedirect: true, + }); + + expect(calls[0]?.method).toBe("GET"); + expect(calls[0]?.url).toBe("http://localhost:8080/auth/oauth/google/authorize?redirect_uri=https%3A%2F%2Fapp%2Fcb"); + expect(calls[0]?.url).not.toContain("provider"); + expect(calls[0]?.url).not.toContain("disable"); + expect(result).toEqual({ url: "https://accounts.google.com/o/oauth2/auth?x=1", redirect: false }); + }); + + it("signIn.social(): navigates via the configured redirectFn by default", async () => { + const redirectFn = vi.fn(() => true); + const { auth } = setup(() => ({ body: { url: "https://provider/auth" } }), redirectFn); + + const result = await auth.signIn.social({ provider: "github" }); + expect(redirectFn).toHaveBeenCalledWith("https://provider/auth"); + expect(result.redirect).toBe(true); + }); + + it("social.tokens()/social.refreshTokens(): provider in path, camelized response", async () => { + const { auth, calls } = setup(() => ({ body: { access_token: "at", refresh_token: "rt" } })); + + const tokens = await auth.social.tokens({ provider: "google" }); + expect(calls[0]?.url).toBe("http://localhost:8080/auth/oauth/google/tokens"); + expect(tokens).toEqual({ accessToken: "at", refreshToken: "rt" }); + + await auth.social.refreshTokens({ provider: "google" }); + expect(calls[1]?.method).toBe("POST"); + expect(calls[1]?.url).toBe("http://localhost:8080/auth/oauth/google/tokens/refresh"); + }); + + it("social.accounts(): array response is camelized by the DEFAULT parse (no explicit `parse`)", async () => { + const { auth, calls } = setup(() => ({ + body: [{ provider: "google", provider_account_id: "u-1", scopes: ["email"], created_at: "t0", updated_at: "t1" }], + })); + const accounts = await auth.social.accounts(); + expect(calls[0]?.url).toBe("http://localhost:8080/auth/oauth/accounts"); + expect(accounts).toEqual([ + { provider: "google", providerAccountId: "u-1", scopes: ["email"], createdAt: "t0", updatedAt: "t1" }, + ]); + }); +}); + +describe("createAuthClient — two-factor as-pinned routes", () => { + it("getTotpUri(): GET /two-factor/totp/uri", async () => { + const { auth, calls } = setup(() => ({ body: { uri: "otpauth://x" } })); + const res = await auth.twoFactor.getTotpUri(); + expect(res).toEqual({ uri: "otpauth://x" }); + expect(calls[0]?.url).toBe("http://localhost:8080/auth/two-factor/totp/uri"); + }); + + it("getBackupCodes() is GET and regenerateBackupCodes() is PUT on the same path", async () => { + const { auth, calls } = setup(() => ({ body: ["a", "b"] })); + await auth.twoFactor.getBackupCodes(); + await auth.twoFactor.regenerateBackupCodes(); + expect(calls[0]?.method).toBe("GET"); + expect(calls[0]?.url).toBe("http://localhost:8080/auth/two-factor/backup-codes"); + expect(calls[1]?.method).toBe("PUT"); + expect(calls[1]?.url).toBe("http://localhost:8080/auth/two-factor/backup-codes"); + }); +}); + +describe("createAuthClient — hooks + overrides", () => { + it("bearer plugin injects the stored access token on later requests", async () => { + const { auth, calls } = setup(() => ({ body: userBody })); + auth.bearer.setTokens({ accessToken: "secret-token" }); + await auth.getSession(); + expect(calls[0]?.headers.get("Authorization")).toBe("Bearer secret-token"); + }); + + it("client `overrides` remount a plugin's base path", async () => { + const { impl, calls } = mockFetch(() => ({ body: { message: "sent" } })); + const auth = createAuthClient({ + baseURL: "http://localhost:8080", + plugins: [magicLinkPlugin()], + overrides: { magicLink: { basePath: "/passwordless" } }, + fetchOptions: { impl }, + crossTabSync: false, + refetchOnWindowFocus: false, + }); + await auth.magicLink.signin({ email: "ada@example.com" }); + expect(calls[0]?.url).toBe("http://localhost:8080/auth/passwordless/signin"); + }); +}); + +describe("createAuthClient — per-call options", () => { + it("onSuccess fires with the resolved value", async () => { + const { auth } = setup(() => ({ body: userBody })); + const onSuccess = vi.fn(); + const session = await auth.signIn.credential({ credential: "ada@example.com", password: "pw" }, { onSuccess }); + expect(onSuccess).toHaveBeenCalledTimes(1); + expect(onSuccess).toHaveBeenCalledWith(session); + expect(session.user.id).toBe("u1"); + }); + + it("onError fires with the LimenError and the call still rejects", async () => { + const { auth } = setup(() => ({ status: 500, body: { message: "boom" } })); + const onError = vi.fn(); + await expect(auth.sessions(undefined, { onError })).rejects.toBeInstanceOf(LimenError); + expect(onError).toHaveBeenCalledTimes(1); + expect(onError.mock.calls[0]?.[0]).toBeInstanceOf(LimenError); + }); +}); + +describe("createAuthClient — assembly", () => { + it("merges contributions from multiple plugins (including the shared signIn namespace)", () => { + const { auth } = setup(() => ({ body: userBody })); + expect(typeof auth.signIn.credential).toBe("function"); + expect(typeof auth.signIn.social).toBe("function"); + expect(typeof auth.password.set).toBe("function"); + expect(typeof auth.getSession).toBe("function"); + expect(typeof auth.sessions).toBe("function"); + expect(typeof auth.social.unlink).toBe("function"); + expect(typeof auth.twoFactor.sendOTP).toBe("function"); + expect(typeof auth.bearer.getTokens).toBe("function"); + }); +}); diff --git a/clients/typescript/packages/client/test/json-deep-equal.test.ts b/clients/typescript/packages/client/test/json-deep-equal.test.ts new file mode 100644 index 0000000..f9b3aa9 --- /dev/null +++ b/clients/typescript/packages/client/test/json-deep-equal.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, it } from "vitest"; +import { deepJsonEqual } from "../src/json-deep-equal"; + +describe("deepJsonEqual", () => { + it("compares arrays by value, order-sensitive", () => { + expect(deepJsonEqual([1, 2, 3], [1, 2, 3])).toBe(true); + expect(deepJsonEqual([{ a: 1 }], [{ a: 1 }])).toBe(true); + expect(deepJsonEqual([1, 2], [2, 1])).toBe(false); + expect(deepJsonEqual([1, 2], [1, 2, 3])).toBe(false); + }); + + it("compares objects independent of key order", () => { + expect(deepJsonEqual({ a: 1, b: 2 }, { b: 2, a: 1 })).toBe(true); + expect(deepJsonEqual({ a: 1 }, { a: 1, b: 2 })).toBe(false); + expect(deepJsonEqual({ a: 1 }, { a: 2 })).toBe(false); + expect(deepJsonEqual({ a: 1 }, { b: 1 })).toBe(false); + }); + + it("compares nested objects with array fields", () => { + const session = { user: { id: "u1", roles: ["admin", "member"], meta: { plan: "pro" } } }; + expect(deepJsonEqual(session, structuredClone(session))).toBe(true); + expect(deepJsonEqual(session, { user: { id: "u1", roles: ["member", "admin"], meta: { plan: "pro" } } })).toBe( + false, + ); + }); + + it("distinguishes arrays from objects and rejects non-JSON structures", () => { + expect(deepJsonEqual([], {})).toBe(false); + expect(deepJsonEqual({ 0: "a" }, ["a"])).toBe(false); + expect(deepJsonEqual(new Date("2026-06-20T00:00:00Z"), new Date("2026-06-20T00:00:00Z"))).toBe(false); + expect(deepJsonEqual(() => "x", () => "x")).toBe(false); + }); +}); diff --git a/clients/typescript/packages/client/test/path.test.ts b/clients/typescript/packages/client/test/path.test.ts new file mode 100644 index 0000000..9bc6149 --- /dev/null +++ b/clients/typescript/packages/client/test/path.test.ts @@ -0,0 +1,49 @@ +import { describe, expect, it } from "vitest"; +import { chainFromDotted, pathToChain, resolvePath } from "../src/path"; + +describe("pathToChain", () => { + it("camelCases kebab segments and drops empties", () => { + expect(pathToChain("/me")).toEqual(["me"]); + expect(pathToChain("/signin/credential")).toEqual(["signin", "credential"]); + expect(pathToChain("/revoke-sessions")).toEqual(["revokeSessions"]); + expect(pathToChain("/otp/send")).toEqual(["otp", "send"]); + }); + + it("drops `:param` segments (params come from input, not the chain)", () => { + expect(pathToChain("/:provider/authorize")).toEqual(["authorize"]); + expect(pathToChain("/:provider/tokens/refresh")).toEqual(["tokens", "refresh"]); + }); +}); + +describe("chainFromDotted", () => { + it("splits absolute `as` chains", () => { + expect(chainFromDotted("twoFactor.getTotpUri")).toEqual(["twoFactor", "getTotpUri"]); + expect(chainFromDotted("passwords.set")).toEqual(["passwords", "set"]); + }); +}); + +describe("resolvePath", () => { + it("passes through when there are no params", () => { + const input = { a: 1 }; + expect(resolvePath("/accounts", undefined, input)).toEqual({ path: "/accounts", rest: input }); + expect(resolvePath("/accounts", [], input)).toEqual({ path: "/accounts", rest: input }); + }); + + it("substitutes params and strips them from the payload", () => { + const { path, rest } = resolvePath("/:provider/authorize", ["provider"], { + provider: "google", + redirectUri: "https://app/cb", + }); + expect(path).toBe("/google/authorize"); + expect(rest).toEqual({ redirectUri: "https://app/cb" }); + }); + + it("encodes param values", () => { + const { path } = resolvePath("/:provider/tokens", ["provider"], { provider: "we/ird id" }); + expect(path).toBe("/we%2Fird%20id/tokens"); + }); + + it("throws when a declared param is missing", () => { + expect(() => resolvePath("/:provider/unlink", ["provider"], {})).toThrow(/provider/); + }); +}); diff --git a/clients/typescript/packages/client/test/session-store.test.ts b/clients/typescript/packages/client/test/session-store.test.ts new file mode 100644 index 0000000..71a1656 --- /dev/null +++ b/clients/typescript/packages/client/test/session-store.test.ts @@ -0,0 +1,134 @@ +import { describe, expect, it, vi } from "vitest"; +import { LimenError } from "../src/errors"; +import { createSessionStore } from "../src/session-store"; +import type { Session } from "../src/types"; + +function user(id: string): Session { + return { user: { id, email: `${id}@example.com`, emailVerifiedAt: null } }; +} + +function tick(): Promise { + return new Promise((resolve) => setTimeout(resolve, 0)); +} + +describe("createSessionStore lifecycle", () => { + it("hydrates from the hydrator on the first subscriber", async () => { + const hydrator = vi.fn(async () => user("user-1")); + const store = createSessionStore({ hydrator }); + + expect(hydrator).not.toHaveBeenCalled(); + const unsubscribe = store.$session.listen(() => {}); + await tick(); + + expect(hydrator).toHaveBeenCalledTimes(1); + expect(store.$session.get().data?.user.id).toBe("user-1"); + unsubscribe(); + }); + + it("does not hydrate when seeded with initialSession", async () => { + const hydrator = vi.fn(async () => user("user-1")); + const store = createSessionStore({ hydrator, initialSession: user("seed") }); + + const unsubscribe = store.$session.listen(() => {}); + await tick(); + + expect(hydrator).not.toHaveBeenCalled(); + expect(store.$session.get().data?.user.id).toBe("seed"); + unsubscribe(); + }); +}); + +describe("createSessionStore state", () => { + it("setData writes the session and clears pending/error", () => { + // Seed so reading via get() does not trigger onMount hydration. + const store = createSessionStore({ hydrator: async () => user("user-1"), initialSession: null }); + + store.setData(user("user-1")); + + expect(store.$session.get()).toEqual({ data: user("user-1"), isPending: false, error: null }); + }); + + it("refetch loads the session from the hydrator", async () => { + const store = createSessionStore({ hydrator: async () => user("user-1"), initialSession: null }); + + await store.refetch(); + + expect(store.$session.get().data?.user.id).toBe("user-1"); + }); + + it("refetch treats a 401 as signed out", async () => { + const store = createSessionStore({ + hydrator: async () => { + throw new LimenError("nope", 401); + }, + initialSession: user("user-1"), + }); + + await store.refetch(); + + expect(store.$session.get().data).toBeNull(); + expect(store.$session.get().error).toBeNull(); + }); + + it("refetch preserves the session and surfaces other failures", async () => { + const store = createSessionStore({ + hydrator: async () => { + throw new LimenError("boom", 500); + }, + initialSession: user("user-1"), + }); + + await store.refetch(); + + expect(store.$session.get().data?.user.id).toBe("user-1"); + expect(store.$session.get().error?.status).toBe(500); + }); + + it("refetch with maxAgeMs skips when a hydration ran within the window", async () => { + vi.useFakeTimers(); + try { + const hydrator = vi.fn(async () => user("user-1")); + const store = createSessionStore({ hydrator, initialSession: null }); + + await store.refetch(); + await store.refetch({ maxAgeMs: 5_000 }); + + expect(hydrator).toHaveBeenCalledTimes(1); + + vi.advanceTimersByTime(5_001); + await store.refetch({ maxAgeMs: 5_000 }); + + expect(hydrator).toHaveBeenCalledTimes(2); + } finally { + vi.useRealTimers(); + } + }); + + it("refetch without maxAgeMs always fetches", async () => { + const hydrator = vi.fn(async () => user("user-1")); + const store = createSessionStore({ hydrator, initialSession: null }); + + await store.refetch(); + await store.refetch(); + + expect(hydrator).toHaveBeenCalledTimes(2); + }); + + it("refetch with skipSignedOut does not fetch while signed out", async () => { + const hydrator = vi.fn(async () => user("user-1")); + const store = createSessionStore({ hydrator, initialSession: null }); + + await store.refetch({ skipSignedOut: true }); + + expect(hydrator).not.toHaveBeenCalled(); + }); + + it("refetch with skipSignedOut fetches when a session is present", async () => { + const hydrator = vi.fn(async () => user("user-1")); + const store = createSessionStore({ hydrator, initialSession: user("seed") }); + + await store.refetch({ skipSignedOut: true }); + + expect(hydrator).toHaveBeenCalledTimes(1); + }); +}); diff --git a/clients/typescript/packages/client/test/session-sync.test.ts b/clients/typescript/packages/client/test/session-sync.test.ts new file mode 100644 index 0000000..8d5076f --- /dev/null +++ b/clients/typescript/packages/client/test/session-sync.test.ts @@ -0,0 +1,277 @@ +import { atom } from "nanostores"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { createSessionSync } from "../src/session-sync"; +import type { SessionState } from "../src/session-store"; +import type { Session } from "../src/types"; + +const STORAGE_KEY = "limen.session-sync"; + +function user(id: string, fields: Partial = {}): Session { + return { user: { id, email: `${id}@example.com`, emailVerifiedAt: null, ...fields } }; +} + +function stateOf(session: Session | null): SessionState { + return { data: session, isPending: false, error: null }; +} + +function tick(): Promise { + return new Promise((resolve) => setTimeout(resolve, 0)); +} + +class FakeBroadcastChannel { + static peers = new Map>(); + static reset(): void { + FakeBroadcastChannel.peers.clear(); + } + + onmessage: ((event: MessageEvent) => void) | null = null; + private closed = false; + + constructor(public readonly name: string) { + const set = FakeBroadcastChannel.peers.get(name) ?? new Set(); + set.add(this); + FakeBroadcastChannel.peers.set(name, set); + } + + postMessage(data: unknown): void { + for (const peer of FakeBroadcastChannel.peers.get(this.name) ?? []) { + if (peer === this || peer.closed) { + continue; + } + peer.onmessage?.({ data: structuredClone(data) } as MessageEvent); + } + } + + close(): void { + this.closed = true; + FakeBroadcastChannel.peers.get(this.name)?.delete(this); + } +} + +function makeStore(initial: Session | null = null) { + const $session = atom(stateOf(initial)); + const setData = vi.fn((session: Session | null) => { + $session.set(stateOf(session)); + }); + const refetch = vi.fn(async () => {}); + return { $session, setData, refetch }; +} + +type SyncTab = ReturnType; + +function syncCrossTab(tab: SyncTab): () => void { + return createSessionSync(tab, { fetchOnMount: false, crossTabSync: true, refetchOnWindowFocus: false }); +} + +describe("createSessionSync hydration", () => { + it("refetches once when fetchOnMount is true", () => { + const tab = makeStore(); + createSessionSync(tab, { fetchOnMount: true, crossTabSync: false, refetchOnWindowFocus: false }); + + expect(tab.refetch).toHaveBeenCalledTimes(1); + }); + + it("does not refetch when fetchOnMount is false", () => { + const tab = makeStore(); + createSessionSync(tab, { fetchOnMount: false, crossTabSync: false, refetchOnWindowFocus: false }); + + expect(tab.refetch).not.toHaveBeenCalled(); + }); +}); + +describe("createSessionSync cross-tab over BroadcastChannel", () => { + beforeEach(() => { + FakeBroadcastChannel.reset(); + vi.stubGlobal("BroadcastChannel", FakeBroadcastChannel); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it("applies a login snapshot to other tabs without any refetch", async () => { + const tabA = makeStore(); + const tabB = makeStore(); + syncCrossTab(tabA); + syncCrossTab(tabB); + + tabA.$session.set(stateOf(user("user-1"))); + await tick(); + + expect(tabB.setData).toHaveBeenCalledTimes(1); + expect(tabB.$session.get().data?.user.id).toBe("user-1"); + expect(tabB.refetch).not.toHaveBeenCalled(); + expect(tabA.setData).not.toHaveBeenCalled(); + }); + + it("applies a non-identity field change (any session change, not just login/logout)", async () => { + const tabA = makeStore(user("user-1")); + const tabB = makeStore(user("user-1")); + syncCrossTab(tabA); + syncCrossTab(tabB); + + tabA.$session.set(stateOf(user("user-1", { emailVerifiedAt: "2026-06-18T00:00:00Z" }))); + await tick(); + + expect(tabB.setData).toHaveBeenCalledTimes(1); + expect(tabB.$session.get().data?.user.emailVerifiedAt).toBe("2026-06-18T00:00:00Z"); + }); + + it("applies a logout snapshot with no refetch", async () => { + const tabA = makeStore(user("user-1")); + const tabB = makeStore(user("user-1")); + syncCrossTab(tabA); + syncCrossTab(tabB); + + tabA.$session.set(stateOf(null)); + await tick(); + + expect(tabB.setData).toHaveBeenCalledTimes(1); + expect(tabB.$session.get().data).toBeNull(); + expect(tabB.refetch).not.toHaveBeenCalled(); + }); + + it("does not re-broadcast a remotely applied change", async () => { + const tabA = makeStore(); + const tabB = makeStore(); + const tabC = makeStore(); + syncCrossTab(tabA); + syncCrossTab(tabB); + syncCrossTab(tabC); + + tabA.$session.set(stateOf(user("user-1"))); + await tick(); + + expect(tabB.setData).toHaveBeenCalledTimes(1); + expect(tabC.setData).toHaveBeenCalledTimes(1); + expect(tabA.setData).not.toHaveBeenCalled(); + }); + + it("does not broadcast when only isPending or error change", async () => { + const tabA = makeStore(user("user-1")); + const tabB = makeStore(user("user-1")); + syncCrossTab(tabA); + syncCrossTab(tabB); + + tabA.$session.set({ data: user("user-1"), isPending: true, error: null }); + await tick(); + + expect(tabB.setData).not.toHaveBeenCalled(); + }); + + it("stops syncing after its dispose is called", async () => { + const tabA = makeStore(); + const tabB = makeStore(); + syncCrossTab(tabA); + const disposeB = syncCrossTab(tabB); + + disposeB(); + tabA.$session.set(stateOf(user("user-1"))); + await tick(); + + expect(tabB.setData).not.toHaveBeenCalled(); + }); +}); + +describe("createSessionSync cross-tab storage fallback", () => { + beforeEach(() => { + vi.stubGlobal("BroadcastChannel", undefined); + globalThis.localStorage.clear(); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + globalThis.localStorage.clear(); + }); + + it("applies a snapshot from a storage event", async () => { + const tab = makeStore(); + syncCrossTab(tab); + + window.dispatchEvent( + new StorageEvent("storage", { key: STORAGE_KEY, newValue: JSON.stringify({ data: user("user-1") }) }), + ); + await tick(); + + expect(tab.setData).toHaveBeenCalledTimes(1); + expect(tab.$session.get().data?.user.id).toBe("user-1"); + }); + + it("writes then clears storage so the snapshot does not stay at rest", async () => { + const tab = makeStore(); + const setItem = vi.spyOn(globalThis.localStorage, "setItem"); + syncCrossTab(tab); + + tab.$session.set(stateOf(user("user-1"))); + await tick(); + + expect(setItem).toHaveBeenCalledWith(STORAGE_KEY, JSON.stringify({ data: user("user-1") })); + expect(globalThis.localStorage.getItem(STORAGE_KEY)).toBeNull(); + + setItem.mockRestore(); + }); +}); + +describe("createSessionSync refetchOnWindowFocus", () => { + function setVisibility(value: "visible" | "hidden"): void { + Object.defineProperty(document, "visibilityState", { configurable: true, value }); + } + + afterEach(() => { + setVisibility("visible"); + }); + + it("refetches with a throttle window when the tab becomes visible", async () => { + const tab = makeStore(user("user-1")); + createSessionSync(tab, { fetchOnMount: false, crossTabSync: false, refetchOnWindowFocus: true }); + + setVisibility("visible"); + document.dispatchEvent(new Event("visibilitychange")); + await tick(); + + expect(tab.refetch).toHaveBeenCalledTimes(1); + expect(tab.refetch).toHaveBeenCalledWith({ maxAgeMs: expect.any(Number), skipSignedOut: true }); + }); + + it("does not refetch while hidden", async () => { + const tab = makeStore(user("user-1")); + createSessionSync(tab, { fetchOnMount: false, crossTabSync: false, refetchOnWindowFocus: true }); + + setVisibility("hidden"); + document.dispatchEvent(new Event("visibilitychange")); + await tick(); + + expect(tab.refetch).not.toHaveBeenCalled(); + }); + + it("removes the focus listener on dispose", async () => { + const tab = makeStore(user("user-1")); + const dispose = createSessionSync(tab, { fetchOnMount: false, crossTabSync: false, refetchOnWindowFocus: true }); + dispose(); + + setVisibility("visible"); + document.dispatchEvent(new Event("visibilitychange")); + await tick(); + + expect(tab.refetch).not.toHaveBeenCalled(); + }); +}); + +describe("createSessionSync in non-browser environments", () => { + it("does not open a channel when window is undefined", () => { + vi.stubGlobal("window", undefined); + vi.stubGlobal("BroadcastChannel", FakeBroadcastChannel); + FakeBroadcastChannel.reset(); + try { + const tab = makeStore(user("user-1")); + const dispose = createSessionSync(tab, { fetchOnMount: false, crossTabSync: true, refetchOnWindowFocus: true }); + + tab.$session.set(stateOf(user("user-2"))); + + expect(FakeBroadcastChannel.peers.size).toBe(0); + expect(() => dispose()).not.toThrow(); + } finally { + vi.unstubAllGlobals(); + } + }); +}); diff --git a/clients/typescript/packages/client/tsconfig.json b/clients/typescript/packages/client/tsconfig.json new file mode 100644 index 0000000..f336ca0 --- /dev/null +++ b/clients/typescript/packages/client/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "jsx": "react-jsx", + "types": [ + "node" + ] + }, + "include": [ + "src/**/*", + "test/**/*" + ], + "exclude": [ + "dist", + "node_modules" + ] +} \ No newline at end of file diff --git a/clients/typescript/packages/client/tsdown.config.ts b/clients/typescript/packages/client/tsdown.config.ts new file mode 100644 index 0000000..ec00018 --- /dev/null +++ b/clients/typescript/packages/client/tsdown.config.ts @@ -0,0 +1,19 @@ +import { defineConfig } from "tsdown"; + +export default defineConfig({ + entry: [ + "src/index.ts", + "src/react/index.ts", + "src/vue/index.ts", + "src/svelte/index.ts", + "src/solid/index.ts", + "src/plugins/index.ts", + "src/plugins/*/index.ts", + ], + format: ["esm"], + dts: true, + clean: true, + treeshake: true, + sourcemap: true, + target: "es2022", +}); diff --git a/clients/typescript/packages/client/vitest.config.ts b/clients/typescript/packages/client/vitest.config.ts new file mode 100644 index 0000000..b78686a --- /dev/null +++ b/clients/typescript/packages/client/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "happy-dom", + include: ["test/**/*.test.{ts,tsx}"], + }, +}); diff --git a/clients/typescript/pnpm-lock.yaml b/clients/typescript/pnpm-lock.yaml new file mode 100644 index 0000000..5295585 --- /dev/null +++ b/clients/typescript/pnpm-lock.yaml @@ -0,0 +1,3201 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + '@changesets/cli': + specifier: ^2.31.0 + version: 2.31.0(@types/node@25.9.1) + '@eslint/js': + specifier: ^10.0.1 + version: 10.0.1(eslint@10.4.0) + '@types/node': + specifier: ^25.9.1 + version: 25.9.1 + eslint: + specifier: ^10.4.0 + version: 10.4.0 + globals: + specifier: ^17.6.0 + version: 17.6.0 + prettier: + specifier: ^3.8.3 + version: 3.8.3 + tsdown: + specifier: ^0.22.2 + version: 0.22.2(typescript@6.0.3) + typescript: + specifier: ^6.0.3 + version: 6.0.3 + typescript-eslint: + specifier: ^8.59.4 + version: 8.59.4(eslint@10.4.0)(typescript@6.0.3) + + packages/client: + dependencies: + nanostores: + specifier: ^1.3.0 + version: 1.3.0 + devDependencies: + '@testing-library/react': + specifier: ^16.1.0 + version: 16.3.2(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@types/react': + specifier: ^19.0.0 + version: 19.2.17 + happy-dom: + specifier: ^20.10.5 + version: 20.10.5 + react: + specifier: ^19.0.0 + version: 19.2.7 + react-dom: + specifier: ^19.0.0 + version: 19.2.7(react@19.2.7) + solid-js: + specifier: ^1.9.0 + version: 1.9.13 + svelte: + specifier: ^5.0.0 + version: 5.56.3(@typescript-eslint/types@8.59.4) + vitest: + specifier: ^4.1.9 + version: 4.1.9(@types/node@25.9.1)(happy-dom@20.10.5)(vite@8.0.16(@types/node@25.9.1)) + vue: + specifier: ^3.5.0 + version: 3.5.38(typescript@6.0.3) + +packages: + + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} + engines: {node: '>=6.9.0'} + + '@babel/generator@8.0.0-rc.6': + resolution: {integrity: sha512-6mIzgVK8DgEzvIapoQwhXTMnnkuE4STQmVv9H03i/tZ2ml8oev3TRvZJgTenK2Bsq0YWNtzOrFdTyNzCMFtjJQ==} + engines: {node: ^22.18.0 || >=24.11.0} + + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@8.0.0-rc.6': + resolution: {integrity: sha512-BCkFy+zN6kXQed3YOT7aJl93NfDSzQc3pBfsvTVPs9gU9X3V0aefEF5kwBT0E+mDWH9QgKaZstYUQN9VdQZT4g==} + engines: {node: ^22.18.0 || >=24.11.0} + + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@8.0.0-rc.6': + resolution: {integrity: sha512-nVJ+1JcCgntv8d78rRo++o2wuODT0Irknx2BF8Np4Ft2CRgjLqIs4qzSZ8b66yGbBdMWGmZBO9WEZv1hhNiSpg==} + engines: {node: ^22.18.0 || >=24.11.0} + + '@babel/parser@7.29.7': + resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/parser@8.0.0-rc.6': + resolution: {integrity: sha512-rOS8IpdO7mQELkTPlCsTgPejO0bFuZdEDCGQJouYbYf9e1FLTym7Fei2pEjq8q7MWbX0ravcd7QQYKs1TxOuog==} + engines: {node: ^22.18.0 || >=24.11.0} + hasBin: true + + '@babel/runtime@7.29.7': + resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.7': + resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} + engines: {node: '>=6.9.0'} + + '@babel/types@8.0.0-rc.6': + resolution: {integrity: sha512-p7/ABylAYlexb31wtRdIfH9L9A0Z2T/9H6zAqzqndkY2PLkvNNc580wGhp/gGKN4Sp9sQvSkhc6Oga8/O+wTyw==} + engines: {node: ^22.18.0 || >=24.11.0} + + '@changesets/apply-release-plan@7.1.1': + resolution: {integrity: sha512-9qPCm/rLx/xoOFXIHGB229+4GOL76S4MC+7tyOuTsR6+1jYlfFDQORdvwR5hDA6y4FL2BPt3qpbcQIS+dW85LA==} + + '@changesets/assemble-release-plan@6.0.10': + resolution: {integrity: sha512-rSDcqdJ9KbVyjpBIuCidhvZNIiVt1XaIYp73ycVQRIA5n/j6wQaEk0ChRLMUQ1vkxZe51PTQ9OIhbg6HQMW45A==} + + '@changesets/changelog-git@0.2.1': + resolution: {integrity: sha512-x/xEleCFLH28c3bQeQIyeZf8lFXyDFVn1SgcBiR2Tw/r4IAWlk1fzxCEZ6NxQAjF2Nwtczoen3OA2qR+UawQ8Q==} + + '@changesets/cli@2.31.0': + resolution: {integrity: sha512-AhI4enNTgHu2IZr6K4WZyf0EPch4XVMn1yOMFmCD9gsfBGqMYaHXls5HyDv6/CL5axVQABz68eG30eCtbr2wFg==} + hasBin: true + + '@changesets/config@3.1.4': + resolution: {integrity: sha512-pf0bvD/v6WI2cRlZ6hzpjtZdSlXDXMAJ+Iz7xfFzV4ZxJ8OGGAON+1qYc99ZPrijnt4xp3VGG7eNvAOGS24V1Q==} + + '@changesets/errors@0.2.0': + resolution: {integrity: sha512-6BLOQUscTpZeGljvyQXlWOItQyU71kCdGz7Pi8H8zdw6BI0g3m43iL4xKUVPWtG+qrrL9DTjpdn8eYuCQSRpow==} + + '@changesets/get-dependents-graph@2.1.4': + resolution: {integrity: sha512-ZsS00x6WvmHq3sQv8oCMwL0f/z3wbXCVuSVTJwCnnmbC/iBdNJGFx1EcbMG4PC6sXRyH69liM4A2WKXzn/kRPg==} + + '@changesets/get-release-plan@4.0.16': + resolution: {integrity: sha512-2K5Om6CrMPm45rtvckfzWo7e9jOVCKLCnXia5eUPaURH7/LWzri7pK1TycdzAuAtehLkW7VPbWLCSExTHmiI6g==} + + '@changesets/get-version-range-type@0.4.0': + resolution: {integrity: sha512-hwawtob9DryoGTpixy1D3ZXbGgJu1Rhr+ySH2PvTLHvkZuQ7sRT4oQwMh0hbqZH1weAooedEjRsbrWcGLCeyVQ==} + + '@changesets/git@3.0.4': + resolution: {integrity: sha512-BXANzRFkX+XcC1q/d27NKvlJ1yf7PSAgi8JG6dt8EfbHFHi4neau7mufcSca5zRhwOL8j9s6EqsxmT+s+/E6Sw==} + + '@changesets/logger@0.1.1': + resolution: {integrity: sha512-OQtR36ZlnuTxKqoW4Sv6x5YIhOmClRd5pWsjZsddYxpWs517R0HkyiefQPIytCVh4ZcC5x9XaG8KTdd5iRQUfg==} + + '@changesets/parse@0.4.3': + resolution: {integrity: sha512-ZDmNc53+dXdWEv7fqIUSgRQOLYoUom5Z40gmLgmATmYR9NbL6FJJHwakcCpzaeCy+1D0m0n7mT4jj2B/MQPl7A==} + + '@changesets/pre@2.0.2': + resolution: {integrity: sha512-HaL/gEyFVvkf9KFg6484wR9s0qjAXlZ8qWPDkTyKF6+zqjBe/I2mygg3MbpZ++hdi0ToqNUF8cjj7fBy0dg8Ug==} + + '@changesets/read@0.6.7': + resolution: {integrity: sha512-D1G4AUYGrBEk8vj8MGwf75k9GpN6XL3wg8i42P2jZZwFLXnlr2Pn7r9yuQNbaMCarP7ZQWNJbV6XLeysAIMhTA==} + + '@changesets/should-skip-package@0.1.2': + resolution: {integrity: sha512-qAK/WrqWLNCP22UDdBTMPH5f41elVDlsNyat180A33dWxuUDyNpg6fPi/FyTZwRriVjg0L8gnjJn2F9XAoF0qw==} + + '@changesets/types@4.1.0': + resolution: {integrity: sha512-LDQvVDv5Kb50ny2s25Fhm3d9QSZimsoUGBsUioj6MC3qbMUCuC8GPIvk/M6IvXx3lYhAs0lwWUQLb+VIEUCECw==} + + '@changesets/types@6.1.0': + resolution: {integrity: sha512-rKQcJ+o1nKNgeoYRHKOS07tAMNd3YSN0uHaJOZYjBAgxfV7TUE7JE+z4BzZdQwb5hKaYbayKN5KrYV7ODb2rAA==} + + '@changesets/write@0.4.0': + resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==} + + '@emnapi/core@1.10.0': + resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} + + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.23.5': + resolution: {integrity: sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/config-helpers@0.6.0': + resolution: {integrity: sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/core@1.2.1': + resolution: {integrity: sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/js@10.0.1': + resolution: {integrity: sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + peerDependencies: + eslint: ^10.0.0 + peerDependenciesMeta: + eslint: + optional: true + + '@eslint/object-schema@3.0.5': + resolution: {integrity: sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/plugin-kit@0.7.1': + resolution: {integrity: sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@humanfs/core@0.19.2': + resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.8': + resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} + engines: {node: '>=18.18.0'} + + '@humanfs/types@0.15.0': + resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@inquirer/external-editor@1.0.3': + resolution: {integrity: sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@manypkg/find-root@1.1.0': + resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} + + '@manypkg/get-packages@1.1.3': + resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==} + + '@napi-rs/wasm-runtime@1.1.4': + resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@oxc-project/types@0.133.0': + resolution: {integrity: sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==} + + '@oxc-project/types@0.134.0': + resolution: {integrity: sha512-T0xuRRKrQFmocH8y+jGfpmSkGcheaJExY9lEihmR1Gm2aH+75B8CzgU2rABRQSzzDxLjZ15Sc0bRVLj5lVeNXQ==} + + '@quansync/fs@1.0.0': + resolution: {integrity: sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==} + + '@rolldown/binding-android-arm64@1.0.3': + resolution: {integrity: sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-android-arm64@1.1.0': + resolution: {integrity: sha512-gCYzGOSkYY6Z034suzd20euvds7lPzMEEla62DJGE/ZAlR4OMBnNbvnBSsIGUCAr52gaWMsloGxP4tVGtN5aCA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.0.3': + resolution: {integrity: sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-arm64@1.1.0': + resolution: {integrity: sha512-JQBD77MNgu+4Z6RAyg69acugdrhhVoWesr3l47zohYZ2YV2fwkWMArkN/2p4l6Ei+Sno7W5q+UsKdVWq5Ens0w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.0.3': + resolution: {integrity: sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.1.0': + resolution: {integrity: sha512-p/8cXUTK4Sob604e+xxPhVSbDFf29E6J0l/xESM9rdCfn3aDai3nEs6TnMHUsdD5aNlFz0+gDbiGlozLKGa2YA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.0.3': + resolution: {integrity: sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-freebsd-x64@1.1.0': + resolution: {integrity: sha512-KbtOSlVv6fElujiZWMcC3aQYhEwLVVf073RcwlSmpGQvIsKZFUqc0ef4sjUuurRwfbiI6JJXji9DQn+86hawmQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.0.3': + resolution: {integrity: sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm-gnueabihf@1.1.0': + resolution: {integrity: sha512-9fZ9i0o0/MQaw7om6Z6TsT7tfCk0jtbEFtC+aPqZL5RNsGWNcHvn6EHgL3dAprjq+AZzPTAQjg2JtpJaMt+6pg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.0.3': + resolution: {integrity: sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.1.0': + resolution: {integrity: sha512-+tog7T66i+yFyIuuAnjL6xmW182W/qTBOUt6BtQ6lBIM1Eikh/fSMz4HGgvuCp5uU0zuIVWng7kDYthjCMOHcg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-arm64-musl@1.0.3': + resolution: {integrity: sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-arm64-musl@1.1.0': + resolution: {integrity: sha512-4b7yruLIIj/oZ3GpcLOvxcLCLDMraohn3IhQfN2hBP4w9UekG0DTIajWguJosRGfySf/+h/NwRUiMKoCpxCrqQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-ppc64-gnu@1.0.3': + resolution: {integrity: sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + + '@rolldown/binding-linux-ppc64-gnu@1.1.0': + resolution: {integrity: sha512-QRDOVZd0bhQ5jLsUsCC3dUxDWdTSVY9WMznowZgCGOrZfLLgctWpelhUASEiBwsXfat/JwYnVd1EaxMhqyT+UQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + + '@rolldown/binding-linux-s390x-gnu@1.0.3': + resolution: {integrity: sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + + '@rolldown/binding-linux-s390x-gnu@1.1.0': + resolution: {integrity: sha512-ypxT+Hq76NFG7woFbNbySnGEajFuYuIXeKz/jfCU+lXUoxfi3zLE6OG/ZQNeK3RpZSYJlAe2bokpsQ046CaieQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + + '@rolldown/binding-linux-x64-gnu@1.0.3': + resolution: {integrity: sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-linux-x64-gnu@1.1.0': + resolution: {integrity: sha512-IdovCmfROFmpTLahdecTDFL74aLERVYN68F/mLZjfVh6LfoplPfI6deyHNMTcVujbokDV5k05XrFO22zfv+qjg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-linux-x64-musl@1.0.3': + resolution: {integrity: sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-linux-x64-musl@1.1.0': + resolution: {integrity: sha512-pcA8xlFp2tyk9T2R6Fi/rPe3bQ1MA+sSMDNUU5Ogu80GHOatkE4P8YCreGAvZErm5Ho2YRXnyvNrWiRncfVysQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-openharmony-arm64@1.0.3': + resolution: {integrity: sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-openharmony-arm64@1.1.0': + resolution: {integrity: sha512-4+fexHayrLCWpriPh4c6dNvL4an34DEZCG7zOM/FD5QNF6h8DT+bDXzyB/kfC8lDJbaFb7jKShtnjDQFXVQEjg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.0.3': + resolution: {integrity: sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@rolldown/binding-wasm32-wasi@1.1.0': + resolution: {integrity: sha512-SbL++MNmOw6QamrwIGDMSSfM4ceTzFr+RjbOExJSLLBinScU4WI5OdA413h1qwPw2yH7lVF1+H4svQ+6mSXKTQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.3': + resolution: {integrity: sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-arm64-msvc@1.1.0': + resolution: {integrity: sha512-+xTE6XC7wBgk0VKRXGG+QAnyW5S9b8vfsFpiMjf0waQTmSQSU8onsH/beyZ8X4aXVveJnotiy7VDjLOaW8bTrg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.0.3': + resolution: {integrity: sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.1.0': + resolution: {integrity: sha512-Ogji1TQNqH3ACLnYr+1Ns1nyrJ0CO2P585u9Hsh02pXvtFiFpgtgT2b3P4PnCOU86VVCvqtAeCN4OftMT8KU4w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@sveltejs/acorn-typescript@1.0.10': + resolution: {integrity: sha512-4WfKk68eTih+MiJD4fSbxN7E8kVBmTMPWHUPYjvl2N0rMs53YLTT8/YjKU5Dtnz5LqDjl7LEw4U7lXR2W3J5WA==} + peerDependencies: + acorn: ^8.9.0 + + '@testing-library/dom@10.4.1': + resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} + engines: {node: '>=18'} + + '@testing-library/react@16.3.2': + resolution: {integrity: sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==} + engines: {node: '>=18'} + peerDependencies: + '@testing-library/dom': ^10.0.0 + '@types/react': ^18.0.0 || ^19.0.0 + '@types/react-dom': ^18.0.0 || ^19.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@tybys/wasm-util@0.10.2': + resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==} + + '@types/aria-query@5.0.4': + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/esrecurse@4.3.1': + resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/jsesc@2.5.1': + resolution: {integrity: sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/node@12.20.55': + resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} + + '@types/node@25.9.1': + resolution: {integrity: sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==} + + '@types/react@19.2.17': + resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==} + + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + + '@types/whatwg-mimetype@3.0.2': + resolution: {integrity: sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==} + + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + + '@typescript-eslint/eslint-plugin@8.59.4': + resolution: {integrity: sha512-PegsU+XfyJJNjd4+u/k6f9yTyp0lEXXiPopUNobZcIAUJFGICFLN+sP0Rb3JehVmiij1Ph0dFGYqODoRo/2+6A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.59.4 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/parser@8.59.4': + resolution: {integrity: sha512-zORHqO/tuhxY1zWuTvMUqddRxpiFJ72xVfcNoWpqdLjs6lfPbuQBJuW4pk+49/uBMy7Ssr4bzgjiKmmDB1UbZQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/project-service@8.59.4': + resolution: {integrity: sha512-Ly00Vu4oAacfDeHp2Zg85ioNG6l8HG+tN1D7J+xTHSxu9y0awYKJ2zH1rFBn8ZSfuGK+7FxK3Cgl3uAz0aZZLg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/scope-manager@8.59.4': + resolution: {integrity: sha512-mUeR/3H1WrTAddJrwut8OoPjfauaztMQmRwV5fQTUyNVJCLiUXXe4lGEyYIL2oFDpP7UtgbGJXCt72wT0z2S3Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.59.4': + resolution: {integrity: sha512-DLCpnKgD4alVxTBSKulK+gU1KCqOgUXfDRDXh2mZgzokQKa/70ax93I2uVO3m/LLvIAtWZIFoiifudmIqAxpMA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/type-utils@8.59.4': + resolution: {integrity: sha512-uonTuPAAKr9XaBGqJ3LjYTh72zy5DyGesljO9gtmk/eFW0W1fRHjnwVYKB35Lm8d5Q5CluEW3gPHjTvZTmgrfA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/types@8.59.4': + resolution: {integrity: sha512-F1o7WJcCq+bc8dwcO/YsSEOudAH8RDtaOhM6wcAQhcUsFhnWQl81JKy48q1hoxAU0qrzM89+31GYh1515Zde3Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.59.4': + resolution: {integrity: sha512-F+RuOmcDXo4+TPdfd/TCLS3m2nw8gE9XXyZLrA3JBfaA5tz9TtdkyD3YJFmPxulyc2cKbEok/CvFE3MgSLWnag==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/utils@8.59.4': + resolution: {integrity: sha512-cYXeNAUsG4lJo5dbc1FcKm+JwIWrj1/UpTORsC6tGMjEZ81DYcvIr9/ueikhMa/Y/gDQYGp+YX9/xQrXje5BJw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/visitor-keys@8.59.4': + resolution: {integrity: sha512-U3gxVaDVnuZKhSspW/MzMxE1kq7zOdc072FcSNoqA1I9p8HyKbBFfEHoWckBAMgNMph4MamwS5iTVzFmrnt8TQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@vitest/expect@4.1.9': + resolution: {integrity: sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==} + + '@vitest/mocker@4.1.9': + resolution: {integrity: sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.1.9': + resolution: {integrity: sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==} + + '@vitest/runner@4.1.9': + resolution: {integrity: sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==} + + '@vitest/snapshot@4.1.9': + resolution: {integrity: sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==} + + '@vitest/spy@4.1.9': + resolution: {integrity: sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==} + + '@vitest/utils@4.1.9': + resolution: {integrity: sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==} + + '@vue/compiler-core@3.5.38': + resolution: {integrity: sha512-s99aGxWYig9ErHbct27KXEGhrBYlRI6c4MwAgXErOAbX9xiW37/uMa+XUDO69zLz83dng8UUZ70CTOJrLrYrEQ==} + + '@vue/compiler-dom@3.5.38': + resolution: {integrity: sha512-JTqp25l8aFfJYF7/KmsXZjAxJz7T+SjmTJLoXVjHtc2BrSgSiW2n9Aem/cWq1OPe68A8JL06B3eVdhlP0H4TVw==} + + '@vue/compiler-sfc@3.5.38': + resolution: {integrity: sha512-DuA2GiZawSEW442iw/9+Fkol8hTgb4Ke5KkhmSry65QA7YuyMbIdy8p0XZRMvNwJdgRz307W8g1CSzdvS4nuNg==} + + '@vue/compiler-ssr@3.5.38': + resolution: {integrity: sha512-7s+W5Gc42FGxZMcuwl8H5B29T8BJPMdBT7KHFE+BbAuZ/iTEdTtv7z2XiMjiaUUw4w3ZcCEdHs36RuYJ2VA7bA==} + + '@vue/reactivity@3.5.38': + resolution: {integrity: sha512-pG6LV/NDNRbKizcUjFFLAfjaL8mcv4DmR9avNcUw2gDHBzZneuS2TWCmp633ynzxz9YYKNeEPK2I8Wraqy2HUQ==} + + '@vue/runtime-core@3.5.38': + resolution: {integrity: sha512-iyW8WVfF1CpCXxncZY5Ei6rSd6oZr5DgEom//fUjRBRl56AXPD+s9ATvukRt77ZFTuYlnVA1bxY+dJB94tWVYw==} + + '@vue/runtime-dom@3.5.38': + resolution: {integrity: sha512-apX2wt9sdfDshS+a2xueFZLVpt0GkRJZSoPmrW/SA4yzXTznhfcMVW59gr7h4YQeY0vJhdJkk2rsIDwgfFgC5A==} + + '@vue/server-renderer@3.5.38': + resolution: {integrity: sha512-vue8vbf2QlV4quHqzwmJy6dWfmRhP1J8l4wtZg60CL6VoKqcPY2oe7may3+1d9qfpedjK5PRLFqd5k3Isj9mUw==} + peerDependencies: + vue: 3.5.38 + + '@vue/shared@3.5.38': + resolution: {integrity: sha512-FTW0AFZNaK5/mOqvGBwVfUlNLU38TiQn4+DQgIFUnrBBJQ1crMJ82yeGQLV5jyKFsO8yRukpbuP7x+nRbH6aug==} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.15.0: + resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} + + ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + + ansis@4.3.1: + resolution: {integrity: sha512-BJ8/l4R5LRE7hW9WdSuGYrLSHi2ynxeFpDFbH0K/CgNeY/tyhk+vO6TYxXC5r5CpUhNVX310xzPsN/H9lCdfOA==} + engines: {node: '>=14'} + + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + + aria-query@5.3.1: + resolution: {integrity: sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==} + engines: {node: '>= 0.4'} + + array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + ast-kit@3.0.0-beta.1: + resolution: {integrity: sha512-trmleAnZ2PxN/loHWVhhx1qeOHSRXq4TDsBBxq3GqeJitfk3+jTQ+v/C1km/KYq9M7wKqCewMh+/NAvVH7m+bw==} + engines: {node: '>=20.19.0'} + + axobject-query@4.1.0: + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} + engines: {node: '>= 0.4'} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + better-path-resolve@1.0.0: + resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} + engines: {node: '>=4'} + + birpc@4.0.0: + resolution: {integrity: sha512-LShSxJP0KTmd101b6DRyGBj57LZxSDYWKitQNW/mi8GRMvZb078Uf9+pveax1DrVL89vm7mWe+TovdI/UDOuPw==} + + brace-expansion@5.0.6: + resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} + engines: {node: 18 || 20 || >=22} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + buffer-image-size@0.6.4: + resolution: {integrity: sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ==} + engines: {node: '>=4.0'} + + cac@7.0.0: + resolution: {integrity: sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ==} + engines: {node: '>=20.19.0'} + + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + + chardet@2.2.0: + resolution: {integrity: sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + defu@6.1.7: + resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + detect-indent@6.1.0: + resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} + engines: {node: '>=8'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + devalue@5.8.1: + resolution: {integrity: sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw==} + + dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + + dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + + dts-resolver@3.0.0: + resolution: {integrity: sha512-1T1f+z+4tl9XD+m+0HBgWoL/nm0bOIffyWaUuUSBlFg/86IWvfx+wjNaO/ybU0AJzG9/Mi5hBUgGV6zCmWEN7Q==} + engines: {node: ^22.18.0 || >=24.0.0} + peerDependencies: + oxc-resolver: '>=11.0.0' + peerDependenciesMeta: + oxc-resolver: + optional: true + + empathic@2.0.1: + resolution: {integrity: sha512-YGRs8knHhKHVShLkFET/rWAU8kmHbOV5LwN938RHI0pljAJ1Gf6SzXsSmRaEzcXTtOOmVqJ5+WtQPL5uigY50Q==} + engines: {node: '>=14'} + + enquirer@2.4.1: + resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} + engines: {node: '>=8.6'} + + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} + engines: {node: '>=0.12'} + + es-module-lexer@2.1.0: + resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-scope@9.1.2: + resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@10.4.0: + resolution: {integrity: sha512-loXy6bWOoP3EP6JA7jo6p5jMpBJmHmsNZM5SFRHLdh1MGOPurMnNBj4ZlAbaqUAaQWbCr7jHV4P7gzAyryZWkQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + esm-env@1.2.2: + resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==} + + espree@11.2.0: + resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrap@2.2.11: + resolution: {integrity: sha512-gPdx+I+BjYEinNMQaBXFjbaJVyoPMU4ZODg5mE+M4DqVG9VusAVHHjcBX+zqyITlI0DIARwDMMzZwAWj36dRoQ==} + peerDependencies: + '@typescript-eslint/types': ^8.2.0 + peerDependenciesMeta: + '@typescript-eslint/types': + optional: true + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + + extendable-error@0.1.7: + resolution: {integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.4.2: + resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + + fs-extra@7.0.1: + resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} + engines: {node: '>=6 <7 || >=8'} + + fs-extra@8.1.0: + resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} + engines: {node: '>=6 <7 || >=8'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + get-tsconfig@5.0.0-beta.5: + resolution: {integrity: sha512-/6gFNr0N04nob252sTQxyFLi3eKFRqIg1I87YcqAMT1i6SQrSF6KujUEQrtrjMV0H/eejTCltLdDSTEMzHbnsQ==} + engines: {node: '>=20.20.0'} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + globals@17.6.0: + resolution: {integrity: sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==} + engines: {node: '>=18'} + + globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + happy-dom@20.10.5: + resolution: {integrity: sha512-0aA6BQoMnpcRE/c1E8ZyF2jXnET7MJskereWOXher4CJuYjrI5esN0Az/1NPMD4KeWUbampBGw2MGqabMPFIbg==} + engines: {node: '>=20.0.0'} + + hookable@6.1.1: + resolution: {integrity: sha512-U9LYDy1CwhMCnprUfeAZWZGByVbhd54hwepegYTK7Pi5NvqEj63ifz5z+xukznehT7i6NIZRu89Ay1AZmRsLEQ==} + + human-id@4.2.0: + resolution: {integrity: sha512-K3GbkIWqyvvlpfhBPlbEvD97TtqBpAYA4kt+cn2lD2x2HuohzZCibcA2nOlnJT6exqvJLggoB5nv2dNf192nEA==} + hasBin: true + + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + engines: {node: '>=0.10.0'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + import-without-cache@0.4.0: + resolution: {integrity: sha512-NkJQA7oZ4YHQhd2+H3BoRFKF3d/XNsiKpHZCQEMH9pDX27hQQLsTyOocyRgaIVtf8gHX3Nt3LPkR4e5EdtPAGQ==} + engines: {node: ^22.18.0 || >=24.0.0} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-reference@3.0.3: + resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} + + is-subdir@1.2.0: + resolution: {integrity: sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw==} + engines: {node: '>=4'} + + is-windows@1.0.2: + resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} + engines: {node: '>=0.10.0'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@3.14.2: + resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} + hasBin: true + + js-yaml@4.2.0: + resolution: {integrity: sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==} + hasBin: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + jsonfile@4.0.0: + resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + + locate-character@3.0.0: + resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} + + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.startcase@4.4.0: + resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} + + lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + + mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.12: + resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + nanostores@1.3.0: + resolution: {integrity: sha512-XPUa/jz+P1oJvN9VBxw4L9MtdFfaH3DAryqPssqhb2kXjmb9npz0dly6rCsgFWOPr4Yg9mTfM3MDZgZZ+7A3lA==} + engines: {node: ^20.0.0 || >=22.0.0} + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + obug@2.1.1: + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + outdent@0.5.0: + resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==} + + p-filter@2.1.0: + resolution: {integrity: sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==} + engines: {node: '>=8'} + + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + p-map@2.1.0: + resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==} + engines: {node: '>=6'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + package-manager-detector@0.2.11: + resolution: {integrity: sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + pify@4.0.1: + resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} + engines: {node: '>=6'} + + postcss@8.5.15: + resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier@2.8.8: + resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} + engines: {node: '>=10.13.0'} + hasBin: true + + prettier@3.8.3: + resolution: {integrity: sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==} + engines: {node: '>=14'} + hasBin: true + + pretty-format@27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + quansync@0.2.11: + resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} + + quansync@1.0.0: + resolution: {integrity: sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA==} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + react-dom@19.2.7: + resolution: {integrity: sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==} + peerDependencies: + react: ^19.2.7 + + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + + react@19.2.7: + resolution: {integrity: sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==} + engines: {node: '>=0.10.0'} + + read-yaml-file@1.1.0: + resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==} + engines: {node: '>=6'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rolldown-plugin-dts@0.25.2: + resolution: {integrity: sha512-nMhN/R+vmR8GM45ZW1FWMSjRTSDDn/6w4GTf8RNrEFCBdl8B1kySWrU1ixPtbwzXoRlcO+R/S88VgXuJQwfdDg==} + engines: {node: ^22.18.0 || >=24.0.0} + peerDependencies: + '@ts-macro/tsc': ^0.3.6 + '@typescript/native-preview': '>=7.0.0-dev.20260325.1' + rolldown: ^1.0.0 + typescript: ^5.0.0 || ^6.0.0 + vue-tsc: ~3.2.0 + peerDependenciesMeta: + '@ts-macro/tsc': + optional: true + '@typescript/native-preview': + optional: true + typescript: + optional: true + vue-tsc: + optional: true + + rolldown@1.0.3: + resolution: {integrity: sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + rolldown@1.1.0: + resolution: {integrity: sha512-zpMvlJhs5PkXRTtKc0CaLBVI9AR/VDiJFpM+kx//hgToEca7FgMlGjaRIisXBcb19T76LswgmKECSQ96hjWr5A==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + semver@7.8.1: + resolution: {integrity: sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==} + engines: {node: '>=10'} + hasBin: true + + seroval-plugins@1.5.4: + resolution: {integrity: sha512-S0xQPhUTefAhNvNWFg0c1J8qJArHt5KdtJ/cFAofo06KD1MVSeFWyl4iiu+ApDIuw0WhjpOfCdgConOfAnLgkw==} + engines: {node: '>=10'} + peerDependencies: + seroval: ^1.0 + + seroval@1.5.4: + resolution: {integrity: sha512-46uFvgrXTVxZcUorgSSRZ4y+ieqLLQRMlG4bnCZKW3qI6BZm7Rg4ntMW4p1mILEEBZWrFlcpp0AyIIlM6jD9iw==} + engines: {node: '>=10'} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + solid-js@1.9.13: + resolution: {integrity: sha512-6hJeJMOcEX8ktqjpDoJZEmld3ijvcvWBDtiXBm7f4332SiFN66QeAQI1REQshvyUoISsSeJ4PHDauKYbwao9JQ==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + spawndamnit@3.0.1: + resolution: {integrity: sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg==} + + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@4.1.0: + resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + svelte@5.56.3: + resolution: {integrity: sha512-w7JvrM5IFl5cmfbY0TLik9o7mjRUJmRMhOR51tBPu708Gr/MjbGs7VnJnr/B0CaXeI4vtnOh7RKxDr0cwhMdDA==} + engines: {node: '>=18'} + + term-size@2.2.1: + resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} + engines: {node: '>=8'} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@1.2.4: + resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==} + engines: {node: '>=18'} + + tinyglobby@0.2.16: + resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} + engines: {node: '>=12.0.0'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} + engines: {node: '>=14.0.0'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + tsdown@0.22.2: + resolution: {integrity: sha512-VX9gsyKXsTnBZjnIM4jsHl9aRv+GfgkE/k1hQslilaBfZMlaw3JuGR+6yhiU0QxWBtOCDnTjwOSoXzgB7Rr50g==} + engines: {node: ^22.18.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@arethetypeswrong/core': ^0.18.1 + '@tsdown/css': 0.22.2 + '@tsdown/exe': 0.22.2 + '@vitejs/devtools': '*' + publint: ^0.3.8 + tsx: '*' + typescript: ^5.0.0 || ^6.0.0 + unplugin-unused: ^0.5.0 + unrun: '*' + peerDependenciesMeta: + '@arethetypeswrong/core': + optional: true + '@tsdown/css': + optional: true + '@tsdown/exe': + optional: true + '@vitejs/devtools': + optional: true + publint: + optional: true + tsx: + optional: true + typescript: + optional: true + unplugin-unused: + optional: true + unrun: + optional: true + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + typescript-eslint@8.59.4: + resolution: {integrity: sha512-Rw6+44QNFaXtgHSjPy+Kw8hrJniMYzR85E9yLmOLcfZ91/rz+JXQbDTCmc6ccxMPY6K6PgAq26f0JCBfR7LIPQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} + engines: {node: '>=14.17'} + hasBin: true + + unconfig-core@7.5.0: + resolution: {integrity: sha512-Su3FauozOGP44ZmKdHy2oE6LPjk51M/TRRjHv2HNCWiDvfvCoxC2lno6jevMA91MYAdCdwP05QnWdWpSbncX/w==} + + undici-types@7.24.6: + resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} + + universalify@0.1.2: + resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} + engines: {node: '>= 4.0.0'} + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + vite@8.0.16: + resolution: {integrity: sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.1.18 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@4.1.9: + resolution: {integrity: sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.9 + '@vitest/browser-preview': 4.1.9 + '@vitest/browser-webdriverio': 4.1.9 + '@vitest/coverage-istanbul': 4.1.9 + '@vitest/coverage-v8': 4.1.9 + '@vitest/ui': 4.1.9 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + vue@3.5.38: + resolution: {integrity: sha512-vAMKHfImQlYSy0C+PBue4s3ERZ2xGKfgZg5GXAsLInq1dyh2H78ILVP5sK0KPFPVW4kv+OGCIvBEondcjpZp7A==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + whatwg-mimetype@3.0.0: + resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} + engines: {node: '>=12'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + ws@8.21.0: + resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + zimmerframe@1.1.4: + resolution: {integrity: sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==} + +snapshots: + + '@babel/code-frame@7.29.7': + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/generator@8.0.0-rc.6': + dependencies: + '@babel/parser': 8.0.0-rc.6 + '@babel/types': 8.0.0-rc.6 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + '@types/jsesc': 2.5.1 + jsesc: 3.1.0 + + '@babel/helper-string-parser@7.29.7': {} + + '@babel/helper-string-parser@8.0.0-rc.6': {} + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/helper-validator-identifier@8.0.0-rc.6': {} + + '@babel/parser@7.29.7': + dependencies: + '@babel/types': 7.29.7 + + '@babel/parser@8.0.0-rc.6': + dependencies: + '@babel/types': 8.0.0-rc.6 + + '@babel/runtime@7.29.7': {} + + '@babel/types@7.29.7': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + + '@babel/types@8.0.0-rc.6': + dependencies: + '@babel/helper-string-parser': 8.0.0-rc.6 + '@babel/helper-validator-identifier': 8.0.0-rc.6 + + '@changesets/apply-release-plan@7.1.1': + dependencies: + '@changesets/config': 3.1.4 + '@changesets/get-version-range-type': 0.4.0 + '@changesets/git': 3.0.4 + '@changesets/should-skip-package': 0.1.2 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + detect-indent: 6.1.0 + fs-extra: 7.0.1 + lodash.startcase: 4.4.0 + outdent: 0.5.0 + prettier: 2.8.8 + resolve-from: 5.0.0 + semver: 7.8.1 + + '@changesets/assemble-release-plan@6.0.10': + dependencies: + '@changesets/errors': 0.2.0 + '@changesets/get-dependents-graph': 2.1.4 + '@changesets/should-skip-package': 0.1.2 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + semver: 7.8.1 + + '@changesets/changelog-git@0.2.1': + dependencies: + '@changesets/types': 6.1.0 + + '@changesets/cli@2.31.0(@types/node@25.9.1)': + dependencies: + '@changesets/apply-release-plan': 7.1.1 + '@changesets/assemble-release-plan': 6.0.10 + '@changesets/changelog-git': 0.2.1 + '@changesets/config': 3.1.4 + '@changesets/errors': 0.2.0 + '@changesets/get-dependents-graph': 2.1.4 + '@changesets/get-release-plan': 4.0.16 + '@changesets/git': 3.0.4 + '@changesets/logger': 0.1.1 + '@changesets/pre': 2.0.2 + '@changesets/read': 0.6.7 + '@changesets/should-skip-package': 0.1.2 + '@changesets/types': 6.1.0 + '@changesets/write': 0.4.0 + '@inquirer/external-editor': 1.0.3(@types/node@25.9.1) + '@manypkg/get-packages': 1.1.3 + ansi-colors: 4.1.3 + enquirer: 2.4.1 + fs-extra: 7.0.1 + mri: 1.2.0 + package-manager-detector: 0.2.11 + picocolors: 1.1.1 + resolve-from: 5.0.0 + semver: 7.8.1 + spawndamnit: 3.0.1 + term-size: 2.2.1 + transitivePeerDependencies: + - '@types/node' + + '@changesets/config@3.1.4': + dependencies: + '@changesets/errors': 0.2.0 + '@changesets/get-dependents-graph': 2.1.4 + '@changesets/logger': 0.1.1 + '@changesets/should-skip-package': 0.1.2 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + fs-extra: 7.0.1 + micromatch: 4.0.8 + + '@changesets/errors@0.2.0': + dependencies: + extendable-error: 0.1.7 + + '@changesets/get-dependents-graph@2.1.4': + dependencies: + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + picocolors: 1.1.1 + semver: 7.8.1 + + '@changesets/get-release-plan@4.0.16': + dependencies: + '@changesets/assemble-release-plan': 6.0.10 + '@changesets/config': 3.1.4 + '@changesets/pre': 2.0.2 + '@changesets/read': 0.6.7 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + + '@changesets/get-version-range-type@0.4.0': {} + + '@changesets/git@3.0.4': + dependencies: + '@changesets/errors': 0.2.0 + '@manypkg/get-packages': 1.1.3 + is-subdir: 1.2.0 + micromatch: 4.0.8 + spawndamnit: 3.0.1 + + '@changesets/logger@0.1.1': + dependencies: + picocolors: 1.1.1 + + '@changesets/parse@0.4.3': + dependencies: + '@changesets/types': 6.1.0 + js-yaml: 4.2.0 + + '@changesets/pre@2.0.2': + dependencies: + '@changesets/errors': 0.2.0 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + fs-extra: 7.0.1 + + '@changesets/read@0.6.7': + dependencies: + '@changesets/git': 3.0.4 + '@changesets/logger': 0.1.1 + '@changesets/parse': 0.4.3 + '@changesets/types': 6.1.0 + fs-extra: 7.0.1 + p-filter: 2.1.0 + picocolors: 1.1.1 + + '@changesets/should-skip-package@0.1.2': + dependencies: + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + + '@changesets/types@4.1.0': {} + + '@changesets/types@6.1.0': {} + + '@changesets/write@0.4.0': + dependencies: + '@changesets/types': 6.1.0 + fs-extra: 7.0.1 + human-id: 4.2.0 + prettier: 2.8.8 + + '@emnapi/core@1.10.0': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.10.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@eslint-community/eslint-utils@4.9.1(eslint@10.4.0)': + dependencies: + eslint: 10.4.0 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.23.5': + dependencies: + '@eslint/object-schema': 3.0.5 + debug: 4.4.3 + minimatch: 10.2.5 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.6.0': + dependencies: + '@eslint/core': 1.2.1 + + '@eslint/core@1.2.1': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/js@10.0.1(eslint@10.4.0)': + optionalDependencies: + eslint: 10.4.0 + + '@eslint/object-schema@3.0.5': {} + + '@eslint/plugin-kit@0.7.1': + dependencies: + '@eslint/core': 1.2.1 + levn: 0.4.1 + + '@humanfs/core@0.19.2': + dependencies: + '@humanfs/types': 0.15.0 + + '@humanfs/node@0.16.8': + dependencies: + '@humanfs/core': 0.19.2 + '@humanfs/types': 0.15.0 + '@humanwhocodes/retry': 0.4.3 + + '@humanfs/types@0.15.0': {} + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@inquirer/external-editor@1.0.3(@types/node@25.9.1)': + dependencies: + chardet: 2.2.0 + iconv-lite: 0.7.2 + optionalDependencies: + '@types/node': 25.9.1 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@manypkg/find-root@1.1.0': + dependencies: + '@babel/runtime': 7.29.7 + '@types/node': 12.20.55 + find-up: 4.1.0 + fs-extra: 8.1.0 + + '@manypkg/get-packages@1.1.3': + dependencies: + '@babel/runtime': 7.29.7 + '@changesets/types': 4.1.0 + '@manypkg/find-root': 1.1.0 + fs-extra: 8.1.0 + globby: 11.1.0 + read-yaml-file: 1.1.0 + + '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.10.2 + optional: true + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + + '@oxc-project/types@0.133.0': {} + + '@oxc-project/types@0.134.0': {} + + '@quansync/fs@1.0.0': + dependencies: + quansync: 1.0.0 + + '@rolldown/binding-android-arm64@1.0.3': + optional: true + + '@rolldown/binding-android-arm64@1.1.0': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.3': + optional: true + + '@rolldown/binding-darwin-arm64@1.1.0': + optional: true + + '@rolldown/binding-darwin-x64@1.0.3': + optional: true + + '@rolldown/binding-darwin-x64@1.1.0': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.3': + optional: true + + '@rolldown/binding-freebsd-x64@1.1.0': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.3': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.1.0': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.3': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.1.0': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.3': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.1.0': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.0.3': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.1.0': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.0.3': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.1.0': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.3': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.1.0': + optional: true + + '@rolldown/binding-linux-x64-musl@1.0.3': + optional: true + + '@rolldown/binding-linux-x64-musl@1.1.0': + optional: true + + '@rolldown/binding-openharmony-arm64@1.0.3': + optional: true + + '@rolldown/binding-openharmony-arm64@1.1.0': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.3': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + optional: true + + '@rolldown/binding-wasm32-wasi@1.1.0': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.3': + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.1.0': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.0.3': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.1.0': + optional: true + + '@rolldown/pluginutils@1.0.1': {} + + '@standard-schema/spec@1.1.0': {} + + '@sveltejs/acorn-typescript@1.0.10(acorn@8.16.0)': + dependencies: + acorn: 8.16.0 + + '@testing-library/dom@10.4.1': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/runtime': 7.29.7 + '@types/aria-query': 5.0.4 + aria-query: 5.3.0 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + picocolors: 1.1.1 + pretty-format: 27.5.1 + + '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@babel/runtime': 7.29.7 + '@testing-library/dom': 10.4.1 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + + '@tybys/wasm-util@0.10.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/aria-query@5.0.4': {} + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/esrecurse@4.3.1': {} + + '@types/estree@1.0.9': {} + + '@types/jsesc@2.5.1': {} + + '@types/json-schema@7.0.15': {} + + '@types/node@12.20.55': {} + + '@types/node@25.9.1': + dependencies: + undici-types: 7.24.6 + + '@types/react@19.2.17': + dependencies: + csstype: 3.2.3 + + '@types/trusted-types@2.0.7': {} + + '@types/whatwg-mimetype@3.0.2': {} + + '@types/ws@8.18.1': + dependencies: + '@types/node': 25.9.1 + + '@typescript-eslint/eslint-plugin@8.59.4(@typescript-eslint/parser@8.59.4(eslint@10.4.0)(typescript@6.0.3))(eslint@10.4.0)(typescript@6.0.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.59.4(eslint@10.4.0)(typescript@6.0.3) + '@typescript-eslint/scope-manager': 8.59.4 + '@typescript-eslint/type-utils': 8.59.4(eslint@10.4.0)(typescript@6.0.3) + '@typescript-eslint/utils': 8.59.4(eslint@10.4.0)(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.59.4 + eslint: 10.4.0 + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.59.4(eslint@10.4.0)(typescript@6.0.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.59.4 + '@typescript-eslint/types': 8.59.4 + '@typescript-eslint/typescript-estree': 8.59.4(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.59.4 + debug: 4.4.3 + eslint: 10.4.0 + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.59.4(typescript@6.0.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.59.4(typescript@6.0.3) + '@typescript-eslint/types': 8.59.4 + debug: 4.4.3 + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.59.4': + dependencies: + '@typescript-eslint/types': 8.59.4 + '@typescript-eslint/visitor-keys': 8.59.4 + + '@typescript-eslint/tsconfig-utils@8.59.4(typescript@6.0.3)': + dependencies: + typescript: 6.0.3 + + '@typescript-eslint/type-utils@8.59.4(eslint@10.4.0)(typescript@6.0.3)': + dependencies: + '@typescript-eslint/types': 8.59.4 + '@typescript-eslint/typescript-estree': 8.59.4(typescript@6.0.3) + '@typescript-eslint/utils': 8.59.4(eslint@10.4.0)(typescript@6.0.3) + debug: 4.4.3 + eslint: 10.4.0 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.59.4': {} + + '@typescript-eslint/typescript-estree@8.59.4(typescript@6.0.3)': + dependencies: + '@typescript-eslint/project-service': 8.59.4(typescript@6.0.3) + '@typescript-eslint/tsconfig-utils': 8.59.4(typescript@6.0.3) + '@typescript-eslint/types': 8.59.4 + '@typescript-eslint/visitor-keys': 8.59.4 + debug: 4.4.3 + minimatch: 10.2.5 + semver: 7.8.1 + tinyglobby: 0.2.16 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.59.4(eslint@10.4.0)(typescript@6.0.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.0) + '@typescript-eslint/scope-manager': 8.59.4 + '@typescript-eslint/types': 8.59.4 + '@typescript-eslint/typescript-estree': 8.59.4(typescript@6.0.3) + eslint: 10.4.0 + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.59.4': + dependencies: + '@typescript-eslint/types': 8.59.4 + eslint-visitor-keys: 5.0.1 + + '@vitest/expect@4.1.9': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.9 + '@vitest/utils': 4.1.9 + chai: 6.2.2 + tinyrainbow: 3.1.0 + + '@vitest/mocker@4.1.9(vite@8.0.16(@types/node@25.9.1))': + dependencies: + '@vitest/spy': 4.1.9 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.0.16(@types/node@25.9.1) + + '@vitest/pretty-format@4.1.9': + dependencies: + tinyrainbow: 3.1.0 + + '@vitest/runner@4.1.9': + dependencies: + '@vitest/utils': 4.1.9 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.9': + dependencies: + '@vitest/pretty-format': 4.1.9 + '@vitest/utils': 4.1.9 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.1.9': {} + + '@vitest/utils@4.1.9': + dependencies: + '@vitest/pretty-format': 4.1.9 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 + + '@vue/compiler-core@3.5.38': + dependencies: + '@babel/parser': 7.29.7 + '@vue/shared': 3.5.38 + entities: 7.0.1 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.38': + dependencies: + '@vue/compiler-core': 3.5.38 + '@vue/shared': 3.5.38 + + '@vue/compiler-sfc@3.5.38': + dependencies: + '@babel/parser': 7.29.7 + '@vue/compiler-core': 3.5.38 + '@vue/compiler-dom': 3.5.38 + '@vue/compiler-ssr': 3.5.38 + '@vue/shared': 3.5.38 + estree-walker: 2.0.2 + magic-string: 0.30.21 + postcss: 8.5.15 + source-map-js: 1.2.1 + + '@vue/compiler-ssr@3.5.38': + dependencies: + '@vue/compiler-dom': 3.5.38 + '@vue/shared': 3.5.38 + + '@vue/reactivity@3.5.38': + dependencies: + '@vue/shared': 3.5.38 + + '@vue/runtime-core@3.5.38': + dependencies: + '@vue/reactivity': 3.5.38 + '@vue/shared': 3.5.38 + + '@vue/runtime-dom@3.5.38': + dependencies: + '@vue/reactivity': 3.5.38 + '@vue/runtime-core': 3.5.38 + '@vue/shared': 3.5.38 + csstype: 3.2.3 + + '@vue/server-renderer@3.5.38(vue@3.5.38(typescript@6.0.3))': + dependencies: + '@vue/compiler-ssr': 3.5.38 + '@vue/shared': 3.5.38 + vue: 3.5.38(typescript@6.0.3) + + '@vue/shared@3.5.38': {} + + acorn-jsx@5.3.2(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + + acorn@8.16.0: {} + + ajv@6.15.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-colors@4.1.3: {} + + ansi-regex@5.0.1: {} + + ansi-styles@5.2.0: {} + + ansis@4.3.1: {} + + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + + argparse@2.0.1: {} + + aria-query@5.3.0: + dependencies: + dequal: 2.0.3 + + aria-query@5.3.1: {} + + array-union@2.1.0: {} + + assertion-error@2.0.1: {} + + ast-kit@3.0.0-beta.1: + dependencies: + '@babel/parser': 8.0.0-rc.6 + estree-walker: 3.0.3 + pathe: 2.0.3 + + axobject-query@4.1.0: {} + + balanced-match@4.0.4: {} + + better-path-resolve@1.0.0: + dependencies: + is-windows: 1.0.2 + + birpc@4.0.0: {} + + brace-expansion@5.0.6: + dependencies: + balanced-match: 4.0.4 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + buffer-image-size@0.6.4: + dependencies: + '@types/node': 25.9.1 + + cac@7.0.0: {} + + chai@6.2.2: {} + + chardet@2.2.0: {} + + clsx@2.1.1: {} + + convert-source-map@2.0.0: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + csstype@3.2.3: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-is@0.1.4: {} + + defu@6.1.7: {} + + dequal@2.0.3: {} + + detect-indent@6.1.0: {} + + detect-libc@2.1.2: {} + + devalue@5.8.1: {} + + dir-glob@3.0.1: + dependencies: + path-type: 4.0.0 + + dom-accessibility-api@0.5.16: {} + + dts-resolver@3.0.0: {} + + empathic@2.0.1: {} + + enquirer@2.4.1: + dependencies: + ansi-colors: 4.1.3 + strip-ansi: 6.0.1 + + entities@7.0.1: {} + + es-module-lexer@2.1.0: {} + + escape-string-regexp@4.0.0: {} + + eslint-scope@9.1.2: + dependencies: + '@types/esrecurse': 4.3.1 + '@types/estree': 1.0.9 + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@5.0.1: {} + + eslint@10.4.0: + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.0) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.23.5 + '@eslint/config-helpers': 0.6.0 + '@eslint/core': 1.2.1 + '@eslint/plugin-kit': 0.7.1 + '@humanfs/node': 0.16.8 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.9 + ajv: 6.15.0 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 9.1.2 + eslint-visitor-keys: 5.0.1 + espree: 11.2.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + minimatch: 10.2.5 + natural-compare: 1.4.0 + optionator: 0.9.4 + transitivePeerDependencies: + - supports-color + + esm-env@1.2.2: {} + + espree@11.2.0: + dependencies: + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) + eslint-visitor-keys: 5.0.1 + + esprima@4.0.1: {} + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrap@2.2.11(@typescript-eslint/types@8.59.4): + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + optionalDependencies: + '@typescript-eslint/types': 8.59.4 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + estree-walker@2.0.2: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + esutils@2.0.3: {} + + expect-type@1.3.0: {} + + extendable-error@0.1.7: {} + + fast-deep-equal@3.1.3: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.4.2 + keyv: 4.5.4 + + flatted@3.4.2: {} + + fs-extra@7.0.1: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + + fs-extra@8.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + + fsevents@2.3.3: + optional: true + + get-tsconfig@5.0.0-beta.5: + dependencies: + resolve-pkg-maps: 1.0.0 + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + globals@17.6.0: {} + + globby@11.1.0: + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.3 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 3.0.0 + + graceful-fs@4.2.11: {} + + happy-dom@20.10.5: + dependencies: + '@types/node': 25.9.1 + '@types/whatwg-mimetype': 3.0.2 + '@types/ws': 8.18.1 + buffer-image-size: 0.6.4 + entities: 7.0.1 + whatwg-mimetype: 3.0.0 + ws: 8.21.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + hookable@6.1.1: {} + + human-id@4.2.0: {} + + iconv-lite@0.7.2: + dependencies: + safer-buffer: 2.1.2 + + ignore@5.3.2: {} + + ignore@7.0.5: {} + + import-without-cache@0.4.0: {} + + imurmurhash@0.1.4: {} + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + is-reference@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + is-subdir@1.2.0: + dependencies: + better-path-resolve: 1.0.0 + + is-windows@1.0.2: {} + + isexe@2.0.0: {} + + js-tokens@4.0.0: {} + + js-yaml@3.14.2: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + + js-yaml@4.2.0: + dependencies: + argparse: 2.0.1 + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + jsonfile@4.0.0: + optionalDependencies: + graceful-fs: 4.2.11 + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + + locate-character@3.0.0: {} + + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.startcase@4.4.0: {} + + lz-string@1.5.0: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.2 + + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.6 + + mri@1.2.0: {} + + ms@2.1.3: {} + + nanoid@3.3.12: {} + + nanostores@1.3.0: {} + + natural-compare@1.4.0: {} + + obug@2.1.1: {} + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + outdent@0.5.0: {} + + p-filter@2.1.0: + dependencies: + p-map: 2.1.0 + + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + p-map@2.1.0: {} + + p-try@2.2.0: {} + + package-manager-detector@0.2.11: + dependencies: + quansync: 0.2.11 + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + path-type@4.0.0: {} + + pathe@2.0.3: {} + + picocolors@1.1.1: {} + + picomatch@2.3.2: {} + + picomatch@4.0.4: {} + + pify@4.0.1: {} + + postcss@8.5.15: + dependencies: + nanoid: 3.3.12 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + prettier@2.8.8: {} + + prettier@3.8.3: {} + + pretty-format@27.5.1: + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + + punycode@2.3.1: {} + + quansync@0.2.11: {} + + quansync@1.0.0: {} + + queue-microtask@1.2.3: {} + + react-dom@19.2.7(react@19.2.7): + dependencies: + react: 19.2.7 + scheduler: 0.27.0 + + react-is@17.0.2: {} + + react@19.2.7: {} + + read-yaml-file@1.1.0: + dependencies: + graceful-fs: 4.2.11 + js-yaml: 3.14.2 + pify: 4.0.1 + strip-bom: 3.0.0 + + resolve-from@5.0.0: {} + + resolve-pkg-maps@1.0.0: {} + + reusify@1.1.0: {} + + rolldown-plugin-dts@0.25.2(rolldown@1.1.0)(typescript@6.0.3): + dependencies: + '@babel/generator': 8.0.0-rc.6 + '@babel/helper-validator-identifier': 8.0.0-rc.6 + '@babel/parser': 8.0.0-rc.6 + ast-kit: 3.0.0-beta.1 + birpc: 4.0.0 + dts-resolver: 3.0.0 + get-tsconfig: 5.0.0-beta.5 + obug: 2.1.1 + rolldown: 1.1.0 + optionalDependencies: + typescript: 6.0.3 + transitivePeerDependencies: + - oxc-resolver + + rolldown@1.0.3: + dependencies: + '@oxc-project/types': 0.133.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.3 + '@rolldown/binding-darwin-arm64': 1.0.3 + '@rolldown/binding-darwin-x64': 1.0.3 + '@rolldown/binding-freebsd-x64': 1.0.3 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.3 + '@rolldown/binding-linux-arm64-gnu': 1.0.3 + '@rolldown/binding-linux-arm64-musl': 1.0.3 + '@rolldown/binding-linux-ppc64-gnu': 1.0.3 + '@rolldown/binding-linux-s390x-gnu': 1.0.3 + '@rolldown/binding-linux-x64-gnu': 1.0.3 + '@rolldown/binding-linux-x64-musl': 1.0.3 + '@rolldown/binding-openharmony-arm64': 1.0.3 + '@rolldown/binding-wasm32-wasi': 1.0.3 + '@rolldown/binding-win32-arm64-msvc': 1.0.3 + '@rolldown/binding-win32-x64-msvc': 1.0.3 + + rolldown@1.1.0: + dependencies: + '@oxc-project/types': 0.134.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.1.0 + '@rolldown/binding-darwin-arm64': 1.1.0 + '@rolldown/binding-darwin-x64': 1.1.0 + '@rolldown/binding-freebsd-x64': 1.1.0 + '@rolldown/binding-linux-arm-gnueabihf': 1.1.0 + '@rolldown/binding-linux-arm64-gnu': 1.1.0 + '@rolldown/binding-linux-arm64-musl': 1.1.0 + '@rolldown/binding-linux-ppc64-gnu': 1.1.0 + '@rolldown/binding-linux-s390x-gnu': 1.1.0 + '@rolldown/binding-linux-x64-gnu': 1.1.0 + '@rolldown/binding-linux-x64-musl': 1.1.0 + '@rolldown/binding-openharmony-arm64': 1.1.0 + '@rolldown/binding-wasm32-wasi': 1.1.0 + '@rolldown/binding-win32-arm64-msvc': 1.1.0 + '@rolldown/binding-win32-x64-msvc': 1.1.0 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + safer-buffer@2.1.2: {} + + scheduler@0.27.0: {} + + semver@7.8.1: {} + + seroval-plugins@1.5.4(seroval@1.5.4): + dependencies: + seroval: 1.5.4 + + seroval@1.5.4: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + siginfo@2.0.0: {} + + signal-exit@4.1.0: {} + + slash@3.0.0: {} + + solid-js@1.9.13: + dependencies: + csstype: 3.2.3 + seroval: 1.5.4 + seroval-plugins: 1.5.4(seroval@1.5.4) + + source-map-js@1.2.1: {} + + spawndamnit@3.0.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + sprintf-js@1.0.3: {} + + stackback@0.0.2: {} + + std-env@4.1.0: {} + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-bom@3.0.0: {} + + svelte@5.56.3(@typescript-eslint/types@8.59.4): + dependencies: + '@jridgewell/remapping': 2.3.5 + '@jridgewell/sourcemap-codec': 1.5.5 + '@sveltejs/acorn-typescript': 1.0.10(acorn@8.16.0) + '@types/estree': 1.0.9 + '@types/trusted-types': 2.0.7 + acorn: 8.16.0 + aria-query: 5.3.1 + axobject-query: 4.1.0 + clsx: 2.1.1 + devalue: 5.8.1 + esm-env: 1.2.2 + esrap: 2.2.11(@typescript-eslint/types@8.59.4) + is-reference: 3.0.3 + locate-character: 3.0.0 + magic-string: 0.30.21 + zimmerframe: 1.1.4 + transitivePeerDependencies: + - '@typescript-eslint/types' + + term-size@2.2.1: {} + + tinybench@2.9.0: {} + + tinyexec@1.2.4: {} + + tinyglobby@0.2.16: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + tinyrainbow@3.1.0: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + tree-kill@1.2.2: {} + + ts-api-utils@2.5.0(typescript@6.0.3): + dependencies: + typescript: 6.0.3 + + tsdown@0.22.2(typescript@6.0.3): + dependencies: + ansis: 4.3.1 + cac: 7.0.0 + defu: 6.1.7 + empathic: 2.0.1 + hookable: 6.1.1 + import-without-cache: 0.4.0 + obug: 2.1.1 + picomatch: 4.0.4 + rolldown: 1.1.0 + rolldown-plugin-dts: 0.25.2(rolldown@1.1.0)(typescript@6.0.3) + semver: 7.8.1 + tinyexec: 1.2.4 + tinyglobby: 0.2.17 + tree-kill: 1.2.2 + unconfig-core: 7.5.0 + optionalDependencies: + typescript: 6.0.3 + transitivePeerDependencies: + - '@ts-macro/tsc' + - '@typescript/native-preview' + - oxc-resolver + - vue-tsc + + tslib@2.8.1: + optional: true + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + typescript-eslint@8.59.4(eslint@10.4.0)(typescript@6.0.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.59.4(@typescript-eslint/parser@8.59.4(eslint@10.4.0)(typescript@6.0.3))(eslint@10.4.0)(typescript@6.0.3) + '@typescript-eslint/parser': 8.59.4(eslint@10.4.0)(typescript@6.0.3) + '@typescript-eslint/typescript-estree': 8.59.4(typescript@6.0.3) + '@typescript-eslint/utils': 8.59.4(eslint@10.4.0)(typescript@6.0.3) + eslint: 10.4.0 + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + typescript@6.0.3: {} + + unconfig-core@7.5.0: + dependencies: + '@quansync/fs': 1.0.0 + quansync: 1.0.0 + + undici-types@7.24.6: {} + + universalify@0.1.2: {} + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + vite@8.0.16(@types/node@25.9.1): + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.4 + postcss: 8.5.15 + rolldown: 1.0.3 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 25.9.1 + fsevents: 2.3.3 + + vitest@4.1.9(@types/node@25.9.1)(happy-dom@20.10.5)(vite@8.0.16(@types/node@25.9.1)): + dependencies: + '@vitest/expect': 4.1.9 + '@vitest/mocker': 4.1.9(vite@8.0.16(@types/node@25.9.1)) + '@vitest/pretty-format': 4.1.9 + '@vitest/runner': 4.1.9 + '@vitest/snapshot': 4.1.9 + '@vitest/spy': 4.1.9 + '@vitest/utils': 4.1.9 + es-module-lexer: 2.1.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 4.1.0 + tinybench: 2.9.0 + tinyexec: 1.2.4 + tinyglobby: 0.2.17 + tinyrainbow: 3.1.0 + vite: 8.0.16(@types/node@25.9.1) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 25.9.1 + happy-dom: 20.10.5 + transitivePeerDependencies: + - msw + + vue@3.5.38(typescript@6.0.3): + dependencies: + '@vue/compiler-dom': 3.5.38 + '@vue/compiler-sfc': 3.5.38 + '@vue/runtime-dom': 3.5.38 + '@vue/server-renderer': 3.5.38(vue@3.5.38(typescript@6.0.3)) + '@vue/shared': 3.5.38 + optionalDependencies: + typescript: 6.0.3 + + whatwg-mimetype@3.0.0: {} + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + word-wrap@1.2.5: {} + + ws@8.21.0: {} + + yocto-queue@0.1.0: {} + + zimmerframe@1.1.4: {} diff --git a/clients/typescript/pnpm-workspace.yaml b/clients/typescript/pnpm-workspace.yaml new file mode 100644 index 0000000..dee51e9 --- /dev/null +++ b/clients/typescript/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +packages: + - "packages/*" diff --git a/clients/typescript/tsconfig.base.json b/clients/typescript/tsconfig.base.json new file mode 100644 index 0000000..1764000 --- /dev/null +++ b/clients/typescript/tsconfig.base.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "Preserve", + "moduleResolution": "bundler", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "strict": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + "noImplicitOverride": true, + "noFallthroughCasesInSwitch": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "isolatedModules": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "verbatimModuleSyntax": true + } +} From 7895222ba64fc8342344313ae51a8b9767ae36b4 Mon Sep 17 00:00:00 2001 From: thecodearcher Date: Sun, 21 Jun 2026 03:12:24 +0100 Subject: [PATCH 02/17] chore: update TypeScript client configuration and dependencies --- clients/typescript/.changeset/config.json | 6 +- clients/typescript/.gitignore | 3 +- clients/typescript/package.json | 5 +- clients/typescript/packages/client/README.md | 57 ++++++------------- .../typescript/packages/client/package.json | 3 +- .../typescript/packages/client/src/index.ts | 6 +- .../typescript/packages/client/src/version.ts | 3 + .../typescript/packages/client/tsconfig.json | 3 - .../packages/client/tsdown.config.ts | 11 ++-- clients/typescript/tsconfig.base.json | 8 +-- 10 files changed, 42 insertions(+), 63 deletions(-) create mode 100644 clients/typescript/packages/client/src/version.ts diff --git a/clients/typescript/.changeset/config.json b/clients/typescript/.changeset/config.json index 5c58ec9..e9331da 100644 --- a/clients/typescript/.changeset/config.json +++ b/clients/typescript/.changeset/config.json @@ -4,8 +4,8 @@ "commit": false, "fixed": [], "linked": [], - "access": "restricted", - "baseBranch": "main", + "access": "public", + "baseBranch": "master", "updateInternalDependencies": "patch", "ignore": [] -} +} \ No newline at end of file diff --git a/clients/typescript/.gitignore b/clients/typescript/.gitignore index fc10e9c..d994942 100644 --- a/clients/typescript/.gitignore +++ b/clients/typescript/.gitignore @@ -8,4 +8,5 @@ coverage/ .idea/ .vscode/ .cursor/ -.claude/ \ No newline at end of file +.claude/ +.npmrc \ No newline at end of file diff --git a/clients/typescript/package.json b/clients/typescript/package.json index b51f66b..b545976 100644 --- a/clients/typescript/package.json +++ b/clients/typescript/package.json @@ -13,7 +13,10 @@ "lint": "eslint packages", "format": "prettier --write \"packages/**/*.{ts,tsx,md}\"", "format:check": "prettier --check \"packages/**/*.{ts,tsx,md}\"", - "test": "pnpm -r --filter './packages/*' run test" + "test": "pnpm -r --filter './packages/*' run test", + "changeset": "changeset", + "version-packages": "changeset version", + "release": "pnpm build && changeset publish" }, "devDependencies": { "@changesets/cli": "^2.31.0", diff --git a/clients/typescript/packages/client/README.md b/clients/typescript/packages/client/README.md index 2fb6c4c..1674088 100644 --- a/clients/typescript/packages/client/README.md +++ b/clients/typescript/packages/client/README.md @@ -1,23 +1,22 @@

- Limen + Limen

-# limen-auth +

+ npm version + GitHub stars +

Official TypeScript client SDK for **[Limen](https://github.com/thecodearcher/limen)** — a modern, composable authentication library for Go. Framework-agnostic core with first-class **React, Vue, Svelte, and Solid** adapters. -> 📖 Full guides and API reference: **[limenauth.dev](https://limenauth.dev)** - ## Install ```bash npm install limen-auth ``` -Works with any framework — or none at all. If you're on React, Vue, Svelte, or Solid, just have that framework installed; there's nothing else to add. - ## Quick start ```ts @@ -29,44 +28,20 @@ export const auth = createAuthClient({ plugins: [credentialPasswordPlugin()], }); +// `auth.$session` is a reactive store for the current user — it loads on its +// own, stays in sync across tabs, and updates whenever you sign in or out. +auth.$session.subscribe(({ data, isPending }) => { + if (isPending) return; + console.log(data ? `Signed in as ${data.user.email}` : "Signed out"); +}); + +// Mutations update `$session` automatically — no manual refetch. await auth.signIn.credential({ credential: "ada@example.com", password: "secret" }); -const session = await auth.getSession(); // Session | null await auth.signout(); ``` -`auth.$session` is a reactive store for the current user. It loads on its own, keeps your open tabs in sync, and updates as you sign in and out — so the UI always reflects the real session. - -## Framework adapters - -Import `createAuthClient` from your framework's entry point and you get a `useSession()` wired to it: - -```tsx -import { createAuthClient } from "limen-auth/react"; -import { credentialPasswordPlugin } from "limen-auth/plugins/credential"; - -export const auth = createAuthClient({ baseURL: "...", plugins: [credentialPasswordPlugin()] }); - -function Profile() { - const { data, isPending } = auth.useSession(); - if (isPending) return

Loading…

; - return data ?

Hi {data.user.email}

:

Signed out

; -} -``` - -Also available from `limen-auth/vue`, `limen-auth/svelte`, and `limen-auth/solid`. - -## Plugins - -Add the sign-in flows you need as plugins (each lives under `limen-auth/plugins/`): - -- `credentialPasswordPlugin` — email/username + password -- `oauthClientPlugin` — social / OAuth providers -- `magicLinkPlugin` — passwordless email links -- `twoFactorPlugin` — TOTP, OTP, and backup codes -- `bearerPlugin` / `sessionJwtPlugin` — token-based sessions - -See the plugin and full API reference at **[limenauth.dev](https://limenauth.dev)**. +Using a framework? `limen-auth/react`, `/vue`, `/svelte`, and `/solid` give you a `useSession()` hook over the same store. -## License +## Documentation -MIT © Brian Iyoha +Full guides, framework adapters (React, Vue, Svelte, Solid), the plugin catalog, and the complete API reference live at **[limenauth.dev](https://limenauth.dev)**. diff --git a/clients/typescript/packages/client/package.json b/clients/typescript/packages/client/package.json index 32edffa..a99f6bc 100644 --- a/clients/typescript/packages/client/package.json +++ b/clients/typescript/packages/client/package.json @@ -18,7 +18,7 @@ "typescript", "sdk" ], - "homepage": "https://github.com/thecodearcher/limen#readme", + "homepage": "https://limenauth.dev", "bugs": "https://github.com/thecodearcher/limen/issues", "repository": { "type": "git", @@ -29,6 +29,7 @@ "author": "Brian Iyoha ", "type": "module", "module": "./dist/index.mjs", + "types": "./dist/index.d.mts", "publishConfig": { "access": "public" }, diff --git a/clients/typescript/packages/client/src/index.ts b/clients/typescript/packages/client/src/index.ts index 8b72124..4e60f4d 100644 --- a/clients/typescript/packages/client/src/index.ts +++ b/clients/typescript/packages/client/src/index.ts @@ -1,8 +1,8 @@ -export const VERSION = "0.0.0"; +export { VERSION } from "./version"; export { createAuthClient } from "./client"; -export { LimenError, deriveErrorCode } from "./errors"; +export { deriveErrorCode, LimenError } from "./errors"; export type { LimenErrorCode } from "./errors"; export { camelizeEach, camelizeKeys } from "./helpers"; @@ -27,8 +27,8 @@ export { route } from "./route"; export { defaultSerialize } from "./serialize"; export type { AnyRouteContext, RouteContext } from "./context"; -export type { RouteCallOptions, RouteHandler } from "./route"; export type { RunRoute } from "./define-plugin"; +export type { RouteCallOptions, RouteHandler } from "./route"; export { coreClientPlugin } from "./routes"; export type { ActiveSession, CoreContribution, VerifyEmailInput } from "./routes"; diff --git a/clients/typescript/packages/client/src/version.ts b/clients/typescript/packages/client/src/version.ts new file mode 100644 index 0000000..f206acc --- /dev/null +++ b/clients/typescript/packages/client/src/version.ts @@ -0,0 +1,3 @@ +import pkg from "../package.json"; + +export const VERSION = pkg.version; diff --git a/clients/typescript/packages/client/tsconfig.json b/clients/typescript/packages/client/tsconfig.json index f336ca0..038f1aa 100644 --- a/clients/typescript/packages/client/tsconfig.json +++ b/clients/typescript/packages/client/tsconfig.json @@ -2,9 +2,6 @@ "extends": "../../tsconfig.base.json", "compilerOptions": { "jsx": "react-jsx", - "types": [ - "node" - ] }, "include": [ "src/**/*", diff --git a/clients/typescript/packages/client/tsdown.config.ts b/clients/typescript/packages/client/tsdown.config.ts index ec00018..e6e81d4 100644 --- a/clients/typescript/packages/client/tsdown.config.ts +++ b/clients/typescript/packages/client/tsdown.config.ts @@ -1,6 +1,11 @@ import { defineConfig } from "tsdown"; export default defineConfig({ + format: ["esm"], + dts: true, + clean: true, + treeshake: true, + unbundle: true, entry: [ "src/index.ts", "src/react/index.ts", @@ -10,10 +15,4 @@ export default defineConfig({ "src/plugins/index.ts", "src/plugins/*/index.ts", ], - format: ["esm"], - dts: true, - clean: true, - treeshake: true, - sourcemap: true, - target: "es2022", }); diff --git a/clients/typescript/tsconfig.base.json b/clients/typescript/tsconfig.base.json index 1764000..44a9e84 100644 --- a/clients/typescript/tsconfig.base.json +++ b/clients/typescript/tsconfig.base.json @@ -1,9 +1,9 @@ { "compilerOptions": { - "target": "ES2022", + "target": "ESNext", "module": "Preserve", "moduleResolution": "bundler", - "lib": ["ES2022", "DOM", "DOM.Iterable"], + "lib": ["ESNext", "DOM", "DOM.Iterable"], "strict": true, "noUncheckedIndexedAccess": true, "exactOptionalPropertyTypes": true, @@ -15,8 +15,8 @@ "forceConsistentCasingInFileNames": true, "skipLibCheck": true, "declaration": true, - "declarationMap": true, - "sourceMap": true, + "declarationMap": false, + "sourceMap": false, "verbatimModuleSyntax": true } } From ba2451b783a92dfe783be925bdacc955e0782e4e Mon Sep 17 00:00:00 2001 From: thecodearcher Date: Sun, 21 Jun 2026 03:17:24 +0100 Subject: [PATCH 03/17] chore: initialize CHANGELOG and update package version and description for limen-auth --- clients/typescript/packages/client/CHANGELOG.md | 7 +++++++ clients/typescript/packages/client/package.json | 4 ++-- 2 files changed, 9 insertions(+), 2 deletions(-) create mode 100644 clients/typescript/packages/client/CHANGELOG.md diff --git a/clients/typescript/packages/client/CHANGELOG.md b/clients/typescript/packages/client/CHANGELOG.md new file mode 100644 index 0000000..0a03f1d --- /dev/null +++ b/clients/typescript/packages/client/CHANGELOG.md @@ -0,0 +1,7 @@ +# limen-auth + +## 0.0.1 + +### Patch Changes + +- Pre-release test diff --git a/clients/typescript/packages/client/package.json b/clients/typescript/packages/client/package.json index a99f6bc..2cc9646 100644 --- a/clients/typescript/packages/client/package.json +++ b/clients/typescript/packages/client/package.json @@ -1,7 +1,7 @@ { "name": "limen-auth", - "version": "0.0.0", - "description": "TypeScript authentication client for Limen — framework-agnostic core with React, Vue, Svelte, and Solid adapters.", + "version": "0.0.1", + "description": "Official TypeScript SDK for Limen, a composable Go auth library. Framework-agnostic core with React, Vue, Svelte, and Solid adapters.", "keywords": [ "limen", "auth", From f79a78def016982fde85b31d5b5abe8bb7504090 Mon Sep 17 00:00:00 2001 From: thecodearcher Date: Sun, 21 Jun 2026 03:52:32 +0100 Subject: [PATCH 04/17] chore: update README to reflect multiple TypeScript client SDKs and remove development instructions --- clients/typescript/README.md | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/clients/typescript/README.md b/clients/typescript/README.md index 1ff6d84..a471f61 100644 --- a/clients/typescript/README.md +++ b/clients/typescript/README.md @@ -1,21 +1,7 @@ # limen-ts -TypeScript client SDK for [Limen](https://github.com/thecodearcher/limen) — a modern, composable authentication library for Go. +TypeScript client SDKs for [Limen](https://github.com/thecodearcher/limen) — a modern, composable authentication library for Go. ## Packages - [`limen-auth`](./packages/client) — framework-agnostic core -- [`limen-auth/react`](./packages/client/src/react) — React hooks adapter (subpath of `limen-auth`) -- [`limen-auth/vue`](./packages/client/src/vue) — Vue composables adapter (subpath of `limen-auth`) -- [`limen-auth/svelte`](./packages/client/src/svelte) — Svelte stores adapter (subpath of `limen-auth`) -- [`limen-auth/solid`](./packages/client/src/solid) — Solid primitives adapter (subpath of `limen-auth`) - -## Development - -```bash -pnpm install -pnpm typecheck -pnpm lint -pnpm test -pnpm build -``` From 2ccf476c254f883f050e415b3347e39e88f8f9f3 Mon Sep 17 00:00:00 2001 From: thecodearcher Date: Sun, 21 Jun 2026 04:09:10 +0100 Subject: [PATCH 05/17] chore: update readme --- clients/typescript/packages/client/README.md | 2 ++ clients/typescript/packages/client/src/errors.ts | 3 +-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/clients/typescript/packages/client/README.md b/clients/typescript/packages/client/README.md index 1674088..e775ac1 100644 --- a/clients/typescript/packages/client/README.md +++ b/clients/typescript/packages/client/README.md @@ -9,6 +9,8 @@ GitHub stars

+## Limen Auth + Official TypeScript client SDK for **[Limen](https://github.com/thecodearcher/limen)** — a modern, composable authentication library for Go. Framework-agnostic core with first-class **React, Vue, Svelte, and Solid** adapters. ## Install diff --git a/clients/typescript/packages/client/src/errors.ts b/clients/typescript/packages/client/src/errors.ts index 24fd996..cf8e21a 100644 --- a/clients/typescript/packages/client/src/errors.ts +++ b/clients/typescript/packages/client/src/errors.ts @@ -23,8 +23,7 @@ export function deriveErrorCode(status: number): LimenErrorCode { /** * The single error type every SDK call throws on non-2xx. Carries the raw - * server message, the HTTP status, and a derived typed code. Subclass-free so - * `instanceof LimenError` is the only check consumers need. + * server message, the HTTP status, and a derived typed code. */ export class LimenError extends Error { override readonly name = "LimenError"; From 4a7111836586d1125a0518dbf313e9b2e765d7db Mon Sep 17 00:00:00 2001 From: thecodearcher Date: Sun, 21 Jun 2026 04:33:28 +0100 Subject: [PATCH 06/17] fix(verification): change column type from String to Text --- verification_introspect.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/verification_introspect.go b/verification_introspect.go index 87f0f17..494fb2f 100644 --- a/verification_introspect.go +++ b/verification_introspect.go @@ -50,7 +50,7 @@ func (v *VerificationSchema) getDefaultColumns(config *SchemaConfig) []ColumnDef { Name: string(VerificationSchemaValueField), LogicalField: VerificationSchemaValueField, - Type: ColumnTypeString, + Type: ColumnTypeText, IsNullable: false, IsPrimaryKey: false, Tags: map[string]string{ From 7e3447e27aed437f5842581fdb2859db8e36b4a2 Mon Sep 17 00:00:00 2001 From: thecodearcher Date: Sun, 21 Jun 2026 04:33:39 +0100 Subject: [PATCH 07/17] chore: remove unused banner SVG file from TypeScript client package --- clients/typescript/packages/client/banner.svg | 45 ------------------- 1 file changed, 45 deletions(-) delete mode 100644 clients/typescript/packages/client/banner.svg diff --git a/clients/typescript/packages/client/banner.svg b/clients/typescript/packages/client/banner.svg deleted file mode 100644 index a6b6b83..0000000 --- a/clients/typescript/packages/client/banner.svg +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From 426d2ed1bad3197afe3c8b82ef4493ac17aa7600 Mon Sep 17 00:00:00 2001 From: thecodearcher Date: Sun, 21 Jun 2026 05:43:02 +0100 Subject: [PATCH 08/17] chore: enter beta pre mode --- clients/typescript/.changeset/pre.json | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 clients/typescript/.changeset/pre.json diff --git a/clients/typescript/.changeset/pre.json b/clients/typescript/.changeset/pre.json new file mode 100644 index 0000000..cb1a4d6 --- /dev/null +++ b/clients/typescript/.changeset/pre.json @@ -0,0 +1,8 @@ +{ + "mode": "pre", + "tag": "beta", + "initialVersions": { + "limen-auth": "0.0.1" + }, + "changesets": [] +} From 078f3f0ec1def79795148758538c428ec34d53c4 Mon Sep 17 00:00:00 2001 From: thecodearcher Date: Sun, 21 Jun 2026 05:55:05 +0100 Subject: [PATCH 09/17] chore: add CI workflows for TypeScript client --- .github/workflows/ci.yml | 4 ++ .github/workflows/ts-client-ci.yml | 51 +++++++++++++++++++ .github/workflows/ts-client-release.yml | 68 +++++++++++++++++++++++++ 3 files changed, 123 insertions(+) create mode 100644 .github/workflows/ts-client-ci.yml create mode 100644 .github/workflows/ts-client-release.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 49224f0..9de5e74 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,8 +3,12 @@ name: CI on: push: branches: [master] + paths-ignore: + - "clients/typescript/**" pull_request: branches: [master] + paths-ignore: + - "clients/typescript/**" permissions: contents: read diff --git a/.github/workflows/ts-client-ci.yml b/.github/workflows/ts-client-ci.yml new file mode 100644 index 0000000..7afc609 --- /dev/null +++ b/.github/workflows/ts-client-ci.yml @@ -0,0 +1,51 @@ +name: TS Client CI + +on: + push: + branches: [master, next] + paths: ["clients/typescript/**"] + pull_request: + branches: [master, next] + paths: ["clients/typescript/**"] + +permissions: + contents: read + +defaults: + run: + working-directory: clients/typescript + +jobs: + verify: + name: Verify + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up pnpm + uses: pnpm/action-setup@v4 + with: + package_json_file: clients/typescript/package.json + + - name: Set up Node + uses: actions/setup-node@v6 + with: + node-version: 24 + cache: pnpm + cache-dependency-path: clients/typescript/pnpm-lock.yaml + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Typecheck + run: pnpm typecheck + + - name: Lint + run: pnpm lint + + - name: Run tests + run: pnpm test + + - name: Build + run: pnpm build diff --git a/.github/workflows/ts-client-release.yml b/.github/workflows/ts-client-release.yml new file mode 100644 index 0000000..8b52c23 --- /dev/null +++ b/.github/workflows/ts-client-release.yml @@ -0,0 +1,68 @@ +name: TS Client Release + +on: + push: + branches: [master, next] + paths: ["clients/typescript/**"] + +concurrency: + group: ts-client-release-${{ github.ref }} + cancel-in-progress: false + +permissions: + contents: write + pull-requests: write + id-token: write + +defaults: + run: + working-directory: clients/typescript + +jobs: + release: + name: Version or publish limen-auth + runs-on: ubuntu-latest + environment: release + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up pnpm + uses: pnpm/action-setup@v4 + with: + package_json_file: clients/typescript/package.json + + - name: Set up Node + uses: actions/setup-node@v6 + with: + node-version: 26 + cache: pnpm + cache-dependency-path: clients/typescript/pnpm-lock.yaml + registry-url: https://registry.npmjs.org + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Changesets release + id: changesets + uses: changesets/action@v1 + with: + cwd: clients/typescript + version: pnpm version-packages + publish: pnpm release + title: "chore(client-ts): release" + commit: "chore(client-ts): release" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + NPM_CONFIG_PROVENANCE: "true" + + - name: Stamp release PR title with the version + if: steps.changesets.outputs.pullRequestNumber + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + version="$(jq -r '.version' packages/client/package.json)" + gh pr edit "${{ steps.changesets.outputs.pullRequestNumber }}" \ + --title "chore(client-ts): release v${version}" From a0779e173f5caf2a16219c7c182c2c93ec976588 Mon Sep 17 00:00:00 2001 From: thecodearcher Date: Sun, 21 Jun 2026 05:58:04 +0100 Subject: [PATCH 10/17] chore: add changelog entry --- clients/typescript/.changeset/sour-cars-kiss.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 clients/typescript/.changeset/sour-cars-kiss.md diff --git a/clients/typescript/.changeset/sour-cars-kiss.md b/clients/typescript/.changeset/sour-cars-kiss.md new file mode 100644 index 0000000..09965dd --- /dev/null +++ b/clients/typescript/.changeset/sour-cars-kiss.md @@ -0,0 +1,5 @@ +--- +"limen-auth": patch +--- + +Testing CI From 92d3242f2521e787cbbe72dc671ff48cb6a0ac48 Mon Sep 17 00:00:00 2001 From: thecodearcher Date: Sun, 21 Jun 2026 06:03:01 +0100 Subject: [PATCH 11/17] chore: add concurrency configuration to TypeScript client CI workflow --- .github/workflows/ts-client-ci.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ts-client-ci.yml b/.github/workflows/ts-client-ci.yml index 7afc609..fc8d7f0 100644 --- a/.github/workflows/ts-client-ci.yml +++ b/.github/workflows/ts-client-ci.yml @@ -5,9 +5,13 @@ on: branches: [master, next] paths: ["clients/typescript/**"] pull_request: - branches: [master, next] paths: ["clients/typescript/**"] +# Cancel superseded runs on the same ref so rapid pushes don't pile up. +concurrency: + group: ts-client-ci-${{ github.ref }} + cancel-in-progress: true + permissions: contents: read From 15b16fdf2e89e9e50e31321112cdc632342cdd1c Mon Sep 17 00:00:00 2001 From: thecodearcher Date: Sun, 21 Jun 2026 06:08:19 +0100 Subject: [PATCH 12/17] chore: ci --- clients/typescript/packages/client/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clients/typescript/packages/client/README.md b/clients/typescript/packages/client/README.md index e775ac1..e29e5f1 100644 --- a/clients/typescript/packages/client/README.md +++ b/clients/typescript/packages/client/README.md @@ -34,7 +34,7 @@ export const auth = createAuthClient({ // own, stays in sync across tabs, and updates whenever you sign in or out. auth.$session.subscribe(({ data, isPending }) => { if (isPending) return; - console.log(data ? `Signed in as ${data.user.email}` : "Signed out"); + console.log(data ? `Signed in as ${data.user.email}` : "Signed out!"); }); // Mutations update `$session` automatically — no manual refetch. From 0885e1c3cf1ba9f248c856437c2469daac8cbea4 Mon Sep 17 00:00:00 2001 From: thecodearcher Date: Sun, 21 Jun 2026 18:21:54 +0100 Subject: [PATCH 13/17] chore: enhance release workflow with app token generation and beta pre mode validation --- .github/workflows/ts-client-release.yml | 31 +++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ts-client-release.yml b/.github/workflows/ts-client-release.yml index 8b52c23..7f9d98e 100644 --- a/.github/workflows/ts-client-release.yml +++ b/.github/workflows/ts-client-release.yml @@ -24,8 +24,35 @@ jobs: runs-on: ubuntu-latest environment: release steps: + - name: Generate release app token + id: app-token + uses: actions/create-github-app-token@v2 + with: + app-id: ${{ vars.RELEASE_APP_ID }} + private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} + - name: Check out repository uses: actions/checkout@v4 + with: + token: ${{ steps.app-token.outputs.token }} + + # The prerelease line must never publish a stable version to @latest, so + # refuse to release from `next` unless it's in Changesets beta pre mode. + - name: Require beta pre mode on next + if: github.ref_name == 'next' + run: | + file=.changeset/pre.json + if [ ! -f "$file" ]; then + echo "::error::'next' must be in beta pre mode before releasing, but $file is missing. Run 'pnpm changeset pre enter beta'." + exit 1 + fi + mode="$(jq -r '.mode // ""' "$file")" + tag="$(jq -r '.tag // ""' "$file")" + if [ "$mode" != "pre" ] || [ "$tag" != "beta" ]; then + echo "::error::'next' must be in beta pre mode (found mode='$mode', tag='$tag')." + exit 1 + fi + echo "next is in beta pre mode." - name: Set up pnpm uses: pnpm/action-setup@v4 @@ -53,7 +80,7 @@ jobs: title: "chore(client-ts): release" commit: "chore(client-ts): release" env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }} NPM_CONFIG_PROVENANCE: "true" @@ -61,7 +88,7 @@ jobs: - name: Stamp release PR title with the version if: steps.changesets.outputs.pullRequestNumber env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ steps.app-token.outputs.token }} run: | version="$(jq -r '.version' packages/client/package.json)" gh pr edit "${{ steps.changesets.outputs.pullRequestNumber }}" \ From 593756eb90928272664777114624cbc1c1c17ef6 Mon Sep 17 00:00:00 2001 From: thecodearcher Date: Sun, 21 Jun 2026 18:56:05 +0100 Subject: [PATCH 14/17] chore: add TS Client Canary workflow for publishing canary snapshots --- .github/workflows/ts-client-canary.yml | 53 ++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 .github/workflows/ts-client-canary.yml diff --git a/.github/workflows/ts-client-canary.yml b/.github/workflows/ts-client-canary.yml new file mode 100644 index 0000000..2c22109 --- /dev/null +++ b/.github/workflows/ts-client-canary.yml @@ -0,0 +1,53 @@ +name: TS Client Canary + +on: + workflow_dispatch: {} + +permissions: + contents: read + id-token: write + +defaults: + run: + working-directory: clients/typescript + +jobs: + canary: + name: Publish canary + runs-on: ubuntu-latest + environment: release + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up pnpm + uses: pnpm/action-setup@v4 + with: + package_json_file: clients/typescript/package.json + + - name: Set up Node + uses: actions/setup-node@v6 + with: + node-version: 26 + cache: pnpm + cache-dependency-path: clients/typescript/pnpm-lock.yaml + registry-url: https://registry.npmjs.org + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Version as a canary snapshot + run: | + # Ignore pre mode so canaries work from any branch (including next). + # This only affects the throwaway runner checkout — never a commit. + rm -f .changeset/pre.json + pnpm changeset version --snapshot canary + + - name: Build + run: pnpm build + + - name: Publish to the canary tag + run: pnpm changeset publish --tag canary --no-git-tag + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + NPM_CONFIG_PROVENANCE: "true" From 975ddf40a569370bfa239dfb4bbb51c9aca2e961 Mon Sep 17 00:00:00 2001 From: thecodearcher Date: Sun, 21 Jun 2026 18:57:53 +0100 Subject: [PATCH 15/17] chore: update README --- clients/typescript/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clients/typescript/README.md b/clients/typescript/README.md index a471f61..53ee0f9 100644 --- a/clients/typescript/README.md +++ b/clients/typescript/README.md @@ -1,4 +1,4 @@ -# limen-ts +# Typescript Clients SDKs TypeScript client SDKs for [Limen](https://github.com/thecodearcher/limen) — a modern, composable authentication library for Go. From 9e3eb9b5f81bdbe457f8d40639af71a3302fad4b Mon Sep 17 00:00:00 2001 From: "limen-release[bot]" <295635032+limen-release[bot]@users.noreply.github.com> Date: Sun, 21 Jun 2026 19:02:44 +0100 Subject: [PATCH 16/17] chore(client-ts): release (beta) (#13) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- clients/typescript/.changeset/pre.json | 4 +++- clients/typescript/packages/client/CHANGELOG.md | 6 ++++++ clients/typescript/packages/client/package.json | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/clients/typescript/.changeset/pre.json b/clients/typescript/.changeset/pre.json index cb1a4d6..d862366 100644 --- a/clients/typescript/.changeset/pre.json +++ b/clients/typescript/.changeset/pre.json @@ -4,5 +4,7 @@ "initialVersions": { "limen-auth": "0.0.1" }, - "changesets": [] + "changesets": [ + "sour-cars-kiss" + ] } diff --git a/clients/typescript/packages/client/CHANGELOG.md b/clients/typescript/packages/client/CHANGELOG.md index 0a03f1d..fbccfcd 100644 --- a/clients/typescript/packages/client/CHANGELOG.md +++ b/clients/typescript/packages/client/CHANGELOG.md @@ -1,5 +1,11 @@ # limen-auth +## 0.0.2-beta.0 + +### Patch Changes + +- a0779e1: Testing CI + ## 0.0.1 ### Patch Changes diff --git a/clients/typescript/packages/client/package.json b/clients/typescript/packages/client/package.json index 2cc9646..76a9b11 100644 --- a/clients/typescript/packages/client/package.json +++ b/clients/typescript/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "limen-auth", - "version": "0.0.1", + "version": "0.0.2-beta.0", "description": "Official TypeScript SDK for Limen, a composable Go auth library. Framework-agnostic core with React, Vue, Svelte, and Solid adapters.", "keywords": [ "limen", From e9a7880314646540e9b3b9bf92090120098f3f0a Mon Sep 17 00:00:00 2001 From: thecodearcher Date: Sun, 21 Jun 2026 19:28:07 +0100 Subject: [PATCH 17/17] chore: update release command to prevent git tagging --- .github/workflows/ts-client-canary.yml | 1 - .github/workflows/ts-client-release.yml | 2 -- clients/typescript/package.json | 2 +- 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/ts-client-canary.yml b/.github/workflows/ts-client-canary.yml index 2c22109..7f9fd5b 100644 --- a/.github/workflows/ts-client-canary.yml +++ b/.github/workflows/ts-client-canary.yml @@ -49,5 +49,4 @@ jobs: - name: Publish to the canary tag run: pnpm changeset publish --tag canary --no-git-tag env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} NPM_CONFIG_PROVENANCE: "true" diff --git a/.github/workflows/ts-client-release.yml b/.github/workflows/ts-client-release.yml index 7f9d98e..f6d6878 100644 --- a/.github/workflows/ts-client-release.yml +++ b/.github/workflows/ts-client-release.yml @@ -81,8 +81,6 @@ jobs: commit: "chore(client-ts): release" env: GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} NPM_CONFIG_PROVENANCE: "true" - name: Stamp release PR title with the version diff --git a/clients/typescript/package.json b/clients/typescript/package.json index b545976..3c1145e 100644 --- a/clients/typescript/package.json +++ b/clients/typescript/package.json @@ -16,7 +16,7 @@ "test": "pnpm -r --filter './packages/*' run test", "changeset": "changeset", "version-packages": "changeset version", - "release": "pnpm build && changeset publish" + "release": "pnpm build && changeset publish --no-git-tag" }, "devDependencies": { "@changesets/cli": "^2.31.0",