diff --git a/.env.test b/.env.test index c0f4268..de4cc52 100644 --- a/.env.test +++ b/.env.test @@ -1,10 +1,11 @@ APP_ENV=test -PORT=3021 -GOOGLE_CLIENT_ID=...test.apps.googleusercontent.com -CLIENT_SECRET=GOC...test -REDIRECT_URI=http://localhost:7737/api/auth/google - +CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT=1 CLICKHOUSE_HOST=http://localhost:8443 -CLICKHOUSE_USER=default CLICKHOUSE_PASSWORD=token_pass -CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT=1 \ No newline at end of file +CLICKHOUSE_USER=default +CLIENT_SECRET=GOC...test +GOOGLE_CLIENT_ID=...test.apps.googleusercontent.com +PORT=3021 +REDIRECT_URI=http://localhost:7737/api/auth/google +# Get values from `deno task dev:env` +# then copy missing variables and replace APP_ENV to the matching values diff --git a/.github/workflows/ga-build-image.yml b/.github/workflows/ga-build-image.yml index 5ae0bd8..dcf19aa 100644 --- a/.github/workflows/ga-build-image.yml +++ b/.github/workflows/ga-build-image.yml @@ -1,11 +1,7 @@ # Workflow to build and push a Docker image to GitHub Container Registry name: 'đŸŗ Build Docker Image' - -on: - push: - branches: ['master'] - +on: { push: { branches: ['master'] } } jobs: build-image: name: đŸ—ī¸ Build Image diff --git a/.github/workflows/ga-compliance.yml b/.github/workflows/ga-compliance.yml index 3397ef9..d6f007f 100644 --- a/.github/workflows/ga-compliance.yml +++ b/.github/workflows/ga-compliance.yml @@ -1,20 +1,16 @@ # Workflow for DevTools Compliance - name: DevTools Compliance - on: pull_request: types: - [ - opened, - edited, - synchronize, - reopened, - labeled, - unlabeled, - assigned, - unassigned, - ] + - opened + - edited + - synchronize + - reopened + - labeled + - unlabeled + - assigned + - unassigned jobs: check-compliance: @@ -23,9 +19,8 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 + uses: actions/checkout@v5 + with: { fetch-depth: 0 } - name: Show PR Labels and Assignees run: | diff --git a/.github/workflows/ga-linter-fmt.yml b/.github/workflows/ga-linter-fmt.yml index adb8703..f92f9c5 100644 --- a/.github/workflows/ga-linter-fmt.yml +++ b/.github/workflows/ga-linter-fmt.yml @@ -1,38 +1,22 @@ # Workflow for DevTools CI/CD # This workflow runs linting and formatting checks on pull requests. # It ensures that the code adheres to style guidelines and is free of linting errors. - name: DevTools CI/CD - on: - pull_request: - types: [opened, reopened, synchronize] + pull_request: { types: [opened, reopened, synchronize] } jobs: lint-and-format: name: Lint and Format Check runs-on: ubuntu-latest - steps: - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 + uses: actions/checkout@v5 + with: { fetch-depth: 0 } - name: Setup Deno uses: denoland/setup-deno@v2 - with: - deno-version: v2.x - - - name: Cache Deno dependencies - uses: actions/cache@v4 - with: - path: | - ~/.deno - ~/node_modules - ~/.cache/deno - - key: ${{ runner.os }}-deno-v2-${{ hashFiles('deno.json') }} + with: { deno-version: v2.x, cache: true } - name: 🔍 Get all changed files id: changed-file-list @@ -45,7 +29,7 @@ jobs: - name: Run formatter if: steps.changed-file-list.outputs.changed_files != '' - run: deno task fmt --permit-no-files --check ${{ steps.changed-file-list.outputs.changed_files }} + run: deno task fmt --permit-no-files ${{ steps.changed-file-list.outputs.changed_files }} - name: Run check if: steps.changed-file-list.outputs.changed_files != '' diff --git a/.github/workflows/ga-redeploy.yml b/.github/workflows/ga-redeploy.yml index d6cf819..b667273 100644 --- a/.github/workflows/ga-redeploy.yml +++ b/.github/workflows/ga-redeploy.yml @@ -11,11 +11,7 @@ jobs: name: đŸ’Ŋ Redeploy DevTools runs-on: devtools.01edu.ai environment: production - - permissions: - packages: read - contents: read - + permissions: { packages: read, contents: read } steps: - name: 🔐 Login to GitHub Container Registry run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin diff --git a/api/auth.ts b/api/auth.ts index 7fef7ce..8b35c41 100644 --- a/api/auth.ts +++ b/api/auth.ts @@ -7,10 +7,10 @@ import { verifyGoogleToken, verifyState, } from '/api/lib/google-oauth.ts' -import { respond } from '/api/lib/response.ts' +import { respond } from '@01edu/api/response' import { authenticateOauthUser } from '/api/user.ts' import { savePicture } from '/api/picture.ts' -import type { RequestContext } from '/api/lib/context.ts' +import type { RequestContext } from '@01edu/api/context' interface GoogleTokens { access_token: string diff --git a/api/clickhouse-client.ts b/api/clickhouse-client.ts index bf07026..0952b02 100644 --- a/api/clickhouse-client.ts +++ b/api/clickhouse-client.ts @@ -1,13 +1,20 @@ -import { createClient } from 'npm:@clickhouse/client' +import { createClient } from '@clickhouse/client' import { CLICKHOUSE_HOST, CLICKHOUSE_PASSWORD, CLICKHOUSE_USER, } from './lib/env.ts' -import { respond } from './lib/response.ts' +import { respond } from '@01edu/api/response' import { log } from './lib/log.ts' -import { ARR, NUM, OBJ, optional, STR, UNION } from './lib/validator.ts' -import { Asserted } from './lib/router.ts' +import { + ARR, + type Asserted, + NUM, + OBJ, + optional, + STR, + UNION, +} from '@01edu/api/validator' const LogSchema = OBJ({ timestamp: NUM('The timestamp of the log event'), diff --git a/api/lib/context.ts b/api/lib/context.ts deleted file mode 100644 index 40fb18b..0000000 --- a/api/lib/context.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { AsyncLocalStorage } from 'node:async_hooks' -import { startTime } from '/api/lib/time.ts' -import { User } from '../schema.ts' - -type Readonly = { - readonly [P in keyof T]: - // deno-lint-ignore ban-types - T[P] extends Function ? T[P] - : T[P] extends object ? Readonly - : T[P] -} - -// Define the route structure with supported methods -// export type Session = { id: number; createdAt: number; userId: number } -export type RequestContext = { - readonly req: Readonly - readonly url: Readonly - readonly cookies: Readonly> - readonly user: User | undefined - readonly trace: number - readonly span: number | undefined - resource: string | undefined -} - -// we set default values so we don't have to check everytime if they exists -export const makeContext = ( - urlInit: string | URL, - extra?: Partial, -): RequestContext => { - const url = new URL(urlInit, 'http://locahost') - const req = new Request(url) - return { - trace: startTime, - cookies: {}, - user: undefined, - span: undefined, - resource: undefined, - url, - req, - ...extra, - } -} - -const defaultContext = makeContext('/') -export const requestContext = new AsyncLocalStorage() -export const getContext = () => requestContext.getStore() || defaultContext diff --git a/api/lib/env.ts b/api/lib/env.ts index 1f5169e..8b228c4 100644 --- a/api/lib/env.ts +++ b/api/lib/env.ts @@ -1,46 +1,22 @@ -const env = Deno.env.toObject() +import { ENV } from '@01edu/api/env' -type AppEnvironments = 'dev' | 'test' | 'prod' - -export const APP_ENV = env.APP_ENV || 'dev' as AppEnvironments -if (APP_ENV !== 'dev' && APP_ENV !== 'test' && APP_ENV !== 'prod') { - throw Error(`APP_ENV: "${env.APP_ENV}" must be "dev", "test" or "prod"`) -} - -export const PORT = Number(env.PORT) || 2119 -export const Picture_Dir = env.PICTURE_DIR || './.picture' - -export const GOOGLE_CLIENT_ID = env.GOOGLE_CLIENT_ID -if (!GOOGLE_CLIENT_ID) { - throw Error('GOOGLE_CLIENT_ID: field required in the env') -} -export const CLIENT_SECRET = env.CLIENT_SECRET -if (!CLIENT_SECRET) { - throw Error('CLIENT_SECRET: field required in the env') -} -export const REDIRECT_URI = env.REDIRECT_URI -if (!REDIRECT_URI) { - throw Error('REDIRECT_URI: field required in the env') -} +export const PORT = Number(ENV('PORT', '2119')) +export const PICTURE_DIR = ENV('PICTURE_DIR', './.picture') +export const GOOGLE_CLIENT_ID = ENV('GOOGLE_CLIENT_ID') +export const CLIENT_SECRET = ENV('CLIENT_SECRET') +export const REDIRECT_URI = ENV('REDIRECT_URI') export const ORIGIN = new URL(REDIRECT_URI).origin +export const SECRET = ENV( + 'SECRET', + 'iUokBru8WPSMAuMspijlt7F-Cnpqyg84F36b1G681h0', +) -export const SECRET = env.SECRET || - 'iUokBru8WPSMAuMspijlt7F-Cnpqyg84F36b1G681h0' - -export const CLICKHOUSE_HOST = env.CLICKHOUSE_HOST -if (!CLICKHOUSE_HOST) { - throw Error('CLICKHOUSE_HOST: field required in the env') -} -export const CLICKHOUSE_USER = env.CLICKHOUSE_USER -if (!CLICKHOUSE_USER) { - throw Error('CLICKHOUSE_USER: field required in the env') -} -export const CLICKHOUSE_PASSWORD = env.CLICKHOUSE_PASSWORD -if (!CLICKHOUSE_PASSWORD) { - throw Error('CLICKHOUSE_PASSWORD: field required in the env') -} +export const CLICKHOUSE_HOST = ENV('CLICKHOUSE_HOST') +export const CLICKHOUSE_USER = ENV('CLICKHOUSE_USER') +export const CLICKHOUSE_PASSWORD = ENV('CLICKHOUSE_PASSWORD') // Optional interval (ms) for refreshing external SQL database schemas // Defaults to 24 hours -export const DB_SCHEMA_REFRESH_MS = Number(env.DB_SCHEMA_REFRESH_MS) || - 24 * 60 * 60 * 1000 +export const DB_SCHEMA_REFRESH_MS = Number( + ENV('DB_SCHEMA_REFRESH_MS', `${24 * 60 * 60 * 1000}`), +) diff --git a/api/lib/json_store.test.ts b/api/lib/json_store.test.ts index ebd457a..26ec259 100644 --- a/api/lib/json_store.test.ts +++ b/api/lib/json_store.test.ts @@ -1,13 +1,8 @@ // db_test.ts -import { afterEach, beforeEach, describe, it } from 'jsr:@std/testing/bdd' -import { - assert, - assertEquals, - assertExists, - assertRejects, -} from 'jsr:@std/assert' +import { afterEach, beforeEach, describe, it } from '@std/testing/bdd' +import { assert, assertEquals, assertExists, assertRejects } from '@std/assert' import { createCollection } from './json_store.ts' -import { ensureDir } from 'jsr:@std/fs' +import { ensureDir } from '@std/fs' type User = { id: number diff --git a/api/lib/json_store.ts b/api/lib/json_store.ts index 497219d..14700c3 100644 --- a/api/lib/json_store.ts +++ b/api/lib/json_store.ts @@ -1,7 +1,7 @@ // zero-db.ts -import { join } from 'jsr:@std/path' -import { APP_ENV } from './env.ts' -import { ensureDir } from 'jsr:@std/fs' +import { join } from '@std/path' +import { APP_ENV } from '@01edu/api/env' +import { ensureDir } from '@std/fs' const DB_DIR = APP_ENV === 'test' ? './db_test' : './db' diff --git a/api/lib/log.ts b/api/lib/log.ts index 99ab5c9..d7183b3 100644 --- a/api/lib/log.ts +++ b/api/lib/log.ts @@ -1,157 +1,2 @@ -import { APP_ENV } from '/api/lib/env.ts' -import { now, startTime } from '/api/lib/time.ts' -import { getContext } from '/api/lib/context.ts' -import { - blue, - brightBlue, - brightCyan, - brightGreen, - brightMagenta, - brightRed, - brightYellow, - cyan, - gray, - green, - magenta, - red, - yellow, -} from 'jsr:@std/fmt/colors' - -type LogLevel = - | 'info' // default level - | 'error' - | 'warn' - | 'debug' - -type LogFunction = ( - level: LogLevel, - event: string, - props?: Record, -) => void - -type BoundLogFunction = (event: string, props?: Record) => void - -interface Log extends LogFunction { - error: BoundLogFunction - debug: BoundLogFunction - warn: BoundLogFunction - info: BoundLogFunction -} - -const levels = { - debug: { ico: '🐛', color: green }, - info: { ico: 'â„šī¸', color: cyan }, - warn: { ico: 'âš ī¸', color: yellow }, - error: { ico: 'đŸ’Ĩ', color: red }, -} as const - -// set to recursive to not fail if already exists -const logDir = `${Deno.cwd()}/.logs` -await Deno.mkdir(logDir, { recursive: true }) - -// format: https://jsonlines.org/ -// TODO: see if we can aquire a write lock for the file -export const logFilePath = `${logDir}/api_${APP_ENV}${ - APP_ENV === 'prod' ? `_${startTime.toString(36).split('.')[0]}` : '' -}.jsonl` -// when not in prod, we clean logs between boots -const logFile = await Deno.open(logFilePath, { - append: true, - ...(APP_ENV === 'prod' ? { createNew: true } : { create: true }), -}) -APP_ENV === 'prod' || await logFile.truncate(0).catch(() => { - // console.log('unable to truncate', { logFilePath }) -}) -const encoder = new TextEncoder() - -function replacer(_key: string, value: unknown) { - if (value instanceof Error) return value.stack || value.message - return value -} - -const colors = [ - ...[green, yellow, blue, magenta, cyan, brightRed], - ...[brightGreen, brightYellow, brightBlue, brightMagenta, brightCyan], -] -const colored: Record = { 'Object.fetch': cyan('serve') } -const makePrettyTimestamp = (level: LogLevel, event: string) => { - const at = new Date() - const hh = String(at.getHours()).padStart(2, '0') - const mm = String(at.getMinutes()).padStart(2, '0') - const ss = String(at.getSeconds()).padStart(2, '0') - const ms = String(at.getMilliseconds()).padStart(2, '0').slice(0, 2) - const lvl = levels[level] - return `${gray(`${hh}h${mm}:${ss}.${ms}`)} ${lvl.ico} ${lvl.color(event)}` -} - -const bannedTestEvents = new Set( - ['step-end', 'step-start', 'test-end', 'test-start'], -) - -const rootDir = - import.meta.dirname?.slice(0, -'/lib'.length).replaceAll('\\', '/') || '' -const loggers: Record = { - test: (level, event, props) => { - const { trace, span, url } = getContext() - const data = { - level, - trace, - span, - event, - props, - at: now(), - file: url.pathname, - } - logFile.writeSync(encoder.encode(`${JSON.stringify(data, replacer)}\n`)) - if (bannedTestEvents.has(event)) return - const ev = makePrettyTimestamp(level, event) - props ? console[level](ev, props) : console[level](ev) - }, - dev: (level, event, props) => { - let callChain = '' - for (const s of Error('').stack!.split('\n').slice(2).reverse()) { - if (!s.includes(rootDir)) continue - const fnName = s.split(' ').at(-2) - if (!fnName || fnName === 'async' || fnName === 'at') continue - const coloredName = colored[fnName] || - (colored[fnName] = colors[Object.keys(colored).length % colors.length]( - fnName, - )) - callChain = callChain ? `${callChain}/${coloredName}` : coloredName - } - const { trace, span } = getContext() - const data = { level, trace, span, event, props, at: now() } - const logStr = JSON.stringify(data, replacer) - logFile.writeSync(encoder.encode(`${logStr}\n`)) - const bytes = encoder.encode(`data: ${logStr}\r\n\r\n`) - for (const controller of logListeners) { - try { - controller.enqueue(bytes) - } catch (err) { - console.error('unable to send log', err) - } - } - const ev = `${makePrettyTimestamp(level, event)} ${callChain}`.trim() - props ? console[level](ev, props) : console[level](ev) - }, - prod: (level, event, props) => { - const { trace, span } = getContext() - console.log(JSON.stringify({ level, trace, span, event, props, at: now() })) - }, -} - -export const log = loggers[APP_ENV] as Log - -// Bind is used over wrapping for reducing error stack -log.error = log.bind(null, 'error') -log.debug = log.bind(null, 'debug') -log.warn = log.bind(null, 'warn') -log.info = log.bind(null, 'info') - -export const error = log.error -export const debug = log.debug -export const warn = log.warn -export const info = log.info -export const logListeners = new Set>() - -// TODO: target betterstack, use open telemetry fields +import { logger } from '@01edu/api/log' +export const log = await logger({}) diff --git a/api/lib/response.ts b/api/lib/response.ts deleted file mode 100644 index d33b40b..0000000 --- a/api/lib/response.ts +++ /dev/null @@ -1,164 +0,0 @@ -import { STATUS_CODE, STATUS_TEXT } from 'jsr:@std/http/status' - -const defaultHeaderEntries: [string, string][] = [ - ['content-type', 'application/json'], -] - -const defaultHeaders = new Headers(defaultHeaderEntries) - -const json = (data?: unknown, init?: ResponseInit) => { - if (data == null) return new Response(null, init) - if (!init) { - init = { headers: defaultHeaders } - } else if (!init.headers) { - init.headers = defaultHeaders - } else { - if (!(init.headers instanceof Headers)) { - init.headers = new Headers(init.headers) - } - const h = init.headers as Headers - for (const entry of defaultHeaderEntries) { - h.set(entry[0], entry[1]) - } - } - - return new Response(JSON.stringify(data), init) -} - -class ResponseError extends Error { - public response: Response - - constructor(message: string, response: Response) { - super(message) - this.name = 'ResponseError' - this.response = response - } -} - -type StatusCodeWithoutBody = - | 'Continue' - | 'SwitchingProtocols' - | 'Processing' - | 'EarlyHints' - | 'NoContent' - | 'ResetContent' - | 'NotModified' - -const withoutBody = new Set([ - 100, // Continue - 101, // SwitchingProtocols - 102, // Processing - 103, // EarlyHints - 204, // NoContent - 205, // ResetContent - 304, // NotModified -]) - -type StatusNotErrors = - | 'OK' - | 'Created' - | 'Accepted' - | 'NonAuthoritativeInfo' - | 'NoContent' - | 'ResetContent' - | 'PartialContent' - | 'MultiStatus' - | 'AlreadyReported' - | 'IMUsed' - | 'MultipleChoices' - | 'MovedPermanently' - | 'Found' - | 'SeeOther' - | 'NotModified' - | 'UseProxy' - | 'TemporaryRedirect' - | 'PermanentRedirect' - -const notErrors = new Set([ - 200, // OK - 201, // Created - 202, // Accepted - 203, // NonAuthoritativeInfo - 204, // NoContent - 205, // ResetContent - 206, // PartialContent - 207, // MultiStatus - 208, // AlreadyReported - 226, // IMUsed - 300, // MultipleChoices - 301, // MovedPermanently - 302, // Found - 303, // SeeOther - 304, // NotModified - 305, // UseProxy - 307, // TemporaryRedirect - 308, // PermanentRedirect -]) - -type ErrorStatus = Exclude< - Exclude, - StatusNotErrors -> - -export const respond = Object.fromEntries([ - ...Object.entries(STATUS_CODE).map(([key, status]) => { - const statusText = STATUS_TEXT[status] - const defaultData = new TextEncoder().encode( - JSON.stringify({ message: statusText }) + '\n', - ) - - const makeResponse = withoutBody.has(status) - ? (headers?: HeadersInit) => - headers === undefined - ? json(null, { headers: defaultHeaders, status, statusText }) - : json(null, { headers, status, statusText }) - : (data?: unknown, headers?: HeadersInit) => - data === undefined - ? new Response(defaultData, { - headers: defaultHeaders, - status, - statusText, - }) - : json(data, { headers, status, statusText }) - - return [key, makeResponse] - }), - - ...Object.entries(STATUS_CODE) - .filter(([_, status]) => !withoutBody.has(status) && !notErrors.has(status)) - .map(([key, status]) => { - const statusText = STATUS_TEXT[status] - const name = `${key}Error` - return [ - name, - class extends ResponseError { - constructor(data?: unknown, headers?: HeadersInit) { - super(statusText, respond[key as ErrorStatus](data, headers)) - this.name = name - } - }, - ] - }), - ['ResponseError', ResponseError], -]) as ( - & { - [k in Exclude]: ( - data?: unknown, - headers?: HeadersInit, - ) => Response - } - & { - [k in Extract]: ( - headers?: HeadersInit, - ) => Response - } - & { - [ - k in `${Exclude< - Exclude, - StatusNotErrors - >}Error` - ]: new (data?: unknown, headers?: HeadersInit) => ResponseError - } - & { ResponseError: typeof ResponseError } -) diff --git a/api/lib/router.test.js b/api/lib/router.test.js deleted file mode 100644 index 90fc55c..0000000 --- a/api/lib/router.test.js +++ /dev/null @@ -1,265 +0,0 @@ -import { eq, test } from '/api/lib/test.ts' -import { makeContext } from '/api/lib/context.ts' -import { makeRouter } from '/api/lib/router.ts' -import { ARR, BOOL, NUM, OBJ, optional, STR } from '/api/lib/validator.ts' -import { startTime } from '/api/lib/time.ts' - -const router = (route) => makeRouter(route).handle -const makeContextReq = (path, init) => { - const url = new URL(path, 'http://localhost') - return makeContext(url, { req: new Request(url, init) }) -} - -test('Router - Basic HTTP Methods', async (step) => { - await step('GET request without params', async () => { - const r = router({ - 'GET/health': { - fn: () => ({ status: 'ok' }), - output: OBJ({ status: STR() }), - }, - }) - const response = await r(makeContext('/health')) - eq(response.status, 200) - - const data = await response.json() - eq(data, { status: 'ok' }) - }) - - await step('GET request with query params', async () => { - const r = router({ - 'GET/users': { - fn: (_ctx, input) => ({ id: input.id, name: 'John' }), - input: OBJ({ id: STR() }), - output: OBJ({ id: STR(), name: STR() }), - }, - }) - const response = await r(makeContext('/users?id=123')) - eq(response.status, 200) - - const data = await response.json() - eq(data, { id: '123', name: 'John' }) - }) - - await step('POST request with body', async () => { - const r = router({ - 'POST/users': { - fn: (_ctx, input) => ({ ...input, id: 1 }), - input: OBJ({ name: STR(), age: NUM() }), - output: OBJ({ id: NUM(), name: STR(), age: NUM() }), - }, - }) - const response = await r( - makeContextReq('/users', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ name: 'John', age: 30 }), - }), - ) - const data = await response.json() - - eq(response.status, 200) - eq(data, { id: 1, name: 'John', age: 30 }) - }) - - await step('DELETE request with params', async () => { - const r = router({ - 'DELETE/users': { - fn: (_ctx, input) => ({ deleted: input.id }), - input: OBJ({ id: NUM() }), - output: OBJ({ deleted: NUM() }), - }, - }) - const response = await r( - makeContextReq('/users', { - method: 'DELETE', - body: JSON.stringify({ id: 123 }), - }), - ) - eq(response.status, 200) - - const data = await response.json() - eq(data, { deleted: 123 }) - }) -}) - -test('Router - Complex Data Structures', async (step) => { - await step('Nested objects and arrays', async () => { - const r = router({ - 'POST/articles': { - fn: (_ctx, input) => ({ id: 1, ...input, created: true }), - input: OBJ({ - title: STR(), - content: STR(), - tags: ARR(STR()), - author: OBJ({ name: STR(), email: STR() }), - }), - output: OBJ({ - id: NUM(), - title: STR(), - content: STR(), - tags: ARR(STR()), - author: OBJ({ name: STR(), email: STR() }), - created: BOOL(), - }), - }, - }) - - const response = await r( - makeContextReq('/articles', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - title: 'Test Article', - content: 'Content here', - tags: ['test', 'article'], - author: { - name: 'John', - email: 'john@test.com', - }, - }), - }), - ) - const data = await response.json() - - eq(response.status, 200) - eq(data, { - created: true, - id: 1, - title: 'Test Article', - content: 'Content here', - tags: ['test', 'article'], - author: { - name: 'John', - email: 'john@test.com', - }, - }) - }) - - await step('Optional fields handling', async () => { - const r = router({ - 'POST/posts': { - fn: (_ctx, input) => ({ id: 1, ...input }), - input: OBJ({ - title: STR(), - content: STR(), - tags: optional(ARR(STR())), - draft: optional(BOOL()), - }), - output: OBJ({ - id: NUM(), - title: STR(), - content: STR(), - tags: optional(ARR(STR())), - draft: optional(BOOL()), - }), - }, - }) - - const response = await r( - makeContextReq('/posts', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ title: 'Test Post', content: 'Content here' }), - }), - ) - - eq(response.status, 200) - - const data = await response.json() - eq(data.title, 'Test Post') - eq(data.tags, undefined) - }) -}) - -test('Router - Error Handling', async (step) => { - await step('404 - Route not found', async () => { - const r = router({ - 'GET/test': { - fn: () => ({ ok: true }), - input: OBJ({}), - output: OBJ({ ok: BOOL() }), - }, - }) - const response = await r(makeContext('/nonexistent')) - eq(response.status, 404) - }) - - await step('400 - Invalid input type', async () => { - const r = router({ - 'POST/users': { - fn: (_ctx, input) => input, - input: OBJ({ age: NUM() }), - output: OBJ({ age: NUM() }), - }, - }) - const response = await r( - makeContextReq('/users', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ age: 'not a number' }), - }), - ) - eq(response.status, 400) - }) - - await step('400 - Missing required field', async () => { - const r = router({ - 'POST/users': { - fn: (_ctx, input) => input, - input: OBJ({ name: STR(), age: NUM() }), - output: OBJ({ name: STR(), age: NUM() }), - }, - }) - const response = await r( - makeContextReq('/users', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ name: 'John' }), // missing age - }), - ) - eq(response.status, 400) - }) - - await step('400 - Invalid JSON body', async () => { - const r = router({ - 'POST/users': { - fn: (_ctx, input) => input, - input: OBJ({ name: STR() }), - output: OBJ({ name: STR() }), - }, - }) - const response = await r( - makeContextReq('/users', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: 'invalid json', - }), - ) - eq(response.status, 400) - }) -}) - -test('Router - Context should be passed to handler', async (step) => { - await step('should handle custom headers', async () => { - const r = router({ - 'GET/auth': { - fn: (ctx) => ({ trace: ctx.trace }), - input: OBJ({}), - output: OBJ({ trace: NUM() }), - }, - }) - const response = await r(makeContext('/auth')) - eq(response.status, 200) - - const data = await response.json() - eq(data, { trace: startTime }) - }) -}) - -// TODO: -// add tests for no ouput -// add tests for no input and no output -// add test that failing handler error should be passed untouched -// add check that GET can only have OBJ({ [k]: STR() }) as input -// add stringify / parse optional methods to have payload formatting and validation -// ex: handle Date type, numbers in GET requests etc... diff --git a/api/lib/router.ts b/api/lib/router.ts deleted file mode 100644 index 67aab57..0000000 --- a/api/lib/router.ts +++ /dev/null @@ -1,121 +0,0 @@ -import type { Def } from '/api/lib/validator.ts' -import { respond } from '/api/lib/response.ts' -import type { RequestContext as Ctx } from '/api/lib/context.ts' - -// Supported HTTP methods -export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' -type Path = `/${string}` -export type RoutePattern = `${HttpMethod}${Path}` - -type RequestHandler = (ctx: Ctx) => Awaitable -export type Awaitable = Promise | T -export type Asserted = ReturnType -type Nullish = null | undefined | void -type Respond = Awaitable -type HandlerFn = [TInput] extends [Def] - ? [TOutput] extends [Def] - ? (ctx: Ctx, input: Asserted) => Respond> - : (ctx: Ctx, input: Asserted) => Respond - : [TOutput] extends [Def] ? (ctx: Ctx) => Respond> - : (ctx: Ctx) => Respond - -type AuthorizeHandler = [TInput] extends [Def] - ? (ctx: Ctx, input: Asserted) => Awaitable - : (ctx: Ctx) => Awaitable - -export type Handler = { - authorize?: AuthorizeHandler - fn: HandlerFn - description?: string - input?: TInput - output?: TOutput -} - -export const route = ( - h: TInput extends Def ? TOutput extends Def ? Handler - : Handler - : TOutput extends Def ? Handler - : Handler, -) => h - -export type Router = { - [P in T]: Handler -} - -const getPayloadParams = (ctx: Ctx) => Object.fromEntries(ctx.url.searchParams) -const getPayloadBody = async (ctx: Ctx) => { - try { - return await ctx.req.json() - } catch { - return {} - } -} - -type Route = Record -type SimpleHandler = (ctx: Ctx, payload: unknown) => Respond - -export const makeRouter = (defs: Router) => { - const routeMaps: Record = Object.create(null) - - for (const key in defs) { - const slashIndex = key.indexOf('/') - const method = key.slice(0, slashIndex) as HttpMethod - const url = key.slice(slashIndex) - if (!routeMaps[url]) { - routeMaps[url] = Object.create(null) as Route - routeMaps[`${url}/`] = routeMaps[url] - } - const { fn, input, authorize } = defs[key] as Handler - const simpleHandler = async ( - ctx: Ctx, - payload?: Asserted, - ) => { - try { - await authorize?.(ctx, payload as Asserted) - } catch (err) { - const message = err instanceof Error ? err.message : 'Unauthorized' - return respond.Unauthorized({ message }) - } - const result = await (fn as SimpleHandler)(ctx, payload) - if (result == null) return respond.NoContent() - return result instanceof Response ? result : respond.OK(result) - } - if (input) { - const getPayload = method === 'GET' ? getPayloadParams : getPayloadBody - const assert = input.assert - const report = input.report || (() => [`Expect a ${input?.type}`]) - routeMaps[url][method] = async (ctx: Ctx) => { - const payload = await getPayload(ctx) - let asserted - try { - asserted = assert(payload) - } catch { - const message = 'Input validation failed' - const failures = report(payload) - return respond.BadRequest({ message, failures }) - } - try { - await authorize?.(ctx, payload) - } catch (err) { - const message = err instanceof Error ? err.message : 'Unauthorized' - return respond.Unauthorized({ message }) - } - return simpleHandler(ctx, asserted) - } - } else { - routeMaps[url][method] = simpleHandler - } - } - - const handle = (ctx: Ctx) => { - const route = routeMaps[ctx.url.pathname] - if (!route) return respond.NotFound() - - const handler = route[ctx.req.method as HttpMethod] - if (!handler) return respond.MethodNotAllowed() - - return handler(ctx) - } - - return { handle } -} diff --git a/api/lib/test.ts b/api/lib/test.ts deleted file mode 100644 index f544b21..0000000 --- a/api/lib/test.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { - assertEquals as eq, - assertRejects as rejects, - assertThrows as throws, -} from 'jsr:@std/assert' - -import { now } from '/api/lib/time.ts' -import { debug } from '/api/lib/log.ts' -import { APP_ENV } from '/api/lib/env.ts' -import { type RequestContext, requestContext } from '/api/lib/context.ts' - -// short aliases for most used asserts -export { eq, rejects, throws } - -// test wrapper that sets a context -type TestStep = Deno.TestContext['step'] -export const test = ( - name: string, - handler: TestStep, - opts?: Omit, -) => { - if (APP_ENV !== 'test') return - const ignore = APP_ENV !== 'test' || opts?.ignore - const url = new URL( - Error('').stack!.split('\n')[1].match(/\([^)]+\)/)![0].slice(1, -1), - ) - const ctx = { - req: new Request(url), - url, - cookies: {}, - trace: now(), - } as RequestContext - const fn = async (t: Deno.TestContext) => { - debug('test-start', { name, origin: t.origin }) - await handler(async (...args) => { - await requestContext.run({ ...ctx, span: now() }, async () => { - const stepName = args[0]?.name || args[0] - debug('step-start', { name: stepName, origin: t.origin }) - const result = await t.step( - ...args as unknown as Parameters, - ) - debug('step-end', { name: stepName, origin: t.origin }) - return result - }) - }) - } - return requestContext.run(ctx, () => Deno.test({ ...opts, name, fn, ignore })) -} diff --git a/api/lib/time.ts b/api/lib/time.ts deleted file mode 100644 index e61b4d7..0000000 --- a/api/lib/time.ts +++ /dev/null @@ -1,26 +0,0 @@ -// Shared functions to manage time - -export const startTime = performance.timeOrigin / 1000 -/** - * Return the current timestamp in seconds since the epoch. - * Uses the performance API for high precision timing. - * Ensured to be unique per process. - * - * @example - * const timestamp = now(); - * console.log(timestamp); // Output: 1731521396.557123 (example timestamp) - */ -let lastTime = startTime -export const now = (): number => { - const time = startTime + performance.now() / 1000 - if (time === lastTime) return now() - lastTime = time - return time -} - -export const SEC = 1 -export const MIN = 60 -export const HOUR = 60 * MIN -export const DAY = 24 * HOUR -export const WEEK = 7 * DAY -export const YEAR = 365.2422 * DAY diff --git a/api/lib/validator.test.js b/api/lib/validator.test.js deleted file mode 100644 index ba8d99c..0000000 --- a/api/lib/validator.test.js +++ /dev/null @@ -1,72 +0,0 @@ -import { eq, test, throws } from '/api/lib/test.ts' -import { ARR, NUM, OBJ, optional, STR } from '/api/lib/validator.ts' - -test('Validator - Success cases', async (step) => { - await step('should validate simple object', () => { - const schema = OBJ({ - name: STR('User name'), - age: NUM('User age'), - }) - - const data = { name: 'John', age: 30 } - const result = schema.assert(data) - eq(result, data) - }) - - await step('should validate nested object with arrays', () => { - const schema = OBJ({ - user: OBJ({ - name: STR(), - tags: ARR(STR()), - }), - }) - - const data = { - user: { - name: 'John', - tags: ['admin', 'user'], - }, - } - const result = schema.assert(data) - eq(result, data) - }) - - await step('should handle optional fields', () => { - const schema = OBJ({ - name: STR(), - age: optional(NUM()), - }) - - const data = { name: 'John' } - const result = schema.assert(data) - eq(result, data) - }) -}) - -test('Validator - Error cases', async (step) => { - await step('should throw on missing required field', () => { - const schema = OBJ({ - name: STR(), - age: NUM(), - }) - - throws( - () => schema.assert({ name: 'John' }), - Error, - 'type assertion failed', - ) - }) - - await step('should throw on invalid type', () => { - const schema = OBJ({ - name: STR(), - age: NUM(), - }) - - throws( - () => schema.assert({ name: 'John', age: '30kds' }), - Error, - 'type assertion failed', - ) - }) -}) diff --git a/api/lib/validator.ts b/api/lib/validator.ts deleted file mode 100644 index 993c126..0000000 --- a/api/lib/validator.ts +++ /dev/null @@ -1,315 +0,0 @@ -// deno-lint-ignore-file no-explicit-any - -type ValidatorFailure = { - type: T['type'] - path: (string | number)[] - value: unknown -} - -type Validator = ( - value: unknown, - path?: (string | number)[], -) => ValidatorFailure[] - -type DefArray = { - type: 'array' - of: Def - report: Validator - optional?: boolean - description?: string - assert: (value: unknown) => ReturnType[] -} - -type DefList = { - type: 'list' - of: T - report: Validator> - optional?: boolean - description?: string - assert: (value: unknown) => T[number] -} - -type DefUnion = { - type: 'union' - of: T - report: Validator> - optional?: boolean - description?: string - assert: (value: unknown) => ReturnType -} - -type DefObject> = { - type: 'object' - properties: { [K in keyof T]: T[K] } - report: Validator - optional?: boolean - description?: string - assert: (value: unknown) => { [K in keyof T]: ReturnType } -} - -type DefString = { - type: 'string' - assert: AssertType - report: Validator - optional?: boolean - description?: string -} - -type DefNumber = { - type: 'number' - assert: AssertType - report: Validator - optional?: boolean - description?: string -} - -type DefBoolean = { - type: 'boolean' - assert: AssertType - report: Validator - optional?: boolean - description?: string -} - -export type DefBase = - | DefString - | DefNumber - | DefBoolean - | DefArray - | DefObject> - | DefList - | DefUnion - -type OptionalAssert = ( - value: unknown, -) => ReturnType | undefined - -type Optional = T & { - assert: OptionalAssert -} - -type AssertType = (value: unknown) => T - -export type Def = T extends DefBase ? DefArray - : T extends Record ? DefObject - : DefBase - -const reportObject = >(properties: T) => { - const body = [ - 'if (!o || typeof o !== "object") return [{ path: p, type: "object", value: o }]', - 'const failures = []', - ...Object.entries(properties).map(([key, def], i) => { - const k = JSON.stringify(key) - const path = `[...p, ${k}]` - if (def.type === 'object' || def.type === 'array') { - const check = ` - const _${i} = v[${k}].report(o[${k}], ${path}); - _${i}.length && failures.push(..._${i}) - ` - return def.optional ? `if (o[${k}] !== undefined) {${check}}` : check - } - const opt = def.optional ? `o[${k}] === undefined || ` : '' - return (`${opt}typeof o[${k}] === "${def.type}" || failures.push({ ${ - [`path: ${path}`, `type: "${def.type}"`, `value: o[${k}]`].join(', ') - } })`) - }), - 'return failures', - ].join('\n') - - return new Function('v, o, p = []', body).bind( - globalThis, - properties, - ) as DefObject['report'] -} - -const assertObject = >(properties: T) => { - const body = [ - 'if (!o || typeof o !== "object") throw Error("type assertion failed")', - ...Object.entries(properties).map(([key, def]) => { - const k = JSON.stringify(key) - return `${ - def.optional ? `v[${k}] === undefined ||` : '' - }v[${k}].assert(o[${k}])` - }), - 'return o', - ].join('\n') - - return new Function('v, o', body).bind(globalThis, properties) as DefObject< - T - >['assert'] -} - -const reportArray = (def: Def) => { - const body = [ - 'if (!Array.isArray(a)) return [{ path: p, type: "array", value: a }]', - 'const failures = []', - 'let i = -1; const max = a.length', - 'while (++i < max) {', - ' const e = a[i]', - def.type === 'object' || def.type === 'array' - ? `const _ = v.report(e, [...p, i]); (_.length && failures.push(..._))` - : `${ - def.optional ? 'e === undefined ||' : '' - }typeof e === "${def.type}" || failures.push({ ${ - [ - `path: [...p, i]`, - `type: "${def.type}"`, - `value: e`, - ].join(', ') - } })`, - ' if (failures.length > 9) return failures', - '}', - 'return failures', - ].join('\n') - - return new Function('v, a, p = []', body) -} - -const assertArray = (assert: T) => (a: unknown) => { - if (!Array.isArray(a)) throw Error('type assertion failed') - a.forEach(assert) - return a as ReturnType[] -} - -const assertNumber = (value: unknown) => { - if (typeof value === 'number' && !isNaN(value)) return value - throw Error(`type assertion failed`) -} - -const assertString = (value: unknown) => { - if (typeof value === 'string') return value - throw Error(`type assertion failed`) -} - -const assertBoolean = (value: unknown) => { - if (typeof value === 'boolean') return value - throw Error(`type assertion failed`) -} - -export const NUM = (description?: string) => - ({ - type: 'number', - assert: assertNumber, - description, - report: (value: unknown) => [{ type: 'number', value, path: [] }], - }) satisfies DefNumber - -export const STR = (description?: string) => - ({ - type: 'string', - assert: assertString, - description, - report: (value: unknown) => [{ type: 'string', value, path: [] }], - }) satisfies DefString - -export const BOOL = (description?: string) => - ({ - type: 'boolean', - assert: assertBoolean, - description, - report: (value: unknown) => [{ type: 'boolean', value, path: [] }], - }) satisfies DefBoolean - -export const optional = (def: T): Optional => { - const { assert, description, ...rest } = def - const optionalAssert: OptionalAssert = (value: unknown) => - value === undefined ? undefined : assert(value) - return { - ...rest, - description, - optional: true, - assert: optionalAssert, - } as Optional -} - -export const OBJ = >( - properties: T, - description?: string, -): DefObject => { - const report = reportObject(properties) - const assert = assertObject(properties) - return { type: 'object', properties, report, assert, description } -} - -export const ARR = ( - def: T, - description?: string, -): DefArray => ({ - type: 'array', - of: def, - report: reportArray(def).bind(globalThis, def), - assert: assertArray(def.assert) as DefArray['assert'], - description, -}) - -export const LIST = ( - possibleValues: T, - description?: string, -): DefList => ({ - type: 'list', - of: possibleValues, - report: (value: unknown, path: (string | number)[] = []) => { - if (possibleValues.includes(value as T[number])) return [] - return [{ - path, - type: 'list', - value, - expected: possibleValues, - }] - }, - assert: (value: unknown): T[number] => { - if (possibleValues.includes(value as T[number])) { - return value as T[number] - } - throw new Error( - `Invalid value. Expected one of: ${possibleValues.join(', ')}`, - ) - }, - description, -}) - -export const UNION = (...types: T): DefUnion => ({ - type: 'union', - of: types, - report: (value: unknown, path: (string | number)[] = []) => { - const failures: ValidatorFailure>[] = [] - for (const type of types) { - const result = type.report(value, path) - if (result.length === 0) return [] - failures.push(...result) - } - return failures - }, - assert: (value: unknown): ReturnType => { - for (const type of types) { - try { - return type.assert(value) - } catch { - // Ignore - } - } - throw new Error( - `Invalid value. Expected one of: ${types.map((t) => t.type).join(', ')}`, - ) - }, -}) - -// const Article = OBJ({ -// id: NUM("Unique identifier for the article"), -// title: STR("Title of the article"), -// isDraft: optional(BOOL("Whether the article is in draft state")), -// tags: ARR(STR("A tag name"), "List of tags associated with the article"), -// author: optional(OBJ({ -// id: NUM("Author's unique identifier"), -// }, "Author information")), -// }, "Article object containing all article information"); - -// type ArticleType = ReturnType; - -// const aaa = Article.report({ -// id: 5, -// title: "hello", -// isDraft: true, -// tags: [], -// // author: { id: 1 }, -// }); diff --git a/api/picture.ts b/api/picture.ts index 82047e7..8c43268 100644 --- a/api/picture.ts +++ b/api/picture.ts @@ -1,16 +1,16 @@ -import { crypto } from 'jsr:@std/crypto/crypto' -import { encodeBase64Url } from 'jsr:@std/encoding/base64url' -import { ensureDirSync, exists } from 'jsr:@std/fs' -import { Picture_Dir } from './lib/env.ts' +import { crypto } from '@std/crypto/crypto' +import { encodeBase64Url } from '@std/encoding/base64url' +import { ensureDirSync, exists } from '@std/fs' +import { PICTURE_DIR } from '/api/lib/env.ts' -ensureDirSync(Picture_Dir) +ensureDirSync(PICTURE_DIR) const encoder = new TextEncoder() export const savePicture = async (url?: string) => { if (!url) return const bytes = await crypto.subtle.digest('BLAKE3', encoder.encode(url)) const hash = encodeBase64Url(bytes) - const file = `${Picture_Dir}/${hash}` + const file = `${PICTURE_DIR}/${hash}` if (await exists(file)) return hash const req = await fetch(url) const data = await req.arrayBuffer() @@ -19,7 +19,7 @@ export const savePicture = async (url?: string) => { } export const getPicture = async (hash: string) => { - const picture = await Deno.open(`${Picture_Dir}/${hash}`) + const picture = await Deno.open(`${PICTURE_DIR}/${hash}`) return new Response(picture.readable, { headers: { 'content-type': 'image/png', diff --git a/api/project.ts b/api/project.ts deleted file mode 100644 index e69de29..0000000 diff --git a/api/routes.ts b/api/routes.ts index 390f5f3..51e3485 100644 --- a/api/routes.ts +++ b/api/routes.ts @@ -1,6 +1,7 @@ -import { makeRouter, route } from '/api/lib/router.ts' -import type { RequestContext } from '/api/lib/context.ts' -import { handleGoogleCallback, initiateGoogleAuth } from './auth.ts' +import { makeRouter, route } from '@01edu/api/router' +import type { RequestContext } from '@01edu/api/context' +import { handleGoogleCallback, initiateGoogleAuth } from '/api/auth.ts' +import { log } from '/api/lib/log.ts' import { DatabaseSchemasCollection, DeploymentDef, @@ -8,53 +9,47 @@ import { ProjectsCollection, TeamDef, TeamsCollection, - User, UserDef, UsersCollection, } from './schema.ts' -import { ARR, BOOL, LIST, NUM, OBJ, optional, STR } from './lib/validator.ts' -import { respond } from './lib/response.ts' -import { deleteCookie } from 'jsr:@std/http/cookie' +import { ARR, BOOL, LIST, NUM, OBJ, optional, STR } from '@01edu/api/validator' +import { respond } from '@01edu/api/response' +import { deleteCookie } from '@std/http/cookie' import { getPicture } from '/api/picture.ts' import { getLogs, insertLogs, LogSchemaOutput, LogsInputSchema, -} from './clickhouse-client.ts' -import { decryptMessage, encryptMessage } from './user.ts' -import { log } from './lib/log.ts' +} from '/api/clickhouse-client.ts' +import { decodeSession, decryptMessage, encryptMessage } from '/api/user.ts' import { type ColumnInfo, fetchTablesData, runSQL, SQLQueryError, -} from './sql.ts' +} from '/api/sql.ts' -const withUserSession = ({ user }: RequestContext) => { - if (!user) throw Error('Missing user session') +const withUserSession = async ({ cookies }: RequestContext) => { + const session = await decodeSession(cookies.session) + if (!session) throw Error('Missing user session') + return session } -const withAdminSession = ({ user }: RequestContext) => { - if (!user || !user.isAdmin) throw Error('Admin access required') +const withAdminSession = async (ctx: RequestContext) => { + const session = await withUserSession(ctx) + if (!session || !session.isAdmin) throw Error('Admin access required') } const withDeploymentSession = async (ctx: RequestContext) => { const token = ctx.req.headers.get('Authorization')?.replace(/^Bearer /i, '') - if (!token) throw respond.Unauthorized({ message: 'Missing token' }) - try { - const message = await decryptMessage(token) - if (!message) throw respond.Unauthorized({ message: 'Invalid token' }) - const data = JSON.parse(message) - const dep = DeploymentsCollection.get(data?.url) - if (!dep || dep.tokenSalt !== data?.tokenSalt) { - throw respond.Unauthorized({ message: 'Invalid token' }) - } - ctx.resource = dep?.url - } catch (error) { - log.error('Error validating deployment token:', { error }) - throw respond.Unauthorized({ message: 'Invalid token' }) - } + if (!token) throw Error('Missing token') + const message = await decryptMessage(token) + if (!message) throw Error('Invalid token') + const data = JSON.parse(message) + const dep = DeploymentsCollection.get(data?.url) + if (!dep || dep.tokenSalt !== data?.tokenSalt) throw Error('Invalid token') + return dep } const userInTeam = (teamId: string, userEmail?: string) => { @@ -99,7 +94,7 @@ const defs = { }), 'GET/api/user/me': route({ authorize: withUserSession, - fn: ({ user }) => user as User, + fn: ({ session }) => session, output: UserDef, description: 'Handle Google OAuth callback', }), @@ -163,7 +158,11 @@ const defs = { }), 'PUT/api/team': route({ authorize: withAdminSession, - fn: (_ctx, input) => TeamsCollection.update(input.teamId, input), + fn: (_ctx, input) => + TeamsCollection.update(input.teamId, { + teamName: input.teamName, + teamMembers: input.teamMembers || undefined, + }), input: OBJ({ teamId: STR('The ID of the team'), teamName: STR('The name of the team'), @@ -334,10 +333,7 @@ const defs = { const token = await encryptMessage( JSON.stringify({ url: deployment.url, tokenSalt }), ) - return { - ...deployment, - token, - } + return { ...deployment, token } }, input: OBJ({ url: STR('The URL of the deployment') }), output: deploymentOutput, @@ -388,8 +384,8 @@ const defs = { 'POST/api/logs': route({ authorize: withDeploymentSession, fn: (ctx, logs) => { - if (!ctx.resource) throw respond.InternalServerError() - return insertLogs(ctx.resource, logs) + if (!ctx.session.url) throw respond.InternalServerError() + return insertLogs(ctx.session.url, logs) }, input: LogsInputSchema, description: 'Insert logs into ClickHouse NB: a Bearer token is required', @@ -408,8 +404,8 @@ const defs = { } const project = ProjectsCollection.get(deployment.projectId) if (!project) throw respond.NotFound({ message: 'Project not found' }) - if (!project.isPublic && !ctx.user?.isAdmin) { - if (!userInTeam(project.teamId, ctx.user?.userEmail)) { + if (!project.isPublic && !ctx.session.isAdmin) { + if (!userInTeam(project.teamId, ctx.session.userEmail)) { throw respond.Forbidden({ message: 'Access to project logs denied' }) } } @@ -451,7 +447,7 @@ const defs = { throw respond.NotFound({ message: 'Deployment not found' }) } - if (!dep?.databaseEnabled) { + if (!dep.databaseEnabled) { throw respond.BadRequest({ message: 'Database not enabled for deployment', }) @@ -459,8 +455,8 @@ const defs = { const project = ProjectsCollection.get(dep.projectId) if (!project) throw respond.NotFound({ message: 'Project not found' }) - if (!project.isPublic && !ctx.user?.isAdmin) { - if (!userInTeam(project.teamId, ctx.user?.userEmail)) { + if (!project.isPublic && !ctx.session.isAdmin) { + if (!userInTeam(project.teamId, ctx.session.userEmail)) { throw respond.Forbidden({ message: 'Access to project tables denied', }) @@ -480,7 +476,7 @@ const defs = { tableDef.columnsMap as unknown as Map, ) } catch (err) { - console.error('fetchTablesData error', err) + log.error('fetchTablesData-error', { stack: (err as Error)?.stack }) throw err } }, @@ -523,7 +519,7 @@ const defs = { throw new respond.NotFoundError({ message: 'Deployment not found' }) } - if (!dep?.databaseEnabled) { + if (!dep.databaseEnabled) { throw new respond.BadRequestError({ message: 'Database not enabled for deployment', }) @@ -531,8 +527,8 @@ const defs = { const project = ProjectsCollection.get(dep.projectId) if (!project) throw respond.NotFound({ message: 'Project not found' }) - if (!project.isPublic && !ctx.user?.isAdmin) { - if (!userInTeam(project.teamId, ctx.user?.userEmail)) { + if (!project.isPublic && !ctx.session.isAdmin) { + if (!userInTeam(project.teamId, ctx.session.userEmail)) { throw new respond.ForbiddenError({ message: 'Access to project queries denied', }) @@ -582,4 +578,4 @@ const defs = { } as const export type RouteDefinitions = typeof defs -export const routeHandler = makeRouter(defs).handle +export const routeHandler = makeRouter(log, defs) diff --git a/api/schema.ts b/api/schema.ts index 106ed6f..7b10848 100644 --- a/api/schema.ts +++ b/api/schema.ts @@ -1,6 +1,13 @@ -import { ARR, BOOL, NUM, OBJ, optional, STR } from './lib/validator.ts' -import { Asserted } from './lib/router.ts' -import { createCollection } from './lib/json_store.ts' +import { + ARR, + type Asserted, + BOOL, + NUM, + OBJ, + optional, + STR, +} from '@01edu/api/validator' +import { createCollection } from '/api/lib/json_store.ts' export const UserDef = OBJ({ userEmail: STR('The user email address'), diff --git a/api/server.ts b/api/server.ts index a5ec8c1..1e816b3 100644 --- a/api/server.ts +++ b/api/server.ts @@ -1,110 +1,28 @@ -// api/server.ts -import { decodeSession } from '/api/user.ts' -import { respond } from '/api/lib/response.ts' -import { now } from '/api/lib/time.ts' +import { serveDir } from '@std/http/file-server' +import { APP_ENV } from '@01edu/api/env' +import { server } from '@01edu/api/server' import { log } from '/api/lib/log.ts' import { routeHandler } from '/api/routes.ts' -import { getCookies, setCookie } from 'jsr:@std/http/cookie' -import { type RequestContext, requestContext } from '/api/lib/context.ts' -import { join } from 'jsr:@std/path/join' -import { serveDir } from 'jsr:@std/http/file-server' -import { PORT } from './lib/env.ts' -import { startSchemaRefreshLoop } from './sql.ts' -const isProd = Deno.args.includes('--env=prod') -const staticDir = isProd - ? join((import.meta.dirname || Deno.cwd()).split('/api')[0], 'dist/web') - : join(Deno.cwd(), 'dist/web') -const indexHtml = isProd - ? await Deno.readFile(join(staticDir, 'index.html')) - : '' -const htmlContent = { headers: { 'Content-Type': 'text/html' } } -const serveDirOpts = { fsRoot: staticDir } - -const { ResponseError } = respond - -const handleRequest = async (ctx: RequestContext) => { - const logProps: Record = {} - logProps.path = `${ctx.req.method}:${ctx.url.pathname.slice('/api/'.length)}` - log.info('in', logProps) - try { - const res = await routeHandler(ctx) - logProps.status = res.status - logProps.duration = now() - ctx.span! - log.info('out', logProps) - return res - } catch (err) { - let response: Response - if (err instanceof ResponseError) { - response = err.response - logProps.status = response.status - } else { - logProps.status = 500 - logProps.stack = err - response = respond.InternalServerError() - } - - logProps.duration = now() - ctx.span! - log.error('out', logProps) - return response - } +const fetch = server({ log, routeHandler }) +export default { + fetch(req: Request) { + return fetch(req, new URL(req.url)) + }, } -export const fetch = async (req: Request) => { - const url = new URL(req.url) - const method = req.method - if (method === 'OPTIONS') return respond.NoContent() - - if (url.pathname.startsWith('/api')) { - // Build the request context - const cookies = getCookies(req.headers) - const ctx = { - req, - url, - cookies, - trace: cookies.trace ? Number(cookies.trace) : now(), - user: await decodeSession(cookies.session), - span: now(), - resource: undefined, - } - - const res = await requestContext.run(ctx, handleRequest, ctx) - if (!cookies.trace) { - // if the cookies do not yet have a trace, we set it for the future - setCookie(res.headers, { - name: 'trace', - value: String(ctx.trace), - path: '/', - secure: true, - httpOnly: true, - sameSite: 'Lax', - }) - } - return res - } - - // Serve static files in production - if (isProd) { - if (url.pathname.includes('.')) { - return serveDir(req, serveDirOpts) - } - +if (APP_ENV === 'prod') { + const indexHtml = await Deno.readFile( + import.meta.dirname + '/web/dist/index.html', + ) + const htmlContent = { headers: { 'Content-Type': 'text/html' } } + const serveDirOpts = { fsRoot: import.meta.dirname + '/web/dist' } + Deno.serve((req) => { + const url = new URL(req.url) + if (url.pathname.startsWith('/api/')) return fetch(req, url) + if (url.pathname.includes('.')) return serveDir(req, serveDirOpts) return new Response(indexHtml, htmlContent) - } - - // In development, redirect to Vite dev server - return new Response('Use Vite dev server for frontend', { status: 404 }) -} - -log.info('server-start') - -// Start periodic DB schema refresh (non-blocking) -try { - startSchemaRefreshLoop() -} catch (err) { - log.error('schema-loop-start-failed', { err }) + }) +} else { + log.info('server-start') } - -Deno.serve({ - port: PORT, -}, fetch) diff --git a/api/sql.ts b/api/sql.ts index c790ec0..42e10b5 100644 --- a/api/sql.ts +++ b/api/sql.ts @@ -2,9 +2,9 @@ import { DatabaseSchemasCollection, Deployment, DeploymentsCollection, -} from './schema.ts' -import { DB_SCHEMA_REFRESH_MS } from './lib/env.ts' -import { log } from './lib/log.ts' +} from '/api/schema.ts' +import { DB_SCHEMA_REFRESH_MS } from '/api/lib/env.ts' +import { log } from '/api/lib/log.ts' export class SQLQueryError extends Error { constructor(message: string, body: string) { diff --git a/api/user.ts b/api/user.ts index ca3c001..8c6ee0a 100644 --- a/api/user.ts +++ b/api/user.ts @@ -1,7 +1,6 @@ -import { SECRET } from './lib/env.ts' -import { UsersCollection } from './schema.ts' - -import { decodeBase64Url, encodeBase64Url } from 'jsr:@std/encoding/base64url' +import { decodeBase64Url, encodeBase64Url } from '@std/encoding/base64url' +import { SECRET } from '/api/lib/env.ts' +import { UsersCollection } from '/api/schema.ts' const encoder = new TextEncoder() const decoder = new TextDecoder() diff --git a/deno.json b/deno.json index 38f7405..efdc4ef 100644 --- a/deno.json +++ b/deno.json @@ -1,45 +1,58 @@ { "tasks": { - "api:dev": "deno run -A --env-file=.env.dev api/server.ts --env=dev --port=3021", - "vite:dev": "deno run -A --env-file=.env.dev tasks/vite.js --env=dev", - "clickhouse:dev": "deno run -A --env-file=.env.dev tasks/clickhouse.ts --env=dev", - "dev": { "dependencies": ["clickhouse:dev", "api:dev", "vite:dev"] }, - "seed": "deno run -A --env-file=.env.dev tasks/seed.ts", - "dev:with-seed": "deno task seed && deno task dev", - "api:prod": "deno compile -A --no-check --output dist/api --target x86_64-unknown-linux-gnu --include dist/web api/server.ts --env=prod", - "vite:prod": "deno run -A tasks/vite.js --build --env=prod", - "clickhouse:prod": "deno run -A --env-file=.env.prod tasks/clickhouse.ts --env=prod", - "prod": "deno task vite:prod && deno task api:prod", - "start:prod": "deno task clickhouse:prod && dist/api --env=prod", - "fmt": "deno fmt", - "lint": "deno lint", - "check": "deno check", - "review": "deno run -A https://gistcdn.githack.com/kigiri/7658b4af30bb5eaca3e4cad1fcac7b0c/raw/review.js", - "test": "deno test --env-file=.env.test -A --unstable-worker-options --no-check", "all": "deno task check && deno task lint && deno task test --parallel", + "check": "deno check", + "dev": { "dependencies": ["dev:clickhouse", "dev:api", "dev:vite"] }, + "dev:api": "deno serve --port 3021 -A --env-file=.env.dev api/server.ts", + "dev:clickhouse": "deno run -A --env-file=.env.dev tasks/clickhouse.ts", + "dev:env": "deno run -A tasks/env.ts", + "dev:vite": "deno run -A --env-file=.env.dev tasks/vite.ts", + "dev:with-seed": "deno task seed && deno task dev", "docker:build": "docker build -t devtools .", + "docker:clean": "docker rm -f devtools-app && docker rmi devtools", + "docker:exec": "docker exec -it devtools-app /bin/sh", + "docker:logs": "docker logs -f devtools-app", "docker:prod": "docker run --name devtools-app -v \"$(pwd)/db:/app/db\" -p 8877:3021 --env-file .env.prod devtools", - "docker:stop": "docker stop devtools-app", - "docker:start": "docker start devtools-app", "docker:restart": "docker restart devtools-app", "docker:rm": "docker rm -f devtools-app", - "docker:logs": "docker logs -f devtools-app", - "docker:exec": "docker exec -it devtools-app /bin/sh", - "docker:clean": "docker rm -f devtools-app && docker rmi devtools", - "env:dev": "deno run -A tasks/env.ts" + "docker:start": "docker start devtools-app", + "docker:stop": "docker stop devtools-app", + "fmt": "deno fmt", + "lint": "deno lint", + "prod": "deno task vite:prod && deno task api:prod", + "prod:api": "deno compile -A --no-check --output dist/api --target x86_64-unknown-linux-gnu --include dist/web api/server.ts --env=prod", + "prod:clickhouse": "APP_ENV=prod deno run -A --env-file tasks/clickhouse.ts", + "prod:start": "deno task clickhouse:prod && dist/api", + "prod:vite": "deno run --env-file -A tasks/vite.ts", + "review": "deno run -A https://gistcdn.githack.com/kigiri/7658b4af30bb5eaca3e4cad1fcac7b0c/raw/review.js", + "seed": "deno run -A --env-file=.env.dev tasks/seed.ts", + "test": "deno test --env-file=.env.test -A --unstable-worker-options --no-check" }, "imports": { "./": "./", "/": "./", - "@std/assert": "jsr:@std/assert@1", - "vite": "npm:vite@^7.0.4", - "preact": "npm:preact@^10.26.9", + "@01edu/api": "jsr:@01edu/api@^0.1.3", + "@01edu/api-client": "jsr:@01edu/api-client@^0.1.3", + "@01edu/api-proxy": "jsr:@01edu/api-proxy@^0.1.2", + "@01edu/signal-router": "jsr:@01edu/signal-router@^0.1.6", + "@01edu/time": "jsr:@01edu/time@^0.1.0", + "@deno/vite-plugin": "npm:@deno/vite-plugin@^1.0.5", + "@std/assert": "jsr:@std/assert@^1.0.16", + "@std/crypto": "jsr:@std/crypto@^1.0.5", + "@std/encoding": "jsr:@std/encoding@^1.0.10", + "@std/fmt": "jsr:@std/fmt@^1.0.8", + "@std/fs": "jsr:@std/fs@^1.0.20", + "@std/http": "jsr:@std/http@^1.0.22", + "@std/path": "jsr:@std/path@^1.1.3", + "@std/testing": "jsr:@std/testing@^1.0.16", + "vite": "npm:vite@^7.3.0", + "preact": "npm:preact@^10.28.0", "@preact/preset-vite": "npm:@preact/preset-vite@^2.10.2", - "@preact/signals": "npm:@preact/signals", - "@clickhouse/client": "npm:@clickhouse/client", - "@tailwindcss/vite": "npm:@tailwindcss/vite@^4.1.11", - "tailwindcss": "npm:tailwindcss@^4.1.11", - "daisyui": "npm:daisyui@^5.0.46", + "@preact/signals": "npm:@preact/signals@^2.5.1", + "@clickhouse/client": "npm:@clickhouse/client@^1.14.0", + "@tailwindcss/vite": "npm:@tailwindcss/vite@^4.1.17", + "tailwindcss": "npm:tailwindcss@^4.1.17", + "daisyui": "npm:daisyui@^5.5.8", "lucide-preact": "npm:lucide-preact@^0.525.0" }, "fmt": { diff --git a/deno.lock b/deno.lock index 8a950eb..e1b60a7 100644 --- a/deno.lock +++ b/deno.lock @@ -1,131 +1,159 @@ { "version": "5", "specifiers": { - "jsr:@std/assert@*": "1.0.13", - "jsr:@std/assert@1": "1.0.11", - "jsr:@std/assert@^1.0.13": "1.0.13", - "jsr:@std/cli@*": "1.0.11", - "jsr:@std/cli@^1.0.8": "1.0.11", - "jsr:@std/crypto@*": "1.0.5", - "jsr:@std/encoding@*": "1.0.6", - "jsr:@std/encoding@^1.0.5": "1.0.6", - "jsr:@std/fmt@*": "1.0.4", - "jsr:@std/fmt@^1.0.3": "1.0.4", - "jsr:@std/fs@*": "1.0.19", - "jsr:@std/html@^1.0.3": "1.0.3", - "jsr:@std/http@*": "1.0.12", - "jsr:@std/internal@^1.0.10": "1.0.10", - "jsr:@std/internal@^1.0.5": "1.0.10", - "jsr:@std/internal@^1.0.6": "1.0.10", - "jsr:@std/internal@^1.0.9": "1.0.10", + "jsr:@01edu/api-client@~0.1.3": "0.1.3", + "jsr:@01edu/api-proxy@~0.1.2": "0.1.2", + "jsr:@01edu/api@~0.1.3": "0.1.3", + "jsr:@01edu/signal-router@~0.1.6": "0.1.6", + "jsr:@01edu/time@0.1": "0.1.0", + "jsr:@01edu/types@~0.1.2": "0.1.2", + "jsr:@std/assert@^1.0.15": "1.0.16", + "jsr:@std/assert@^1.0.16": "1.0.16", + "jsr:@std/async@^1.0.15": "1.0.15", + "jsr:@std/cli@^1.0.24": "1.0.24", + "jsr:@std/crypto@^1.0.5": "1.0.5", + "jsr:@std/data-structures@^1.0.9": "1.0.9", + "jsr:@std/encoding@^1.0.10": "1.0.10", + "jsr:@std/fmt@^1.0.8": "1.0.8", + "jsr:@std/fs@^1.0.19": "1.0.20", + "jsr:@std/fs@^1.0.20": "1.0.20", + "jsr:@std/html@^1.0.5": "1.0.5", + "jsr:@std/http@^1.0.22": "1.0.22", + "jsr:@std/internal@^1.0.12": "1.0.12", "jsr:@std/media-types@^1.1.0": "1.1.0", - "jsr:@std/net@^1.0.4": "1.0.4", - "jsr:@std/path@*": "1.1.1", - "jsr:@std/path@^1.0.8": "1.0.8", - "jsr:@std/path@^1.1.1": "1.1.1", - "jsr:@std/streams@^1.0.8": "1.0.8", - "jsr:@std/testing@*": "1.0.15", - "npm:@clickhouse/client@*": "1.12.1", - "npm:@preact/preset-vite@^2.10.2": "2.10.2_@babel+core@7.28.0_vite@7.0.4__picomatch@4.0.2__@types+node@22.15.15_preact@10.26.9_@types+node@22.15.15", - "npm:@preact/signals@*": "2.2.1_preact@10.26.9", - "npm:@tailwindcss/vite@^4.1.11": "4.1.11_vite@7.0.4__picomatch@4.0.2__@types+node@22.15.15_@types+node@22.15.15", - "npm:@types/node@*": "22.15.15", - "npm:create-vite-extra@latest": "4.0.0", - "npm:daisyui@^5.0.46": "5.0.46", - "npm:lucide-preact@0.525": "0.525.0_preact@10.26.9", - "npm:preact@^10.26.9": "10.26.9", - "npm:tailwindcss@^4.1.11": "4.1.11", - "npm:vite@*": "7.0.4_picomatch@4.0.2_@types+node@22.15.15", - "npm:vite@^7.0.4": "7.0.4_picomatch@4.0.2_@types+node@22.15.15" + "jsr:@std/net@^1.0.6": "1.0.6", + "jsr:@std/path@^1.1.2": "1.1.3", + "jsr:@std/path@^1.1.3": "1.1.3", + "jsr:@std/streams@^1.0.14": "1.0.14", + "jsr:@std/testing@^1.0.16": "1.0.16", + "npm:@clickhouse/client@^1.14.0": "1.15.0", + "npm:@deno/vite-plugin@^1.0.5": "1.0.5_vite@7.3.0__picomatch@4.0.3", + "npm:@preact/preset-vite@^2.10.2": "2.10.2_@babel+core@7.28.5_vite@7.3.0__picomatch@4.0.3_preact@10.28.0", + "npm:@preact/signals@^2.5.1": "2.5.1_preact@10.28.0", + "npm:@tailwindcss/vite@^4.1.17": "4.1.18_vite@7.3.0__picomatch@4.0.3", + "npm:daisyui@^5.5.8": "5.5.14", + "npm:lucide-preact@0.525": "0.525.0_preact@10.28.0", + "npm:preact@^10.27.2": "10.28.0", + "npm:preact@^10.28.0": "10.28.0", + "npm:tailwindcss@^4.1.17": "4.1.18", + "npm:vite@^7.2.4": "7.3.0_picomatch@4.0.3", + "npm:vite@^7.3.0": "7.3.0_picomatch@4.0.3" }, "jsr": { - "@std/assert@1.0.11": { - "integrity": "2461ef3c368fe88bc60e186e7744a93112f16fd110022e113a0849e94d1c83c1", + "@01edu/api@0.1.3": { + "integrity": "fcb88766e2772c6f6e5c2b9d7ede5460f532f9398c4c20f466c6d1f667d2a0d1", "dependencies": [ - "jsr:@std/internal@^1.0.5" + "jsr:@01edu/time", + "jsr:@01edu/types", + "jsr:@std/fmt", + "jsr:@std/http" ] }, - "@std/assert@1.0.13": { - "integrity": "ae0d31e41919b12c656c742b22522c32fb26ed0cba32975cb0de2a273cb68b29", + "@01edu/api-client@0.1.3": { + "integrity": "292c41437b7b77aa0b0c128a152e1b09434f45d84e8f252b5f8ce088c07f01b4", "dependencies": [ - "jsr:@std/internal@^1.0.6" + "jsr:@01edu/types", + "npm:@preact/signals" ] }, - "@std/cli@1.0.11": { - "integrity": "ec219619fdcd31bcf0d8e53bee1e2706ec9a02f70255365a094f69755dadd340" + "@01edu/api-proxy@0.1.2": { + "integrity": "1fa3fbd790dfd435893d3b3554f509749411ef342f3b62515a78cdd83a09b80e", + "dependencies": [ + "npm:vite@^7.2.4" + ] + }, + "@01edu/signal-router@0.1.6": { + "integrity": "816485c671b21472da571f692731901a15ed77725ac9dfffdcee1dba0d736c22", + "dependencies": [ + "npm:@preact/signals", + "npm:preact@^10.27.2" + ] + }, + "@01edu/time@0.1.0": { + "integrity": "638ea7d2d00bfbf487e5262b4d6207de7f2101793d0d27a63d492e113a07dcb2" + }, + "@01edu/types@0.1.2": { + "integrity": "58c6925af51586a33bb8a42a2c191c760044aec7f85fb00e8824781176d7b6e2" + }, + "@std/assert@1.0.16": { + "integrity": "6a7272ed1eaa77defe76e5ff63ca705d9c495077e2d5fd0126d2b53fc5bd6532", + "dependencies": [ + "jsr:@std/internal" + ] + }, + "@std/async@1.0.15": { + "integrity": "55d1d9d04f99403fe5730ab16bdcc3c47f658a6bf054cafb38a50f046238116e" + }, + "@std/cli@1.0.24": { + "integrity": "b655a5beb26aa94f98add6bc8889f5fb9bc3ee2cc3fc954e151201f4c4200a5e" }, "@std/crypto@1.0.5": { "integrity": "0dcfbb319fe0bba1bd3af904ceb4f948cde1b92979ec1614528380ed308a3b40" }, - "@std/encoding@1.0.6": { - "integrity": "ca87122c196e8831737d9547acf001766618e78cd8c33920776c7f5885546069" + "@std/data-structures@1.0.9": { + "integrity": "033d6e17e64bf1f84a614e647c1b015fa2576ae3312305821e1a4cb20674bb4d" }, - "@std/fmt@1.0.4": { - "integrity": "e14fe5bedee26f80877e6705a97a79c7eed599e81bb1669127ef9e8bc1e29a74" + "@std/encoding@1.0.10": { + "integrity": "8783c6384a2d13abd5e9e87a7ae0520a30e9f56aeeaa3bdf910a3eaaf5c811a1" }, - "@std/fs@1.0.19": { - "integrity": "051968c2b1eae4d2ea9f79a08a3845740ef6af10356aff43d3e2ef11ed09fb06", + "@std/fmt@1.0.8": { + "integrity": "71e1fc498787e4434d213647a6e43e794af4fd393ef8f52062246e06f7e372b7" + }, + "@std/fs@1.0.20": { + "integrity": "e953206aae48d46ee65e8783ded459f23bec7dd1f3879512911c35e5484ea187", "dependencies": [ - "jsr:@std/internal@^1.0.9", - "jsr:@std/path@^1.1.1" + "jsr:@std/internal", + "jsr:@std/path@^1.1.3" ] }, - "@std/html@1.0.3": { - "integrity": "7a0ac35e050431fb49d44e61c8b8aac1ebd55937e0dc9ec6409aa4bab39a7988" + "@std/html@1.0.5": { + "integrity": "4e2d693f474cae8c16a920fa5e15a3b72267b94b84667f11a50c6dd1cb18d35e" }, - "@std/http@1.0.12": { - "integrity": "85246d8bfe9c8e2538518725b158bdc31f616e0869255f4a8d9e3de919cab2aa", + "@std/http@1.0.22": { + "integrity": "53f0bb70e23a2eec3e17c4240a85bb23d185b2e20635adb37ce0f03cc4ca012a", "dependencies": [ - "jsr:@std/cli@^1.0.8", - "jsr:@std/encoding@^1.0.5", - "jsr:@std/fmt@^1.0.3", + "jsr:@std/cli", + "jsr:@std/encoding", + "jsr:@std/fmt", + "jsr:@std/fs@^1.0.20", "jsr:@std/html", "jsr:@std/media-types", "jsr:@std/net", - "jsr:@std/path@^1.0.8", + "jsr:@std/path@^1.1.3", "jsr:@std/streams" ] }, - "@std/internal@1.0.5": { - "integrity": "54a546004f769c1ac9e025abd15a76b6671ddc9687e2313b67376125650dc7ba" - }, - "@std/internal@1.0.10": { - "integrity": "e3be62ce42cab0e177c27698e5d9800122f67b766a0bea6ca4867886cbde8cf7" + "@std/internal@1.0.12": { + "integrity": "972a634fd5bc34b242024402972cd5143eac68d8dffaca5eaa4dba30ce17b027" }, "@std/media-types@1.1.0": { "integrity": "c9d093f0c05c3512932b330e3cc1fe1d627b301db33a4c2c2185c02471d6eaa4" }, - "@std/net@1.0.4": { - "integrity": "2f403b455ebbccf83d8a027d29c5a9e3a2452fea39bb2da7f2c04af09c8bc852" - }, - "@std/path@1.0.8": { - "integrity": "548fa456bb6a04d3c1a1e7477986b6cffbce95102d0bb447c67c4ee70e0364be" + "@std/net@1.0.6": { + "integrity": "110735f93e95bb9feb95790a8b1d1bf69ec0dc74f3f97a00a76ea5efea25500c" }, - "@std/path@1.1.1": { - "integrity": "fe00026bd3a7e6a27f73709b83c607798be40e20c81dde655ce34052fd82ec76", + "@std/path@1.1.3": { + "integrity": "b015962d82a5e6daea980c32b82d2c40142149639968549c649031a230b1afb3", "dependencies": [ - "jsr:@std/internal@^1.0.9" + "jsr:@std/internal" ] }, - "@std/streams@1.0.8": { - "integrity": "b41332d93d2cf6a82fe4ac2153b930adf1a859392931e2a19d9fabfb6f154fb3" + "@std/streams@1.0.14": { + "integrity": "c0df6cdd73bd4bbcbe4baa89e323b88418c90ceb2d926f95aa99bdcdbfca2411" }, - "@std/testing@1.0.15": { - "integrity": "a490169f5ccb0f3ae9c94fbc69d2cd43603f2cffb41713a85f99bbb0e3087cbc", + "@std/testing@1.0.16": { + "integrity": "a917ffdeb5924c9be436dc78bc32e511760e14d3a96e49c607fc5ecca86d0092", "dependencies": [ - "jsr:@std/assert@^1.0.13", - "jsr:@std/internal@^1.0.10" + "jsr:@std/assert@^1.0.15", + "jsr:@std/async", + "jsr:@std/data-structures", + "jsr:@std/fs@^1.0.19", + "jsr:@std/internal", + "jsr:@std/path@^1.1.2" ] } }, "npm": { - "@ampproject/remapping@2.3.0": { - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dependencies": [ - "@jridgewell/gen-mapping", - "@jridgewell/trace-mapping" - ] - }, "@babel/code-frame@7.27.1": { "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", "dependencies": [ @@ -134,13 +162,12 @@ "picocolors" ] }, - "@babel/compat-data@7.28.0": { - "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==" + "@babel/compat-data@7.28.5": { + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==" }, - "@babel/core@7.28.0": { - "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", + "@babel/core@7.28.5": { + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dependencies": [ - "@ampproject/remapping", "@babel/code-frame", "@babel/generator", "@babel/helper-compilation-targets", @@ -150,6 +177,7 @@ "@babel/template", "@babel/traverse", "@babel/types", + "@jridgewell/remapping", "convert-source-map", "debug", "gensync", @@ -157,8 +185,8 @@ "semver" ] }, - "@babel/generator@7.28.0": { - "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==", + "@babel/generator@7.28.5": { + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", "dependencies": [ "@babel/parser", "@babel/types", @@ -193,8 +221,8 @@ "@babel/types" ] }, - "@babel/helper-module-transforms@7.27.3_@babel+core@7.28.0": { - "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", + "@babel/helper-module-transforms@7.28.3_@babel+core@7.28.5": { + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", "dependencies": [ "@babel/core", "@babel/helper-module-imports", @@ -208,41 +236,41 @@ "@babel/helper-string-parser@7.27.1": { "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==" }, - "@babel/helper-validator-identifier@7.27.1": { - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==" + "@babel/helper-validator-identifier@7.28.5": { + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==" }, "@babel/helper-validator-option@7.27.1": { "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==" }, - "@babel/helpers@7.27.6": { - "integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==", + "@babel/helpers@7.28.4": { + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", "dependencies": [ "@babel/template", "@babel/types" ] }, - "@babel/parser@7.28.0": { - "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", + "@babel/parser@7.28.5": { + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", "dependencies": [ "@babel/types" ], "bin": true }, - "@babel/plugin-syntax-jsx@7.27.1_@babel+core@7.28.0": { + "@babel/plugin-syntax-jsx@7.27.1_@babel+core@7.28.5": { "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", "dependencies": [ "@babel/core", "@babel/helper-plugin-utils" ] }, - "@babel/plugin-transform-react-jsx-development@7.27.1_@babel+core@7.28.0": { + "@babel/plugin-transform-react-jsx-development@7.27.1_@babel+core@7.28.5": { "integrity": "sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q==", "dependencies": [ "@babel/core", "@babel/plugin-transform-react-jsx" ] }, - "@babel/plugin-transform-react-jsx@7.27.1_@babel+core@7.28.0": { + "@babel/plugin-transform-react-jsx@7.27.1_@babel+core@7.28.5": { "integrity": "sha512-2KH4LWGSrJIkVf5tSiBFYuXDAoWRq2MMwgivCf+93dd0GQi8RXLjKA/0EvRnVV5G0hrHczsquXuD01L8s6dmBw==", "dependencies": [ "@babel/core", @@ -261,8 +289,8 @@ "@babel/types" ] }, - "@babel/traverse@7.28.0": { - "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==", + "@babel/traverse@7.28.5": { + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", "dependencies": [ "@babel/code-frame", "@babel/generator", @@ -273,206 +301,186 @@ "debug" ] }, - "@babel/types@7.28.0": { - "integrity": "sha512-jYnje+JyZG5YThjHiF28oT4SIZLnYOcSBb6+SDaFIyzDVSkXQmQQYclJ2R+YxcdmK0AX6x1E5OQNtuh3jHDrUg==", + "@babel/types@7.28.5": { + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", "dependencies": [ "@babel/helper-string-parser", "@babel/helper-validator-identifier" ] }, - "@clickhouse/client-common@1.12.1": { - "integrity": "sha512-ccw1N6hB4+MyaAHIaWBwGZ6O2GgMlO99FlMj0B0UEGfjxM9v5dYVYql6FpP19rMwrVAroYs/IgX2vyZEBvzQLg==" + "@clickhouse/client-common@1.15.0": { + "integrity": "sha512-/1BXaNNsBzH2w5ALWiH6M9zS+cJwlM9uMnMj9e8ETwvDjJzO+nIYlyxzp8Prc+9pEDZ5iAilZ4F8c0YVCzzNaA==" }, - "@clickhouse/client@1.12.1": { - "integrity": "sha512-7ORY85rphRazqHzImNXMrh4vsaPrpetFoTWpZYueCO2bbO6PXYDXp/GQ4DgxnGIqbWB/Di1Ai+Xuwq2o7DJ36A==", + "@clickhouse/client@1.15.0": { + "integrity": "sha512-QmW+p4c/r0oa3X6Un6lcBs4GZtJEQUdvf//x8GeqM5ru6m4oIUg3WwvermP3HE31kpEGoFOQfKbMN5ooR5gvNw==", "dependencies": [ "@clickhouse/client-common" ] }, - "@emnapi/core@1.4.4": { - "integrity": "sha512-A9CnAbC6ARNMKcIcrQwq6HeHCjpcBZ5wSx4U01WXCqEKlrzB9F9315WDNHkrs2xbx7YjjSxbUYxuN6EQzpcY2g==", - "dependencies": [ - "@emnapi/wasi-threads", - "tslib" - ] - }, - "@emnapi/runtime@1.4.4": { - "integrity": "sha512-hHyapA4A3gPaDCNfiqyZUStTMqIkKRshqPIuDOXv1hcBnD4U3l8cP0T1HMCfGRxQ6V64TGCcoswChANyOAwbQg==", - "dependencies": [ - "tslib" - ] - }, - "@emnapi/wasi-threads@1.0.3": { - "integrity": "sha512-8K5IFFsQqF9wQNJptGbS6FNKgUTsSRYnTqNCG1vPP8jFdjSv18n2mQfJpkt2Oibo9iBEzcDnDxNwKTzC7svlJw==", + "@deno/vite-plugin@1.0.5_vite@7.3.0__picomatch@4.0.3": { + "integrity": "sha512-tLja5n4dyMhcze1NzvSs2iiriBymfBlDCZIrjMTxb9O2ru0gvmV6mn5oBD2teNw5Sd92cj3YJzKwsAs8tMJXlg==", "dependencies": [ - "tslib" + "vite" ] }, - "@esbuild/aix-ppc64@0.25.6": { - "integrity": "sha512-ShbM/3XxwuxjFiuVBHA+d3j5dyac0aEVVq1oluIDf71hUw0aRF59dV/efUsIwFnR6m8JNM2FjZOzmaZ8yG61kw==", + "@esbuild/aix-ppc64@0.27.2": { + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", "os": ["aix"], "cpu": ["ppc64"] }, - "@esbuild/android-arm64@0.25.6": { - "integrity": "sha512-hd5zdUarsK6strW+3Wxi5qWws+rJhCCbMiC9QZyzoxfk5uHRIE8T287giQxzVpEvCwuJ9Qjg6bEjcRJcgfLqoA==", + "@esbuild/android-arm64@0.27.2": { + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", "os": ["android"], "cpu": ["arm64"] }, - "@esbuild/android-arm@0.25.6": { - "integrity": "sha512-S8ToEOVfg++AU/bHwdksHNnyLyVM+eMVAOf6yRKFitnwnbwwPNqKr3srzFRe7nzV69RQKb5DgchIX5pt3L53xg==", + "@esbuild/android-arm@0.27.2": { + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", "os": ["android"], "cpu": ["arm"] }, - "@esbuild/android-x64@0.25.6": { - "integrity": "sha512-0Z7KpHSr3VBIO9A/1wcT3NTy7EB4oNC4upJ5ye3R7taCc2GUdeynSLArnon5G8scPwaU866d3H4BCrE5xLW25A==", + "@esbuild/android-x64@0.27.2": { + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", "os": ["android"], "cpu": ["x64"] }, - "@esbuild/darwin-arm64@0.25.6": { - "integrity": "sha512-FFCssz3XBavjxcFxKsGy2DYK5VSvJqa6y5HXljKzhRZ87LvEi13brPrf/wdyl/BbpbMKJNOr1Sd0jtW4Ge1pAA==", + "@esbuild/darwin-arm64@0.27.2": { + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", "os": ["darwin"], "cpu": ["arm64"] }, - "@esbuild/darwin-x64@0.25.6": { - "integrity": "sha512-GfXs5kry/TkGM2vKqK2oyiLFygJRqKVhawu3+DOCk7OxLy/6jYkWXhlHwOoTb0WqGnWGAS7sooxbZowy+pK9Yg==", + "@esbuild/darwin-x64@0.27.2": { + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", "os": ["darwin"], "cpu": ["x64"] }, - "@esbuild/freebsd-arm64@0.25.6": { - "integrity": "sha512-aoLF2c3OvDn2XDTRvn8hN6DRzVVpDlj2B/F66clWd/FHLiHaG3aVZjxQX2DYphA5y/evbdGvC6Us13tvyt4pWg==", + "@esbuild/freebsd-arm64@0.27.2": { + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", "os": ["freebsd"], "cpu": ["arm64"] }, - "@esbuild/freebsd-x64@0.25.6": { - "integrity": "sha512-2SkqTjTSo2dYi/jzFbU9Plt1vk0+nNg8YC8rOXXea+iA3hfNJWebKYPs3xnOUf9+ZWhKAaxnQNUf2X9LOpeiMQ==", + "@esbuild/freebsd-x64@0.27.2": { + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", "os": ["freebsd"], "cpu": ["x64"] }, - "@esbuild/linux-arm64@0.25.6": { - "integrity": "sha512-b967hU0gqKd9Drsh/UuAm21Khpoh6mPBSgz8mKRq4P5mVK8bpA+hQzmm/ZwGVULSNBzKdZPQBRT3+WuVavcWsQ==", + "@esbuild/linux-arm64@0.27.2": { + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", "os": ["linux"], "cpu": ["arm64"] }, - "@esbuild/linux-arm@0.25.6": { - "integrity": "sha512-SZHQlzvqv4Du5PrKE2faN0qlbsaW/3QQfUUc6yO2EjFcA83xnwm91UbEEVx4ApZ9Z5oG8Bxz4qPE+HFwtVcfyw==", + "@esbuild/linux-arm@0.27.2": { + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", "os": ["linux"], "cpu": ["arm"] }, - "@esbuild/linux-ia32@0.25.6": { - "integrity": "sha512-aHWdQ2AAltRkLPOsKdi3xv0mZ8fUGPdlKEjIEhxCPm5yKEThcUjHpWB1idN74lfXGnZ5SULQSgtr5Qos5B0bPw==", + "@esbuild/linux-ia32@0.27.2": { + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", "os": ["linux"], "cpu": ["ia32"] }, - "@esbuild/linux-loong64@0.25.6": { - "integrity": "sha512-VgKCsHdXRSQ7E1+QXGdRPlQ/e08bN6WMQb27/TMfV+vPjjTImuT9PmLXupRlC90S1JeNNW5lzkAEO/McKeJ2yg==", + "@esbuild/linux-loong64@0.27.2": { + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", "os": ["linux"], "cpu": ["loong64"] }, - "@esbuild/linux-mips64el@0.25.6": { - "integrity": "sha512-WViNlpivRKT9/py3kCmkHnn44GkGXVdXfdc4drNmRl15zVQ2+D2uFwdlGh6IuK5AAnGTo2qPB1Djppj+t78rzw==", + "@esbuild/linux-mips64el@0.27.2": { + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", "os": ["linux"], "cpu": ["mips64el"] }, - "@esbuild/linux-ppc64@0.25.6": { - "integrity": "sha512-wyYKZ9NTdmAMb5730I38lBqVu6cKl4ZfYXIs31Baf8aoOtB4xSGi3THmDYt4BTFHk7/EcVixkOV2uZfwU3Q2Jw==", + "@esbuild/linux-ppc64@0.27.2": { + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", "os": ["linux"], "cpu": ["ppc64"] }, - "@esbuild/linux-riscv64@0.25.6": { - "integrity": "sha512-KZh7bAGGcrinEj4qzilJ4hqTY3Dg2U82c8bv+e1xqNqZCrCyc+TL9AUEn5WGKDzm3CfC5RODE/qc96OcbIe33w==", + "@esbuild/linux-riscv64@0.27.2": { + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", "os": ["linux"], "cpu": ["riscv64"] }, - "@esbuild/linux-s390x@0.25.6": { - "integrity": "sha512-9N1LsTwAuE9oj6lHMyyAM+ucxGiVnEqUdp4v7IaMmrwb06ZTEVCIs3oPPplVsnjPfyjmxwHxHMF8b6vzUVAUGw==", + "@esbuild/linux-s390x@0.27.2": { + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", "os": ["linux"], "cpu": ["s390x"] }, - "@esbuild/linux-x64@0.25.6": { - "integrity": "sha512-A6bJB41b4lKFWRKNrWoP2LHsjVzNiaurf7wyj/XtFNTsnPuxwEBWHLty+ZE0dWBKuSK1fvKgrKaNjBS7qbFKig==", + "@esbuild/linux-x64@0.27.2": { + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", "os": ["linux"], "cpu": ["x64"] }, - "@esbuild/netbsd-arm64@0.25.6": { - "integrity": "sha512-IjA+DcwoVpjEvyxZddDqBY+uJ2Snc6duLpjmkXm/v4xuS3H+3FkLZlDm9ZsAbF9rsfP3zeA0/ArNDORZgrxR/Q==", + "@esbuild/netbsd-arm64@0.27.2": { + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", "os": ["netbsd"], "cpu": ["arm64"] }, - "@esbuild/netbsd-x64@0.25.6": { - "integrity": "sha512-dUXuZr5WenIDlMHdMkvDc1FAu4xdWixTCRgP7RQLBOkkGgwuuzaGSYcOpW4jFxzpzL1ejb8yF620UxAqnBrR9g==", + "@esbuild/netbsd-x64@0.27.2": { + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", "os": ["netbsd"], "cpu": ["x64"] }, - "@esbuild/openbsd-arm64@0.25.6": { - "integrity": "sha512-l8ZCvXP0tbTJ3iaqdNf3pjaOSd5ex/e6/omLIQCVBLmHTlfXW3zAxQ4fnDmPLOB1x9xrcSi/xtCWFwCZRIaEwg==", + "@esbuild/openbsd-arm64@0.27.2": { + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", "os": ["openbsd"], "cpu": ["arm64"] }, - "@esbuild/openbsd-x64@0.25.6": { - "integrity": "sha512-hKrmDa0aOFOr71KQ/19JC7az1P0GWtCN1t2ahYAf4O007DHZt/dW8ym5+CUdJhQ/qkZmI1HAF8KkJbEFtCL7gw==", + "@esbuild/openbsd-x64@0.27.2": { + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", "os": ["openbsd"], "cpu": ["x64"] }, - "@esbuild/openharmony-arm64@0.25.6": { - "integrity": "sha512-+SqBcAWoB1fYKmpWoQP4pGtx+pUUC//RNYhFdbcSA16617cchuryuhOCRpPsjCblKukAckWsV+aQ3UKT/RMPcA==", + "@esbuild/openharmony-arm64@0.27.2": { + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", "os": ["openharmony"], "cpu": ["arm64"] }, - "@esbuild/sunos-x64@0.25.6": { - "integrity": "sha512-dyCGxv1/Br7MiSC42qinGL8KkG4kX0pEsdb0+TKhmJZgCUDBGmyo1/ArCjNGiOLiIAgdbWgmWgib4HoCi5t7kA==", + "@esbuild/sunos-x64@0.27.2": { + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", "os": ["sunos"], "cpu": ["x64"] }, - "@esbuild/win32-arm64@0.25.6": { - "integrity": "sha512-42QOgcZeZOvXfsCBJF5Afw73t4veOId//XD3i+/9gSkhSV6Gk3VPlWncctI+JcOyERv85FUo7RxuxGy+z8A43Q==", + "@esbuild/win32-arm64@0.27.2": { + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", "os": ["win32"], "cpu": ["arm64"] }, - "@esbuild/win32-ia32@0.25.6": { - "integrity": "sha512-4AWhgXmDuYN7rJI6ORB+uU9DHLq/erBbuMoAuB4VWJTu5KtCgcKYPynF0YI1VkBNuEfjNlLrFr9KZPJzrtLkrQ==", + "@esbuild/win32-ia32@0.27.2": { + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", "os": ["win32"], "cpu": ["ia32"] }, - "@esbuild/win32-x64@0.25.6": { - "integrity": "sha512-NgJPHHbEpLQgDH2MjQu90pzW/5vvXIZ7KOnPyNBm92A6WgZ/7b6fJyUBjoumLqeOQQGqY2QjQxRo97ah4Sj0cA==", + "@esbuild/win32-x64@0.27.2": { + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", "os": ["win32"], "cpu": ["x64"] }, - "@isaacs/fs-minipass@4.0.1": { - "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "@jridgewell/gen-mapping@0.3.13": { + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "dependencies": [ - "minipass" + "@jridgewell/sourcemap-codec", + "@jridgewell/trace-mapping" ] }, - "@jridgewell/gen-mapping@0.3.12": { - "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", + "@jridgewell/remapping@2.3.5": { + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", "dependencies": [ - "@jridgewell/sourcemap-codec", + "@jridgewell/gen-mapping", "@jridgewell/trace-mapping" ] }, "@jridgewell/resolve-uri@3.1.2": { "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==" }, - "@jridgewell/sourcemap-codec@1.5.4": { - "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==" + "@jridgewell/sourcemap-codec@1.5.5": { + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==" }, - "@jridgewell/trace-mapping@0.3.29": { - "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", + "@jridgewell/trace-mapping@0.3.31": { + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "dependencies": [ "@jridgewell/resolve-uri", "@jridgewell/sourcemap-codec" ] }, - "@napi-rs/wasm-runtime@0.2.11": { - "integrity": "sha512-9DPkXtvHydrcOsopiYpUgPHpmj0HWZKMUnL2dZqpvC42lsratuBG06V5ipyno0fUek5VlFsNQ+AcFATSrJXgMA==", - "dependencies": [ - "@emnapi/core", - "@emnapi/runtime", - "@tybys/wasm-util" - ] - }, - "@preact/preset-vite@2.10.2_@babel+core@7.28.0_vite@7.0.4__picomatch@4.0.2__@types+node@22.15.15_preact@10.26.9_@types+node@22.15.15": { + "@preact/preset-vite@2.10.2_@babel+core@7.28.5_vite@7.3.0__picomatch@4.0.3_preact@10.28.0": { "integrity": "sha512-K9wHlJOtkE+cGqlyQ5v9kL3Ge0Ql4LlIZjkUTL+1zf3nNdF88F9UZN6VTV8jdzBX9Fl7WSzeNMSDG7qECPmSmg==", "dependencies": [ "@babel/core", @@ -483,15 +491,15 @@ "babel-plugin-transform-hook-names", "debug", "picocolors", - "vite@7.0.4_picomatch@4.0.2_@types+node@22.15.15", + "vite", "vite-prerender-plugin" ] }, - "@preact/signals-core@1.11.0": { - "integrity": "sha512-jglbibeWHuFRzEWVFY/TT7wB1PppJxmcSfUHcK+2J9vBRtiooMfw6tAPttojNYrrpdGViqAYCbPpmWYlMm+eMQ==" + "@preact/signals-core@1.12.1": { + "integrity": "sha512-BwbTXpj+9QutoZLQvbttRg5x3l5468qaV2kufh+51yha1c53ep5dY4kTuZR35+3pAZxpfQerGJiQqg34ZNZ6uA==" }, - "@preact/signals@2.2.1_preact@10.26.9": { - "integrity": "sha512-cX3mijdjHbbz3dBoJ6z687CGYEOp9ifj3uFnm4UKW+DxXKPMvE2y/VSdm0PXhXmHnr6F0iSnDJ+dLwmV7CYT5A==", + "@preact/signals@2.5.1_preact@10.28.0": { + "integrity": "sha512-VPjk5YFt7i11Fi4UK0tzaEe5xLwfhUxXL3l89ocxQ5aPz7bRo8M5+N73LjBMPklyXKYKz6YsNo4Smp8n6nplng==", "dependencies": [ "@preact/signals-core", "preact" @@ -500,8 +508,8 @@ "@prefresh/babel-plugin@0.5.2": { "integrity": "sha512-AOl4HG6dAxWkJ5ndPHBgBa49oo/9bOiJuRDKHLSTyH+Fd9x00shTXpdiTj1W41l6oQIwUOAgJeHMn4QwIDpHkA==" }, - "@prefresh/core@1.5.5_preact@10.26.9": { - "integrity": "sha512-H6GTXUl4V4fe3ijz7yhSa/mZ+pGSOh7XaJb6uP/sQsagBx9yl0D1HKDaeoMQA8Ad2Xm27LqvbitMGSdY9UFSKQ==", + "@prefresh/core@1.5.9_preact@10.28.0": { + "integrity": "sha512-IKBKCPaz34OFVC+adiQ2qaTF5qdztO2/4ZPf4KsRTgjKosWqxVXmEbxCiUydYZRY8GVie+DQlKzQr9gt6HQ+EQ==", "dependencies": [ "preact" ] @@ -509,8 +517,8 @@ "@prefresh/utils@1.2.1": { "integrity": "sha512-vq/sIuN5nYfYzvyayXI4C2QkprfNaHUQ9ZX+3xLD8nL3rWyzpxOm1+K7RtMbhd+66QcaISViK7amjnheQ/4WZw==" }, - "@prefresh/vite@2.4.8_preact@10.26.9_vite@7.0.4__picomatch@4.0.2__@types+node@22.15.15_@types+node@22.15.15": { - "integrity": "sha512-H7vlo9UbJInuRbZhRQrdgVqLP7qKjDoX7TgYWWwIVhEHeHO0hZ4zyicvwBrV1wX5A3EPOmArgRkUaN7cPI2VXQ==", + "@prefresh/vite@2.4.11_preact@10.28.0_vite@7.3.0__picomatch@4.0.3": { + "integrity": "sha512-/XjURQqdRiCG3NpMmWqE9kJwrg9IchIOWHzulCfqg2sRe/8oQ1g5De7xrk9lbqPIQLn7ntBkKdqWXIj4E9YXyg==", "dependencies": [ "@babel/core", "@prefresh/babel-plugin", @@ -518,7 +526,7 @@ "@prefresh/utils", "@rollup/pluginutils", "preact", - "vite@7.0.4_picomatch@4.0.2_@types+node@22.15.15" + "vite" ] }, "@rollup/pluginutils@4.2.1": { @@ -528,110 +536,120 @@ "picomatch@2.3.1" ] }, - "@rollup/rollup-android-arm-eabi@4.44.2": { - "integrity": "sha512-g0dF8P1e2QYPOj1gu7s/3LVP6kze9A7m6x0BZ9iTdXK8N5c2V7cpBKHV3/9A4Zd8xxavdhK0t4PnqjkqVmUc9Q==", + "@rollup/rollup-android-arm-eabi@4.53.5": { + "integrity": "sha512-iDGS/h7D8t7tvZ1t6+WPK04KD0MwzLZrG0se1hzBjSi5fyxlsiggoJHwh18PCFNn7tG43OWb6pdZ6Y+rMlmyNQ==", "os": ["android"], "cpu": ["arm"] }, - "@rollup/rollup-android-arm64@4.44.2": { - "integrity": "sha512-Yt5MKrOosSbSaAK5Y4J+vSiID57sOvpBNBR6K7xAaQvk3MkcNVV0f9fE20T+41WYN8hDn6SGFlFrKudtx4EoxA==", + "@rollup/rollup-android-arm64@4.53.5": { + "integrity": "sha512-wrSAViWvZHBMMlWk6EJhvg8/rjxzyEhEdgfMMjREHEq11EtJ6IP6yfcCH57YAEca2Oe3FNCE9DSTgU70EIGmVw==", "os": ["android"], "cpu": ["arm64"] }, - "@rollup/rollup-darwin-arm64@4.44.2": { - "integrity": "sha512-EsnFot9ZieM35YNA26nhbLTJBHD0jTwWpPwmRVDzjylQT6gkar+zenfb8mHxWpRrbn+WytRRjE0WKsfaxBkVUA==", + "@rollup/rollup-darwin-arm64@4.53.5": { + "integrity": "sha512-S87zZPBmRO6u1YXQLwpveZm4JfPpAa6oHBX7/ghSiGH3rz/KDgAu1rKdGutV+WUI6tKDMbaBJomhnT30Y2t4VQ==", "os": ["darwin"], "cpu": ["arm64"] }, - "@rollup/rollup-darwin-x64@4.44.2": { - "integrity": "sha512-dv/t1t1RkCvJdWWxQ2lWOO+b7cMsVw5YFaS04oHpZRWehI1h0fV1gF4wgGCTyQHHjJDfbNpwOi6PXEafRBBezw==", + "@rollup/rollup-darwin-x64@4.53.5": { + "integrity": "sha512-YTbnsAaHo6VrAczISxgpTva8EkfQus0VPEVJCEaboHtZRIb6h6j0BNxRBOwnDciFTZLDPW5r+ZBmhL/+YpTZgA==", "os": ["darwin"], "cpu": ["x64"] }, - "@rollup/rollup-freebsd-arm64@4.44.2": { - "integrity": "sha512-W4tt4BLorKND4qeHElxDoim0+BsprFTwb+vriVQnFFtT/P6v/xO5I99xvYnVzKWrK6j7Hb0yp3x7V5LUbaeOMg==", + "@rollup/rollup-freebsd-arm64@4.53.5": { + "integrity": "sha512-1T8eY2J8rKJWzaznV7zedfdhD1BqVs1iqILhmHDq/bqCUZsrMt+j8VCTHhP0vdfbHK3e1IQ7VYx3jlKqwlf+vw==", "os": ["freebsd"], "cpu": ["arm64"] }, - "@rollup/rollup-freebsd-x64@4.44.2": { - "integrity": "sha512-tdT1PHopokkuBVyHjvYehnIe20fxibxFCEhQP/96MDSOcyjM/shlTkZZLOufV3qO6/FQOSiJTBebhVc12JyPTA==", + "@rollup/rollup-freebsd-x64@4.53.5": { + "integrity": "sha512-sHTiuXyBJApxRn+VFMaw1U+Qsz4kcNlxQ742snICYPrY+DDL8/ZbaC4DVIB7vgZmp3jiDaKA0WpBdP0aqPJoBQ==", "os": ["freebsd"], "cpu": ["x64"] }, - "@rollup/rollup-linux-arm-gnueabihf@4.44.2": { - "integrity": "sha512-+xmiDGGaSfIIOXMzkhJ++Oa0Gwvl9oXUeIiwarsdRXSe27HUIvjbSIpPxvnNsRebsNdUo7uAiQVgBD1hVriwSQ==", + "@rollup/rollup-linux-arm-gnueabihf@4.53.5": { + "integrity": "sha512-dV3T9MyAf0w8zPVLVBptVlzaXxka6xg1f16VAQmjg+4KMSTWDvhimI/Y6mp8oHwNrmnmVl9XxJ/w/mO4uIQONA==", "os": ["linux"], "cpu": ["arm"] }, - "@rollup/rollup-linux-arm-musleabihf@4.44.2": { - "integrity": "sha512-bDHvhzOfORk3wt8yxIra8N4k/N0MnKInCW5OGZaeDYa/hMrdPaJzo7CSkjKZqX4JFUWjUGm88lI6QJLCM7lDrA==", + "@rollup/rollup-linux-arm-musleabihf@4.53.5": { + "integrity": "sha512-wIGYC1x/hyjP+KAu9+ewDI+fi5XSNiUi9Bvg6KGAh2TsNMA3tSEs+Sh6jJ/r4BV/bx/CyWu2ue9kDnIdRyafcQ==", "os": ["linux"], "cpu": ["arm"] }, - "@rollup/rollup-linux-arm64-gnu@4.44.2": { - "integrity": "sha512-NMsDEsDiYghTbeZWEGnNi4F0hSbGnsuOG+VnNvxkKg0IGDvFh7UVpM/14mnMwxRxUf9AdAVJgHPvKXf6FpMB7A==", + "@rollup/rollup-linux-arm64-gnu@4.53.5": { + "integrity": "sha512-Y+qVA0D9d0y2FRNiG9oM3Hut/DgODZbU9I8pLLPwAsU0tUKZ49cyV1tzmB/qRbSzGvY8lpgGkJuMyuhH7Ma+Vg==", "os": ["linux"], "cpu": ["arm64"] }, - "@rollup/rollup-linux-arm64-musl@4.44.2": { - "integrity": "sha512-lb5bxXnxXglVq+7imxykIp5xMq+idehfl+wOgiiix0191av84OqbjUED+PRC5OA8eFJYj5xAGcpAZ0pF2MnW+A==", + "@rollup/rollup-linux-arm64-musl@4.53.5": { + "integrity": "sha512-juaC4bEgJsyFVfqhtGLz8mbopaWD+WeSOYr5E16y+1of6KQjc0BpwZLuxkClqY1i8sco+MdyoXPNiCkQou09+g==", "os": ["linux"], "cpu": ["arm64"] }, - "@rollup/rollup-linux-loongarch64-gnu@4.44.2": { - "integrity": "sha512-Yl5Rdpf9pIc4GW1PmkUGHdMtbx0fBLE1//SxDmuf3X0dUC57+zMepow2LK0V21661cjXdTn8hO2tXDdAWAqE5g==", + "@rollup/rollup-linux-loong64-gnu@4.53.5": { + "integrity": "sha512-rIEC0hZ17A42iXtHX+EPJVL/CakHo+tT7W0pbzdAGuWOt2jxDFh7A/lRhsNHBcqL4T36+UiAgwO8pbmn3dE8wA==", "os": ["linux"], "cpu": ["loong64"] }, - "@rollup/rollup-linux-powerpc64le-gnu@4.44.2": { - "integrity": "sha512-03vUDH+w55s680YYryyr78jsO1RWU9ocRMaeV2vMniJJW/6HhoTBwyyiiTPVHNWLnhsnwcQ0oH3S9JSBEKuyqw==", + "@rollup/rollup-linux-ppc64-gnu@4.53.5": { + "integrity": "sha512-T7l409NhUE552RcAOcmJHj3xyZ2h7vMWzcwQI0hvn5tqHh3oSoclf9WgTl+0QqffWFG8MEVZZP1/OBglKZx52Q==", "os": ["linux"], "cpu": ["ppc64"] }, - "@rollup/rollup-linux-riscv64-gnu@4.44.2": { - "integrity": "sha512-iYtAqBg5eEMG4dEfVlkqo05xMOk6y/JXIToRca2bAWuqjrJYJlx/I7+Z+4hSrsWU8GdJDFPL4ktV3dy4yBSrzg==", + "@rollup/rollup-linux-riscv64-gnu@4.53.5": { + "integrity": "sha512-7OK5/GhxbnrMcxIFoYfhV/TkknarkYC1hqUw1wU2xUN3TVRLNT5FmBv4KkheSG2xZ6IEbRAhTooTV2+R5Tk0lQ==", "os": ["linux"], "cpu": ["riscv64"] }, - "@rollup/rollup-linux-riscv64-musl@4.44.2": { - "integrity": "sha512-e6vEbgaaqz2yEHqtkPXa28fFuBGmUJ0N2dOJK8YUfijejInt9gfCSA7YDdJ4nYlv67JfP3+PSWFX4IVw/xRIPg==", + "@rollup/rollup-linux-riscv64-musl@4.53.5": { + "integrity": "sha512-GwuDBE/PsXaTa76lO5eLJTyr2k8QkPipAyOrs4V/KJufHCZBJ495VCGJol35grx9xryk4V+2zd3Ri+3v7NPh+w==", "os": ["linux"], "cpu": ["riscv64"] }, - "@rollup/rollup-linux-s390x-gnu@4.44.2": { - "integrity": "sha512-evFOtkmVdY3udE+0QKrV5wBx7bKI0iHz5yEVx5WqDJkxp9YQefy4Mpx3RajIVcM6o7jxTvVd/qpC1IXUhGc1Mw==", + "@rollup/rollup-linux-s390x-gnu@4.53.5": { + "integrity": "sha512-IAE1Ziyr1qNfnmiQLHBURAD+eh/zH1pIeJjeShleII7Vj8kyEm2PF77o+lf3WTHDpNJcu4IXJxNO0Zluro8bOw==", "os": ["linux"], "cpu": ["s390x"] }, - "@rollup/rollup-linux-x64-gnu@4.44.2": { - "integrity": "sha512-/bXb0bEsWMyEkIsUL2Yt5nFB5naLAwyOWMEviQfQY1x3l5WsLKgvZf66TM7UTfED6erckUVUJQ/jJ1FSpm3pRQ==", + "@rollup/rollup-linux-x64-gnu@4.53.5": { + "integrity": "sha512-Pg6E+oP7GvZ4XwgRJBuSXZjcqpIW3yCBhK4BcsANvb47qMvAbCjR6E+1a/U2WXz1JJxp9/4Dno3/iSJLcm5auw==", "os": ["linux"], "cpu": ["x64"] }, - "@rollup/rollup-linux-x64-musl@4.44.2": { - "integrity": "sha512-3D3OB1vSSBXmkGEZR27uiMRNiwN08/RVAcBKwhUYPaiZ8bcvdeEwWPvbnXvvXHY+A/7xluzcN+kaiOFNiOZwWg==", + "@rollup/rollup-linux-x64-musl@4.53.5": { + "integrity": "sha512-txGtluxDKTxaMDzUduGP0wdfng24y1rygUMnmlUJ88fzCCULCLn7oE5kb2+tRB+MWq1QDZT6ObT5RrR8HFRKqg==", "os": ["linux"], "cpu": ["x64"] }, - "@rollup/rollup-win32-arm64-msvc@4.44.2": { - "integrity": "sha512-VfU0fsMK+rwdK8mwODqYeM2hDrF2WiHaSmCBrS7gColkQft95/8tphyzv2EupVxn3iE0FI78wzffoULH1G+dkw==", + "@rollup/rollup-openharmony-arm64@4.53.5": { + "integrity": "sha512-3DFiLPnTxiOQV993fMc+KO8zXHTcIjgaInrqlG8zDp1TlhYl6WgrOHuJkJQ6M8zHEcntSJsUp1XFZSY8C1DYbg==", + "os": ["openharmony"], + "cpu": ["arm64"] + }, + "@rollup/rollup-win32-arm64-msvc@4.53.5": { + "integrity": "sha512-nggc/wPpNTgjGg75hu+Q/3i32R00Lq1B6N1DO7MCU340MRKL3WZJMjA9U4K4gzy3dkZPXm9E1Nc81FItBVGRlA==", "os": ["win32"], "cpu": ["arm64"] }, - "@rollup/rollup-win32-ia32-msvc@4.44.2": { - "integrity": "sha512-+qMUrkbUurpE6DVRjiJCNGZBGo9xM4Y0FXU5cjgudWqIBWbcLkjE3XprJUsOFgC6xjBClwVa9k6O3A7K3vxb5Q==", + "@rollup/rollup-win32-ia32-msvc@4.53.5": { + "integrity": "sha512-U/54pTbdQpPLBdEzCT6NBCFAfSZMvmjr0twhnD9f4EIvlm9wy3jjQ38yQj1AGznrNO65EWQMgm/QUjuIVrYF9w==", "os": ["win32"], "cpu": ["ia32"] }, - "@rollup/rollup-win32-x64-msvc@4.44.2": { - "integrity": "sha512-3+QZROYfJ25PDcxFF66UEk8jGWigHJeecZILvkPkyQN7oc5BvFo4YEXFkOs154j3FTMp9mn9Ky8RCOwastduEA==", + "@rollup/rollup-win32-x64-gnu@4.53.5": { + "integrity": "sha512-2NqKgZSuLH9SXBBV2dWNRCZmocgSOx8OJSdpRaEcRlIfX8YrKxUT6z0F1NpvDVhOsl190UFTRh2F2WDWWCYp3A==", "os": ["win32"], "cpu": ["x64"] }, - "@tailwindcss/node@4.1.11": { - "integrity": "sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q==", + "@rollup/rollup-win32-x64-msvc@4.53.5": { + "integrity": "sha512-JRpZUhCfhZ4keB5v0fe02gQJy05GqboPOaxvjugW04RLSYYoB/9t2lx2u/tMs/Na/1NXfY8QYjgRljRpN+MjTQ==", + "os": ["win32"], + "cpu": ["x64"] + }, + "@tailwindcss/node@4.1.18": { + "integrity": "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==", "dependencies": [ - "@ampproject/remapping", + "@jridgewell/remapping", "enhanced-resolve", "jiti", "lightningcss", @@ -640,79 +658,67 @@ "tailwindcss" ] }, - "@tailwindcss/oxide-android-arm64@4.1.11": { - "integrity": "sha512-3IfFuATVRUMZZprEIx9OGDjG3Ou3jG4xQzNTvjDoKmU9JdmoCohQJ83MYd0GPnQIu89YoJqvMM0G3uqLRFtetg==", + "@tailwindcss/oxide-android-arm64@4.1.18": { + "integrity": "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==", "os": ["android"], "cpu": ["arm64"] }, - "@tailwindcss/oxide-darwin-arm64@4.1.11": { - "integrity": "sha512-ESgStEOEsyg8J5YcMb1xl8WFOXfeBmrhAwGsFxxB2CxY9evy63+AtpbDLAyRkJnxLy2WsD1qF13E97uQyP1lfQ==", + "@tailwindcss/oxide-darwin-arm64@4.1.18": { + "integrity": "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==", "os": ["darwin"], "cpu": ["arm64"] }, - "@tailwindcss/oxide-darwin-x64@4.1.11": { - "integrity": "sha512-EgnK8kRchgmgzG6jE10UQNaH9Mwi2n+yw1jWmof9Vyg2lpKNX2ioe7CJdf9M5f8V9uaQxInenZkOxnTVL3fhAw==", + "@tailwindcss/oxide-darwin-x64@4.1.18": { + "integrity": "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==", "os": ["darwin"], "cpu": ["x64"] }, - "@tailwindcss/oxide-freebsd-x64@4.1.11": { - "integrity": "sha512-xdqKtbpHs7pQhIKmqVpxStnY1skuNh4CtbcyOHeX1YBE0hArj2romsFGb6yUmzkq/6M24nkxDqU8GYrKrz+UcA==", + "@tailwindcss/oxide-freebsd-x64@4.1.18": { + "integrity": "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==", "os": ["freebsd"], "cpu": ["x64"] }, - "@tailwindcss/oxide-linux-arm-gnueabihf@4.1.11": { - "integrity": "sha512-ryHQK2eyDYYMwB5wZL46uoxz2zzDZsFBwfjssgB7pzytAeCCa6glsiJGjhTEddq/4OsIjsLNMAiMlHNYnkEEeg==", + "@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18": { + "integrity": "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==", "os": ["linux"], "cpu": ["arm"] }, - "@tailwindcss/oxide-linux-arm64-gnu@4.1.11": { - "integrity": "sha512-mYwqheq4BXF83j/w75ewkPJmPZIqqP1nhoghS9D57CLjsh3Nfq0m4ftTotRYtGnZd3eCztgbSPJ9QhfC91gDZQ==", + "@tailwindcss/oxide-linux-arm64-gnu@4.1.18": { + "integrity": "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==", "os": ["linux"], "cpu": ["arm64"] }, - "@tailwindcss/oxide-linux-arm64-musl@4.1.11": { - "integrity": "sha512-m/NVRFNGlEHJrNVk3O6I9ggVuNjXHIPoD6bqay/pubtYC9QIdAMpS+cswZQPBLvVvEF6GtSNONbDkZrjWZXYNQ==", + "@tailwindcss/oxide-linux-arm64-musl@4.1.18": { + "integrity": "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==", "os": ["linux"], "cpu": ["arm64"] }, - "@tailwindcss/oxide-linux-x64-gnu@4.1.11": { - "integrity": "sha512-YW6sblI7xukSD2TdbbaeQVDysIm/UPJtObHJHKxDEcW2exAtY47j52f8jZXkqE1krdnkhCMGqP3dbniu1Te2Fg==", + "@tailwindcss/oxide-linux-x64-gnu@4.1.18": { + "integrity": "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==", "os": ["linux"], "cpu": ["x64"] }, - "@tailwindcss/oxide-linux-x64-musl@4.1.11": { - "integrity": "sha512-e3C/RRhGunWYNC3aSF7exsQkdXzQ/M+aYuZHKnw4U7KQwTJotnWsGOIVih0s2qQzmEzOFIJ3+xt7iq67K/p56Q==", + "@tailwindcss/oxide-linux-x64-musl@4.1.18": { + "integrity": "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==", "os": ["linux"], "cpu": ["x64"] }, - "@tailwindcss/oxide-wasm32-wasi@4.1.11": { - "integrity": "sha512-Xo1+/GU0JEN/C/dvcammKHzeM6NqKovG+6921MR6oadee5XPBaKOumrJCXvopJ/Qb5TH7LX/UAywbqrP4lax0g==", - "dependencies": [ - "@emnapi/core", - "@emnapi/runtime", - "@emnapi/wasi-threads", - "@napi-rs/wasm-runtime", - "@tybys/wasm-util", - "tslib" - ], + "@tailwindcss/oxide-wasm32-wasi@4.1.18": { + "integrity": "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==", "cpu": ["wasm32"] }, - "@tailwindcss/oxide-win32-arm64-msvc@4.1.11": { - "integrity": "sha512-UgKYx5PwEKrac3GPNPf6HVMNhUIGuUh4wlDFR2jYYdkX6pL/rn73zTq/4pzUm8fOjAn5L8zDeHp9iXmUGOXZ+w==", + "@tailwindcss/oxide-win32-arm64-msvc@4.1.18": { + "integrity": "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==", "os": ["win32"], "cpu": ["arm64"] }, - "@tailwindcss/oxide-win32-x64-msvc@4.1.11": { - "integrity": "sha512-YfHoggn1j0LK7wR82TOucWc5LDCguHnoS879idHekmmiR7g9HUtMw9MI0NHatS28u/Xlkfi9w5RJWgz2Dl+5Qg==", + "@tailwindcss/oxide-win32-x64-msvc@4.1.18": { + "integrity": "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==", "os": ["win32"], "cpu": ["x64"] }, - "@tailwindcss/oxide@4.1.11": { - "integrity": "sha512-Q69XzrtAhuyfHo+5/HMgr1lAiPP/G40OMFAnws7xcFEYqcypZmdW8eGXaOUIeOl1dzPJBPENXgbjsOyhg2nkrg==", - "dependencies": [ - "detect-libc", - "tar" - ], + "@tailwindcss/oxide@4.1.18": { + "integrity": "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==", "optionalDependencies": [ "@tailwindcss/oxide-android-arm64", "@tailwindcss/oxide-darwin-arm64", @@ -726,45 +732,37 @@ "@tailwindcss/oxide-wasm32-wasi", "@tailwindcss/oxide-win32-arm64-msvc", "@tailwindcss/oxide-win32-x64-msvc" - ], - "scripts": true + ] }, - "@tailwindcss/vite@4.1.11_vite@7.0.4__picomatch@4.0.2__@types+node@22.15.15_@types+node@22.15.15": { - "integrity": "sha512-RHYhrR3hku0MJFRV+fN2gNbDNEh3dwKvY8XJvTxCSXeMOsCRSr+uKvDWQcbizrHgjML6ZmTE5OwMrl5wKcujCw==", + "@tailwindcss/vite@4.1.18_vite@7.3.0__picomatch@4.0.3": { + "integrity": "sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA==", "dependencies": [ "@tailwindcss/node", "@tailwindcss/oxide", "tailwindcss", - "vite@7.0.4_picomatch@4.0.2_@types+node@22.15.15" - ] - }, - "@tybys/wasm-util@0.9.0": { - "integrity": "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==", - "dependencies": [ - "tslib" + "vite" ] }, "@types/estree@1.0.8": { "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==" }, - "@types/node@22.15.15": { - "integrity": "sha512-R5muMcZob3/Jjchn5LcO8jdKwSCbzqmPB6ruBxMcf9kbxtniZHP327s6C37iOfuw8mbKK3cAQa7sEl7afLrQ8A==", - "dependencies": [ - "undici-types" - ] - }, - "babel-plugin-transform-hook-names@1.0.2_@babel+core@7.28.0": { + "babel-plugin-transform-hook-names@1.0.2_@babel+core@7.28.5": { "integrity": "sha512-5gafyjyyBTTdX/tQQ0hRgu4AhNHG/hqWi0ZZmg2xvs2FgRkJXzDNKBZCyoYqgFkovfDrgM8OoKg8karoUvWeCw==", "dependencies": [ "@babel/core" ] }, + "baseline-browser-mapping@2.9.9": { + "integrity": "sha512-V8fbOCSeOFvlDj7LLChUcqbZrdKD9RU/VR260piF1790vT0mfLSwGc/Qzxv3IqiTukOpNtItePa0HBpMAj7MDg==", + "bin": true + }, "boolbase@1.0.0": { "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" }, - "browserslist@4.25.1": { - "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", + "browserslist@4.28.1": { + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", "dependencies": [ + "baseline-browser-mapping", "caniuse-lite", "electron-to-chromium", "node-releases", @@ -772,24 +770,12 @@ ], "bin": true }, - "caniuse-lite@1.0.30001727": { - "integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==" - }, - "chownr@3.0.0": { - "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==" + "caniuse-lite@1.0.30001760": { + "integrity": "sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==" }, "convert-source-map@2.0.0": { "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" }, - "create-vite-extra@4.0.0": { - "integrity": "sha512-gbXn1XuWI76xVHOwSAMzAPJ5Rpdd3ExqGB60SBu9FfaIoKpc8MPSfUkS6lGX1UpU5cr8tpLi1L9ljBoBzHeJDw==", - "dependencies": [ - "minimist", - "picocolors", - "prompts" - ], - "bin": true - }, "css-select@5.2.2": { "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", "dependencies": [ @@ -803,17 +789,17 @@ "css-what@6.2.2": { "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==" }, - "daisyui@5.0.46": { - "integrity": "sha512-vMDZK1tI/bOb2Mc3Mk5WpquBG3ZqBz1YKZ0xDlvpOvey60dOS4/5Qhdowq1HndbQl7PgDLDYysxAjjUjwR7/eQ==" + "daisyui@5.5.14": { + "integrity": "sha512-L47rvw7I7hK68TA97VB8Ee0woHew+/ohR6Lx6Ah/krfISOqcG4My7poNpX5Mo5/ytMxiR40fEaz6njzDi7cuSg==" }, - "debug@4.4.1": { - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "debug@4.4.3": { + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dependencies": [ "ms" ] }, - "detect-libc@2.0.4": { - "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==" + "detect-libc@2.1.2": { + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==" }, "dom-serializer@2.0.0": { "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", @@ -840,11 +826,11 @@ "domhandler" ] }, - "electron-to-chromium@1.5.181": { - "integrity": "sha512-+ISMj8OIQ+0qEeDj14Rt8WwcTOiqHyAB+5bnK1K7xNNLjBJ4hRCQfUkw8RWtcLbfBzDwc15ZnKH0c7SNOfwiyA==" + "electron-to-chromium@1.5.267": { + "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==" }, - "enhanced-resolve@5.18.2": { - "integrity": "sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==", + "enhanced-resolve@5.18.4": { + "integrity": "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==", "dependencies": [ "graceful-fs", "tapable" @@ -853,8 +839,8 @@ "entities@4.5.0": { "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==" }, - "esbuild@0.25.6": { - "integrity": "sha512-GVuzuUwtdsghE3ocJ9Bs8PNoF13HNQ5TXbEi2AhvVb8xU1Iwt9Fos9FEamfoee+u/TOsn7GUWc04lz46n2bbTg==", + "esbuild@0.27.2": { + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", "optionalDependencies": [ "@esbuild/aix-ppc64", "@esbuild/android-arm", @@ -892,13 +878,13 @@ "estree-walker@2.0.2": { "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" }, - "fdir@6.4.6_picomatch@4.0.2": { - "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", + "fdir@6.5.0_picomatch@4.0.3": { + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dependencies": [ - "picomatch@4.0.2" + "picomatch@4.0.3" ], "optionalPeers": [ - "picomatch@4.0.2" + "picomatch@4.0.3" ] }, "fsevents@2.3.3": { @@ -916,8 +902,8 @@ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", "bin": true }, - "jiti@2.4.2": { - "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", + "jiti@2.6.1": { + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", "bin": true }, "js-tokens@4.0.0": { @@ -931,68 +917,71 @@ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "bin": true }, - "kleur@3.0.3": { - "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==" - }, "kolorist@1.8.0": { "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==" }, - "lightningcss-darwin-arm64@1.30.1": { - "integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==", + "lightningcss-android-arm64@1.30.2": { + "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", + "os": ["android"], + "cpu": ["arm64"] + }, + "lightningcss-darwin-arm64@1.30.2": { + "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", "os": ["darwin"], "cpu": ["arm64"] }, - "lightningcss-darwin-x64@1.30.1": { - "integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==", + "lightningcss-darwin-x64@1.30.2": { + "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", "os": ["darwin"], "cpu": ["x64"] }, - "lightningcss-freebsd-x64@1.30.1": { - "integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==", + "lightningcss-freebsd-x64@1.30.2": { + "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", "os": ["freebsd"], "cpu": ["x64"] }, - "lightningcss-linux-arm-gnueabihf@1.30.1": { - "integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==", + "lightningcss-linux-arm-gnueabihf@1.30.2": { + "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", "os": ["linux"], "cpu": ["arm"] }, - "lightningcss-linux-arm64-gnu@1.30.1": { - "integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==", + "lightningcss-linux-arm64-gnu@1.30.2": { + "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", "os": ["linux"], "cpu": ["arm64"] }, - "lightningcss-linux-arm64-musl@1.30.1": { - "integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==", + "lightningcss-linux-arm64-musl@1.30.2": { + "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", "os": ["linux"], "cpu": ["arm64"] }, - "lightningcss-linux-x64-gnu@1.30.1": { - "integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==", + "lightningcss-linux-x64-gnu@1.30.2": { + "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", "os": ["linux"], "cpu": ["x64"] }, - "lightningcss-linux-x64-musl@1.30.1": { - "integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==", + "lightningcss-linux-x64-musl@1.30.2": { + "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", "os": ["linux"], "cpu": ["x64"] }, - "lightningcss-win32-arm64-msvc@1.30.1": { - "integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==", + "lightningcss-win32-arm64-msvc@1.30.2": { + "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", "os": ["win32"], "cpu": ["arm64"] }, - "lightningcss-win32-x64-msvc@1.30.1": { - "integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==", + "lightningcss-win32-x64-msvc@1.30.2": { + "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", "os": ["win32"], "cpu": ["x64"] }, - "lightningcss@1.30.1": { - "integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==", + "lightningcss@1.30.2": { + "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", "dependencies": [ "detect-libc" ], "optionalDependencies": [ + "lightningcss-android-arm64", "lightningcss-darwin-arm64", "lightningcss-darwin-x64", "lightningcss-freebsd-x64", @@ -1008,37 +997,21 @@ "lru-cache@5.1.1": { "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", "dependencies": [ - "yallist@3.1.1" + "yallist" ] }, - "lucide-preact@0.525.0_preact@10.26.9": { + "lucide-preact@0.525.0_preact@10.28.0": { "integrity": "sha512-6nvEe7KdEBa36IZ/jjoOHZxd6YyrjfE3Lf/M2PxJXAVeIY8ywKRVkRwjkcXKNa+F9qiN7Bfv334TeREAk8iAtQ==", "dependencies": [ "preact" ] }, - "magic-string@0.30.17": { - "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "magic-string@0.30.21": { + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", "dependencies": [ "@jridgewell/sourcemap-codec" ] }, - "minimist@1.2.8": { - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==" - }, - "minipass@7.1.2": { - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==" - }, - "minizlib@3.0.2": { - "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", - "dependencies": [ - "minipass" - ] - }, - "mkdirp@3.0.1": { - "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", - "bin": true - }, "ms@2.1.3": { "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, @@ -1053,8 +1026,8 @@ "he" ] }, - "node-releases@2.0.19": { - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==" + "node-releases@2.0.27": { + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==" }, "nth-check@2.1.1": { "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", @@ -1068,8 +1041,8 @@ "picomatch@2.3.1": { "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==" }, - "picomatch@4.0.2": { - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==" + "picomatch@4.0.3": { + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==" }, "postcss@8.5.6": { "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", @@ -1079,18 +1052,11 @@ "source-map-js" ] }, - "preact@10.26.9": { - "integrity": "sha512-SSjF9vcnF27mJK1XyFMNJzFd5u3pQiATFqoaDy03XuN00u4ziveVVEGt5RKJrDR8MHE/wJo9Nnad56RLzS2RMA==" - }, - "prompts@2.4.2": { - "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", - "dependencies": [ - "kleur", - "sisteransi" - ] + "preact@10.28.0": { + "integrity": "sha512-rytDAoiXr3+t6OIP3WGlDd0ouCUG1iCWzkcY3++Nreuoi17y6T5i/zRhe6uYfoVcxq6YU+sBtJouuRDsq8vvqA==" }, - "rollup@4.44.2": { - "integrity": "sha512-PVoapzTwSEcelaWGth3uR66u7ZRo6qhPHc0f2uRO9fX6XDVNrIiGYS0Pj9+R8yIIYSD/mCx2b16Ws9itljKSPg==", + "rollup@4.53.5": { + "integrity": "sha512-iTNAbFSlRpcHeeWu73ywU/8KuU/LZmNCSxp6fjQkJBD3ivUb8tpDrXhIxEzA05HlYMEwmtaUnb3RP+YNv162OQ==", "dependencies": [ "@types/estree" ], @@ -1105,15 +1071,17 @@ "@rollup/rollup-linux-arm-musleabihf", "@rollup/rollup-linux-arm64-gnu", "@rollup/rollup-linux-arm64-musl", - "@rollup/rollup-linux-loongarch64-gnu", - "@rollup/rollup-linux-powerpc64le-gnu", + "@rollup/rollup-linux-loong64-gnu", + "@rollup/rollup-linux-ppc64-gnu", "@rollup/rollup-linux-riscv64-gnu", "@rollup/rollup-linux-riscv64-musl", "@rollup/rollup-linux-s390x-gnu", "@rollup/rollup-linux-x64-gnu", "@rollup/rollup-linux-x64-musl", + "@rollup/rollup-openharmony-arm64", "@rollup/rollup-win32-arm64-msvc", "@rollup/rollup-win32-ia32-msvc", + "@rollup/rollup-win32-x64-gnu", "@rollup/rollup-win32-x64-msvc", "fsevents" ], @@ -1129,50 +1097,30 @@ "kolorist" ] }, - "sisteransi@1.0.5": { - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==" - }, "source-map-js@1.2.1": { "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==" }, - "source-map@0.7.4": { - "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==" + "source-map@0.7.6": { + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==" }, "stack-trace@1.0.0-pre2": { "integrity": "sha512-2ztBJRek8IVofG9DBJqdy2N5kulaacX30Nz7xmkYF6ale9WBVmIy6mFBchvGX7Vx/MyjBhx+Rcxqrj+dbOnQ6A==" }, - "tailwindcss@4.1.11": { - "integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==" + "tailwindcss@4.1.18": { + "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==" }, - "tapable@2.2.2": { - "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==" - }, - "tar@7.4.3": { - "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", - "dependencies": [ - "@isaacs/fs-minipass", - "chownr", - "minipass", - "minizlib", - "mkdirp", - "yallist@5.0.0" - ] + "tapable@2.3.0": { + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==" }, - "tinyglobby@0.2.14_picomatch@4.0.2": { - "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", + "tinyglobby@0.2.15_picomatch@4.0.3": { + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", "dependencies": [ "fdir", - "picomatch@4.0.2" + "picomatch@4.0.3" ] }, - "tslib@2.8.1": { - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" - }, - "undici-types@6.21.0": { - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==" - }, - "update-browserslist-db@1.1.3_browserslist@4.25.1": { - "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "update-browserslist-db@1.2.3_browserslist@4.28.1": { + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", "dependencies": [ "browserslist", "escalade", @@ -1180,8 +1128,8 @@ ], "bin": true }, - "vite-prerender-plugin@0.5.11_vite@7.0.4__picomatch@4.0.2__@types+node@22.15.15_@types+node@22.15.15": { - "integrity": "sha512-xWOhb8Ef2zoJIiinYVunIf3omRfUbEXcPEvrkQcrDpJ2yjDokxhvQ26eSJbkthRhymntWx6816jpATrJphh+ug==", + "vite-prerender-plugin@0.5.12_vite@7.3.0__picomatch@4.0.3": { + "integrity": "sha512-EiwhbMn+flg14EysbLTmZSzq8NGTxhytgK3bf4aGRF1evWLGwZiHiUJ1KZDvbxgKbMf2pG6fJWGEa3UZXOnR1g==", "dependencies": [ "kolorist", "magic-string", @@ -1189,31 +1137,15 @@ "simple-code-frame", "source-map", "stack-trace", - "vite@7.0.4_picomatch@4.0.2_@types+node@22.15.15" + "vite" ] }, - "vite@7.0.4_picomatch@4.0.2": { - "integrity": "sha512-SkaSguuS7nnmV7mfJ8l81JGBFV7Gvzp8IzgE8A8t23+AxuNX61Q5H1Tpz5efduSN7NHC8nQXD3sKQKZAu5mNEA==", - "dependencies": [ - "esbuild", - "fdir", - "picomatch@4.0.2", - "postcss", - "rollup", - "tinyglobby" - ], - "optionalDependencies": [ - "fsevents" - ], - "bin": true - }, - "vite@7.0.4_picomatch@4.0.2_@types+node@22.15.15": { - "integrity": "sha512-SkaSguuS7nnmV7mfJ8l81JGBFV7Gvzp8IzgE8A8t23+AxuNX61Q5H1Tpz5efduSN7NHC8nQXD3sKQKZAu5mNEA==", + "vite@7.3.0_picomatch@4.0.3": { + "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", "dependencies": [ - "@types/node", "esbuild", "fdir", - "picomatch@4.0.2", + "picomatch@4.0.3", "postcss", "rollup", "tinyglobby" @@ -1221,33 +1153,37 @@ "optionalDependencies": [ "fsevents" ], - "optionalPeers": [ - "@types/node" - ], "bin": true }, "yallist@3.1.1": { "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" - }, - "yallist@5.0.0": { - "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==" } }, - "remote": { - "https://gistcdn.githack.com/kigiri/21df06d173fcdced5281b86ba6ac1382/raw/crypto.js": "e85976e655898538dbade9d87b05ca0a6bb167b3128cd4098622000a582f5f6d" - }, "workspace": { "dependencies": [ - "jsr:@std/assert@1", - "npm:@clickhouse/client@*", + "jsr:@01edu/api-client@~0.1.3", + "jsr:@01edu/api-proxy@~0.1.2", + "jsr:@01edu/api@~0.1.3", + "jsr:@01edu/signal-router@~0.1.6", + "jsr:@01edu/time@0.1", + "jsr:@std/assert@^1.0.16", + "jsr:@std/crypto@^1.0.5", + "jsr:@std/encoding@^1.0.10", + "jsr:@std/fmt@^1.0.8", + "jsr:@std/fs@^1.0.20", + "jsr:@std/http@^1.0.22", + "jsr:@std/path@^1.1.3", + "jsr:@std/testing@^1.0.16", + "npm:@clickhouse/client@^1.14.0", + "npm:@deno/vite-plugin@^1.0.5", "npm:@preact/preset-vite@^2.10.2", - "npm:@preact/signals@*", - "npm:@tailwindcss/vite@^4.1.11", - "npm:daisyui@^5.0.46", + "npm:@preact/signals@^2.5.1", + "npm:@tailwindcss/vite@^4.1.17", + "npm:daisyui@^5.5.8", "npm:lucide-preact@0.525", - "npm:preact@^10.26.9", - "npm:tailwindcss@^4.1.11", - "npm:vite@^7.0.4" + "npm:preact@^10.28.0", + "npm:tailwindcss@^4.1.17", + "npm:vite@^7.3.0" ] } } diff --git a/tasks/env.ts b/tasks/env.ts index fb09dce..fb5e4d0 100644 --- a/tasks/env.ts +++ b/tasks/env.ts @@ -1,5 +1,4 @@ import { decrypt } from 'https://gistcdn.githack.com/kigiri/21df06d173fcdced5281b86ba6ac1382/raw/crypto.js' - const env = await decrypt( '45641083e50bf3bc5b65dd16c16f7455367074d2c8a3ea16845704a7be6f457bde06b40740ae3456874486092d447eeae44341f5f2f53f9ed974d8182709c53a315a7942eb9699d993159aa2710de5e3eb1eaa780c832ad61c7e95e832bbfdf2ea704904c815e45ed901464ef680456f8ca7cdf561d7c4a100dad7d427383fa8ebb125f58ef4ad9c23029bcfd7a86a712dcc19ceec98e0c513cd297d43c547561f012c823790712391a5c186d9f2e52e971e2a71f4920331a00ea5532b3a6b28280c0b955fc90647dd48591ed9f782dac9fcead5709dbc0c27de142de663998040cb862f', localStorage.password || (localStorage.password = prompt('password')), diff --git a/tasks/vite.js b/tasks/vite.js deleted file mode 100644 index baf1d26..0000000 --- a/tasks/vite.js +++ /dev/null @@ -1,77 +0,0 @@ -// tasks/vite.js -import { join } from 'node:path' -import { Readable } from 'node:stream' -import { pipeline } from 'node:stream/promises' -import { build, createServer } from 'vite' -import preact from '@preact/preset-vite' -import tailwindcss from '@tailwindcss/vite' -const isBuild = Deno.args.includes('--build') -const PORT = Number(Deno.env.get('PORT')) || 2119 - -const denoProxy = () => ({ - name: 'deno-proxy', - configureServer(server) { - server.middlewares.use((req, res, next) => { - if (!req.url.startsWith('/api/')) return next() - const hasBody = !(req.method === 'GET' || req.method === 'HEAD') - const controller = new AbortController() - res.on('close', () => controller.abort()) - fetch(`http://localhost:${PORT}${req.url}`, { - method: req.method, - signal: controller.signal, - headers: { ...req.headers }, - body: hasBody ? Readable.toWeb(req) : undefined, - redirect: 'manual', - }) - .then((apiRes) => { - const headers = Object.fromEntries(apiRes.headers) - const cookies = apiRes.headers.getSetCookie() - if (cookies.length > 0) headers['set-cookie'] = cookies - res.writeHead(apiRes.status, headers) - return apiRes.body - ? pipeline(Readable.fromWeb(apiRes.body), res) - : res.end() - }) - .catch((err) => { - if (controller.signal.aborted) return - console.error('Error while attempting to proxy', req.method, req.url) - console.error(err) - next() - }) - }) - }, -}) - -if (isBuild) { - // Production build - await build({ - configFile: false, - root: join(import.meta.dirname, '../web'), - plugins: [ - preact({ jsxImportSource: 'preact' }), - tailwindcss(), - ], - build: { - outDir: '../dist/web', - emptyOutDir: true, - }, - }) -} else { - // Development server - const server = await createServer({ - configFile: false, - root: join(import.meta.dirname, '../web'), - plugins: [ - preact({ jsxImportSource: 'preact' }), - tailwindcss(), - denoProxy(), - ], - server: { - port: 7737, - host: true, - }, - }) - await server.listen() - server.printUrls() - server.bindCLIShortcuts({ print: true }) -} diff --git a/tasks/vite.ts b/tasks/vite.ts new file mode 100644 index 0000000..0fb51f7 --- /dev/null +++ b/tasks/vite.ts @@ -0,0 +1,55 @@ +// tasks/vite.js +import { join } from 'node:path' +import { AliasOptions, build, createServer } from 'vite' +import { apiProxy } from '@01edu/api-proxy' +import deno from '@deno/vite-plugin' +import preact from '@preact/preset-vite' +import tailwindcss from '@tailwindcss/vite' +import { APP_ENV } from '@01edu/api/env' + +const plugins = [ + preact({ jsxImportSource: 'preact' }), + tailwindcss(), + deno(), +] + +const alias: AliasOptions = [ + { + find: 'npm:@preact/signals@^2.5.1', + replacement: '@preact/signals', + }, + { + find: 'npm:preact@^10.27.2', + replacement: 'preact', + }, +] +// Production build +if (APP_ENV === 'prod') { + await build({ + configFile: false, + root: join(import.meta.dirname!, '../web'), + plugins, + resolve: { alias }, + build: { + outDir: '../dist/web', + emptyOutDir: true, + }, + }) + Deno.exit(0) +} + +// Development server +const PORT = Number(Deno.env.get('PORT')) || 2119 +const server = await createServer({ + configFile: false, + root: join(import.meta.dirname!, '../web'), + plugins: [...plugins, apiProxy({ port: PORT, prefix: '/api/' })], + resolve: { alias }, + server: { + port: 7737, + host: true, + }, +}) +await server.listen() +server.printUrls() +server.bindCLIShortcuts({ print: true }) diff --git a/web/components/BackgroundPattern.tsx b/web/components/BackgroundPattern.tsx index 19f65cb..f2e4c78 100644 --- a/web/components/BackgroundPattern.tsx +++ b/web/components/BackgroundPattern.tsx @@ -246,18 +246,18 @@ export const BackgroundPattern = () => { } return ( -
+
@@ -267,7 +267,7 @@ export const BackgroundPattern = () => { return (
{ }} > {el.type === 'icon' && el.component - ? + ? : (
{el.content} @@ -292,7 +292,7 @@ export const BackgroundPattern = () => { {borderElements.map((el, i) => (
{ transform: `rotate(${el.rotation}deg)`, }} > - +
))} {floatingElements.map((el, i) => (
{ fontSize: `${el.size * 0.2}rem`, }} > - +
))} {bands.map((band, i) => (
{ {band.items.map((item, j) => (
{item.text} @@ -359,7 +359,7 @@ export const BackgroundPattern = () => { {['top', 'right', 'bottom', 'left'].map((position) => (
+const Icon = (title: string, path: string) => (props: SVGProps) => ( + + {title} + + +) + +export const GitHub = Icon( + 'GibHub', + 'M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12', +) diff --git a/web/components/Dialog.tsx b/web/components/Dialog.tsx index f048f86..e21f195 100644 --- a/web/components/Dialog.tsx +++ b/web/components/Dialog.tsx @@ -1,12 +1,12 @@ -import type { JSX } from 'preact' +import type { HTMLAttributes } from 'preact' import { useState } from 'preact/hooks' -import { navigate, url } from '../lib/router.tsx' +import { navigate, url } from '@01edu/signal-router' type DialogProps = { id: string children: preact.ComponentChildren -} & JSX.HTMLAttributes +} & HTMLAttributes export const Dialog = ({ id, diff --git a/web/components/Filtre.tsx b/web/components/Filtre.tsx index 329ff05..962c110 100644 --- a/web/components/Filtre.tsx +++ b/web/components/Filtre.tsx @@ -1,5 +1,5 @@ import { ArrowUpDown, Filter, Plus } from 'lucide-preact' -import { navigate, url } from '../lib/router.tsx' +import { navigate, url } from '@01edu/signal-router' type FilterRow = { idx: number; key: string; op: string; value: string } @@ -15,7 +15,7 @@ const filterOperators = [ ] as const export function parseFilters(prefix: string): FilterRow[] { - const data = url.getAll(`f${prefix}`) + const data = url.value.searchParams.getAll(`f${prefix}`) const rows: FilterRow[] = [] for (let i = 0; i < data.length; i++) { const str = data[i] @@ -74,7 +74,7 @@ function removeFilter(prefix: string, idx: number) { type SortRow = { idx: number; key: string; dir: 'asc' | 'desc' } export function parseSort(prefix: string): SortRow[] { - const data = url.getAll(`s${prefix}`) + const data = url.value.searchParams.getAll(`s${prefix}`) const rows: SortRow[] = [] for (let i = 0; i < data.length; i++) { const str = data[i] diff --git a/web/components/Layout.tsx b/web/components/Layout.tsx index 4437486..32f2307 100644 --- a/web/components/Layout.tsx +++ b/web/components/Layout.tsx @@ -11,8 +11,8 @@ export const PageLayout = ( ) export const PageHeader = ( - { children, className }: { - className?: string + { children, class: className }: { + class?: string children: JSX.Element | JSX.Element[] }, ) => ( diff --git a/web/components/QueryHistory.tsx b/web/components/QueryHistory.tsx index 080361a..32049ba 100644 --- a/web/components/QueryHistory.tsx +++ b/web/components/QueryHistory.tsx @@ -1,5 +1,5 @@ import { ChevronRight, Clock, Play, Search, Trash2 } from 'lucide-preact' -import { A, navigate, url } from '../lib/router.tsx' +import { A, navigate, url } from '@01edu/signal-router' import { queriesHistory, runQuery } from '../lib/shared.tsx' const deleteQuery = (hash: string) => { diff --git a/web/components/SideBar.tsx b/web/components/SideBar.tsx index 854949b..b1b5f79 100644 --- a/web/components/SideBar.tsx +++ b/web/components/SideBar.tsx @@ -6,7 +6,7 @@ import { Settings, } from 'lucide-preact' import { user } from '../lib/session.ts' -import { A, url } from '../lib/router.tsx' +import { A, url } from '@01edu/signal-router' export type SidebarItem = { label: string diff --git a/web/components/forms.tsx b/web/components/forms.tsx index 8d6fd90..6e0fc47 100644 --- a/web/components/forms.tsx +++ b/web/components/forms.tsx @@ -1,6 +1,6 @@ import { JSX } from 'preact' import { useId } from 'preact/hooks' -import { A, LinkProps } from '../lib/router.tsx' +import { A, LinkProps } from '@01edu/signal-router' // Card component export const Card = ( diff --git a/web/index.tsx b/web/index.tsx index 7634b3c..c070327 100644 --- a/web/index.tsx +++ b/web/index.tsx @@ -4,7 +4,7 @@ import { ProjectsPage } from './pages/ProjectsPage.tsx' import { BackgroundPattern } from './components/BackgroundPattern.tsx' import { Header } from './layout.tsx' import { user } from './lib/session.ts' -import { url } from './lib/router.tsx' +import { url } from '@01edu/signal-router' import { ProjectPage } from './pages/ProjectPage.tsx' const renderPage = () => { @@ -19,14 +19,14 @@ const renderPage = () => { } const App = () => { return ( -
-
+
+
-
+
-
+
{renderPage()}
diff --git a/web/layout.tsx b/web/layout.tsx index 0e99f50..e549da4 100644 --- a/web/layout.tsx +++ b/web/layout.tsx @@ -1,7 +1,8 @@ import { effect, signal } from '@preact/signals' -import { Code, Github, LogOut, Moon, Sun } from 'lucide-preact' +import { A, url } from '@01edu/signal-router' +import { Code, LogOut, Moon, Sun } from 'lucide-preact' +import { GitHub } from './components/BrandIcons.tsx' import { user } from './lib/session.ts' -import { A, url } from './lib/router.tsx' const $theme = signal(localStorage.theme || 'dark') @@ -61,7 +62,7 @@ export const Header = () => ( href='/' class='btn btn-ghost text-xl gap-2 cursor-pointer' > - + DevTools
@@ -74,7 +75,7 @@ export const Header = () => ( class='btn btn-ghost btn-square' aria-label='GitHub repository' > - + {'/login' !== url.path && } diff --git a/web/lib/api.ts b/web/lib/api.ts index 823d499..01b8e1c 100644 --- a/web/lib/api.ts +++ b/web/lib/api.ts @@ -1,163 +1,6 @@ -import { Signal } from '@preact/signals' -import { useMemo } from 'preact/hooks' +import { makeClient } from '@01edu/api-client' import type { RouteDefinitions } from '../../api/routes.ts' -import type { Asserted, Handler, HttpMethod } from '../../api/lib/router.ts' -import type { Def } from '../../api/lib/validator.ts' - -export class ErrorWithData extends Error { - public data: Record - constructor(message: string, data: Record) { - super(message) - this.name = 'ErrorWithData' - this.data = data - } -} - -export class ErrorWithBody extends ErrorWithData { - public body: string - constructor(body: string, data: Record) { - super('Failed to parse body', data) - this.name = 'ErrorWithBody' - this.body = body - } -} - -type ExtractAndAssert = T extends Def ? Asserted - : undefined - -// I made the other field always explicitly undefined and optional -// this way we do not have to check all the time that they exists -type RequestState = - | { data: T; pending?: undefined; controller?: undefined; error?: undefined } - | { - data?: T | undefined - pending: number - controller?: AbortController - error?: undefined - } - | { - data?: T | undefined - pending?: undefined - controller?: undefined - error: ErrorWithBody | ErrorWithData | Error - } - -type Options = { - headers?: HeadersInit - signal?: AbortSignal -} - -const withoutBody = new Set([ - 204, // NoContent - 205, // ResetContent - 304, // NotModified -]) - -function createApiClient(baseUrl = '') { - type HandlerIO = T[K] extends - Handler - ? [ExtractAndAssert, ExtractAndAssert] - : never - - function makeClientCall(urlKey: K) { - type IO = HandlerIO - type Input = IO[0] - type Output = IO[1] - const key = urlKey as string - const slashIndex = key.indexOf('/') - const method = key.slice(0, slashIndex) as HttpMethod - const path = key.slice(slashIndex) - const defaultHeaders = { 'Content-Type': 'application/json' } - - async function fetcher(input?: Input, options?: Options | undefined) { - let url = `${baseUrl}${path}` - let headers = options?.headers - if (!headers) { - headers = defaultHeaders - } else { - headers instanceof Headers || (headers = new Headers(headers)) - for (const [key, value] of Object.entries(defaultHeaders)) { - headers.set(key, value) - } - } - - let bodyInput: string | undefined = undefined - if (input) { - method === 'GET' - ? (url += `?${new URLSearchParams(input as Record)}`) - : (bodyInput = JSON.stringify(input)) - } - - const response = await fetch( - url, - { ...options, method, headers, body: bodyInput }, - ) - if (withoutBody.has(response.status)) return null as unknown as Output - const body = await response.text() - let payload - try { - payload = JSON.parse(body) - if (response.ok) return payload as Output - } catch { - throw new ErrorWithBody(body, { response }) - } - const { message, ...data } = payload - throw new ErrorWithData(message, data) - } - - const signal = () => { - const $ = new Signal>({ pending: 0 }) - return { - $, - reset: () => { - $.peek().controller?.abort() - $.value = { pending: 0 } - }, - fetch: async (input, headers: HeadersInit) => { - const prev = $.peek() - try { - const controller = new AbortController() - prev.controller?.abort() - $.value = { pending: Date.now(), controller, data: prev.data } - const { signal } = controller - $.value = { data: await fetcher(input, { signal, headers }) } - } catch (err) { - $.value = (err instanceof DOMException && err.name === 'AbortError') - ? { pending: 0, data: prev.data } - : { - error: err as (ErrorWithBody | ErrorWithData | Error), - data: prev.data, - } - } - }, - get data() { - return $.value.data - }, - get error() { - return $.value.error - }, - get pending() { - return $.value.pending - }, - } as RequestState & { - $: Signal> - reset: () => void - fetch: (input?: Input, options?: Options | undefined) => Promise - } - } - - const use = () => useMemo(signal, []) - return { fetch: fetcher, use, signal } - } - - const client = {} as { [K in keyof T]: ReturnType> } - const lazy = (k: keyof T) => client[k] || (client[k] = makeClientCall(k)) - return new Proxy(client, { - get: (_, key: string) => lazy(key as keyof T), - }) -} - -export const api = createApiClient() +export const api = makeClient() export type ApiOutput = { [K in keyof typeof api]: Awaited> diff --git a/web/lib/router.tsx b/web/lib/router.tsx deleted file mode 100644 index fbe9f0a..0000000 --- a/web/lib/router.tsx +++ /dev/null @@ -1,214 +0,0 @@ -import type { JSX } from 'preact' -import { computed, Signal } from '@preact/signals' - -const isCurrentURL = (alt: URL) => { - const url = urlSignal.value - if (url.href === alt.href) return true - if (url.origin !== alt.origin) return false - if (url.pathname !== alt.pathname) return false - // handle special case, same params but different order. - // must still be equal - if (alt.searchParams.size !== url.searchParams.size) return false - if (alt.searchParams.size === 0) return false // no params -> not the same - // both urls have the same numbers of params, now let's confirm they are - // all the same values - for (const [k, v] of alt.searchParams) { - if (url.searchParams.get(k) !== v) return false - } - return true -} - -// ensure we never have trailing / -const initialUrl = new URL(location.href) -if (location.pathname.at(-1) === '/') { - initialUrl.pathname = initialUrl.pathname.slice(0, -1) - history.replaceState({}, '', initialUrl.href) -} - -const urlSignal = new Signal(initialUrl) -const { origin } = initialUrl - -const dispatchNavigation = () => { - // If the path did change, we update the local state and trigger the change - const url = new URL(location.href) - url.pathname.at(-1) === '/' && (url.pathname = url.pathname.slice(0, -1)) - if (isCurrentURL(url)) return - urlSignal.value = url -} - -addEventListener('popstate', dispatchNavigation) -addEventListener('hashchange', dispatchNavigation) - -const navigateUrl = (to: string, replace = false) => { - history[replace ? 'replaceState' : 'pushState']({}, '', to) - dispatchNavigation() -} - -type ParamPrimitive = string | number | boolean -type ParamValue = ParamPrimitive | null | undefined | ParamPrimitive[] -type GetUrlProps = { - href?: string - hash?: string - // params supports arrays to allow multiple identical keys: { tag: ['a','b'] } -> ?tag=a&tag=b - params?: URLSearchParams | Record -} - -const getUrl = ({ href, hash, params }: GetUrlProps) => { - const currentUrl = urlSignal.value - const url = new URL(href || currentUrl, origin) - hash != null && (url.hash = hash) - url.pathname.at(-1) === '/' && (url.pathname = url.pathname.slice(0, -1)) - if (!params) { - if (url.pathname !== currentUrl.pathname) return url - url.search = `?${currentUrl.searchParams}` - return url - } - for (const [key, value] of Object.entries(params)) { - if (Array.isArray(value)) { - // Remove existing then append each to preserve ordering - url.searchParams.delete(key) - for (const v of value) { - if (v === false || v == null) continue // skip deletions inside arrays - if (v === true) { - url.searchParams.append(key, '') - } else { - url.searchParams.append(key, v) - } - } - continue - } - if (value === true) { - url.searchParams.set(key, '') - } else if (value === false || value == null) { - url.searchParams.delete(key) - } else { - url.searchParams.set(key, value) - } - } - return url -} - -export const navigate = (props: GetUrlProps & { replace?: boolean }) => - navigateUrl(getUrl(props).href, props.replace) - -export type LinkProps = - & { replace?: boolean } - & JSX.HTMLAttributes - & GetUrlProps - -export const A = ({ - href, - hash, - params, - replace, - onClick, - onMouseDown, - ...props -}: LinkProps) => { - const url = getUrl({ href, hash, params }) - const noRouting = url.origin !== origin || - url.pathname.startsWith('/api/') - - if (noRouting) { - return ( - - ) - } - - const mouseDownHandler = ( - event: JSX.TargetedMouseEvent, - ) => { - // Experimenting with using `onMouseDown` to trigger - // it's faster but not cancellable, let's see how it's percieved - if (typeof onMouseDown === 'function') { - onMouseDown(event) - } - // We don't want to skip if it's a special click - // that would break the default browser behaviour - const shouldSkip = event.defaultPrevented || - event.button || - event.metaKey || - event.altKey || - event.ctrlKey || - event.shiftKey - - if (shouldSkip) return - - // In the normal case we handle the routing internally - event.preventDefault() - navigateUrl(url.href, replace) - } - - return ( - { - if (typeof onClick === 'function') { - onClick(event) - } - - const notMouse = event.clientX === 0 && event.clientY === 0 - if (notMouse) { - mouseDownHandler(event) - } else { - event.preventDefault() - } - }} - {...props} - /> - ) -} - -const toDeletedParam = (k: string) => [k, null] -export const replaceParams = ( - newParams: Record, -) => ({ - ...Object.fromEntries( - urlSignal.value.searchParams.keys().map(toDeletedParam), - ), - ...newParams, -}) - -// wrap params behind a proxy to allow accessing -const params = new Proxy({} as Record>, { - // this allow enumeration to work, so Object.keys(), {...params} will work - ownKeys: () => [...urlSignal.value.searchParams.keys()], - getOwnPropertyDescriptor: (_, key) => ({ - enumerable: true, - configurable: true, - value: urlSignal.value.searchParams.get(key as string), - }), - - // this is when we get a single key - get: (cache, key) => - (typeof key !== 'string' || !key) ? null : (cache[key] || ( - cache[key] = computed(() => urlSignal.value.searchParams.get(key)) - )).value, -}) as unknown as Record - -// http://localhost:8000/user/settings?id=454&options=open#display -// url.path: 'user/settings' -// url.hash: 'display' -// url.params: { id: 454, option: 'open' } -const hashSignal = computed(() => urlSignal.value.hash) -const pathSignal = computed(() => urlSignal.value.pathname) -export const url = { - get path() { - return pathSignal.value - }, - get hash() { - return hashSignal.value - }, - params, - // Retrieve all values (including duplicates) for a given key - getAll: (key: string) => urlSignal.value.searchParams.getAll(key), - // All param entries preserving duplicates & order - paramEntries: () => [...urlSignal.value.searchParams.entries()], - equals: (url: URL) => isCurrentURL(url), -} diff --git a/web/lib/shared.tsx b/web/lib/shared.tsx index 0e10ad3..acf506f 100644 --- a/web/lib/shared.tsx +++ b/web/lib/shared.tsx @@ -2,7 +2,7 @@ import { HardDrive, ListTodo } from 'lucide-preact' import { api } from './api.ts' import { DeploymentPage } from '../pages/DeploymentPage.tsx' import { SidebarItem } from '../components/SideBar.tsx' -import { url } from './router.tsx' +import { url } from '@01edu/signal-router' import { Signal } from '@preact/signals' // export diff --git a/web/pages/DeploymentPage.tsx b/web/pages/DeploymentPage.tsx index cbf2965..18a6e73 100644 --- a/web/pages/DeploymentPage.tsx +++ b/web/pages/DeploymentPage.tsx @@ -1,4 +1,4 @@ -import { A, navigate, url } from '../lib/router.tsx' +import { A, navigate, url } from '@01edu/signal-router' import { AlertCircle, AlertTriangle, @@ -1064,7 +1064,7 @@ const Drawer = () => ( />