diff --git a/ROADMAP.md b/ROADMAP.md index 8704b8c935..20f583a7e3 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -220,24 +220,21 @@ These are the backbone of ObjectStack's enterprise capabilities. --- -## Phase 5: Framework Adapters (🟡 Mostly Complete) +## Phase 5: Framework Adapters (✅ Complete) > **Goal:** First-class integration with popular web frameworks. ### Completed -- [x] **Next.js Adapter** — App Router, Auth/GraphQL/Meta/Data/Storage handlers (9/10) +- [x] **Next.js Adapter** — App Router, Auth/GraphQL/Meta/Data/Storage handlers (10/10) - [x] **NestJS Adapter** — Full DI module, Express/Fastify support (10/10) - [x] **Hono Server Plugin** — Production HTTP server with static file serving - -### Remaining - -- [ ] **Hono Adapter** — Currently a stub (middleware only). Needs full route dispatchers for Auth/GraphQL/Meta/Data/Storage matching Next.js/NestJS completeness -- [ ] **Next.js Server Actions** — Support for React Server Actions pattern -- [ ] **Express Adapter** — Standalone Express integration (currently via NestJS only) -- [ ] **Fastify Adapter** — Standalone Fastify integration (currently via NestJS only) -- [ ] **SvelteKit Adapter** — Community request -- [ ] **Nuxt Adapter** — Community request +- [x] **Hono Adapter** — Full route dispatchers for Auth/GraphQL/Meta/Data/Storage with createHonoApp +- [x] **Next.js Server Actions** — createServerActions with query/getById/create/update/remove/getMetadata +- [x] **Express Adapter** — Standalone Express v5 router with all ObjectStack routes +- [x] **Fastify Adapter** — Fastify plugin with full route dispatchers +- [x] **SvelteKit Adapter** — Web-standard Request/Response based handler for SvelteKit routes +- [x] **Nuxt Adapter** — h3 router integration for Nuxt server routes --- @@ -400,9 +397,13 @@ These are the backbone of ObjectStack's enterprise capabilities. | `@objectstack/plugin-dev` | 3.0.2 | — | ✅ Stable | 10/10 | | `@objectstack/plugin-hono-server` | 3.0.2 | — | ✅ Stable | 9/10 | | `@objectstack/plugin-msw` | 3.0.2 | — | ✅ Stable | 9/10 | -| `@objectstack/nextjs` | 3.0.2 | ✅ | ✅ Stable | 9/10 | +| `@objectstack/nextjs` | 3.0.2 | ✅ | ✅ Stable | 10/10 | | `@objectstack/nestjs` | 3.0.2 | ✅ | ✅ Stable | 10/10 | -| `@objectstack/hono` | 3.0.2 | ✅ | 🔴 Stub | 2/10 | +| `@objectstack/hono` | 3.0.2 | ✅ | ✅ Stable | 10/10 | +| `@objectstack/express` | 3.0.2 | ✅ | ✅ Stable | 9/10 | +| `@objectstack/fastify` | 3.0.2 | ✅ | ✅ Stable | 9/10 | +| `@objectstack/sveltekit` | 3.0.2 | ✅ | ✅ Stable | 9/10 | +| `@objectstack/nuxt` | 3.0.2 | ✅ | ✅ Stable | 9/10 | | `@objectstack/types` | 3.0.2 | — | 🟡 Minimal | 3/10 | | `objectstack-vscode` | 0.1.0 | — | 🟡 Early | 4/10 | | `create-objectstack` | 3.0.0 | — | 🟡 Basic | 5/10 | diff --git a/packages/adapters/express/README.md b/packages/adapters/express/README.md new file mode 100644 index 0000000000..c3ebe9f2b7 --- /dev/null +++ b/packages/adapters/express/README.md @@ -0,0 +1,22 @@ +# @objectstack/express + +The official Express adapter for ObjectStack. + +## Features +- Standalone Express router integration +- Full Auth/GraphQL/Metadata/Data/Storage routes +- AuthPlugin service support with Web Request conversion +- Middleware mode for attaching kernel to requests + +## Usage + +```typescript +import express from 'express'; +import { createExpressRouter } from '@objectstack/express'; + +const app = express(); +app.use(express.json()); +app.use('/api', createExpressRouter({ kernel })); + +app.listen(3000); +``` diff --git a/packages/adapters/express/package.json b/packages/adapters/express/package.json new file mode 100644 index 0000000000..c0594cc0ef --- /dev/null +++ b/packages/adapters/express/package.json @@ -0,0 +1,30 @@ +{ + "name": "@objectstack/express", + "version": "3.0.2", + "license": "Apache-2.0", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.mjs", + "require": "./dist/index.js" + } + }, + "scripts": { + "build": "tsup --config ../../../tsup.config.ts", + "test": "vitest run", + "test:watch": "vitest" + }, + "peerDependencies": { + "@objectstack/runtime": "workspace:*", + "express": "^5.1.0" + }, + "devDependencies": { + "@objectstack/runtime": "workspace:*", + "@types/express": "^5.0.3", + "express": "^5.1.0", + "typescript": "^5.0.0", + "vitest": "^4.0.18" + } +} diff --git a/packages/adapters/express/src/__mocks__/runtime.ts b/packages/adapters/express/src/__mocks__/runtime.ts new file mode 100644 index 0000000000..7961fd466c --- /dev/null +++ b/packages/adapters/express/src/__mocks__/runtime.ts @@ -0,0 +1,4 @@ +// Stub for @objectstack/runtime - replaced by vi.mock in tests +export const HttpDispatcher = class {}; +export type ObjectKernel = any; +export type HttpDispatcherResult = any; diff --git a/packages/adapters/express/src/express.test.ts b/packages/adapters/express/src/express.test.ts new file mode 100644 index 0000000000..e1a981d102 --- /dev/null +++ b/packages/adapters/express/src/express.test.ts @@ -0,0 +1,85 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +// Mock dispatcher instance +const mockDispatcher = { + getDiscoveryInfo: vi.fn().mockReturnValue({ version: '1.0', endpoints: [] }), + handleAuth: vi.fn().mockResolvedValue({ handled: true, response: { body: { ok: true }, status: 200 } }), + handleGraphQL: vi.fn().mockResolvedValue({ data: {} }), + handleMetadata: vi.fn().mockResolvedValue({ handled: true, response: { body: { objects: [] }, status: 200 } }), + handleData: vi.fn().mockResolvedValue({ handled: true, response: { body: { records: [] }, status: 200 } }), + handleStorage: vi.fn().mockResolvedValue({ handled: true, response: { body: {}, status: 200 } }), +}; + +vi.mock('@objectstack/runtime', () => { + return { + HttpDispatcher: function HttpDispatcher() { + return mockDispatcher; + }, + }; +}); + +import { createExpressRouter, objectStackMiddleware } from './index'; + +const mockKernel = { name: 'test-kernel' } as any; + +function createMockRes() { + const res: any = { + _status: 200, + _body: null, + _headers: {} as Record, + _redirectUrl: null as string | null, + status(code: number) { res._status = code; return res; }, + json(body: any) { res._body = body; return res; }, + send(body: any) { res._body = body; return res; }, + set(k: string, v: string) { res._headers[k] = v; return res; }, + redirect(url: string) { res._redirectUrl = url; return res; }, + }; + return res; +} + +function createMockReq(overrides: any = {}) { + return { + method: 'GET', + path: '/', + url: '/', + originalUrl: '/', + body: {}, + query: {}, + headers: {}, + params: {}, + protocol: 'http', + get: (key: string) => key === 'host' ? 'localhost' : undefined, + ...overrides, + }; +} + +describe('createExpressRouter', () => { + let router: any; + + beforeEach(() => { + vi.clearAllMocks(); + router = createExpressRouter({ kernel: mockKernel }); + }); + + it('returns a router with registered routes', () => { + expect(router).toBeDefined(); + expect(router.stack).toBeDefined(); + expect(router.stack.length).toBeGreaterThan(0); + }); +}); + +describe('objectStackMiddleware', () => { + it('attaches kernel to request', () => { + const middleware = objectStackMiddleware(mockKernel); + const req: any = {}; + const res = createMockRes(); + const next = vi.fn(); + + middleware(req, res, next); + + expect(req.objectStack).toBe(mockKernel); + expect(next).toHaveBeenCalled(); + }); +}); diff --git a/packages/adapters/express/src/index.ts b/packages/adapters/express/src/index.ts new file mode 100644 index 0000000000..35f28f068e --- /dev/null +++ b/packages/adapters/express/src/index.ts @@ -0,0 +1,195 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +import { type Router, type Request, type Response, type NextFunction, Router as createRouter } from 'express'; +import { type ObjectKernel, HttpDispatcher, HttpDispatcherResult } from '@objectstack/runtime'; + +export interface ExpressAdapterOptions { + kernel: ObjectKernel; + prefix?: string; +} + +/** + * Auth service interface with handleRequest method + */ +interface AuthService { + handleRequest(request: Request): Promise; +} + +/** + * Creates an Express Router with all ObjectStack route dispatchers. + * Provides Auth, GraphQL, Metadata, Data, and Storage routes. + * + * @example + * ```ts + * import express from 'express'; + * import { createExpressRouter } from '@objectstack/express'; + * + * const app = express(); + * app.use('/api', createExpressRouter({ kernel })); + * app.listen(3000); + * ``` + */ +export function createExpressRouter(options: ExpressAdapterOptions): Router { + const router = createRouter(); + const dispatcher = new HttpDispatcher(options.kernel); + const prefix = options.prefix || '/api'; + + const sendResult = (result: HttpDispatcherResult, res: Response) => { + if (result.handled) { + if (result.response) { + if (result.response.headers) { + Object.entries(result.response.headers).forEach(([k, v]) => res.set(k, v as string)); + } + return res.status(result.response.status).json(result.response.body); + } + if (result.result) { + const response = result.result; + if (response.type === 'redirect' && response.url) { + return res.redirect(response.url); + } + if (response.type === 'stream' && response.stream) { + if (response.headers) { + Object.entries(response.headers).forEach(([k, v]) => res.set(k, v as string)); + } + response.stream.pipe(res); + return; + } + return res.status(200).json(response); + } + } + return res.status(404).json({ success: false, error: { message: 'Not Found', code: 404 } }); + }; + + const errorResponse = (err: any, res: Response) => { + return res.status(err.statusCode || 500).json({ + success: false, + error: { + message: err.message || 'Internal Server Error', + code: err.statusCode || 500, + }, + }); + }; + + // --- Discovery --- + router.get('/', (_req: Request, res: Response) => { + res.json({ data: dispatcher.getDiscoveryInfo(prefix) }); + }); + + // --- Auth --- + router.all('/auth/{*path}', async (req: Request, res: Response) => { + try { + const path = (req.params as any).path; + const method = req.method; + + // Try AuthPlugin service first + const authService = typeof options.kernel.getService === 'function' + ? options.kernel.getService('auth') + : null; + + if (authService && typeof authService.handleRequest === 'function') { + const protocol = req.protocol || 'http'; + const host = req.get?.('host') || req.headers?.host || 'localhost'; + const url = `${protocol}://${host}${req.originalUrl || req.url}`; + const headers = new Headers(); + if (req.headers) { + Object.entries(req.headers).forEach(([k, v]) => { + if (typeof v === 'string') headers.set(k, v); + else if (Array.isArray(v)) headers.set(k, v.join(', ')); + }); + } + const init: RequestInit = { method, headers }; + if (method !== 'GET' && method !== 'HEAD' && req.body) { + init.body = JSON.stringify(req.body); + if (!headers.has('content-type')) { + headers.set('content-type', 'application/json'); + } + } + const webRequest = new Request(url, init); + const response = await authService.handleRequest(webRequest); + res.status(response.status); + response.headers.forEach((v: string, k: string) => res.set(k, v)); + const text = await response.text(); + return res.send(text); + } + + // Fallback to dispatcher + const body = method === 'GET' || method === 'HEAD' ? {} : req.body || {}; + const result = await dispatcher.handleAuth(path, method, body, { request: req, response: res }); + return sendResult(result, res); + } catch (err: any) { + return errorResponse(err, res); + } + }); + + // --- GraphQL --- + router.post('/graphql', async (req: Request, res: Response) => { + try { + const result = await dispatcher.handleGraphQL(req.body, { request: req }); + return res.json(result); + } catch (err: any) { + return errorResponse(err, res); + } + }); + + // --- Metadata --- + router.all('/meta/{*path}', async (req: Request, res: Response) => { + try { + const subPath = '/' + (req.params as any).path; + const method = req.method; + const body = (method === 'PUT' || method === 'POST') ? req.body : undefined; + const result = await dispatcher.handleMetadata(subPath, { request: req }, method, body); + return sendResult(result, res); + } catch (err: any) { + return errorResponse(err, res); + } + }); + + router.all('/meta', async (req: Request, res: Response) => { + try { + const method = req.method; + const body = (method === 'PUT' || method === 'POST') ? req.body : undefined; + const result = await dispatcher.handleMetadata('', { request: req }, method, body); + return sendResult(result, res); + } catch (err: any) { + return errorResponse(err, res); + } + }); + + // --- Data --- + router.all('/data/{*path}', async (req: Request, res: Response) => { + try { + const subPath = '/' + (req.params as any).path; + const method = req.method; + const body = (method === 'POST' || method === 'PATCH') ? req.body : {}; + const result = await dispatcher.handleData(subPath, method, body, req.query, { request: req }); + return sendResult(result, res); + } catch (err: any) { + return errorResponse(err, res); + } + }); + + // --- Storage --- + router.all('/storage/{*path}', async (req: Request, res: Response) => { + try { + const subPath = '/' + (req.params as any).path; + const method = req.method; + const file = (req as any).file || (req as any).files?.file; + const result = await dispatcher.handleStorage(subPath, method, file, { request: req }); + return sendResult(result, res); + } catch (err: any) { + return errorResponse(err, res); + } + }); + + return router; +} + +/** + * Middleware that attaches the ObjectStack kernel to the request. + */ +export function objectStackMiddleware(kernel: ObjectKernel) { + return (req: Request, _res: Response, next: NextFunction) => { + (req as any).objectStack = kernel; + next(); + }; +} diff --git a/packages/adapters/express/tsconfig.json b/packages/adapters/express/tsconfig.json new file mode 100644 index 0000000000..66ec4500ef --- /dev/null +++ b/packages/adapters/express/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "esModuleInterop": true, + "skipLibCheck": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/adapters/express/vitest.config.ts b/packages/adapters/express/vitest.config.ts new file mode 100644 index 0000000000..e2051df210 --- /dev/null +++ b/packages/adapters/express/vitest.config.ts @@ -0,0 +1,16 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +import { defineConfig } from 'vitest/config'; +import path from 'node:path'; + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + }, + resolve: { + alias: { + '@objectstack/runtime': path.resolve(__dirname, 'src/__mocks__/runtime.ts'), + }, + }, +}); diff --git a/packages/adapters/fastify/README.md b/packages/adapters/fastify/README.md new file mode 100644 index 0000000000..83272a17ea --- /dev/null +++ b/packages/adapters/fastify/README.md @@ -0,0 +1,21 @@ +# @objectstack/fastify + +The official Fastify adapter for ObjectStack. + +## Features +- Fastify plugin integration +- Full Auth/GraphQL/Metadata/Data/Storage routes +- AuthPlugin service support with Web Request conversion +- Decorator mode for attaching kernel to requests + +## Usage + +```typescript +import Fastify from 'fastify'; +import { objectStackPlugin } from '@objectstack/fastify'; + +const app = Fastify(); +app.register(objectStackPlugin, { kernel, prefix: '/api' }); + +app.listen({ port: 3000 }); +``` diff --git a/packages/adapters/fastify/package.json b/packages/adapters/fastify/package.json new file mode 100644 index 0000000000..e2b0c859f4 --- /dev/null +++ b/packages/adapters/fastify/package.json @@ -0,0 +1,29 @@ +{ + "name": "@objectstack/fastify", + "version": "3.0.2", + "license": "Apache-2.0", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.mjs", + "require": "./dist/index.js" + } + }, + "scripts": { + "build": "tsup --config ../../../tsup.config.ts", + "test": "vitest run", + "test:watch": "vitest" + }, + "peerDependencies": { + "@objectstack/runtime": "workspace:*", + "fastify": "^5.7.2" + }, + "devDependencies": { + "@objectstack/runtime": "workspace:*", + "fastify": "^5.7.2", + "typescript": "^5.0.0", + "vitest": "^4.0.18" + } +} diff --git a/packages/adapters/fastify/src/__mocks__/runtime.ts b/packages/adapters/fastify/src/__mocks__/runtime.ts new file mode 100644 index 0000000000..7961fd466c --- /dev/null +++ b/packages/adapters/fastify/src/__mocks__/runtime.ts @@ -0,0 +1,4 @@ +// Stub for @objectstack/runtime - replaced by vi.mock in tests +export const HttpDispatcher = class {}; +export type ObjectKernel = any; +export type HttpDispatcherResult = any; diff --git a/packages/adapters/fastify/src/fastify.test.ts b/packages/adapters/fastify/src/fastify.test.ts new file mode 100644 index 0000000000..43f80ac0f1 --- /dev/null +++ b/packages/adapters/fastify/src/fastify.test.ts @@ -0,0 +1,183 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +// Mock dispatcher instance +const mockDispatcher = { + getDiscoveryInfo: vi.fn().mockReturnValue({ version: '1.0', endpoints: [] }), + handleAuth: vi.fn().mockResolvedValue({ handled: true, response: { body: { ok: true }, status: 200 } }), + handleGraphQL: vi.fn().mockResolvedValue({ data: {} }), + handleMetadata: vi.fn().mockResolvedValue({ handled: true, response: { body: { objects: [] }, status: 200 } }), + handleData: vi.fn().mockResolvedValue({ handled: true, response: { body: { records: [] }, status: 200 } }), + handleStorage: vi.fn().mockResolvedValue({ handled: true, response: { body: {}, status: 200 } }), +}; + +vi.mock('@objectstack/runtime', () => { + return { + HttpDispatcher: function HttpDispatcher() { + return mockDispatcher; + }, + }; +}); + +import Fastify from 'fastify'; +import { objectStackPlugin, objectStackDecorator } from './index'; + +const mockKernel = { name: 'test-kernel' } as any; + +describe('objectStackPlugin', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('registers discovery route', async () => { + const app = Fastify(); + await app.register(objectStackPlugin, { kernel: mockKernel }); + await app.ready(); + + const res = await app.inject({ method: 'GET', url: '/api' }); + expect(res.statusCode).toBe(200); + const json = JSON.parse(res.payload); + expect(json.data).toBeDefined(); + expect(json.data.version).toBe('1.0'); + expect(mockDispatcher.getDiscoveryInfo).toHaveBeenCalledWith('/api'); + }); + + it('registers auth route', async () => { + const app = Fastify(); + await app.register(objectStackPlugin, { kernel: mockKernel }); + await app.ready(); + + const res = await app.inject({ + method: 'POST', + url: '/api/auth/login', + payload: { email: 'a@b.com' }, + }); + expect(res.statusCode).toBe(200); + const json = JSON.parse(res.payload); + expect(json.ok).toBe(true); + expect(mockDispatcher.handleAuth).toHaveBeenCalledWith( + 'login', + 'POST', + { email: 'a@b.com' }, + expect.objectContaining({ request: expect.anything() }), + ); + }); + + it('registers graphql route', async () => { + const app = Fastify(); + await app.register(objectStackPlugin, { kernel: mockKernel }); + await app.ready(); + + const body = { query: '{ objects { name } }' }; + const res = await app.inject({ + method: 'POST', + url: '/api/graphql', + payload: body, + }); + expect(res.statusCode).toBe(200); + const json = JSON.parse(res.payload); + expect(json.data).toBeDefined(); + expect(mockDispatcher.handleGraphQL).toHaveBeenCalledWith( + body, + expect.objectContaining({ request: expect.anything() }), + ); + }); + + it('registers metadata route', async () => { + const app = Fastify(); + await app.register(objectStackPlugin, { kernel: mockKernel }); + await app.ready(); + + const res = await app.inject({ method: 'GET', url: '/api/meta/objects' }); + expect(res.statusCode).toBe(200); + const json = JSON.parse(res.payload); + expect(json.objects).toBeDefined(); + expect(mockDispatcher.handleMetadata).toHaveBeenCalledWith( + '/objects', + expect.objectContaining({ request: expect.anything() }), + 'GET', + undefined, + ); + }); + + it('registers data route', async () => { + const app = Fastify(); + await app.register(objectStackPlugin, { kernel: mockKernel }); + await app.ready(); + + const res = await app.inject({ method: 'GET', url: '/api/data/account' }); + expect(res.statusCode).toBe(200); + const json = JSON.parse(res.payload); + expect(json.records).toBeDefined(); + expect(mockDispatcher.handleData).toHaveBeenCalledWith( + '/account', + 'GET', + {}, + expect.any(Object), + expect.objectContaining({ request: expect.anything() }), + ); + }); + + it('registers storage route', async () => { + const app = Fastify(); + await app.register(objectStackPlugin, { kernel: mockKernel }); + await app.ready(); + + const res = await app.inject({ method: 'GET', url: '/api/storage/files' }); + expect(res.statusCode).toBe(200); + expect(mockDispatcher.handleStorage).toHaveBeenCalledWith( + '/files', + 'GET', + undefined, + expect.objectContaining({ request: expect.anything() }), + ); + }); + + it('uses custom prefix', async () => { + const app = Fastify(); + // Pass prefix as part of options but not via Fastify's built-in prefix + await app.register(async (instance) => { + await objectStackPlugin(instance, { kernel: mockKernel, prefix: '/v2' }); + }); + await app.ready(); + + const res = await app.inject({ method: 'GET', url: '/v2' }); + expect(res.statusCode).toBe(200); + expect(mockDispatcher.getDiscoveryInfo).toHaveBeenCalledWith('/v2'); + }); + + it('returns error on exception', async () => { + mockDispatcher.handleData.mockRejectedValueOnce( + Object.assign(new Error('Forbidden'), { statusCode: 403 }), + ); + const app = Fastify(); + await app.register(objectStackPlugin, { kernel: mockKernel }); + await app.ready(); + + const res = await app.inject({ method: 'GET', url: '/api/data/account' }); + expect(res.statusCode).toBe(403); + const json = JSON.parse(res.payload); + expect(json.success).toBe(false); + expect(json.error.message).toBe('Forbidden'); + }); +}); + +describe('objectStackDecorator', () => { + it('attaches kernel to request via hook', async () => { + const app = Fastify(); + // The decorator plugin adds an onRequest hook + const decoratorPlugin = objectStackDecorator(mockKernel); + await decoratorPlugin(app); + + let capturedKernel: any = null; + app.get('/test', async (request) => { + capturedKernel = (request as any).objectStack; + return { ok: true }; + }); + + await app.ready(); + await app.inject({ method: 'GET', url: '/test' }); + expect(capturedKernel).toBe(mockKernel); + }); +}); diff --git a/packages/adapters/fastify/src/index.ts b/packages/adapters/fastify/src/index.ts new file mode 100644 index 0000000000..c942bf0bcc --- /dev/null +++ b/packages/adapters/fastify/src/index.ts @@ -0,0 +1,200 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +import { type FastifyInstance, type FastifyRequest, type FastifyReply } from 'fastify'; +import { type ObjectKernel, HttpDispatcher, HttpDispatcherResult } from '@objectstack/runtime'; + +export interface FastifyAdapterOptions { + kernel: ObjectKernel; + prefix?: string; +} + +/** + * Auth service interface with handleRequest method + */ +interface AuthService { + handleRequest(request: Request): Promise; +} + +/** + * Registers ObjectStack routes as a Fastify plugin. + * Provides Auth, GraphQL, Metadata, Data, and Storage routes. + * + * @example + * ```ts + * import Fastify from 'fastify'; + * import { objectStackPlugin } from '@objectstack/fastify'; + * + * const app = Fastify(); + * app.register(objectStackPlugin, { kernel, prefix: '/api' }); + * app.listen({ port: 3000 }); + * ``` + */ +export async function objectStackPlugin(fastify: FastifyInstance, options: FastifyAdapterOptions) { + const dispatcher = new HttpDispatcher(options.kernel); + const prefix = options.prefix || '/api'; + + const sendResult = (result: HttpDispatcherResult, reply: FastifyReply) => { + if (result.handled) { + if (result.response) { + if (result.response.headers) { + Object.entries(result.response.headers).forEach(([k, v]) => reply.header(k, v as string)); + } + return reply.status(result.response.status).send(result.response.body); + } + if (result.result) { + const response = result.result; + if (response.type === 'redirect' && response.url) { + return reply.redirect(response.url); + } + if (response.type === 'stream' && response.stream) { + if (response.headers) { + Object.entries(response.headers).forEach(([k, v]) => reply.header(k, v as string)); + } + return reply.send(response.stream); + } + return reply.status(200).send(response); + } + } + return reply.status(404).send({ success: false, error: { message: 'Not Found', code: 404 } }); + }; + + const errorResponse = (err: any, reply: FastifyReply) => { + return reply.status(err.statusCode || 500).send({ + success: false, + error: { + message: err.message || 'Internal Server Error', + code: err.statusCode || 500, + }, + }); + }; + + // --- Discovery --- + fastify.get(`${prefix}`, async (_request: FastifyRequest, reply: FastifyReply) => { + return reply.send({ data: dispatcher.getDiscoveryInfo(prefix) }); + }); + + // --- .well-known --- + fastify.get('/.well-known/objectstack', async (_request: FastifyRequest, reply: FastifyReply) => { + return reply.redirect(prefix); + }); + + // --- Auth --- + fastify.all(`${prefix}/auth/*`, async (request: FastifyRequest, reply: FastifyReply) => { + try { + const path = request.url.substring(`${prefix}/auth/`.length).split('?')[0]; + const method = request.method; + + // Try AuthPlugin service first + const authService = typeof options.kernel.getService === 'function' + ? options.kernel.getService('auth') + : null; + + if (authService && typeof authService.handleRequest === 'function') { + const protocol = request.protocol || 'http'; + const host = request.hostname || 'localhost'; + const url = `${protocol}://${host}${request.url}`; + const headers = new Headers(); + if (request.headers) { + Object.entries(request.headers).forEach(([k, v]) => { + if (typeof v === 'string') headers.set(k, v); + else if (Array.isArray(v)) headers.set(k, v.join(', ')); + }); + } + const init: RequestInit = { method, headers }; + if (method !== 'GET' && method !== 'HEAD' && request.body) { + init.body = JSON.stringify(request.body); + if (!headers.has('content-type')) { + headers.set('content-type', 'application/json'); + } + } + const webRequest = new Request(url, init); + const response = await authService.handleRequest(webRequest); + reply.status(response.status); + response.headers.forEach((v: string, k: string) => reply.header(k, v)); + const text = await response.text(); + return reply.send(text); + } + + // Fallback to dispatcher + const body = method === 'GET' || method === 'HEAD' ? {} : (request.body as any) || {}; + const result = await dispatcher.handleAuth(path, method, body, { request: request.raw }); + return sendResult(result, reply); + } catch (err: any) { + return errorResponse(err, reply); + } + }); + + // --- GraphQL --- + fastify.post(`${prefix}/graphql`, async (request: FastifyRequest, reply: FastifyReply) => { + try { + const result = await dispatcher.handleGraphQL(request.body as any, { request: request.raw }); + return reply.send(result); + } catch (err: any) { + return errorResponse(err, reply); + } + }); + + // --- Metadata --- + fastify.all(`${prefix}/meta/*`, async (request: FastifyRequest, reply: FastifyReply) => { + try { + const urlPath = request.url.split('?')[0]; + const subPath = urlPath.substring(`${prefix}/meta`.length); + const method = request.method; + const body = (method === 'PUT' || method === 'POST') ? request.body : undefined; + const result = await dispatcher.handleMetadata(subPath, { request: request.raw }, method, body); + return sendResult(result, reply); + } catch (err: any) { + return errorResponse(err, reply); + } + }); + + fastify.all(`${prefix}/meta`, async (request: FastifyRequest, reply: FastifyReply) => { + try { + const method = request.method; + const body = (method === 'PUT' || method === 'POST') ? request.body : undefined; + const result = await dispatcher.handleMetadata('', { request: request.raw }, method, body); + return sendResult(result, reply); + } catch (err: any) { + return errorResponse(err, reply); + } + }); + + // --- Data --- + fastify.all(`${prefix}/data/*`, async (request: FastifyRequest, reply: FastifyReply) => { + try { + const urlPath = request.url.split('?')[0]; + const subPath = urlPath.substring(`${prefix}/data`.length); + const method = request.method; + const body = (method === 'POST' || method === 'PATCH') ? request.body : {}; + const result = await dispatcher.handleData(subPath, method, body, request.query, { request: request.raw }); + return sendResult(result, reply); + } catch (err: any) { + return errorResponse(err, reply); + } + }); + + // --- Storage --- + fastify.all(`${prefix}/storage/*`, async (request: FastifyRequest, reply: FastifyReply) => { + try { + const urlPath = request.url.split('?')[0]; + const subPath = urlPath.substring(`${prefix}/storage`.length); + const method = request.method; + const file = (request as any).file || (request.body as any)?.file; + const result = await dispatcher.handleStorage(subPath, method, file, { request: request.raw }); + return sendResult(result, reply); + } catch (err: any) { + return errorResponse(err, reply); + } + }); +} + +/** + * Fastify plugin that attaches the ObjectStack kernel to each request. + */ +export function objectStackDecorator(kernel: ObjectKernel) { + return async function (fastify: FastifyInstance) { + fastify.addHook('onRequest', async (request) => { + (request as any).objectStack = kernel; + }); + }; +} diff --git a/packages/adapters/fastify/tsconfig.json b/packages/adapters/fastify/tsconfig.json new file mode 100644 index 0000000000..66ec4500ef --- /dev/null +++ b/packages/adapters/fastify/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "esModuleInterop": true, + "skipLibCheck": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/adapters/fastify/vitest.config.ts b/packages/adapters/fastify/vitest.config.ts new file mode 100644 index 0000000000..e2051df210 --- /dev/null +++ b/packages/adapters/fastify/vitest.config.ts @@ -0,0 +1,16 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +import { defineConfig } from 'vitest/config'; +import path from 'node:path'; + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + }, + resolve: { + alias: { + '@objectstack/runtime': path.resolve(__dirname, 'src/__mocks__/runtime.ts'), + }, + }, +}); diff --git a/packages/adapters/hono/src/__mocks__/runtime.ts b/packages/adapters/hono/src/__mocks__/runtime.ts index 7961fd466c..137a7c13ad 100644 --- a/packages/adapters/hono/src/__mocks__/runtime.ts +++ b/packages/adapters/hono/src/__mocks__/runtime.ts @@ -1,4 +1,16 @@ -// Stub for @objectstack/runtime - replaced by vi.mock in tests -export const HttpDispatcher = class {}; +// Stub for @objectstack/runtime - resolved via vitest alias +import { vi } from 'vitest'; + +export class HttpDispatcher { + getDiscoveryInfo = vi.fn().mockReturnValue({ version: '1.0', endpoints: [] }); + handleGraphQL = vi.fn().mockResolvedValue({ data: {} }); + handleAuth = vi.fn().mockResolvedValue({ handled: true, response: { status: 200, body: { ok: true } } }); + handleMetadata = vi.fn().mockResolvedValue({ handled: true, response: { status: 200, body: { objects: [] } } }); + handleData = vi.fn().mockResolvedValue({ handled: true, response: { status: 200, body: { records: [] } } }); + handleStorage = vi.fn().mockResolvedValue({ handled: true, response: { status: 200, body: {} } }); + + constructor(_kernel: any) {} +} + export type ObjectKernel = any; export type HttpDispatcherResult = any; diff --git a/packages/adapters/hono/src/hono.test.ts b/packages/adapters/hono/src/hono.test.ts index 172929ca7e..e899cb571b 100644 --- a/packages/adapters/hono/src/hono.test.ts +++ b/packages/adapters/hono/src/hono.test.ts @@ -3,7 +3,25 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { Hono } from 'hono'; -import { objectStackMiddleware } from './index'; +// Mock dispatcher instance accessible across tests +const mockDispatcher = { + getDiscoveryInfo: vi.fn().mockReturnValue({ version: '1.0', endpoints: [] }), + handleAuth: vi.fn().mockResolvedValue({ handled: true, response: { body: { ok: true }, status: 200 } }), + handleGraphQL: vi.fn().mockResolvedValue({ data: {} }), + handleMetadata: vi.fn().mockResolvedValue({ handled: true, response: { body: { objects: [] }, status: 200 } }), + handleData: vi.fn().mockResolvedValue({ handled: true, response: { body: { records: [] }, status: 200 } }), + handleStorage: vi.fn().mockResolvedValue({ handled: true, response: { body: {}, status: 200 } }), +}; + +vi.mock('@objectstack/runtime', () => { + return { + HttpDispatcher: function HttpDispatcher() { + return mockDispatcher; + }, + }; +}); + +import { objectStackMiddleware, createHonoApp } from './index'; const mockKernel = { name: 'test-kernel' } as any; @@ -61,3 +79,327 @@ describe('objectStackMiddleware', () => { expect(json.name).toBe('test-kernel'); }); }); + +describe('createHonoApp', () => { + let app: Hono; + + beforeEach(() => { + vi.clearAllMocks(); + app = createHonoApp({ kernel: mockKernel }); + }); + + describe('Discovery Endpoint', () => { + it('GET /api returns discovery info', async () => { + const res = await app.request('/api'); + expect(res.status).toBe(200); + const json = await res.json(); + expect(json.data).toBeDefined(); + expect(json.data.version).toBe('1.0'); + expect(mockDispatcher.getDiscoveryInfo).toHaveBeenCalledWith('/api'); + }); + + it('uses custom prefix for discovery', async () => { + const customApp = createHonoApp({ kernel: mockKernel, prefix: '/v2' }); + const res = await customApp.request('/v2'); + expect(res.status).toBe(200); + const json = await res.json(); + expect(json.data).toBeDefined(); + expect(mockDispatcher.getDiscoveryInfo).toHaveBeenCalledWith('/v2'); + }); + }); + + describe('.well-known Endpoint', () => { + it('GET /.well-known/objectstack redirects to prefix', async () => { + const res = await app.request('/.well-known/objectstack', { redirect: 'manual' }); + expect(res.status).toBe(302); + expect(res.headers.get('location')).toBe('/api'); + }); + }); + + describe('Auth Endpoint', () => { + it('POST /api/auth/login calls handleAuth', async () => { + const res = await app.request('/api/auth/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email: 'a@b.com' }), + }); + expect(res.status).toBe(200); + const json = await res.json(); + expect(json.ok).toBe(true); + expect(mockDispatcher.handleAuth).toHaveBeenCalledWith( + 'login', + 'POST', + { email: 'a@b.com' }, + expect.objectContaining({ request: expect.anything() }), + ); + }); + + it('GET /api/auth/callback calls handleAuth with empty body', async () => { + const res = await app.request('/api/auth/callback'); + expect(res.status).toBe(200); + expect(mockDispatcher.handleAuth).toHaveBeenCalledWith( + 'callback', + 'GET', + {}, + expect.objectContaining({ request: expect.anything() }), + ); + }); + + it('returns error on handleAuth exception', async () => { + mockDispatcher.handleAuth.mockRejectedValueOnce( + Object.assign(new Error('Unauthorized'), { statusCode: 401 }), + ); + const res = await app.request('/api/auth/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({}), + }); + expect(res.status).toBe(401); + const json = await res.json(); + expect(json.success).toBe(false); + expect(json.error.message).toBe('Unauthorized'); + }); + }); + + describe('Auth via AuthPlugin service', () => { + it('uses kernel.getService("auth") when available', async () => { + const mockHandleRequest = vi.fn().mockResolvedValue( + new Response(JSON.stringify({ user: { id: '1' } }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }), + ); + const kernelWithAuth = { + ...mockKernel, + getService: vi.fn().mockReturnValue({ handleRequest: mockHandleRequest }), + }; + const authApp = createHonoApp({ kernel: kernelWithAuth }); + const res = await authApp.request('/api/auth/sign-in/email', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email: 'a@b.com', password: 'pass' }), + }); + expect(res.status).toBe(200); + expect(kernelWithAuth.getService).toHaveBeenCalledWith('auth'); + expect(mockHandleRequest).toHaveBeenCalled(); + }); + + it('falls back to dispatcher when auth service is not available', async () => { + const kernelWithoutAuth = { + ...mockKernel, + getService: vi.fn().mockReturnValue(null), + }; + const authApp = createHonoApp({ kernel: kernelWithoutAuth }); + const res = await authApp.request('/api/auth/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email: 'a@b.com' }), + }); + expect(res.status).toBe(200); + expect(mockDispatcher.handleAuth).toHaveBeenCalled(); + }); + }); + + describe('GraphQL Endpoint', () => { + it('POST /api/graphql calls handleGraphQL', async () => { + const body = { query: '{ objects { name } }' }; + const res = await app.request('/api/graphql', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }); + expect(res.status).toBe(200); + const json = await res.json(); + expect(json.data).toBeDefined(); + expect(mockDispatcher.handleGraphQL).toHaveBeenCalledWith( + body, + expect.objectContaining({ request: expect.anything() }), + ); + }); + + it('returns error on handleGraphQL exception', async () => { + mockDispatcher.handleGraphQL.mockRejectedValueOnce(new Error('Parse error')); + const res = await app.request('/api/graphql', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ query: 'bad' }), + }); + expect(res.status).toBe(500); + const json = await res.json(); + expect(json.success).toBe(false); + expect(json.error.message).toBe('Parse error'); + }); + }); + + describe('Metadata Endpoint', () => { + it('GET /api/meta/objects calls handleMetadata', async () => { + const res = await app.request('/api/meta/objects'); + expect(res.status).toBe(200); + const json = await res.json(); + expect(json.objects).toBeDefined(); + expect(mockDispatcher.handleMetadata).toHaveBeenCalledWith( + '/objects', + expect.objectContaining({ request: expect.anything() }), + 'GET', + undefined, + ); + }); + + it('PUT /api/meta/objects parses JSON body', async () => { + const body = { name: 'test_object' }; + const res = await app.request('/api/meta/objects', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }); + expect(res.status).toBe(200); + expect(mockDispatcher.handleMetadata).toHaveBeenCalledWith( + '/objects', + expect.objectContaining({ request: expect.anything() }), + 'PUT', + body, + ); + }); + + it('GET /api/meta with no trailing path', async () => { + const res = await app.request('/api/meta'); + expect(res.status).toBe(200); + expect(mockDispatcher.handleMetadata).toHaveBeenCalledWith( + '', + expect.objectContaining({ request: expect.anything() }), + 'GET', + undefined, + ); + }); + }); + + describe('Data Endpoint', () => { + it('GET /api/data/account calls handleData', async () => { + const res = await app.request('/api/data/account'); + expect(res.status).toBe(200); + const json = await res.json(); + expect(json.records).toBeDefined(); + expect(mockDispatcher.handleData).toHaveBeenCalledWith( + '/account', + 'GET', + {}, + expect.any(Object), + expect.objectContaining({ request: expect.anything() }), + ); + }); + + it('POST /api/data/account parses JSON body', async () => { + const body = { name: 'Acme' }; + const res = await app.request('/api/data/account', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }); + expect(res.status).toBe(200); + expect(mockDispatcher.handleData).toHaveBeenCalledWith( + '/account', + 'POST', + body, + expect.any(Object), + expect.objectContaining({ request: expect.anything() }), + ); + }); + + it('PATCH /api/data/account parses JSON body', async () => { + const body = { name: 'Updated' }; + const res = await app.request('/api/data/account', { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }); + expect(res.status).toBe(200); + expect(mockDispatcher.handleData).toHaveBeenCalledWith( + '/account', + 'PATCH', + body, + expect.any(Object), + expect.objectContaining({ request: expect.anything() }), + ); + }); + + it('returns 404 when result is not handled', async () => { + mockDispatcher.handleData.mockResolvedValueOnce({ handled: false }); + const res = await app.request('/api/data/missing'); + expect(res.status).toBe(404); + const json = await res.json(); + expect(json.success).toBe(false); + }); + }); + + describe('Storage Endpoint', () => { + it('GET /api/storage/files calls handleStorage', async () => { + const res = await app.request('/api/storage/files'); + expect(res.status).toBe(200); + expect(mockDispatcher.handleStorage).toHaveBeenCalledWith( + '/files', + 'GET', + undefined, + expect.objectContaining({ request: expect.anything() }), + ); + }); + }); + + describe('Error Handling', () => { + it('returns 500 with default message on generic error', async () => { + mockDispatcher.handleData.mockRejectedValueOnce(new Error()); + const res = await app.request('/api/data/account'); + expect(res.status).toBe(500); + const json = await res.json(); + expect(json.error.message).toBe('Internal Server Error'); + }); + + it('uses custom statusCode from error', async () => { + mockDispatcher.handleData.mockRejectedValueOnce( + Object.assign(new Error('Forbidden'), { statusCode: 403 }), + ); + const res = await app.request('/api/data/account'); + expect(res.status).toBe(403); + const json = await res.json(); + expect(json.error.message).toBe('Forbidden'); + }); + }); + + describe('toResponse', () => { + it('handles redirect result', async () => { + mockDispatcher.handleData.mockResolvedValueOnce({ + handled: true, + result: { type: 'redirect', url: 'https://example.com' }, + }); + const res = await app.request('/api/data/redir', { redirect: 'manual' }); + expect(res.status).toBe(302); + expect(res.headers.get('location')).toBe('https://example.com'); + }); + + it('handles generic result objects with 200 status', async () => { + mockDispatcher.handleData.mockResolvedValueOnce({ + handled: true, + result: { foo: 'bar' }, + }); + const res = await app.request('/api/data/custom'); + expect(res.status).toBe(200); + const json = await res.json(); + expect(json.foo).toBe('bar'); + }); + + it('sets custom headers from response', async () => { + mockDispatcher.handleData.mockResolvedValueOnce({ + handled: true, + response: { status: 201, body: { id: 1 }, headers: { 'X-Custom': 'yes' } }, + }); + const res = await app.request('/api/data/account', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name: 'test' }), + }); + expect(res.status).toBe(201); + expect(res.headers.get('X-Custom')).toBe('yes'); + const json = await res.json(); + expect(json.id).toBe(1); + }); + }); +}); diff --git a/packages/adapters/hono/src/index.ts b/packages/adapters/hono/src/index.ts index 13497d4a42..12f0dd9642 100644 --- a/packages/adapters/hono/src/index.ts +++ b/packages/adapters/hono/src/index.ts @@ -1,12 +1,20 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. -import { type ObjectKernel } from '@objectstack/runtime'; +import { Hono } from 'hono'; +import { type ObjectKernel, HttpDispatcher, HttpDispatcherResult } from '@objectstack/runtime'; export interface ObjectStackHonoOptions { kernel: ObjectKernel; prefix?: string; } +/** + * Auth service interface with handleRequest method + */ +interface AuthService { + handleRequest(request: Request): Promise; +} + /** * Middleware mode for existing Hono apps */ @@ -16,3 +24,177 @@ export function objectStackMiddleware(kernel: ObjectKernel) { await next(); }; } + +/** + * Creates a full-featured Hono app with all ObjectStack route dispatchers. + * Provides Auth, GraphQL, Metadata, Data, and Storage routes matching + * Next.js/NestJS adapter completeness. + * + * @example + * ```ts + * import { createHonoApp } from '@objectstack/hono'; + * const app = createHonoApp({ kernel }); + * export default app; + * ``` + */ +export function createHonoApp(options: ObjectStackHonoOptions): Hono { + const app = new Hono(); + const prefix = options.prefix || '/api'; + const dispatcher = new HttpDispatcher(options.kernel); + + const errorJson = (c: any, message: string, code: number = 500) => { + return c.json({ success: false, error: { message, code } }, code); + }; + + const toResponse = (c: any, result: HttpDispatcherResult) => { + if (result.handled) { + if (result.response) { + if (result.response.headers) { + Object.entries(result.response.headers).forEach(([k, v]) => c.header(k, v as string)); + } + return c.json(result.response.body, result.response.status); + } + if (result.result) { + const res = result.result; + if (res.type === 'redirect' && res.url) { + return c.redirect(res.url); + } + if (res.type === 'stream' && res.stream) { + if (res.headers) { + Object.entries(res.headers).forEach(([k, v]) => c.header(k, v as string)); + } + return new Response(res.stream, { status: 200 }); + } + return c.json(res, 200); + } + } + return errorJson(c, 'Not Found', 404); + }; + + // --- Discovery --- + app.get(`${prefix}`, (c) => { + return c.json({ data: dispatcher.getDiscoveryInfo(prefix) }); + }); + + // --- .well-known --- + app.get('/.well-known/objectstack', (c) => { + return c.redirect(prefix); + }); + + // --- Auth --- + app.all(`${prefix}/auth/*`, async (c) => { + try { + const path = c.req.path.substring(`${prefix}/auth/`.length); + const method = c.req.method; + + // Try AuthPlugin service first (preferred path) + const authService = typeof options.kernel.getService === 'function' + ? options.kernel.getService('auth') + : null; + + if (authService && typeof authService.handleRequest === 'function') { + const response = await authService.handleRequest(c.req.raw); + return new Response(response.body, { + status: response.status, + headers: response.headers, + }); + } + + // Fallback to legacy dispatcher + const body = method === 'GET' || method === 'HEAD' + ? {} + : await c.req.json().catch(() => ({})); + const result = await dispatcher.handleAuth(path, method, body, { request: c.req.raw }); + return toResponse(c, result); + } catch (err: any) { + return errorJson(c, err.message || 'Internal Server Error', err.statusCode || 500); + } + }); + + // --- GraphQL --- + app.post(`${prefix}/graphql`, async (c) => { + try { + const body = await c.req.json(); + const result = await dispatcher.handleGraphQL(body, { request: c.req.raw }); + return c.json(result); + } catch (err: any) { + return errorJson(c, err.message || 'Internal Server Error', err.statusCode || 500); + } + }); + + // --- Metadata --- + app.all(`${prefix}/meta/*`, async (c) => { + try { + const subPath = c.req.path.substring(`${prefix}/meta`.length); + const method = c.req.method; + + let body: any = undefined; + if (method === 'PUT' || method === 'POST') { + body = await c.req.json().catch(() => ({})); + } + + const result = await dispatcher.handleMetadata(subPath, { request: c.req.raw }, method, body); + return toResponse(c, result); + } catch (err: any) { + return errorJson(c, err.message || 'Internal Server Error', err.statusCode || 500); + } + }); + + // Also handle /meta with no trailing path + app.all(`${prefix}/meta`, async (c) => { + try { + const method = c.req.method; + let body: any = undefined; + if (method === 'PUT' || method === 'POST') { + body = await c.req.json().catch(() => ({})); + } + const result = await dispatcher.handleMetadata('', { request: c.req.raw }, method, body); + return toResponse(c, result); + } catch (err: any) { + return errorJson(c, err.message || 'Internal Server Error', err.statusCode || 500); + } + }); + + // --- Data --- + app.all(`${prefix}/data/*`, async (c) => { + try { + const subPath = c.req.path.substring(`${prefix}/data`.length); + const method = c.req.method; + + let body: any = {}; + if (method === 'POST' || method === 'PATCH') { + body = await c.req.json().catch(() => ({})); + } + + const queryParams: Record = {}; + const url = new URL(c.req.url); + url.searchParams.forEach((val, key) => { queryParams[key] = val; }); + + const result = await dispatcher.handleData(subPath, method, body, queryParams, { request: c.req.raw }); + return toResponse(c, result); + } catch (err: any) { + return errorJson(c, err.message || 'Internal Server Error', err.statusCode || 500); + } + }); + + // --- Storage --- + app.all(`${prefix}/storage/*`, async (c) => { + try { + const subPath = c.req.path.substring(`${prefix}/storage`.length); + const method = c.req.method; + + let file: any = undefined; + if (method === 'POST' && subPath === '/upload') { + const formData = await c.req.formData(); + file = formData.get('file'); + } + + const result = await dispatcher.handleStorage(subPath, method, file, { request: c.req.raw }); + return toResponse(c, result); + } catch (err: any) { + return errorJson(c, err.message || 'Internal Server Error', err.statusCode || 500); + } + }); + + return app; +} diff --git a/packages/adapters/nextjs/src/index.ts b/packages/adapters/nextjs/src/index.ts index 51e42abc34..dcf3ce30ac 100644 --- a/packages/adapters/nextjs/src/index.ts +++ b/packages/adapters/nextjs/src/index.ts @@ -160,3 +160,135 @@ export function createDiscoveryHandler(options: NextAdapterOptions) { return NextResponse.redirect(targetUrl); } } + +// ─── Server Actions ────────────────────────────────────────────────────────── + +/** + * Result type for server actions + */ +export interface ServerActionResult { + success: boolean; + data?: T; + error?: { message: string; code: number }; +} + +/** + * Creates type-safe React Server Actions for ObjectStack data operations. + * Each action maps to a dispatcher method and can be called directly from + * React Server Components or client components via `"use server"`. + * + * @example + * ```ts + * // app/actions.ts + * "use server"; + * import { createServerActions } from '@objectstack/nextjs'; + * import { kernel } from '@/lib/kernel'; + * + * const actions = createServerActions({ kernel }); + * + * export async function getAccounts() { + * return actions.query('account'); + * } + * + * export async function createAccount(formData: FormData) { + * return actions.create('account', { + * name: formData.get('name') as string, + * }); + * } + * ``` + */ +export function createServerActions(options: NextAdapterOptions) { + const dispatcher = new HttpDispatcher(options.kernel); + + return { + /** + * Query records from an object + */ + async query(objectName: string, params?: Record): Promise { + try { + const result = await dispatcher.handleData(`/${objectName}`, 'GET', {}, params || {}, {}); + if (result.handled && result.response) { + return { success: true, data: result.response.body }; + } + return { success: false, error: { message: 'Not found', code: 404 } }; + } catch (err: any) { + return { success: false, error: { message: err.message || 'Internal Server Error', code: err.statusCode || 500 } }; + } + }, + + /** + * Get a single record by ID + */ + async getById(objectName: string, id: string): Promise { + try { + const result = await dispatcher.handleData(`/${objectName}/${id}`, 'GET', {}, {}, {}); + if (result.handled && result.response) { + return { success: true, data: result.response.body }; + } + return { success: false, error: { message: 'Not found', code: 404 } }; + } catch (err: any) { + return { success: false, error: { message: err.message || 'Internal Server Error', code: err.statusCode || 500 } }; + } + }, + + /** + * Create a new record + */ + async create(objectName: string, data: Record): Promise { + try { + const result = await dispatcher.handleData(`/${objectName}`, 'POST', data, {}, {}); + if (result.handled && result.response) { + return { success: true, data: result.response.body }; + } + return { success: false, error: { message: 'Create failed', code: 500 } }; + } catch (err: any) { + return { success: false, error: { message: err.message || 'Internal Server Error', code: err.statusCode || 500 } }; + } + }, + + /** + * Update a record by ID + */ + async update(objectName: string, id: string, data: Record): Promise { + try { + const result = await dispatcher.handleData(`/${objectName}/${id}`, 'PATCH', data, {}, {}); + if (result.handled && result.response) { + return { success: true, data: result.response.body }; + } + return { success: false, error: { message: 'Update failed', code: 500 } }; + } catch (err: any) { + return { success: false, error: { message: err.message || 'Internal Server Error', code: err.statusCode || 500 } }; + } + }, + + /** + * Delete a record by ID + */ + async remove(objectName: string, id: string): Promise { + try { + const result = await dispatcher.handleData(`/${objectName}/${id}`, 'DELETE', {}, {}, {}); + if (result.handled && result.response) { + return { success: true, data: result.response.body }; + } + return { success: false, error: { message: 'Delete failed', code: 500 } }; + } catch (err: any) { + return { success: false, error: { message: err.message || 'Internal Server Error', code: err.statusCode || 500 } }; + } + }, + + /** + * Get metadata for objects + */ + async getMetadata(path?: string): Promise { + try { + const result = await dispatcher.handleMetadata(path || '', {}, 'GET'); + if (result.handled && result.response) { + return { success: true, data: result.response.body }; + } + return { success: false, error: { message: 'Not found', code: 404 } }; + } catch (err: any) { + return { success: false, error: { message: err.message || 'Internal Server Error', code: err.statusCode || 500 } }; + } + }, + }; +} diff --git a/packages/adapters/nextjs/src/nextjs.test.ts b/packages/adapters/nextjs/src/nextjs.test.ts index d1ce2a3ef7..e44760f3e4 100644 --- a/packages/adapters/nextjs/src/nextjs.test.ts +++ b/packages/adapters/nextjs/src/nextjs.test.ts @@ -453,3 +453,97 @@ describe('createDiscoveryHandler', () => { expect((res as any).redirectUrl).toBe('http://localhost/v2/api'); }); }); + +// --- Server Actions Tests --- +import { createServerActions } from './index'; + +describe('createServerActions', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('query returns data on success', async () => { + const actions = createServerActions({ kernel: mockKernel }); + const result = await actions.query('account'); + expect(result.success).toBe(true); + expect(result.data).toEqual({ records: [] }); + expect(mockDispatcher.handleData).toHaveBeenCalledWith( + '/account', 'GET', {}, {}, + expect.anything(), + ); + }); + + it('query passes params', async () => { + const actions = createServerActions({ kernel: mockKernel }); + await actions.query('account', { limit: 10 }); + expect(mockDispatcher.handleData).toHaveBeenCalledWith( + '/account', 'GET', {}, { limit: 10 }, + expect.anything(), + ); + }); + + it('getById returns single record', async () => { + const actions = createServerActions({ kernel: mockKernel }); + const result = await actions.getById('account', '123'); + expect(result.success).toBe(true); + expect(mockDispatcher.handleData).toHaveBeenCalledWith( + '/account/123', 'GET', {}, {}, + expect.anything(), + ); + }); + + it('create sends data', async () => { + const actions = createServerActions({ kernel: mockKernel }); + const result = await actions.create('account', { name: 'Acme' }); + expect(result.success).toBe(true); + expect(mockDispatcher.handleData).toHaveBeenCalledWith( + '/account', 'POST', { name: 'Acme' }, {}, + expect.anything(), + ); + }); + + it('update sends data with id', async () => { + const actions = createServerActions({ kernel: mockKernel }); + const result = await actions.update('account', '123', { name: 'Updated' }); + expect(result.success).toBe(true); + expect(mockDispatcher.handleData).toHaveBeenCalledWith( + '/account/123', 'PATCH', { name: 'Updated' }, {}, + expect.anything(), + ); + }); + + it('remove deletes record', async () => { + const actions = createServerActions({ kernel: mockKernel }); + const result = await actions.remove('account', '123'); + expect(result.success).toBe(true); + expect(mockDispatcher.handleData).toHaveBeenCalledWith( + '/account/123', 'DELETE', {}, {}, + expect.anything(), + ); + }); + + it('getMetadata returns metadata', async () => { + const actions = createServerActions({ kernel: mockKernel }); + const result = await actions.getMetadata('/objects'); + expect(result.success).toBe(true); + expect(mockDispatcher.handleMetadata).toHaveBeenCalledWith( + '/objects', expect.anything(), 'GET', + ); + }); + + it('returns error on exception', async () => { + mockDispatcher.handleData.mockRejectedValueOnce(new Error('Server down')); + const actions = createServerActions({ kernel: mockKernel }); + const result = await actions.query('account'); + expect(result.success).toBe(false); + expect(result.error?.message).toBe('Server down'); + }); + + it('returns error when not handled', async () => { + mockDispatcher.handleData.mockResolvedValueOnce({ handled: false }); + const actions = createServerActions({ kernel: mockKernel }); + const result = await actions.query('missing'); + expect(result.success).toBe(false); + expect(result.error?.code).toBe(404); + }); +}); diff --git a/packages/adapters/nuxt/README.md b/packages/adapters/nuxt/README.md new file mode 100644 index 0000000000..0598b07288 --- /dev/null +++ b/packages/adapters/nuxt/README.md @@ -0,0 +1,20 @@ +# @objectstack/nuxt + +The official Nuxt adapter for ObjectStack, built on h3. + +## Features +- h3 router integration for Nuxt server routes +- Full Auth/GraphQL/Metadata/Data/Storage routes +- AuthPlugin service support +- Works with standalone h3 apps too + +## Usage + +```typescript +// server/api/[...].ts +import { createH3Router } from '@objectstack/nuxt'; +import { kernel } from '../kernel'; + +const router = createH3Router({ kernel }); +export default defineEventHandler(router.handler); +``` diff --git a/packages/adapters/nuxt/package.json b/packages/adapters/nuxt/package.json new file mode 100644 index 0000000000..48f7f2e3b4 --- /dev/null +++ b/packages/adapters/nuxt/package.json @@ -0,0 +1,29 @@ +{ + "name": "@objectstack/nuxt", + "version": "3.0.2", + "license": "Apache-2.0", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.mjs", + "require": "./dist/index.js" + } + }, + "scripts": { + "build": "tsup --config ../../../tsup.config.ts", + "test": "vitest run", + "test:watch": "vitest" + }, + "peerDependencies": { + "@objectstack/runtime": "workspace:*", + "h3": "^1.15.5" + }, + "devDependencies": { + "@objectstack/runtime": "workspace:*", + "h3": "^1.15.5", + "typescript": "^5.0.0", + "vitest": "^4.0.18" + } +} diff --git a/packages/adapters/nuxt/src/__mocks__/runtime.ts b/packages/adapters/nuxt/src/__mocks__/runtime.ts new file mode 100644 index 0000000000..7961fd466c --- /dev/null +++ b/packages/adapters/nuxt/src/__mocks__/runtime.ts @@ -0,0 +1,4 @@ +// Stub for @objectstack/runtime - replaced by vi.mock in tests +export const HttpDispatcher = class {}; +export type ObjectKernel = any; +export type HttpDispatcherResult = any; diff --git a/packages/adapters/nuxt/src/index.ts b/packages/adapters/nuxt/src/index.ts new file mode 100644 index 0000000000..db2f4e67ab --- /dev/null +++ b/packages/adapters/nuxt/src/index.ts @@ -0,0 +1,239 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +import { + createRouter, + defineEventHandler, + getQuery, + readBody, + sendRedirect, + setResponseHeader, + setResponseStatus, + type H3Event, + type Router, +} from 'h3'; +import { type ObjectKernel, HttpDispatcher, HttpDispatcherResult } from '@objectstack/runtime'; + +export interface NuxtAdapterOptions { + kernel: ObjectKernel; + prefix?: string; +} + +/** + * Auth service interface with handleRequest method + */ +interface AuthService { + handleRequest(request: Request): Promise; +} + +/** + * Creates an h3 router with all ObjectStack route dispatchers. + * Designed for use in Nuxt server routes or standalone h3 apps. + * + * @example + * ```ts + * // server/api/[...].ts + * import { createH3Router } from '@objectstack/nuxt'; + * import { kernel } from '../kernel'; + * + * const router = createH3Router({ kernel }); + * export default defineEventHandler(router.handler); + * ``` + */ +export function createH3Router(options: NuxtAdapterOptions): Router { + const router = createRouter(); + const dispatcher = new HttpDispatcher(options.kernel); + const prefix = options.prefix || '/api'; + + const errorJson = (event: H3Event, message: string, code: number = 500) => { + setResponseStatus(event, code); + return { success: false, error: { message, code } }; + }; + + const toResponse = (event: H3Event, result: HttpDispatcherResult) => { + if (result.handled) { + if (result.response) { + if (result.response.headers) { + Object.entries(result.response.headers).forEach(([k, v]) => + setResponseHeader(event, k, v as string), + ); + } + setResponseStatus(event, result.response.status); + return result.response.body; + } + if (result.result) { + const res = result.result; + if (res.type === 'redirect' && res.url) { + return sendRedirect(event, res.url); + } + if (res.type === 'stream' && res.stream) { + if (res.headers) { + Object.entries(res.headers).forEach(([k, v]) => + setResponseHeader(event, k, v as string), + ); + } + return res.stream; + } + return res; + } + } + return errorJson(event, 'Not Found', 404); + }; + + // --- Discovery --- + router.get( + `${prefix}`, + defineEventHandler(() => { + return { data: dispatcher.getDiscoveryInfo(prefix) }; + }), + ); + + // --- .well-known --- + router.get( + '/.well-known/objectstack', + defineEventHandler((event) => { + return sendRedirect(event, prefix); + }), + ); + + // --- Auth --- + router.use( + `${prefix}/auth/**`, + defineEventHandler(async (event) => { + try { + const urlPath = event.path || event.node.req.url || ''; + const path = urlPath.substring(`${prefix}/auth/`.length).split('?')[0]; + const method = event.method; + + // Try AuthPlugin service first + const authService = typeof options.kernel.getService === 'function' + ? options.kernel.getService('auth') + : null; + + if (authService && typeof authService.handleRequest === 'function') { + const host = event.node.req.headers.host || 'localhost'; + const protocol = (event.node.req.socket as any)?.encrypted ? 'https' : 'http'; + const url = `${protocol}://${host}${urlPath}`; + const headers = new Headers(); + if (event.node.req.headers) { + Object.entries(event.node.req.headers).forEach(([k, v]) => { + if (typeof v === 'string') headers.set(k, v); + else if (Array.isArray(v)) headers.set(k, v.join(', ')); + }); + } + const init: RequestInit = { method, headers }; + if (method !== 'GET' && method !== 'HEAD') { + const body = await readBody(event); + if (body) { + init.body = typeof body === 'string' ? body : JSON.stringify(body); + if (!headers.has('content-type')) { + headers.set('content-type', 'application/json'); + } + } + } + const webRequest = new Request(url, init); + const response = await authService.handleRequest(webRequest); + setResponseStatus(event, response.status); + response.headers.forEach((v: string, k: string) => setResponseHeader(event, k, v)); + return await response.text(); + } + + // Fallback to dispatcher + const body = method === 'GET' || method === 'HEAD' + ? {} + : await readBody(event).catch(() => ({})); + const result = await dispatcher.handleAuth(path, method, body, { request: event.node.req }); + return toResponse(event, result); + } catch (err: any) { + return errorJson(event, err.message || 'Internal Server Error', err.statusCode || 500); + } + }), + ); + + // --- GraphQL --- + router.post( + `${prefix}/graphql`, + defineEventHandler(async (event) => { + try { + const body = await readBody(event); + const result = await dispatcher.handleGraphQL(body, { request: event.node.req }); + return result; + } catch (err: any) { + return errorJson(event, err.message || 'Internal Server Error', err.statusCode || 500); + } + }), + ); + + // --- Metadata --- + router.use( + `${prefix}/meta/**`, + defineEventHandler(async (event) => { + try { + const urlPath = (event.path || event.node.req.url || '').split('?')[0]; + const subPath = urlPath.substring(`${prefix}/meta`.length); + const method = event.method; + const body = (method === 'PUT' || method === 'POST') + ? await readBody(event) + : undefined; + const result = await dispatcher.handleMetadata(subPath, { request: event.node.req }, method, body); + return toResponse(event, result); + } catch (err: any) { + return errorJson(event, err.message || 'Internal Server Error', err.statusCode || 500); + } + }), + ); + + router.use( + `${prefix}/meta`, + defineEventHandler(async (event) => { + try { + const method = event.method; + const body = (method === 'PUT' || method === 'POST') + ? await readBody(event) + : undefined; + const result = await dispatcher.handleMetadata('', { request: event.node.req }, method, body); + return toResponse(event, result); + } catch (err: any) { + return errorJson(event, err.message || 'Internal Server Error', err.statusCode || 500); + } + }), + ); + + // --- Data --- + router.use( + `${prefix}/data/**`, + defineEventHandler(async (event) => { + try { + const urlPath = (event.path || event.node.req.url || '').split('?')[0]; + const subPath = urlPath.substring(`${prefix}/data`.length); + const method = event.method; + const body = (method === 'POST' || method === 'PATCH') + ? await readBody(event) + : {}; + const query = getQuery(event); + const result = await dispatcher.handleData(subPath, method, body, query, { request: event.node.req }); + return toResponse(event, result); + } catch (err: any) { + return errorJson(event, err.message || 'Internal Server Error', err.statusCode || 500); + } + }), + ); + + // --- Storage --- + router.use( + `${prefix}/storage/**`, + defineEventHandler(async (event) => { + try { + const urlPath = (event.path || event.node.req.url || '').split('?')[0]; + const subPath = urlPath.substring(`${prefix}/storage`.length); + const method = event.method; + const file = undefined; // File upload requires multipart parsing (e.g., formidable) + const result = await dispatcher.handleStorage(subPath, method, file, { request: event.node.req }); + return toResponse(event, result); + } catch (err: any) { + return errorJson(event, err.message || 'Internal Server Error', err.statusCode || 500); + } + }), + ); + + return router; +} diff --git a/packages/adapters/nuxt/src/nuxt.test.ts b/packages/adapters/nuxt/src/nuxt.test.ts new file mode 100644 index 0000000000..d9198ef2cc --- /dev/null +++ b/packages/adapters/nuxt/src/nuxt.test.ts @@ -0,0 +1,133 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +// Mock dispatcher instance +const mockDispatcher = { + getDiscoveryInfo: vi.fn().mockReturnValue({ version: '1.0', endpoints: [] }), + handleAuth: vi.fn().mockResolvedValue({ handled: true, response: { body: { ok: true }, status: 200 } }), + handleGraphQL: vi.fn().mockResolvedValue({ data: {} }), + handleMetadata: vi.fn().mockResolvedValue({ handled: true, response: { body: { objects: [] }, status: 200 } }), + handleData: vi.fn().mockResolvedValue({ handled: true, response: { body: { records: [] }, status: 200 } }), + handleStorage: vi.fn().mockResolvedValue({ handled: true, response: { body: {}, status: 200 } }), +}; + +vi.mock('@objectstack/runtime', () => { + return { + HttpDispatcher: function HttpDispatcher() { + return mockDispatcher; + }, + }; +}); + +import { createApp, toNodeListener, createEvent } from 'h3'; +import { createH3Router } from './index'; +import http from 'node:http'; + +const mockKernel = { name: 'test-kernel' } as any; + +function createTestApp() { + const app = createApp(); + const router = createH3Router({ kernel: mockKernel }); + app.use(router); + return app; +} + +async function makeRequest(app: any, url: string, method = 'GET', body?: any) { + const listener = toNodeListener(app); + + return new Promise<{ status: number; body: any }>((resolve, reject) => { + const server = http.createServer(listener); + server.listen(0, () => { + const addr = server.address() as any; + const reqOpts: http.RequestOptions = { + hostname: 'localhost', + port: addr.port, + path: url, + method, + headers: body ? { 'Content-Type': 'application/json' } : {}, + }; + const req = http.request(reqOpts, (res) => { + let data = ''; + res.on('data', (chunk) => { data += chunk; }); + res.on('end', () => { + server.close(); + try { + resolve({ status: res.statusCode || 500, body: JSON.parse(data) }); + } catch { + resolve({ status: res.statusCode || 500, body: data }); + } + }); + }); + req.on('error', (err) => { server.close(); reject(err); }); + if (body) req.write(JSON.stringify(body)); + req.end(); + }); + }); +} + +describe('createH3Router', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('handles discovery endpoint', async () => { + const app = createTestApp(); + const res = await makeRequest(app, '/api'); + expect(res.status).toBe(200); + expect(res.body.data).toBeDefined(); + expect(res.body.data.version).toBe('1.0'); + expect(mockDispatcher.getDiscoveryInfo).toHaveBeenCalledWith('/api'); + }); + + it('handles auth route', async () => { + const app = createTestApp(); + const res = await makeRequest(app, '/api/auth/login', 'POST', { email: 'a@b.com' }); + expect(res.status).toBe(200); + expect(res.body.ok).toBe(true); + expect(mockDispatcher.handleAuth).toHaveBeenCalled(); + }); + + it('handles graphql route', async () => { + const app = createTestApp(); + const body = { query: '{ objects { name } }' }; + const res = await makeRequest(app, '/api/graphql', 'POST', body); + expect(res.status).toBe(200); + expect(res.body.data).toBeDefined(); + expect(mockDispatcher.handleGraphQL).toHaveBeenCalled(); + }); + + it('handles metadata route', async () => { + const app = createTestApp(); + const res = await makeRequest(app, '/api/meta/objects'); + expect(res.status).toBe(200); + expect(res.body.objects).toBeDefined(); + expect(mockDispatcher.handleMetadata).toHaveBeenCalled(); + }); + + it('handles data route', async () => { + const app = createTestApp(); + const res = await makeRequest(app, '/api/data/account'); + expect(res.status).toBe(200); + expect(res.body.records).toBeDefined(); + expect(mockDispatcher.handleData).toHaveBeenCalled(); + }); + + it('handles storage route', async () => { + const app = createTestApp(); + const res = await makeRequest(app, '/api/storage/files'); + expect(res.status).toBe(200); + expect(mockDispatcher.handleStorage).toHaveBeenCalled(); + }); + + it('handles errors', async () => { + mockDispatcher.handleData.mockRejectedValueOnce( + Object.assign(new Error('Forbidden'), { statusCode: 403 }), + ); + const app = createTestApp(); + const res = await makeRequest(app, '/api/data/account'); + expect(res.status).toBe(403); + expect(res.body.success).toBe(false); + expect(res.body.error.message).toBe('Forbidden'); + }); +}); diff --git a/packages/adapters/nuxt/tsconfig.json b/packages/adapters/nuxt/tsconfig.json new file mode 100644 index 0000000000..66ec4500ef --- /dev/null +++ b/packages/adapters/nuxt/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "esModuleInterop": true, + "skipLibCheck": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/adapters/nuxt/vitest.config.ts b/packages/adapters/nuxt/vitest.config.ts new file mode 100644 index 0000000000..e2051df210 --- /dev/null +++ b/packages/adapters/nuxt/vitest.config.ts @@ -0,0 +1,16 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +import { defineConfig } from 'vitest/config'; +import path from 'node:path'; + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + }, + resolve: { + alias: { + '@objectstack/runtime': path.resolve(__dirname, 'src/__mocks__/runtime.ts'), + }, + }, +}); diff --git a/packages/adapters/sveltekit/README.md b/packages/adapters/sveltekit/README.md new file mode 100644 index 0000000000..d3bc3662b4 --- /dev/null +++ b/packages/adapters/sveltekit/README.md @@ -0,0 +1,25 @@ +# @objectstack/sveltekit + +The official SvelteKit adapter for ObjectStack. + +## Features +- SvelteKit API route handler +- Full Auth/GraphQL/Metadata/Data/Storage routes +- AuthPlugin service support +- Handle hook for attaching kernel to event.locals + +## Usage + +```typescript +// src/routes/api/[...path]/+server.ts +import { createRequestHandler } from '@objectstack/sveltekit'; +import { kernel } from '$lib/kernel'; + +const handler = createRequestHandler({ kernel }); + +export const GET = handler; +export const POST = handler; +export const PUT = handler; +export const PATCH = handler; +export const DELETE = handler; +``` diff --git a/packages/adapters/sveltekit/package.json b/packages/adapters/sveltekit/package.json new file mode 100644 index 0000000000..adb8f1e341 --- /dev/null +++ b/packages/adapters/sveltekit/package.json @@ -0,0 +1,29 @@ +{ + "name": "@objectstack/sveltekit", + "version": "3.0.2", + "license": "Apache-2.0", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.mjs", + "require": "./dist/index.js" + } + }, + "scripts": { + "build": "tsup --config ../../../tsup.config.ts", + "test": "vitest run", + "test:watch": "vitest" + }, + "peerDependencies": { + "@objectstack/runtime": "workspace:*", + "@sveltejs/kit": "^2.21.5" + }, + "devDependencies": { + "@objectstack/runtime": "workspace:*", + "@sveltejs/kit": "^2.21.5", + "typescript": "^5.0.0", + "vitest": "^4.0.18" + } +} diff --git a/packages/adapters/sveltekit/src/__mocks__/runtime.ts b/packages/adapters/sveltekit/src/__mocks__/runtime.ts new file mode 100644 index 0000000000..7961fd466c --- /dev/null +++ b/packages/adapters/sveltekit/src/__mocks__/runtime.ts @@ -0,0 +1,4 @@ +// Stub for @objectstack/runtime - replaced by vi.mock in tests +export const HttpDispatcher = class {}; +export type ObjectKernel = any; +export type HttpDispatcherResult = any; diff --git a/packages/adapters/sveltekit/src/index.ts b/packages/adapters/sveltekit/src/index.ts new file mode 100644 index 0000000000..4e9dd7595c --- /dev/null +++ b/packages/adapters/sveltekit/src/index.ts @@ -0,0 +1,213 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +import { type ObjectKernel, HttpDispatcher, HttpDispatcherResult } from '@objectstack/runtime'; + +export interface SvelteKitAdapterOptions { + kernel: ObjectKernel; + prefix?: string; +} + +/** + * Auth service interface with handleRequest method + */ +interface AuthService { + handleRequest(request: Request): Promise; +} + +/** + * SvelteKit request event type (minimal interface to avoid hard dependency on @sveltejs/kit types at runtime) + */ +interface RequestEvent { + request: Request; + url: URL; + params: Record; +} + +/** + * Creates a SvelteKit request handler for ObjectStack API routes. + * Use in a catch-all `+server.ts` route like `src/routes/api/[...path]/+server.ts`. + * + * @example + * ```ts + * // src/routes/api/[...path]/+server.ts + * import { createRequestHandler } from '@objectstack/sveltekit'; + * import { kernel } from '$lib/kernel'; + * + * const handler = createRequestHandler({ kernel }); + * + * export const GET = handler; + * export const POST = handler; + * export const PUT = handler; + * export const PATCH = handler; + * export const DELETE = handler; + * ``` + */ +export function createRequestHandler(options: SvelteKitAdapterOptions) { + const dispatcher = new HttpDispatcher(options.kernel); + const prefix = options.prefix || '/api'; + + const errorJson = (message: string, code: number = 500) => { + return new Response(JSON.stringify({ success: false, error: { message, code } }), { + status: code, + headers: { 'Content-Type': 'application/json' }, + }); + }; + + const toResponse = (result: HttpDispatcherResult): Response => { + if (result.handled) { + if (result.response) { + const headers = new Headers({ 'Content-Type': 'application/json' }); + if (result.response.headers) { + Object.entries(result.response.headers).forEach(([k, v]) => headers.set(k, v as string)); + } + return new Response(JSON.stringify(result.response.body), { + status: result.response.status, + headers, + }); + } + if (result.result) { + const res = result.result; + if (res.type === 'redirect' && res.url) { + return new Response(null, { + status: 302, + headers: { Location: res.url }, + }); + } + if (res.type === 'stream' && res.stream) { + const headers = new Headers(); + if (res.headers) { + Object.entries(res.headers).forEach(([k, v]) => headers.set(k, v as string)); + } + return new Response(res.stream, { status: 200, headers }); + } + return new Response(JSON.stringify(res), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }); + } + } + return errorJson('Not Found', 404); + }; + + return async function handler(event: RequestEvent): Promise { + const { request, url } = event; + const method = request.method; + const path = url.pathname.substring(prefix.length); + const segments = path.split('/').filter(Boolean); + + // --- Discovery --- + if (segments.length === 0 && method === 'GET') { + return new Response(JSON.stringify({ data: dispatcher.getDiscoveryInfo(prefix) }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }); + } + + try { + // --- Auth --- + if (segments[0] === 'auth') { + const subPath = segments.slice(1).join('/'); + + // Try AuthPlugin service first + const authService = typeof options.kernel.getService === 'function' + ? options.kernel.getService('auth') + : null; + + if (authService && typeof authService.handleRequest === 'function') { + return await authService.handleRequest(request); + } + + // Fallback to dispatcher + const body = method === 'GET' || method === 'HEAD' + ? {} + : await request.json().catch(() => ({})); + const result = await dispatcher.handleAuth(subPath, method, body, { request }); + return toResponse(result); + } + + // --- GraphQL --- + if (segments[0] === 'graphql' && method === 'POST') { + const body = await request.json(); + const result = await dispatcher.handleGraphQL(body, { request }); + return new Response(JSON.stringify(result), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }); + } + + // --- Metadata --- + if (segments[0] === 'meta') { + const subPath = segments.slice(1).join('/'); + let body: any = undefined; + if (method === 'PUT' || method === 'POST') { + body = await request.json().catch(() => ({})); + } + const result = await dispatcher.handleMetadata( + subPath ? `/${subPath}` : '', + { request }, + method, + body, + ); + return toResponse(result); + } + + // --- Data --- + if (segments[0] === 'data') { + const subPath = segments.slice(1).join('/'); + let body: any = {}; + if (method === 'POST' || method === 'PATCH') { + body = await request.json().catch(() => ({})); + } + const queryParams: Record = {}; + url.searchParams.forEach((val, key) => { queryParams[key] = val; }); + + const result = await dispatcher.handleData( + subPath ? `/${subPath}` : '', + method, + body, + queryParams, + { request }, + ); + return toResponse(result); + } + + // --- Storage --- + if (segments[0] === 'storage') { + const subPath = segments.slice(1).join('/'); + let file: any = undefined; + if (method === 'POST' && subPath === 'upload') { + const formData = await request.formData(); + file = formData.get('file'); + } + const result = await dispatcher.handleStorage( + subPath ? `/${subPath}` : '', + method, + file, + { request }, + ); + return toResponse(result); + } + + return errorJson('Not Found', 404); + } catch (err: any) { + return errorJson(err.message || 'Internal Server Error', err.statusCode || 500); + } + }; +} + +/** + * Creates a SvelteKit handle hook that attaches the kernel to event.locals. + * + * @example + * ```ts + * // src/hooks.server.ts + * import { createHandle } from '@objectstack/sveltekit'; + * export const handle = createHandle({ kernel }); + * ``` + */ +export function createHandle(options: SvelteKitAdapterOptions) { + return async function handle({ event, resolve }: { event: any; resolve: (event: any) => Promise }) { + event.locals.objectStack = options.kernel; + return resolve(event); + }; +} diff --git a/packages/adapters/sveltekit/src/sveltekit.test.ts b/packages/adapters/sveltekit/src/sveltekit.test.ts new file mode 100644 index 0000000000..822d4d59aa --- /dev/null +++ b/packages/adapters/sveltekit/src/sveltekit.test.ts @@ -0,0 +1,249 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +// Mock dispatcher instance +const mockDispatcher = { + getDiscoveryInfo: vi.fn().mockReturnValue({ version: '1.0', endpoints: [] }), + handleAuth: vi.fn().mockResolvedValue({ handled: true, response: { body: { ok: true }, status: 200 } }), + handleGraphQL: vi.fn().mockResolvedValue({ data: {} }), + handleMetadata: vi.fn().mockResolvedValue({ handled: true, response: { body: { objects: [] }, status: 200 } }), + handleData: vi.fn().mockResolvedValue({ handled: true, response: { body: { records: [] }, status: 200 } }), + handleStorage: vi.fn().mockResolvedValue({ handled: true, response: { body: {}, status: 200 } }), +}; + +vi.mock('@objectstack/runtime', () => { + return { + HttpDispatcher: function HttpDispatcher() { + return mockDispatcher; + }, + }; +}); + +import { createRequestHandler, createHandle } from './index'; + +const mockKernel = { name: 'test-kernel' } as any; + +function makeEvent(url: string, method = 'GET', body?: any): any { + const parsedUrl = new URL(url, 'http://localhost'); + const init: RequestInit = { method }; + if (body) { + init.body = JSON.stringify(body); + init.headers = { 'Content-Type': 'application/json' }; + } + return { + request: new Request(parsedUrl, init), + url: parsedUrl, + params: {}, + }; +} + +describe('createRequestHandler', () => { + let handler: ReturnType; + + beforeEach(() => { + vi.clearAllMocks(); + handler = createRequestHandler({ kernel: mockKernel }); + }); + + describe('Discovery', () => { + it('GET /api returns discovery info', async () => { + const event = makeEvent('http://localhost/api'); + const res = await handler(event); + expect(res.status).toBe(200); + const json = await res.json(); + expect(json.data.version).toBe('1.0'); + expect(mockDispatcher.getDiscoveryInfo).toHaveBeenCalledWith('/api'); + }); + + it('uses custom prefix', async () => { + const customHandler = createRequestHandler({ kernel: mockKernel, prefix: '/v2' }); + const event = makeEvent('http://localhost/v2'); + const res = await customHandler(event); + expect(res.status).toBe(200); + expect(mockDispatcher.getDiscoveryInfo).toHaveBeenCalledWith('/v2'); + }); + }); + + describe('Auth', () => { + it('POST /api/auth/login calls handleAuth', async () => { + const event = makeEvent('http://localhost/api/auth/login', 'POST', { email: 'a@b.com' }); + const res = await handler(event); + expect(res.status).toBe(200); + const json = await res.json(); + expect(json.ok).toBe(true); + expect(mockDispatcher.handleAuth).toHaveBeenCalledWith( + 'login', 'POST', { email: 'a@b.com' }, + expect.objectContaining({ request: expect.anything() }), + ); + }); + + it('GET /api/auth/callback calls handleAuth with empty body', async () => { + const event = makeEvent('http://localhost/api/auth/callback'); + const res = await handler(event); + expect(res.status).toBe(200); + expect(mockDispatcher.handleAuth).toHaveBeenCalledWith( + 'callback', 'GET', {}, + expect.objectContaining({ request: expect.anything() }), + ); + }); + + it('returns error on exception', async () => { + mockDispatcher.handleAuth.mockRejectedValueOnce( + Object.assign(new Error('Unauthorized'), { statusCode: 401 }), + ); + const event = makeEvent('http://localhost/api/auth/login', 'POST', {}); + const res = await handler(event); + expect(res.status).toBe(401); + const json = await res.json(); + expect(json.success).toBe(false); + }); + }); + + describe('Auth via AuthPlugin service', () => { + it('uses kernel.getService("auth") when available', async () => { + const mockHandleRequest = vi.fn().mockResolvedValue( + new Response(JSON.stringify({ user: { id: '1' } }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }), + ); + const kernelWithAuth = { + ...mockKernel, + getService: vi.fn().mockReturnValue({ handleRequest: mockHandleRequest }), + }; + const authHandler = createRequestHandler({ kernel: kernelWithAuth }); + const event = makeEvent('http://localhost/api/auth/sign-in/email', 'POST', { email: 'a@b.com' }); + const res = await authHandler(event); + expect(res.status).toBe(200); + expect(kernelWithAuth.getService).toHaveBeenCalledWith('auth'); + expect(mockHandleRequest).toHaveBeenCalled(); + }); + }); + + describe('GraphQL', () => { + it('POST /api/graphql calls handleGraphQL', async () => { + const body = { query: '{ objects { name } }' }; + const event = makeEvent('http://localhost/api/graphql', 'POST', body); + const res = await handler(event); + expect(res.status).toBe(200); + const json = await res.json(); + expect(json.data).toBeDefined(); + expect(mockDispatcher.handleGraphQL).toHaveBeenCalledWith( + body, expect.objectContaining({ request: expect.anything() }), + ); + }); + }); + + describe('Metadata', () => { + it('GET /api/meta/objects calls handleMetadata', async () => { + const event = makeEvent('http://localhost/api/meta/objects'); + const res = await handler(event); + expect(res.status).toBe(200); + expect(mockDispatcher.handleMetadata).toHaveBeenCalledWith( + '/objects', + expect.objectContaining({ request: expect.anything() }), + 'GET', undefined, + ); + }); + }); + + describe('Data', () => { + it('GET /api/data/account calls handleData', async () => { + const event = makeEvent('http://localhost/api/data/account'); + const res = await handler(event); + expect(res.status).toBe(200); + const json = await res.json(); + expect(json.records).toBeDefined(); + expect(mockDispatcher.handleData).toHaveBeenCalledWith( + '/account', 'GET', {}, + expect.any(Object), + expect.objectContaining({ request: expect.anything() }), + ); + }); + + it('POST /api/data/account parses body', async () => { + const body = { name: 'Acme' }; + const event = makeEvent('http://localhost/api/data/account', 'POST', body); + const res = await handler(event); + expect(res.status).toBe(200); + expect(mockDispatcher.handleData).toHaveBeenCalledWith( + '/account', 'POST', body, + expect.any(Object), + expect.objectContaining({ request: expect.anything() }), + ); + }); + + it('returns 404 when not handled', async () => { + mockDispatcher.handleData.mockResolvedValueOnce({ handled: false }); + const event = makeEvent('http://localhost/api/data/missing'); + const res = await handler(event); + expect(res.status).toBe(404); + }); + }); + + describe('Storage', () => { + it('GET /api/storage/files calls handleStorage', async () => { + const event = makeEvent('http://localhost/api/storage/files'); + const res = await handler(event); + expect(res.status).toBe(200); + expect(mockDispatcher.handleStorage).toHaveBeenCalledWith( + '/files', 'GET', undefined, + expect.objectContaining({ request: expect.anything() }), + ); + }); + }); + + describe('Error handling', () => { + it('returns 404 for unknown routes', async () => { + const event = makeEvent('http://localhost/api/unknown'); + const res = await handler(event); + expect(res.status).toBe(404); + }); + + it('returns 500 on generic error', async () => { + mockDispatcher.handleData.mockRejectedValueOnce(new Error()); + const event = makeEvent('http://localhost/api/data/account'); + const res = await handler(event); + expect(res.status).toBe(500); + }); + }); + + describe('toResponse', () => { + it('handles redirect result', async () => { + mockDispatcher.handleData.mockResolvedValueOnce({ + handled: true, + result: { type: 'redirect', url: 'https://example.com' }, + }); + const event = makeEvent('http://localhost/api/data/redir'); + const res = await handler(event); + expect(res.status).toBe(302); + expect(res.headers.get('location')).toBe('https://example.com'); + }); + + it('handles generic result objects', async () => { + mockDispatcher.handleData.mockResolvedValueOnce({ + handled: true, + result: { foo: 'bar' }, + }); + const event = makeEvent('http://localhost/api/data/custom'); + const res = await handler(event); + expect(res.status).toBe(200); + const json = await res.json(); + expect(json.foo).toBe('bar'); + }); + }); +}); + +describe('createHandle', () => { + it('attaches kernel to event.locals', async () => { + const handle = createHandle({ kernel: mockKernel }); + const event: any = { locals: {} }; + const resolve = vi.fn().mockResolvedValue(new Response('ok')); + + await handle({ event, resolve }); + + expect(event.locals.objectStack).toBe(mockKernel); + expect(resolve).toHaveBeenCalledWith(event); + }); +}); diff --git a/packages/adapters/sveltekit/tsconfig.json b/packages/adapters/sveltekit/tsconfig.json new file mode 100644 index 0000000000..66ec4500ef --- /dev/null +++ b/packages/adapters/sveltekit/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "esModuleInterop": true, + "skipLibCheck": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/adapters/sveltekit/vitest.config.ts b/packages/adapters/sveltekit/vitest.config.ts new file mode 100644 index 0000000000..e2051df210 --- /dev/null +++ b/packages/adapters/sveltekit/vitest.config.ts @@ -0,0 +1,16 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +import { defineConfig } from 'vitest/config'; +import path from 'node:path'; + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + }, + resolve: { + alias: { + '@objectstack/runtime': path.resolve(__dirname, 'src/__mocks__/runtime.ts'), + }, + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 833404aaf7..137b9265d6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -337,6 +337,39 @@ importers: specifier: ^5.3.0 version: 5.9.3 + packages/adapters/express: + devDependencies: + '@objectstack/runtime': + specifier: workspace:* + version: link:../../runtime + '@types/express': + specifier: ^5.0.3 + version: 5.0.6 + express: + specifier: ^5.1.0 + version: 5.2.1 + typescript: + specifier: ^5.0.0 + version: 5.9.3 + vitest: + specifier: ^4.0.18 + version: 4.0.18(@types/node@25.2.2)(happy-dom@20.5.3)(jiti@2.6.1)(lightningcss@1.30.2)(msw@2.12.9(@types/node@25.2.2)(typescript@5.9.3))(tsx@4.21.0) + + packages/adapters/fastify: + devDependencies: + '@objectstack/runtime': + specifier: workspace:* + version: link:../../runtime + fastify: + specifier: ^5.7.2 + version: 5.7.4 + typescript: + specifier: ^5.0.0 + version: 5.9.3 + vitest: + specifier: ^4.0.18 + version: 4.0.18(@types/node@25.2.2)(happy-dom@20.5.3)(jiti@2.6.1)(lightningcss@1.30.2)(msw@2.12.9(@types/node@25.2.2)(typescript@5.9.3))(tsx@4.21.0) + packages/adapters/hono: devDependencies: '@objectstack/runtime': @@ -391,6 +424,36 @@ importers: specifier: ^4.0.18 version: 4.0.18(@types/node@25.2.2)(happy-dom@20.5.3)(jiti@2.6.1)(lightningcss@1.30.2)(msw@2.12.9(@types/node@25.2.2)(typescript@5.9.3))(tsx@4.21.0) + packages/adapters/nuxt: + devDependencies: + '@objectstack/runtime': + specifier: workspace:* + version: link:../../runtime + h3: + specifier: ^1.15.5 + version: 1.15.5 + typescript: + specifier: ^5.0.0 + version: 5.9.3 + vitest: + specifier: ^4.0.18 + version: 4.0.18(@types/node@25.2.2)(happy-dom@20.5.3)(jiti@2.6.1)(lightningcss@1.30.2)(msw@2.12.9(@types/node@25.2.2)(typescript@5.9.3))(tsx@4.21.0) + + packages/adapters/sveltekit: + devDependencies: + '@objectstack/runtime': + specifier: workspace:* + version: link:../../runtime + '@sveltejs/kit': + specifier: ^2.21.5 + version: 2.51.0(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.50.3)(vite@7.3.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)))(svelte@5.50.3)(typescript@5.9.3)(vite@7.3.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)) + typescript: + specifier: ^5.0.0 + version: 5.9.3 + vitest: + specifier: ^4.0.18 + version: 4.0.18(@types/node@25.2.2)(happy-dom@20.5.3)(jiti@2.6.1)(lightningcss@1.30.2)(msw@2.12.9(@types/node@25.2.2)(typescript@5.9.3))(tsx@4.21.0) + packages/cli: dependencies: '@objectstack/core': @@ -637,7 +700,7 @@ importers: version: link:../../spec better-auth: specifier: ^1.4.18 - version: 1.4.18(drizzle-orm@0.41.0(kysely@0.28.11))(next@16.1.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@4.0.18(@types/node@25.2.2)(happy-dom@20.5.3)(jiti@2.6.1)(lightningcss@1.30.2)(msw@2.12.9(@types/node@25.2.2)(typescript@5.9.3))(tsx@4.21.0)) + version: 1.4.18(@sveltejs/kit@2.51.0(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.50.3)(vite@7.3.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)))(svelte@5.50.3)(typescript@5.9.3)(vite@7.3.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)))(drizzle-orm@0.41.0(kysely@0.28.11))(next@16.1.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.50.3)(vitest@4.0.18(@types/node@25.2.2)(happy-dom@20.5.3)(jiti@2.6.1)(lightningcss@1.30.2)(msw@2.12.9(@types/node@25.2.2)(typescript@5.9.3))(tsx@4.21.0)) devDependencies: '@objectstack/cli': specifier: workspace:* @@ -1400,6 +1463,24 @@ packages: cpu: [x64] os: [win32] + '@fastify/ajv-compiler@4.0.5': + resolution: {integrity: sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A==} + + '@fastify/error@4.2.0': + resolution: {integrity: sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==} + + '@fastify/fast-json-stringify-compiler@5.0.3': + resolution: {integrity: sha512-uik7yYHkLr6fxd8hJSZ8c+xF4WafPK+XzneQDPU+D10r5X19GW8lJcom2YijX2+qtFF1ENJlHXKFM9ouXNJYgQ==} + + '@fastify/forwarded@3.0.1': + resolution: {integrity: sha512-JqDochHFqXs3C3Ml3gOY58zM7OqO9ENqPo0UqAjAjH8L01fRZqwX9iLeX34//kiJubF7r2ZQHtBRU36vONbLlw==} + + '@fastify/merge-json-schemas@0.2.1': + resolution: {integrity: sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==} + + '@fastify/proxy-addr@5.1.0': + resolution: {integrity: sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==} + '@floating-ui/core@1.7.3': resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==} @@ -1787,6 +1868,9 @@ packages: '@pinojs/redact@0.4.0': resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} + '@polka/url@1.0.0-next.29': + resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + '@radix-ui/number@1.1.1': resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} @@ -2658,6 +2742,42 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@sveltejs/acorn-typescript@1.0.9': + resolution: {integrity: sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA==} + peerDependencies: + acorn: ^8.9.0 + + '@sveltejs/kit@2.51.0': + resolution: {integrity: sha512-BkcfxVVYmfxHGYLugemb7TWW+y/MNZbxXHXJ141lyyi2ozq0Q4kbqoV0cBGlh7F0vNtCMvfr7UPnxIaBjcP9gg==} + engines: {node: '>=18.13'} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.0.0 + '@sveltejs/vite-plugin-svelte': ^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 + svelte: ^4.0.0 || ^5.0.0-next.0 + typescript: ^5.3.3 + vite: ^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + typescript: + optional: true + + '@sveltejs/vite-plugin-svelte-inspector@5.0.2': + resolution: {integrity: sha512-TZzRTcEtZffICSAoZGkPSl6Etsj2torOVrx6Uw0KpXxrec9Gg6jFWQ60Q3+LmNGfZSxHRCZL7vXVZIWmuV50Ig==} + engines: {node: ^20.19 || ^22.12 || >=24} + peerDependencies: + '@sveltejs/vite-plugin-svelte': ^6.0.0-next.0 + svelte: ^5.0.0 + vite: ^6.3.0 || ^7.0.0 + + '@sveltejs/vite-plugin-svelte@6.2.4': + resolution: {integrity: sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA==} + engines: {node: ^20.19 || ^22.12 || >=24} + peerDependencies: + svelte: ^5.0.0 + vite: ^6.3.0 || ^7.0.0 + '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} @@ -2795,9 +2915,18 @@ packages: '@types/babel__traverse@7.28.0': resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + '@types/body-parser@1.19.6': + resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} + '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + '@types/connect@3.4.38': + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + + '@types/cookie@0.6.0': + resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} + '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} @@ -2810,9 +2939,18 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/express-serve-static-core@5.1.1': + resolution: {integrity: sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==} + + '@types/express@5.0.6': + resolution: {integrity: sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==} + '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + '@types/http-errors@2.0.5': + resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} + '@types/js-yaml@4.0.9': resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==} @@ -2837,6 +2975,12 @@ packages: '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} + '@types/qs@6.14.0': + resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==} + + '@types/range-parser@1.2.7': + resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + '@types/react-dom@19.2.3': resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} peerDependencies: @@ -2848,6 +2992,12 @@ packages: '@types/sarif@2.1.7': resolution: {integrity: sha512-kRz0VEkJqWLf1LLVN4pT1cg1Z9wAuvI6L97V3m2f5B76Tg8d413ddvLBPTEHAZJlnn4XSvu0FkZtViCQGVyrXQ==} + '@types/send@1.2.1': + resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==} + + '@types/serve-static@2.2.0': + resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==} + '@types/statuses@2.0.6': resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==} @@ -2970,6 +3120,13 @@ packages: engines: {node: '>= 20'} hasBin: true + abstract-logging@2.0.1: + resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==} + + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -2988,6 +3145,14 @@ packages: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + ajv@8.17.1: resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} @@ -3027,6 +3192,10 @@ packages: resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} engines: {node: '>=10'} + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + array-union@2.1.0: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} @@ -3060,6 +3229,13 @@ packages: peerDependencies: postcss: ^8.1.0 + avvio@9.2.0: + resolution: {integrity: sha512-2t/sy01ArdHHE0vRH5Hsay+RtCZt3dLPji7W7/MMOCEgze5b7SNDC4j5H6FnVgPkI1MTNFGzHdHrVXDDl7QSSQ==} + + axobject-query@4.1.0: + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} + engines: {node: '>= 0.4'} + azure-devops-node-api@12.5.0: resolution: {integrity: sha512-R5eFskGvOm3U/GzeAuxRkUsAl0hrAwGgWn6zAd2KrZmrEhWZVqLew4OOupbQlXUuojUzpGtq62SmdhJ06N88og==} @@ -3157,6 +3333,10 @@ packages: bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + body-parser@2.2.2: + resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} + engines: {node: '>=18'} + boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} @@ -3194,6 +3374,10 @@ packages: peerDependencies: esbuild: '>=0.18' + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} @@ -3331,9 +3515,32 @@ packages: resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} engines: {node: ^14.18.0 || >=16.10.0} + content-disposition@1.0.1: + resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==} + engines: {node: '>=18'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie-es@1.2.2: + resolution: {integrity: sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==} + + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.6.0: + resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} + engines: {node: '>= 0.6'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + cookie@1.1.1: resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} engines: {node: '>=18'} @@ -3345,6 +3552,9 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + crossws@0.3.5: + resolution: {integrity: sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==} + css-select@5.2.2: resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==} @@ -3383,6 +3593,10 @@ packages: resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} engines: {node: '>=4.0.0'} + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + default-browser-id@5.0.1: resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==} engines: {node: '>=18'} @@ -3402,10 +3616,17 @@ packages: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} + destr@2.0.5: + resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} + detect-indent@6.1.0: resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} engines: {node: '>=8'} @@ -3417,6 +3638,9 @@ packages: detect-node-es@1.1.0: resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + devalue@5.6.2: + resolution: {integrity: sha512-nPRkjWzzDQlsejL1WVifk5rvcFi/y1onBRxjaFMjZeR9mFpqu2gmAZ9xUB9/IEanEP/vBtGeGganC/GO1fmufg==} + devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} @@ -3541,12 +3765,19 @@ packages: resolution: {integrity: sha512-UgGlf8IW75je7HZjNDpJdCv4cGJWIi6yumFdZ0R7A8/CIhQiWUjyGLCxdHpd8bmyD1gnkfUNK0oeOXqUS2cpfQ==} engines: {ecmascript: '>= es5', node: '>=4'} + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + electron-to-chromium@1.5.286: resolution: {integrity: sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==} emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + encoding-sniffer@0.2.1: resolution: {integrity: sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==} @@ -3616,15 +3847,24 @@ packages: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + escape-string-regexp@5.0.0: resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} engines: {node: '>=12'} + esm-env@1.2.2: + resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==} + esprima@4.0.1: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} engines: {node: '>=4'} hasBin: true + esrap@2.2.3: + resolution: {integrity: sha512-8fOS+GIGCQZl/ZIlhl59htOlms6U8NvX6ZYgYHpRU/b6tVSh3uHkOHZikl3D4cMbYM0JlpBe+p/BkZEi8J9XIQ==} + estree-util-attach-comments@3.0.0: resolution: {integrity: sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==} @@ -3649,6 +3889,10 @@ packages: estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + expand-template@2.0.3: resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} engines: {node: '>=6'} @@ -3657,6 +3901,10 @@ packages: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} @@ -3666,6 +3914,9 @@ packages: fast-copy@4.0.2: resolution: {integrity: sha512-ybA6PDXIXOXivLJK/z9e+Otk7ve13I4ckBvGO5I2RRmBU1gMHLVDJYEuJYhGwez7YNlYji2M2DvVU+a9mSFDlw==} + fast-decode-uri-component@1.0.1: + resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -3673,12 +3924,21 @@ packages: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} + fast-json-stringify@6.3.0: + resolution: {integrity: sha512-oRCntNDY/329HJPlmdNLIdogNtt6Vyjb1WuT01Soss3slIdyUp8kAcDU3saQTOquEK8KFVfwIIF7FebxUAu+yA==} + + fast-querystring@1.1.2: + resolution: {integrity: sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==} + fast-safe-stringify@2.1.1: resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} fast-uri@3.1.0: resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + fastify@5.7.4: + resolution: {integrity: sha512-e6l5NsRdaEP8rdD8VR0ErJASeyaRbzXYpmkrpr2SuvuMq6Si3lvsaVy5C+7gLanEkvjpMDzBXWE5HPeb/hgTxA==} + fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} @@ -3702,6 +3962,14 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + + find-my-way@9.4.0: + resolution: {integrity: sha512-5Ye4vHsypZRYtS01ob/iwHzGRUDELlsoCftI/OZFhcLs1M0tkGPcXldE80TAZC5yYuJMBPJQQ43UHlqbJWiX2w==} + engines: {node: '>=20'} + find-up@4.1.0: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} @@ -3717,6 +3985,10 @@ packages: resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} engines: {node: '>= 6'} + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + fraction.js@5.3.4: resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} @@ -3734,6 +4006,10 @@ packages: react-dom: optional: true + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} @@ -3929,6 +4205,9 @@ packages: resolution: {integrity: sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==} engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + h3@1.15.5: + resolution: {integrity: sha512-xEyq3rSl+dhGX2Lm0+eFQIAzlDN6Fs0EcC4f7BNUmzaRX/PTzeuM+Tr2lHB8FoXggsQIeXLj8EDVgs5ywxyxmg==} + happy-dom@20.5.3: resolution: {integrity: sha512-xqAxGnkRU0KNhheHpxb3uScqg/aehqUiVto/a9ApWMyNvnH9CAqHYq9dEPAovM6bOGbLstmTfGIln5ZIezEU0g==} engines: {node: '>=20.0.0'} @@ -3991,6 +4270,10 @@ packages: htmlparser2@10.1.0: resolution: {integrity: sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==} + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + http-proxy-agent@7.0.2: resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} engines: {node: '>= 14'} @@ -4040,6 +4323,17 @@ packages: inline-style-parser@0.2.7: resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + ipaddr.js@2.3.0: + resolution: {integrity: sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==} + engines: {node: '>= 10'} + + iron-webcrypto@1.2.1: + resolution: {integrity: sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==} + is-alphabetical@2.0.1: resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} @@ -4085,6 +4379,12 @@ packages: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + + 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'} @@ -4154,6 +4454,9 @@ packages: engines: {node: '>=6'} hasBin: true + json-schema-ref-resolver@3.0.0: + resolution: {integrity: sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==} + json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} @@ -4184,6 +4487,10 @@ packages: keytar@7.9.0: resolution: {integrity: sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ==} + kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + kysely@0.28.11: resolution: {integrity: sha512-zpGIFg0HuoC893rIjYX1BETkVWdDnzTzF5e0kWXJFg5lE0k1/LfNWBejrcnOFu8Q2Rfq/hTDTU7XLUM8QOrpzg==} engines: {node: '>=20.0.0'} @@ -4192,6 +4499,9 @@ packages: resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} engines: {node: '>=6'} + light-my-request@6.6.0: + resolution: {integrity: sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==} + lightningcss-android-arm64@1.30.2: resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==} engines: {node: '>= 12.0.0'} @@ -4280,6 +4590,9 @@ packages: resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.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'} @@ -4415,6 +4728,14 @@ packages: mdurl@2.0.0: resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -4532,10 +4853,18 @@ packages: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + mime-types@2.1.35: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + mime@1.6.0: resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} engines: {node: '>=4'} @@ -4592,6 +4921,10 @@ packages: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} + mrmime@2.0.1: + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} + engines: {node: '>=10'} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -4665,6 +4998,9 @@ packages: node-addon-api@4.3.0: resolution: {integrity: sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==} + node-mock-http@1.0.4: + resolution: {integrity: sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ==} + node-releases@2.0.27: resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} @@ -4698,6 +5034,10 @@ packages: resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} engines: {node: '>=14.0.0'} + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} @@ -4766,6 +5106,10 @@ packages: parse5@7.3.0: resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -4884,12 +5228,19 @@ packages: engines: {node: '>=10.13.0'} hasBin: true + process-warning@4.0.1: + resolution: {integrity: sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==} + process-warning@5.0.0: resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==} property-information@7.1.0: resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + pump@3.0.3: resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} @@ -4910,6 +5261,17 @@ packages: quick-format-unescaped@4.0.4: resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + radix3@1.1.2: + resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + rc-config-loader@4.1.3: resolution: {integrity: sha512-kD7FqML7l800i6pS6pvLyIE2ncbk9Du8Q0gp/4hMPhJU6ZxApkoLcGD8ZeqgiAlfwZ6BlETq6qqe+12DUL207w==} @@ -5066,6 +5428,10 @@ packages: resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + ret@0.5.0: + resolution: {integrity: sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==} + engines: {node: '>=10'} + rettime@0.10.1: resolution: {integrity: sha512-uyDrIlUEH37cinabq0AX4QbgV4HbFZ/gqoiunWQ1UqBtRvTTytwhNYjE++pO/MjPTZL5KQCf2bEoJ/BJNVQ5Kw==} @@ -5073,6 +5439,9 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + rollup@4.55.2: resolution: {integrity: sha512-PggGy4dhwx5qaW+CKBilA/98Ql9keyfnb7lh4SR6shQ91QQQi1ORJ1v4UinkdP2i87OBs9AQFooQylcrrRfIcg==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -5086,6 +5455,10 @@ packages: rou3@0.7.12: resolution: {integrity: sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg==} + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + run-applescript@7.1.0: resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} engines: {node: '>=18'} @@ -5096,9 +5469,16 @@ packages: rxjs@7.8.2: resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + sade@1.8.1: + resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} + engines: {node: '>=6'} + safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + safe-regex2@5.0.0: + resolution: {integrity: sha512-YwJwe5a51WlK7KbOJREPdjNrpViQBI3p4T50lfwPuDhZnE3XGVTlGvi+aolc5+RvxDD6bnUmjVsU9n1eboLUYw==} + safe-stable-stringify@2.5.0: resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} engines: {node: '>=10'} @@ -5142,9 +5522,23 @@ packages: engines: {node: '>=10'} hasBin: true + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + set-cookie-parser@2.7.2: resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + set-cookie-parser@3.0.1: + resolution: {integrity: sha512-n7Z7dXZhJbwuAHhNzkTti6Aw9QDDjZtm3JTpTGATIdNzdQz5GuFs22w90BcvF4INfnrL5xrX3oGsuqO5Dx3A1Q==} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + sharp@0.34.5: resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -5189,6 +5583,10 @@ packages: simple-get@4.0.1: resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} + sirv@3.0.2: + resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} + engines: {node: '>=18'} + slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} @@ -5319,6 +5717,10 @@ packages: resolution: {integrity: sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==} engines: {node: '>=14.18'} + svelte@5.50.3: + resolution: {integrity: sha512-5JCO8P/cFlwyfi1LeZ9uppMZvuaHWygyZmqxyKOIqbV3PoHKaddvV1C6njL/InpDXplNYZnAVEbn8mLslycBxQ==} + engines: {node: '>=18'} + table@6.9.0: resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==} engines: {node: '>=10.0.0'} @@ -5403,10 +5805,22 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + toad-cache@3.7.0: + resolution: {integrity: sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==} + engines: {node: '>=12'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + token-types@6.1.2: resolution: {integrity: sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==} engines: {node: '>=14.16'} + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + tough-cookie@6.0.0: resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==} engines: {node: '>=16'} @@ -5514,6 +5928,10 @@ packages: resolution: {integrity: sha512-JnTrzGu+zPV3aXIUhnyWJj4z/wigMsdYajGLIYakqyOW1nPllzXEJee0QQbHj+CTIQtXGlAjuK0UY+2xTyjVAw==} engines: {node: '>=20'} + type-is@2.0.1: + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + engines: {node: '>= 0.6'} + typed-rest-client@1.8.11: resolution: {integrity: sha512-5UvfMpd1oelmUPRbbaVnq+rHP7ng2cE4qoQkQeAqxRL6PklkxsM0g32/HL0yfvruK6ojQ5x8EE+HF4YV6DtuCA==} @@ -5536,6 +5954,9 @@ packages: resolution: {integrity: sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==} engines: {node: '>=18'} + uncrypto@0.1.3: + resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==} + underscore@1.13.7: resolution: {integrity: sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==} @@ -5586,6 +6007,10 @@ packages: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + until-async@3.0.2: resolution: {integrity: sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==} @@ -5636,6 +6061,10 @@ packages: validate-npm-package-license@3.0.4: resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + version-range@4.15.0: resolution: {integrity: sha512-Ck0EJbAGxHwprkzFO966t4/5QkRuzh+/I1RxhLgUKKwEn+Cd8NwM60mE3AqBZg5gYODoXW0EFsQvbZjRlvdqbg==} engines: {node: '>=4'} @@ -5686,6 +6115,14 @@ packages: yaml: optional: true + vitefu@1.1.1: + resolution: {integrity: sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ==} + peerDependencies: + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0 + peerDependenciesMeta: + vite: + optional: true + vitest@4.0.18: resolution: {integrity: sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -5810,6 +6247,9 @@ packages: resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==} engines: {node: '>=18'} + zimmerframe@1.1.4: + resolution: {integrity: sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==} + zod@4.3.6: resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} @@ -6062,7 +6502,7 @@ snapshots: outdent: 0.5.0 prettier: 2.8.8 resolve-from: 5.0.0 - semver: 7.7.3 + semver: 7.7.4 '@changesets/assemble-release-plan@6.0.9': dependencies: @@ -6071,7 +6511,7 @@ snapshots: '@changesets/should-skip-package': 0.1.2 '@changesets/types': 6.1.0 '@manypkg/get-packages': 1.1.3 - semver: 7.7.3 + semver: 7.7.4 '@changesets/changelog-git@0.2.1': dependencies: @@ -6129,7 +6569,7 @@ snapshots: '@changesets/types': 6.1.0 '@manypkg/get-packages': 1.1.3 picocolors: 1.1.1 - semver: 7.7.3 + semver: 7.7.4 '@changesets/get-release-plan@4.0.14': dependencies: @@ -6357,6 +6797,29 @@ snapshots: '@esbuild/win32-x64@0.27.3': optional: true + '@fastify/ajv-compiler@4.0.5': + dependencies: + ajv: 8.17.1 + ajv-formats: 3.0.1(ajv@8.17.1) + fast-uri: 3.1.0 + + '@fastify/error@4.2.0': {} + + '@fastify/fast-json-stringify-compiler@5.0.3': + dependencies: + fast-json-stringify: 6.3.0 + + '@fastify/forwarded@3.0.1': {} + + '@fastify/merge-json-schemas@0.2.1': + dependencies: + dequal: 2.0.3 + + '@fastify/proxy-addr@5.1.0': + dependencies: + '@fastify/forwarded': 3.0.1 + ipaddr.js: 2.3.0 + '@floating-ui/core@1.7.3': dependencies: '@floating-ui/utils': 0.2.10 @@ -6697,6 +7160,8 @@ snapshots: '@pinojs/redact@0.4.0': {} + '@polka/url@1.0.0-next.29': {} + '@radix-ui/number@1.1.1': {} '@radix-ui/primitive@1.1.3': {} @@ -7534,6 +7999,48 @@ snapshots: '@standard-schema/spec@1.1.0': {} + '@sveltejs/acorn-typescript@1.0.9(acorn@8.15.0)': + dependencies: + acorn: 8.15.0 + + '@sveltejs/kit@2.51.0(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.50.3)(vite@7.3.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)))(svelte@5.50.3)(typescript@5.9.3)(vite@7.3.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0))': + dependencies: + '@standard-schema/spec': 1.1.0 + '@sveltejs/acorn-typescript': 1.0.9(acorn@8.15.0) + '@sveltejs/vite-plugin-svelte': 6.2.4(svelte@5.50.3)(vite@7.3.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)) + '@types/cookie': 0.6.0 + acorn: 8.15.0 + cookie: 0.6.0 + devalue: 5.6.2 + esm-env: 1.2.2 + kleur: 4.1.5 + magic-string: 0.30.21 + mrmime: 2.0.1 + sade: 1.8.1 + set-cookie-parser: 3.0.1 + sirv: 3.0.2 + svelte: 5.50.3 + vite: 7.3.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) + optionalDependencies: + typescript: 5.9.3 + + '@sveltejs/vite-plugin-svelte-inspector@5.0.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.50.3)(vite@7.3.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)))(svelte@5.50.3)(vite@7.3.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0))': + dependencies: + '@sveltejs/vite-plugin-svelte': 6.2.4(svelte@5.50.3)(vite@7.3.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)) + obug: 2.1.1 + svelte: 5.50.3 + vite: 7.3.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) + + '@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.50.3)(vite@7.3.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0))': + dependencies: + '@sveltejs/vite-plugin-svelte-inspector': 5.0.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.50.3)(vite@7.3.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)))(svelte@5.50.3)(vite@7.3.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)) + deepmerge: 4.3.1 + magic-string: 0.30.21 + obug: 2.1.1 + svelte: 5.50.3 + vite: 7.3.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) + vitefu: 1.1.1(vite@7.3.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)) + '@swc/helpers@0.5.15': dependencies: tslib: 2.8.1 @@ -7674,11 +8181,22 @@ snapshots: dependencies: '@babel/types': 7.29.0 + '@types/body-parser@1.19.6': + dependencies: + '@types/connect': 3.4.38 + '@types/node': 25.2.2 + '@types/chai@5.2.3': dependencies: '@types/deep-eql': 4.0.2 assertion-error: 2.0.1 + '@types/connect@3.4.38': + dependencies: + '@types/node': 25.2.2 + + '@types/cookie@0.6.0': {} + '@types/debug@4.1.12': dependencies: '@types/ms': 2.1.0 @@ -7691,10 +8209,25 @@ snapshots: '@types/estree@1.0.8': {} + '@types/express-serve-static-core@5.1.1': + dependencies: + '@types/node': 25.2.2 + '@types/qs': 6.14.0 + '@types/range-parser': 1.2.7 + '@types/send': 1.2.1 + + '@types/express@5.0.6': + dependencies: + '@types/body-parser': 1.19.6 + '@types/express-serve-static-core': 5.1.1 + '@types/serve-static': 2.2.0 + '@types/hast@3.0.4': dependencies: '@types/unist': 3.0.3 + '@types/http-errors@2.0.5': {} + '@types/js-yaml@4.0.9': {} '@types/mdast@4.0.4': @@ -7715,6 +8248,10 @@ snapshots: '@types/normalize-package-data@2.4.4': {} + '@types/qs@6.14.0': {} + + '@types/range-parser@1.2.7': {} + '@types/react-dom@19.2.3(@types/react@19.2.13)': dependencies: '@types/react': 19.2.13 @@ -7725,6 +8262,15 @@ snapshots: '@types/sarif@2.1.7': {} + '@types/send@1.2.1': + dependencies: + '@types/node': 25.2.2 + + '@types/serve-static@2.2.0': + dependencies: + '@types/http-errors': 2.0.5 + '@types/node': 25.2.2 + '@types/statuses@2.0.6': {} '@types/unist@2.0.11': {} @@ -7890,6 +8436,13 @@ snapshots: transitivePeerDependencies: - supports-color + abstract-logging@2.0.1: {} + + accepts@2.0.0: + dependencies: + mime-types: 3.0.2 + negotiator: 1.0.0 + acorn-jsx@5.3.2(acorn@8.15.0): dependencies: acorn: 8.15.0 @@ -7902,6 +8455,10 @@ snapshots: agent-base@7.1.4: {} + ajv-formats@3.0.1(ajv@8.17.1): + optionalDependencies: + ajv: 8.17.1 + ajv@8.17.1: dependencies: fast-deep-equal: 3.1.3 @@ -7937,6 +8494,8 @@ snapshots: dependencies: tslib: 2.8.1 + aria-query@5.3.2: {} + array-union@2.1.0: {} assertion-error@2.0.1: {} @@ -7964,6 +8523,13 @@ snapshots: postcss: 8.5.6 postcss-value-parser: 4.2.0 + avvio@9.2.0: + dependencies: + '@fastify/error': 4.2.0 + fastq: 1.20.1 + + axobject-query@4.1.0: {} + azure-devops-node-api@12.5.0: dependencies: tunnel: 0.0.6 @@ -7978,7 +8544,7 @@ snapshots: baseline-browser-mapping@2.9.19: {} - better-auth@1.4.18(drizzle-orm@0.41.0(kysely@0.28.11))(next@16.1.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@4.0.18(@types/node@25.2.2)(happy-dom@20.5.3)(jiti@2.6.1)(lightningcss@1.30.2)(msw@2.12.9(@types/node@25.2.2)(typescript@5.9.3))(tsx@4.21.0)): + better-auth@1.4.18(@sveltejs/kit@2.51.0(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.50.3)(vite@7.3.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)))(svelte@5.50.3)(typescript@5.9.3)(vite@7.3.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)))(drizzle-orm@0.41.0(kysely@0.28.11))(next@16.1.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.50.3)(vitest@4.0.18(@types/node@25.2.2)(happy-dom@20.5.3)(jiti@2.6.1)(lightningcss@1.30.2)(msw@2.12.9(@types/node@25.2.2)(typescript@5.9.3))(tsx@4.21.0)): dependencies: '@better-auth/core': 1.4.18(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0) '@better-auth/telemetry': 1.4.18(@better-auth/core@1.4.18(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0)) @@ -7993,10 +8559,12 @@ snapshots: nanostores: 1.1.0 zod: 4.3.6 optionalDependencies: + '@sveltejs/kit': 2.51.0(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.50.3)(vite@7.3.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)))(svelte@5.50.3)(typescript@5.9.3)(vite@7.3.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)) drizzle-orm: 0.41.0(kysely@0.28.11) next: 16.1.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react: 19.2.4 react-dom: 19.2.4(react@19.2.4) + svelte: 5.50.3 vitest: 4.0.18(@types/node@25.2.2)(happy-dom@20.5.3)(jiti@2.6.1)(lightningcss@1.30.2)(msw@2.12.9(@types/node@25.2.2)(typescript@5.9.3))(tsx@4.21.0) better-call@1.1.8(zod@4.3.6): @@ -8023,6 +8591,20 @@ snapshots: readable-stream: 3.6.2 optional: true + body-parser@2.2.2: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.3 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + on-finished: 2.4.1 + qs: 6.14.2 + raw-body: 3.0.2 + type-is: 2.0.1 + transitivePeerDependencies: + - supports-color + boolbase@1.0.0: {} boundary@2.0.0: {} @@ -8068,6 +8650,8 @@ snapshots: esbuild: 0.27.3 load-tsconfig: 0.2.5 + bytes@3.1.2: {} + cac@6.7.14: {} call-bind-apply-helpers@1.0.2: @@ -8189,8 +8773,20 @@ snapshots: consola@3.4.2: {} + content-disposition@1.0.1: {} + + content-type@1.0.5: {} + convert-source-map@2.0.0: {} + cookie-es@1.2.2: {} + + cookie-signature@1.2.2: {} + + cookie@0.6.0: {} + + cookie@0.7.2: {} + cookie@1.1.1: {} create-require@1.1.1: {} @@ -8201,6 +8797,10 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + crossws@0.3.5: + dependencies: + uncrypto: 0.1.3 + css-select@5.2.2: dependencies: boolbase: 1.0.0 @@ -8233,6 +8833,8 @@ snapshots: deep-extend@0.6.0: optional: true + deepmerge@4.3.1: {} + default-browser-id@5.0.1: {} default-browser@5.5.0: @@ -8246,14 +8848,20 @@ snapshots: delayed-stream@1.0.0: {} + depd@2.0.0: {} + dequal@2.0.3: {} + destr@2.0.5: {} + detect-indent@6.1.0: {} detect-libc@2.1.2: {} detect-node-es@1.1.0: {} + devalue@5.6.2: {} + devlop@1.1.0: dependencies: dequal: 2.0.3 @@ -8301,10 +8909,14 @@ snapshots: dependencies: version-range: 4.15.0 + ee-first@1.1.1: {} + electron-to-chromium@1.5.286: {} emoji-regex@8.0.0: {} + encodeurl@2.0.0: {} + encoding-sniffer@0.2.1: dependencies: iconv-lite: 0.6.3 @@ -8423,10 +9035,18 @@ snapshots: escalade@3.2.0: {} + escape-html@1.0.3: {} + escape-string-regexp@5.0.0: {} + esm-env@1.2.2: {} + esprima@4.0.1: {} + esrap@2.2.3: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + estree-util-attach-comments@3.0.0: dependencies: '@types/estree': 1.0.8 @@ -8464,17 +9084,54 @@ snapshots: dependencies: '@types/estree': 1.0.8 + etag@1.8.1: {} + expand-template@2.0.3: optional: true expect-type@1.3.0: {} + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.2 + content-disposition: 1.0.1 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1 + fresh: 2.0.0 + http-errors: 2.0.1 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.14.2 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 + statuses: 2.0.2 + type-is: 2.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + extend@3.0.2: {} extendable-error@0.1.7: {} fast-copy@4.0.2: {} + fast-decode-uri-component@1.0.1: {} + fast-deep-equal@3.1.3: {} fast-glob@3.3.3: @@ -8485,10 +9142,41 @@ snapshots: merge2: 1.4.1 micromatch: 4.0.8 + fast-json-stringify@6.3.0: + dependencies: + '@fastify/merge-json-schemas': 0.2.1 + ajv: 8.17.1 + ajv-formats: 3.0.1(ajv@8.17.1) + fast-uri: 3.1.0 + json-schema-ref-resolver: 3.0.0 + rfdc: 1.4.1 + + fast-querystring@1.1.2: + dependencies: + fast-decode-uri-component: 1.0.1 + fast-safe-stringify@2.1.1: {} fast-uri@3.1.0: {} + fastify@5.7.4: + dependencies: + '@fastify/ajv-compiler': 4.0.5 + '@fastify/error': 4.2.0 + '@fastify/fast-json-stringify-compiler': 5.0.3 + '@fastify/proxy-addr': 5.1.0 + abstract-logging: 2.0.1 + avvio: 9.2.0 + fast-json-stringify: 6.3.0 + find-my-way: 9.4.0 + light-my-request: 6.6.0 + pino: 10.3.0 + process-warning: 5.0.0 + rfdc: 1.4.1 + secure-json-parse: 4.1.0 + semver: 7.7.4 + toad-cache: 3.7.0 + fastq@1.20.1: dependencies: reusify: 1.1.0 @@ -8514,6 +9202,23 @@ snapshots: dependencies: to-regex-range: 5.0.1 + finalhandler@2.1.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + find-my-way@9.4.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-querystring: 1.1.2 + safe-regex2: 5.0.0 + find-up@4.1.0: dependencies: locate-path: 5.0.0 @@ -8538,6 +9243,8 @@ snapshots: hasown: 2.0.2 mime-types: 2.1.35 + forwarded@0.2.0: {} + fraction.js@5.3.4: {} framer-motion@12.33.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4): @@ -8549,6 +9256,8 @@ snapshots: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) + fresh@2.0.0: {} + fs-constants@1.0.0: optional: true @@ -8752,6 +9461,18 @@ snapshots: graphql@16.12.0: {} + h3@1.15.5: + dependencies: + cookie-es: 1.2.2 + crossws: 0.3.5 + defu: 6.1.4 + destr: 2.0.5 + iron-webcrypto: 1.2.1 + node-mock-http: 1.0.4 + radix3: 1.1.2 + ufo: 1.6.3 + uncrypto: 0.1.3 + happy-dom@20.5.3: dependencies: '@types/node': 25.2.2 @@ -8864,6 +9585,14 @@ snapshots: domutils: 3.2.2 entities: 7.0.1 + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.4 @@ -8898,14 +9627,19 @@ snapshots: index-to-position@1.2.0: {} - inherits@2.0.4: - optional: true + inherits@2.0.4: {} ini@1.3.8: optional: true inline-style-parser@0.2.7: {} + ipaddr.js@1.9.1: {} + + ipaddr.js@2.3.0: {} + + iron-webcrypto@1.2.1: {} + is-alphabetical@2.0.1: {} is-alphanumerical@2.0.1: @@ -8937,6 +9671,12 @@ snapshots: is-plain-obj@4.1.0: {} + is-promise@4.0.0: {} + + is-reference@3.0.3: + dependencies: + '@types/estree': 1.0.8 + is-subdir@1.2.0: dependencies: better-path-resolve: 1.0.0 @@ -8995,6 +9735,10 @@ snapshots: jsesc@3.1.0: {} + json-schema-ref-resolver@3.0.0: + dependencies: + dequal: 2.0.3 + json-schema-traverse@1.0.0: {} json5@2.2.3: {} @@ -9041,10 +9785,18 @@ snapshots: prebuild-install: 7.1.3 optional: true + kleur@4.1.5: {} + kysely@0.28.11: {} leven@3.1.0: {} + light-my-request@6.6.0: + dependencies: + cookie: 1.1.1 + process-warning: 4.0.1 + set-cookie-parser: 2.7.2 + lightningcss-android-arm64@1.30.2: optional: true @@ -9106,6 +9858,8 @@ snapshots: load-tsconfig@0.2.5: {} + locate-character@3.0.0: {} + locate-path@5.0.0: dependencies: p-locate: 4.1.0 @@ -9160,7 +9914,7 @@ snapshots: make-dir@4.0.0: dependencies: - semver: 7.7.3 + semver: 7.7.4 make-error@1.3.6: {} @@ -9344,6 +10098,10 @@ snapshots: mdurl@2.0.0: {} + media-typer@1.1.0: {} + + merge-descriptors@2.0.0: {} + merge2@1.4.1: {} micromark-core-commonmark@2.0.3: @@ -9617,10 +10375,16 @@ snapshots: mime-db@1.52.0: {} + mime-db@1.54.0: {} + mime-types@2.1.35: dependencies: mime-db: 1.52.0 + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + mime@1.6.0: {} mimic-response@3.1.0: @@ -9666,6 +10430,8 @@ snapshots: mri@1.2.0: {} + mrmime@2.0.1: {} + ms@2.1.3: {} msw@2.12.9(@types/node@25.2.2)(typescript@5.9.3): @@ -9749,6 +10515,8 @@ snapshots: node-addon-api@4.3.0: optional: true + node-mock-http@1.0.4: {} + node-releases@2.0.27: {} node-sarif-builder@3.4.0: @@ -9776,6 +10544,10 @@ snapshots: on-exit-leak-free@2.1.2: {} + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + once@1.4.0: dependencies: wrappy: 1.0.2 @@ -9856,6 +10628,8 @@ snapshots: dependencies: entities: 6.0.1 + parseurl@1.3.3: {} + path-exists@4.0.0: {} path-key@3.1.1: {} @@ -9978,10 +10752,17 @@ snapshots: prettier@2.8.8: {} + process-warning@4.0.1: {} + process-warning@5.0.0: {} property-information@7.1.0: {} + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + pump@3.0.3: dependencies: end-of-stream: 1.4.5 @@ -9999,6 +10780,17 @@ snapshots: quick-format-unescaped@4.0.4: {} + radix3@1.1.2: {} + + range-parser@1.2.1: {} + + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + unpipe: 1.0.0 + rc-config-loader@4.1.3: dependencies: debug: 4.4.3 @@ -10203,10 +10995,14 @@ snapshots: resolve-pkg-maps@1.0.0: {} + ret@0.5.0: {} + rettime@0.10.1: {} reusify@1.1.0: {} + rfdc@1.4.1: {} + rollup@4.55.2: dependencies: '@types/estree': 1.0.8 @@ -10271,6 +11067,16 @@ snapshots: rou3@0.7.12: {} + router@2.2.0: + dependencies: + debug: 4.4.3 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.3.0 + transitivePeerDependencies: + - supports-color + run-applescript@7.1.0: {} run-parallel@1.2.0: @@ -10281,8 +11087,16 @@ snapshots: dependencies: tslib: 2.8.1 + sade@1.8.1: + dependencies: + mri: 1.2.0 + safe-buffer@5.2.1: {} + safe-regex2@5.0.0: + dependencies: + ret: 0.5.0 + safe-stable-stringify@2.5.0: {} safer-buffer@2.1.2: {} @@ -10317,8 +11131,37 @@ snapshots: semver@7.7.4: {} + send@1.2.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + set-cookie-parser@2.7.2: {} + set-cookie-parser@3.0.1: {} + + setprototypeof@1.2.0: {} + sharp@0.34.5: dependencies: '@img/colour': 1.0.0 @@ -10410,6 +11253,12 @@ snapshots: simple-concat: 1.0.1 optional: true + sirv@3.0.2: + dependencies: + '@polka/url': 1.0.0-next.29 + mrmime: 2.0.1 + totalist: 3.0.1 + slash@3.0.0: {} slash@5.1.0: {} @@ -10532,6 +11381,24 @@ snapshots: has-flag: 4.0.0 supports-color: 7.2.0 + svelte@5.50.3: + dependencies: + '@jridgewell/remapping': 2.3.5 + '@jridgewell/sourcemap-codec': 1.5.5 + '@sveltejs/acorn-typescript': 1.0.9(acorn@8.15.0) + '@types/estree': 1.0.8 + acorn: 8.15.0 + aria-query: 5.3.2 + axobject-query: 4.1.0 + clsx: 2.1.1 + devalue: 5.6.2 + esm-env: 1.2.2 + esrap: 2.2.3 + is-reference: 3.0.3 + locate-character: 3.0.0 + magic-string: 0.30.21 + zimmerframe: 1.1.4 + table@6.9.0: dependencies: ajv: 8.17.1 @@ -10615,12 +11482,18 @@ snapshots: dependencies: is-number: 7.0.0 + toad-cache@3.7.0: {} + + toidentifier@1.0.1: {} + token-types@6.1.2: dependencies: '@borewit/text-codec': 0.2.1 '@tokenizer/token': 0.3.0 ieee754: 1.2.1 + totalist@3.0.1: {} + tough-cookie@6.0.0: dependencies: tldts: 7.0.23 @@ -10728,6 +11601,12 @@ snapshots: dependencies: tagged-tag: 1.0.0 + type-is@2.0.1: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.2 + typed-rest-client@1.8.11: dependencies: qs: 6.14.2 @@ -10746,6 +11625,8 @@ snapshots: uint8array-extras@1.5.0: {} + uncrypto@0.1.3: {} + underscore@1.13.7: {} undici-types@7.16.0: {} @@ -10802,6 +11683,8 @@ snapshots: universalify@2.0.1: {} + unpipe@1.0.0: {} + until-async@3.0.2: {} update-browserslist-db@1.2.3(browserslist@4.28.1): @@ -10842,6 +11725,8 @@ snapshots: spdx-correct: 3.2.0 spdx-expression-parse: 3.0.1 + vary@1.1.2: {} + version-range@4.15.0: {} vfile-message@4.0.3: @@ -10869,6 +11754,10 @@ snapshots: lightningcss: 1.30.2 tsx: 4.21.0 + vitefu@1.1.1(vite@7.3.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)): + optionalDependencies: + vite: 7.3.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) + vitest@4.0.18(@types/node@25.2.2)(happy-dom@20.5.3)(jiti@2.6.1)(lightningcss@1.30.2)(msw@2.12.9(@types/node@25.2.2)(typescript@5.9.3))(tsx@4.21.0): dependencies: '@vitest/expect': 4.0.18 @@ -10982,6 +11871,8 @@ snapshots: yoctocolors-cjs@2.1.3: {} + zimmerframe@1.1.4: {} + zod@4.3.6: {} zwitch@2.0.4: {}