From 033e76c2f16268aaa1142bb1e21dfc2a0f460e2a Mon Sep 17 00:00:00 2001 From: Neko Ayaka Date: Mon, 20 Apr 2026 20:39:12 +0800 Subject: [PATCH] feat(stage-tamagotchi): built-in local loopback http server --- apps/stage-tamagotchi/package.json | 3 + apps/stage-tamagotchi/src/main/index.ts | 7 +- .../src/main/services/airi/auth.ts | 2 +- .../services/airi/http-server/errors/index.ts | 72 +++ .../airi/http-server/http/auth/index.test.ts | 24 + .../airi/http-server/http/auth/index.ts | 131 +++++ .../extension-static-assets/index.test.ts | 70 +++ .../http/extension-static-assets/index.ts | 179 ++++++ .../extension-static-assets/route.test.ts | 100 ++++ .../http/extension-static-assets/route.ts | 87 +++ .../token-store.test.ts | 64 +++ .../extension-static-assets/token-store.ts | 127 +++++ .../http/extension-static-assets/types.ts | 36 ++ .../services/airi/http-server/index.test.ts | 20 + .../main/services/airi/http-server/index.ts | 38 ++ .../http-server/server-manager/index.test.ts | 61 ++ .../airi/http-server/server-manager/index.ts | 51 ++ .../airi/http-server/server-manager/types.ts | 19 + .../main/services/airi/http-server/server.ts | 73 +++ .../services/airi/plugins/asset-mount.test.ts | 57 ++ .../main/services/airi/plugins/asset-mount.ts | 100 ++++ .../main/services/electron/loopback-server.ts | 146 ----- pnpm-lock.yaml | 529 ++++++++++-------- pnpm-workspace.yaml | 8 + 24 files changed, 1620 insertions(+), 384 deletions(-) create mode 100644 apps/stage-tamagotchi/src/main/services/airi/http-server/errors/index.ts create mode 100644 apps/stage-tamagotchi/src/main/services/airi/http-server/http/auth/index.test.ts create mode 100644 apps/stage-tamagotchi/src/main/services/airi/http-server/http/auth/index.ts create mode 100644 apps/stage-tamagotchi/src/main/services/airi/http-server/http/extension-static-assets/index.test.ts create mode 100644 apps/stage-tamagotchi/src/main/services/airi/http-server/http/extension-static-assets/index.ts create mode 100644 apps/stage-tamagotchi/src/main/services/airi/http-server/http/extension-static-assets/route.test.ts create mode 100644 apps/stage-tamagotchi/src/main/services/airi/http-server/http/extension-static-assets/route.ts create mode 100644 apps/stage-tamagotchi/src/main/services/airi/http-server/http/extension-static-assets/token-store.test.ts create mode 100644 apps/stage-tamagotchi/src/main/services/airi/http-server/http/extension-static-assets/token-store.ts create mode 100644 apps/stage-tamagotchi/src/main/services/airi/http-server/http/extension-static-assets/types.ts create mode 100644 apps/stage-tamagotchi/src/main/services/airi/http-server/index.test.ts create mode 100644 apps/stage-tamagotchi/src/main/services/airi/http-server/index.ts create mode 100644 apps/stage-tamagotchi/src/main/services/airi/http-server/server-manager/index.test.ts create mode 100644 apps/stage-tamagotchi/src/main/services/airi/http-server/server-manager/index.ts create mode 100644 apps/stage-tamagotchi/src/main/services/airi/http-server/server-manager/types.ts create mode 100644 apps/stage-tamagotchi/src/main/services/airi/http-server/server.ts create mode 100644 apps/stage-tamagotchi/src/main/services/airi/plugins/asset-mount.test.ts create mode 100644 apps/stage-tamagotchi/src/main/services/airi/plugins/asset-mount.ts delete mode 100644 apps/stage-tamagotchi/src/main/services/electron/loopback-server.ts diff --git a/apps/stage-tamagotchi/package.json b/apps/stage-tamagotchi/package.json index b32e85a5b..e22c0990f 100644 --- a/apps/stage-tamagotchi/package.json +++ b/apps/stage-tamagotchi/package.json @@ -87,6 +87,7 @@ "alien-signals": "catalog:", "animejs": "^4.3.6", "async-mutex": "catalog:", + "chess.js": "catalog:", "colorjs.io": "^0.6.1", "culori": "^4.0.2", "d3": "catalog:", @@ -127,6 +128,7 @@ "vue-router": "^5.0.4", "vue-sonner": "catalog:", "web-haptics": "catalog:", + "whatwg-mimetype": "catalog:", "xsschema": "catalog:", "zod": "^4.3.6" }, @@ -165,6 +167,7 @@ "@types/semver": "^7.7.1", "@types/splitpanes": "catalog:", "@types/three": "^0.184.0", + "@types/whatwg-mimetype": "catalog:", "@types/yauzl": "^2.10.3", "@vitejs/plugin-vue": "^6.0.6", "@vue-macros/volar": "^3.1.2", diff --git a/apps/stage-tamagotchi/src/main/index.ts b/apps/stage-tamagotchi/src/main/index.ts index 52b318457..0dee48b41 100644 --- a/apps/stage-tamagotchi/src/main/index.ts +++ b/apps/stage-tamagotchi/src/main/index.ts @@ -25,6 +25,7 @@ import { setElectronMainDirname } from './libs/electron/location' import { createI18n } from './libs/i18n' import { createWindowAuthManagerService } from './services/airi/auth' import { setupServerChannel } from './services/airi/channel-server' +import { setupBuiltInServer } from './services/airi/http-server' import { setupMcpStdioManager } from './services/airi/mcp-servers' import { setupPluginHost } from './services/airi/plugins' import { setupAutoUpdater } from './services/electron/auto-updater' @@ -125,6 +126,10 @@ app.whenReady().then(async () => { build: async ({ dependsOn }) => setupServerChannel(dependsOn), }) + const airiHttpServer = injeca.provide('modules:airi-http-server', { + build: async () => setupBuiltInServer({ servers: [] }), + }) + const mcpStdioManager = injeca.provide('modules:mcp-stdio-manager', { build: async () => setupMcpStdioManager(), }) @@ -187,7 +192,7 @@ app.whenReady().then(async () => { }) injeca.invoke({ - dependsOn: { mainWindow, tray, serverChannel, pluginHost, mcpStdioManager, onboardingWindow: onboardingWindowManager }, + dependsOn: { mainWindow, tray, serverChannel, airiHttpServer, pluginHost, mcpStdioManager, onboardingWindow: onboardingWindowManager }, callback: noop, }) diff --git a/apps/stage-tamagotchi/src/main/services/airi/auth.ts b/apps/stage-tamagotchi/src/main/services/airi/auth.ts index 6dd3e224d..b789317bf 100644 --- a/apps/stage-tamagotchi/src/main/services/airi/auth.ts +++ b/apps/stage-tamagotchi/src/main/services/airi/auth.ts @@ -17,7 +17,7 @@ import { electronAuthLogout, electronAuthStartLogin, } from '../../../shared/eventa' -import { startLoopbackServer } from '../electron/loopback-server' +import { startLoopbackServer } from './http-server/http/auth' const log = useLogg('auth-service').useGlobalConfig() diff --git a/apps/stage-tamagotchi/src/main/services/airi/http-server/errors/index.ts b/apps/stage-tamagotchi/src/main/services/airi/http-server/errors/index.ts new file mode 100644 index 000000000..fe541e3c5 --- /dev/null +++ b/apps/stage-tamagotchi/src/main/services/airi/http-server/errors/index.ts @@ -0,0 +1,72 @@ +import { HTTPError } from 'h3' + +export interface HttpErrorInput { + status: number + code: string + message: string + reason?: string + details?: unknown + expose?: boolean +} + +/** + * Unified HTTP error shape for AIRI local HTTP server modules. + * + * Use when: + * - Returning typed errors from internal server modules + * - Keeping stable error code + reason metadata for logging/debugging + * + * Expects: + * - `code` is a stable machine-readable value + * - `message` is safe for clients when `expose` is true + * + * Returns: + * - Error instance with status and structured metadata + */ +export class HttpError extends Error { + readonly status: number + readonly code: string + readonly reason?: string + readonly details?: unknown + readonly expose: boolean + + constructor(input: HttpErrorInput) { + super(input.message) + this.name = 'HttpError' + this.status = input.status + this.code = input.code + this.reason = input.reason + this.details = input.details + this.expose = input.expose ?? false + } +} + +/** + * Converts a local `HttpError` into an h3-compatible throwable error. + * + * Use when: + * - A route catches internal `HttpError` values and must throw an h3 HTTP error + * + * Expects: + * - `error.message` is only exposed when `error.expose` is true + * + * Returns: + * - `HTTPError` preserving status while controlling client-visible message + */ +export function toH3HttpError(error: HttpError) { + return HTTPError.status(error.status, error.expose ? error.message : defaultHttpMessage(error.status)) +} + +function defaultHttpMessage(status: number) { + if (status === 400) + return 'Bad Request' + if (status === 401) + return 'Unauthorized' + if (status === 403) + return 'Forbidden' + if (status === 404) + return 'Not Found' + if (status >= 500) + return 'Internal Server Error' + return 'Request Failed' +} diff --git a/apps/stage-tamagotchi/src/main/services/airi/http-server/http/auth/index.test.ts b/apps/stage-tamagotchi/src/main/services/airi/http-server/http/auth/index.test.ts new file mode 100644 index 000000000..1cf0219ac --- /dev/null +++ b/apps/stage-tamagotchi/src/main/services/airi/http-server/http/auth/index.test.ts @@ -0,0 +1,24 @@ +import { afterEach, describe, expect, it } from 'vitest' + +import { startLoopbackServer } from './index' + +describe('startLoopbackServer', () => { + const servers: Array>> = [] + + afterEach(async () => { + for (const server of servers.splice(0)) { + server.close() + await server.result.catch(() => {}) + } + }) + + it('returns code and state from callback', async () => { + const server = await startLoopbackServer() + servers.push(server) + + const response = await fetch(`http://127.0.0.1:${server.port}/callback?code=ok&state=state-1`) + expect(response.status).toBe(200) + + await expect(server.result).resolves.toEqual({ code: 'ok', state: 'state-1' }) + }) +}) diff --git a/apps/stage-tamagotchi/src/main/services/airi/http-server/http/auth/index.ts b/apps/stage-tamagotchi/src/main/services/airi/http-server/http/auth/index.ts new file mode 100644 index 000000000..738c0153f --- /dev/null +++ b/apps/stage-tamagotchi/src/main/services/airi/http-server/http/auth/index.ts @@ -0,0 +1,131 @@ +import { eventHandler, getQuery, H3, handleCors } from 'h3' + +import { createH3Server } from '../../server' + +export interface LoopbackCallbackResult { + code: string + state: string +} + +/** + * Starts a temporary loopback callback server for the Electron OIDC flow. + * + * Use when: + * - Exchanging authorization code from system browser callback + * + * Expects: + * - Callback request on `GET /callback?code=...&state=...` + * - One-shot lifecycle; first successful callback closes the server + * + * Returns: + * - Random bound port, callback result promise, and manual cancellation method + */ +export async function startLoopbackServer(): Promise<{ + port: number + result: Promise + close: () => void +}> { + const host = '127.0.0.1' + let settled = false + let timeout: ReturnType | undefined + + let resolveResult!: (value: LoopbackCallbackResult) => void + let rejectResult!: (reason: Error) => void + + const result = new Promise((resolve, reject) => { + resolveResult = resolve + rejectResult = reject + }) + + const app = new H3() + const loopbackServer = createH3Server({ app, host }) + const corsOptions = { + origin: '*', + methods: '*', + preflight: { + statusCode: 204, + }, + } as const + + const finish = (callback: () => void) => { + if (settled) { + return + } + + settled = true + if (timeout) { + clearTimeout(timeout) + timeout = undefined + } + callback() + void loopbackServer.stop() + } + + app.options('/callback', eventHandler(async (event) => { + const corsResponse = handleCors(event, corsOptions) + if (corsResponse !== false) { + return corsResponse + } + + return new Response(null, { status: 204 }) + })) + + app.get('/callback', eventHandler(async (event) => { + const corsResponse = handleCors(event, corsOptions) + if (corsResponse !== false) { + return corsResponse + } + + const query = getQuery(event) + const error = typeof query.error === 'string' ? query.error : undefined + if (error) { + const description = typeof query.error_description === 'string' && query.error_description.length > 0 + ? query.error_description + : error + finish(() => { + rejectResult(new Error(description)) + }) + return new Response('

Authentication failed

You can close this window.

', { + status: 200, + headers: { 'Content-Type': 'text/html; charset=utf-8' }, + }) + } + + const code = typeof query.code === 'string' ? query.code : '' + const state = typeof query.state === 'string' ? query.state : '' + + if (!code || !state) { + return new Response('

Missing parameters

', { + status: 400, + headers: { 'Content-Type': 'text/html; charset=utf-8' }, + }) + } + + finish(() => { + resolveResult({ code, state }) + }) + + return new Response('

Authentication successful!

You can close this window and return to the app.

', { + status: 200, + headers: { 'Content-Type': 'text/html; charset=utf-8' }, + }) + })) + + const address = await loopbackServer.start() + + timeout = setTimeout(() => { + finish(() => { + rejectResult(new Error('Sign-in timed out — no callback received')) + }) + }, 5 * 60 * 1000) + + return { + port: address.port, + result, + close: () => { + finish(() => { + rejectResult(new Error('OIDC sign-in attempt cancelled')) + }) + }, + } +} diff --git a/apps/stage-tamagotchi/src/main/services/airi/http-server/http/extension-static-assets/index.test.ts b/apps/stage-tamagotchi/src/main/services/airi/http-server/http/extension-static-assets/index.test.ts new file mode 100644 index 000000000..f967e4b82 --- /dev/null +++ b/apps/stage-tamagotchi/src/main/services/airi/http-server/http/extension-static-assets/index.test.ts @@ -0,0 +1,70 @@ +import { mkdir, mkdtemp, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' + +import { afterEach, describe, expect, it } from 'vitest' + +import { createExtensionStaticAssetServer } from './index' +import { createExtensionAssetTokenStore } from './token-store' + +describe('createExtensionStaticAssetServer', () => { + const servers: Array> = [] + + afterEach(async () => { + while (servers.length > 0) { + const server = servers.pop() + if (!server) { + continue + } + await server.stop() + } + }) + + it('accepts pathPrefix relative to /ui route segment', async () => { + const rootDir = await mkdtemp(join(tmpdir(), 'airi-extension-static-assets-')) + await mkdir(join(rootDir, 'ui', 'assets'), { recursive: true }) + await writeFile(join(rootDir, 'ui', 'assets', 'app.js'), 'console.log("ok")\n') + + const extensionId = 'airi-plugin-game-chess' + const version = '1.0.0' + const tokenStore = createExtensionAssetTokenStore() + const validateInputs: string[] = [] + const server = createExtensionStaticAssetServer({ + getManifestEntryByName: () => new Map([ + [extensionId, { rootDir, version }], + ]), + tokenStore: { + ...tokenStore, + validate(token, input) { + validateInputs.push(input.assetPath) + return tokenStore.validate(token, input) + }, + }, + }) + servers.push(server) + await server.start() + + const token = server.issueToken({ + extensionId, + version, + sessionId: 'session-1', + pathPrefix: 'assets/', + ttlMs: 60_000, + }) + + const baseUrl = server.getBaseUrl() + expect(baseUrl).toBeTruthy() + + const response = await fetch(`${baseUrl}/_airi/extensions/${extensionId}/ui/assets/app.js?t=${token}`) + const responseBody = await response.text() + expect({ + status: response.status, + validateInputs, + responseBody, + }).toEqual({ + status: 200, + validateInputs: ['assets/app.js'], + responseBody: 'console.log("ok")\n', + }) + }) +}) diff --git a/apps/stage-tamagotchi/src/main/services/airi/http-server/http/extension-static-assets/index.ts b/apps/stage-tamagotchi/src/main/services/airi/http-server/http/extension-static-assets/index.ts new file mode 100644 index 000000000..c3f34a8f1 --- /dev/null +++ b/apps/stage-tamagotchi/src/main/services/airi/http-server/http/extension-static-assets/index.ts @@ -0,0 +1,179 @@ +import type { ServerManager } from '../../server-manager/types' +import type { ExtensionAssetTokenStore } from './types' + +import { stat } from 'node:fs/promises' + +import { H3 } from 'h3' + +import { + normalizePluginAssetPath, + resolvePluginAssetFilePath, +} from '../../../plugins/asset-mount' +import { HttpError } from '../../errors' +import { createH3Server } from '../../server' +import { createExtensionStaticAssetRoute } from './route' +import { createExtensionAssetTokenStore } from './token-store' + +export interface ExtensionStaticAssetManifestEntry { + rootDir: string + version: string +} + +export interface ExtensionStaticAssetServer extends ServerManager { + getBaseUrl: () => string | undefined + issueToken: ExtensionAssetTokenStore['issue'] + revokeByExtensionId: ExtensionAssetTokenStore['revokeByExtensionId'] + revokeAll: ExtensionAssetTokenStore['revokeAll'] +} + +/** + * Creates the standalone extension static asset server. + * + * Use when: + * - Main process must serve plugin iframe assets via local loopback HTTP + * - Tokenized auth is required for all plugin asset requests + * + * Expects: + * - `getManifestEntryByName` returns up-to-date plugin root/version map + * + * Returns: + * - Lifecycle service with token issue/revoke APIs and local base URL getter + */ +export function createExtensionStaticAssetServer(options: { + getManifestEntryByName: () => Map + host?: string + tokenStore?: ExtensionAssetTokenStore + getType?: (ext: string) => string | undefined +}): ExtensionStaticAssetServer { + const host = options.host ?? '127.0.0.1' + const tokenStore = options.tokenStore ?? createExtensionAssetTokenStore() + const getType = options.getType ?? defaultExtensionAssetMimeTypeResolver + + const app = new H3() + const serverLifecycle = createH3Server({ app, host }) + + app.get('/_airi/extensions/**', createExtensionStaticAssetRoute({ + getType, + authorize: async ({ token, extensionId, assetPath }) => { + const entry = options.getManifestEntryByName().get(extensionId) + if (!entry) { + return { + ok: false, + error: new HttpError({ + status: 401, + code: 'EXTENSION_ASSET_EXTENSION_NOT_REGISTERED', + message: 'Unauthorized', + reason: 'extension manifest entry does not exist for requested extensionId', + }), + } + } + + return tokenStore.validate(token, { + extensionId, + version: entry.version, + assetPath, + }) + }, + resolveAsset: async ({ extensionId, assetPath }) => { + const entry = options.getManifestEntryByName().get(extensionId) + if (!entry) { + return { + ok: false, + error: new HttpError({ + status: 404, + code: 'EXTENSION_ASSET_EXTENSION_NOT_FOUND', + message: 'Not Found', + reason: 'extension manifest entry does not exist for requested extensionId', + }), + } + } + + const normalizedAssetPath = normalizePluginAssetPath(assetPath) + if (!normalizedAssetPath) { + return { + ok: false, + error: new HttpError({ + status: 400, + code: 'EXTENSION_ASSET_PATH_INVALID', + message: 'Bad Request', + reason: 'asset path could not be normalized', + }), + } + } + + const fullAssetPath = `ui/${normalizedAssetPath}` + const filePath = await resolvePluginAssetFilePath(entry.rootDir, fullAssetPath) + if (!filePath) { + return { + ok: false, + error: new HttpError({ + status: 400, + code: 'EXTENSION_ASSET_PATH_RESOLVE_FAILED', + message: 'Bad Request', + reason: 'resolved asset path is outside extension root', + }), + } + } + + try { + const fileStats = await stat(filePath) + if (!fileStats.isFile()) { + return { + ok: false, + error: new HttpError({ + status: 404, + code: 'EXTENSION_ASSET_NOT_FILE', + message: 'Not Found', + reason: 'resolved path exists but is not a file', + }), + } + } + + return { + ok: true, + filePath, + size: fileStats.size, + mtime: fileStats.mtimeMs, + } + } + catch { + return { + ok: false, + error: new HttpError({ + status: 404, + code: 'EXTENSION_ASSET_NOT_FOUND', + message: 'Not Found', + reason: 'resolved file does not exist', + }), + } + } + }, + })) + + return { + key: 'extension-static-assets', + async start() { + await serverLifecycle.start() + }, + async stop() { + await serverLifecycle.stop() + }, + getBaseUrl() { + return serverLifecycle.getAddress()?.baseUrl + }, + issueToken: tokenStore.issue, + revokeByExtensionId: tokenStore.revokeByExtensionId, + revokeAll: tokenStore.revokeAll, + } +} + +const extensionAssetMimeTypeOverrides: Record = { + '.wasm': 'application/wasm', + '.avif': 'image/avif', + '.heic': 'image/heic', + '.heif': 'image/heif', +} + +function defaultExtensionAssetMimeTypeResolver(ext: string) { + return extensionAssetMimeTypeOverrides[ext.toLowerCase()] +} diff --git a/apps/stage-tamagotchi/src/main/services/airi/http-server/http/extension-static-assets/route.test.ts b/apps/stage-tamagotchi/src/main/services/airi/http-server/http/extension-static-assets/route.test.ts new file mode 100644 index 000000000..860015e2e --- /dev/null +++ b/apps/stage-tamagotchi/src/main/services/airi/http-server/http/extension-static-assets/route.test.ts @@ -0,0 +1,100 @@ +import { mkdtemp, rm, writeFile } from 'node:fs/promises' +import { createServer } from 'node:http' +import { tmpdir } from 'node:os' +import { join } from 'node:path' + +import { H3 } from 'h3' +import { toNodeHandler } from 'h3/node' +import { afterEach, describe, expect, it } from 'vitest' + +import { HttpError } from '../../errors' +import { createExtensionStaticAssetRoute } from './route' + +describe('createExtensionStaticAssetRoute', () => { + let server: ReturnType | undefined + const tempRoots: string[] = [] + + afterEach(async () => { + for (const root of tempRoots) { + await rm(root, { recursive: true, force: true }) + } + tempRoots.length = 0 + + await new Promise((resolve) => { + if (!server) { + resolve() + return + } + + server.close(() => resolve()) + server = undefined + }) + }) + + it('returns 401 when token is missing', async () => { + const app = new H3() + app.get('/_airi/extensions/:extensionId/ui/**assetPath', createExtensionStaticAssetRoute({ + authorize: async () => ({ + ok: false, + error: new HttpError({ + status: 401, + code: 'TOKEN_MISSING', + message: 'Unauthorized', + }), + }), + resolveAsset: async () => ({ + ok: false, + error: new HttpError({ + status: 404, + code: 'NOT_FOUND', + message: 'Not Found', + }), + }), + })) + + server = createServer(toNodeHandler(app)) + await new Promise(resolve => server!.listen(0, '127.0.0.1', () => resolve())) + const address = server.address() + const port = typeof address === 'object' && address ? address.port : 0 + + const response = await fetch(`http://127.0.0.1:${port}/_airi/extensions/a/ui/index.html`) + expect(response.status).toBe(401) + }) + + it('uses custom getType resolver and sets nosniff', async () => { + const root = await mkdtemp(join(tmpdir(), 'airi-extension-static-assets-')) + tempRoots.push(root) + + const wasmFilePath = join(root, 'module.wasm') + await writeFile(wasmFilePath, new Uint8Array([0, 97, 115, 109])) + + let authorizeCalled = false + const app = new H3() + app.get('/_airi/extensions/:extensionId/ui/:assetPath', createExtensionStaticAssetRoute({ + authorize: async () => { + authorizeCalled = true + return { ok: true } + }, + resolveAsset: async () => ({ + ok: true, + filePath: wasmFilePath, + size: 4, + mtime: Date.now(), + }), + getType: ext => ext === '.wasm' ? 'application/wasm' : undefined, + })) + + server = createServer(toNodeHandler(app)) + await new Promise(resolve => server!.listen(0, '127.0.0.1', () => resolve())) + const address = server.address() + const port = typeof address === 'object' && address ? address.port : 0 + + const response = await fetch(`http://127.0.0.1:${port}/_airi/extensions/a/ui/module.wasm?t=test-token`) + if (!authorizeCalled) { + throw new Error(`Expected authorize to be called. response=${response.status} body=${await response.text()}`) + } + expect(response.status).toBe(200) + expect(response.headers.get('content-type')).toBe('application/wasm') + expect(response.headers.get('x-content-type-options')).toBe('nosniff') + }) +}) diff --git a/apps/stage-tamagotchi/src/main/services/airi/http-server/http/extension-static-assets/route.ts b/apps/stage-tamagotchi/src/main/services/airi/http-server/http/extension-static-assets/route.ts new file mode 100644 index 000000000..17e1d927f --- /dev/null +++ b/apps/stage-tamagotchi/src/main/services/airi/http-server/http/extension-static-assets/route.ts @@ -0,0 +1,87 @@ +import type { ExtensionStaticAssetRouteOptions } from './types' + +import { readFile } from 'node:fs/promises' + +import { eventHandler, getQuery, getRequestURL, serveStatic } from 'h3' + +import { + normalizePluginAssetPath, + parsePluginAssetRequestPath, +} from '../../../plugins/asset-mount' +import { HttpError, toH3HttpError } from '../../errors' + +/** + * Creates the secured extension static asset route handler. + * + * Use when: + * - Serving plugin iframe assets under `/_airi/extensions/:extensionId/ui/**assetPath` + * + * Expects: + * - Query token `t` to be present and valid + * - `resolveAsset` to map request params into a validated local file + * + * Returns: + * - H3 event handler that enforces token auth before static file response + */ +export function createExtensionStaticAssetRoute(options: ExtensionStaticAssetRouteOptions) { + return eventHandler(async (event) => { + try { + const requestPath = parsePluginAssetRequestPath(getRequestURL(event).pathname) + const extensionId = requestPath?.extensionId ?? '' + const assetPath = normalizePluginAssetPath(requestPath?.assetPath ?? '') + const queryToken = getQuery(event).t + const token = typeof queryToken === 'string' ? queryToken : '' + + if (!token || !extensionId || !assetPath) { + throw new HttpError({ + status: 401, + code: 'EXTENSION_ASSET_REQUEST_INVALID', + message: 'Unauthorized', + reason: 'required token, extensionId, or assetPath is missing', + }) + } + + const auth = await options.authorize({ token, extensionId, assetPath }) + if (!auth.ok) { + throw auth.error + } + + let resolved: Awaited> | undefined + const resolveOnce = async () => { + if (!resolved) { + resolved = await options.resolveAsset({ extensionId, assetPath }) + } + return resolved + } + + return await serveStatic(event, { + getType: options.getType, + getContents: async () => { + const item = await resolveOnce() + if (!item.ok) { + throw item.error + } + return await readFile(item.filePath) + }, + getMeta: async () => { + const item = await resolveOnce() + if (!item.ok) { + return undefined + } + + event.res.headers.set('X-Content-Type-Options', 'nosniff') + return { + size: item.size, + mtime: item.mtime, + } + }, + }) + } + catch (error) { + if (error instanceof HttpError) { + throw toH3HttpError(error) + } + throw error + } + }) +} diff --git a/apps/stage-tamagotchi/src/main/services/airi/http-server/http/extension-static-assets/token-store.test.ts b/apps/stage-tamagotchi/src/main/services/airi/http-server/http/extension-static-assets/token-store.test.ts new file mode 100644 index 000000000..56048011e --- /dev/null +++ b/apps/stage-tamagotchi/src/main/services/airi/http-server/http/extension-static-assets/token-store.test.ts @@ -0,0 +1,64 @@ +import { describe, expect, it, vi } from 'vitest' + +import { createExtensionAssetTokenStore } from './token-store' + +describe('createExtensionAssetTokenStore', () => { + it('issues and validates short token', () => { + const now = vi.fn(() => 1000) + const store = createExtensionAssetTokenStore({ now }) + + const token = store.issue({ + extensionId: 'airi-plugin-game-chess', + version: '0.1.0', + sessionId: 'session-1', + pathPrefix: 'ui/', + ttlMs: 30_000, + }) + + expect(token.length).toBeLessThanOrEqual(24) + expect(store.validate(token, { + extensionId: 'airi-plugin-game-chess', + version: '0.1.0', + assetPath: 'ui/index.html', + }).ok).toBe(true) + }) + + it('rejects mismatched plugin and revoked tokens', () => { + const now = vi.fn(() => 1000) + const store = createExtensionAssetTokenStore({ now }) + + const token = store.issue({ + extensionId: 'airi-plugin-game-chess', + version: '0.1.0', + sessionId: 'session-1', + pathPrefix: 'ui/', + ttlMs: 30_000, + }) + + expect(store.validate(token, { + extensionId: 'other-plugin', + version: '0.1.0', + assetPath: 'ui/index.html', + })).toMatchObject({ + ok: false, + error: { + status: 401, + code: 'EXTENSION_ASSET_EXTENSION_MISMATCH', + }, + }) + + store.revokeByExtensionId('airi-plugin-game-chess') + + expect(store.validate(token, { + extensionId: 'airi-plugin-game-chess', + version: '0.1.0', + assetPath: 'ui/index.html', + })).toMatchObject({ + ok: false, + error: { + status: 401, + code: 'EXTENSION_ASSET_TOKEN_NOT_FOUND', + }, + }) + }) +}) diff --git a/apps/stage-tamagotchi/src/main/services/airi/http-server/http/extension-static-assets/token-store.ts b/apps/stage-tamagotchi/src/main/services/airi/http-server/http/extension-static-assets/token-store.ts new file mode 100644 index 000000000..b1ee76410 --- /dev/null +++ b/apps/stage-tamagotchi/src/main/services/airi/http-server/http/extension-static-assets/token-store.ts @@ -0,0 +1,127 @@ +import type { + ExtensionAssetTokenIssueInput, + ExtensionAssetTokenStore, + ExtensionAssetTokenValidateInput, + ExtensionAssetTokenValidationResult, +} from './types' + +import { randomBytes } from 'node:crypto' + +import { HttpError } from '../../errors' + +interface ExtensionAssetTokenRecord { + extensionId: string + version: string + sessionId: string + pathPrefix: string + exp: number +} + +function normalizePathPrefix(pathPrefix: string) { + const normalized = pathPrefix.trim().replaceAll('\\', '/') + if (!normalized) { + return '' + } + + return normalized.endsWith('/') ? normalized : `${normalized}/` +} + +function createOpaqueToken() { + return randomBytes(18).toString('base64url') +} + +/** + * Creates an in-memory opaque token store for extension static asset access. + * + * Use when: + * - Main process needs short-lived auth tokens for plugin iframe asset loading + * + * Expects: + * - Tokens are opaque and stored server-side only + * - Callers revoke by plugin id or revoke all on app shutdown + * + * Returns: + * - Issue/validate/revoke operations for extension asset tokens + */ +export function createExtensionAssetTokenStore(options: { now?: () => number } = {}): ExtensionAssetTokenStore { + const now = options.now ?? (() => Date.now()) + const records = new Map() + + const dropIfExpired = (token: string, record: ExtensionAssetTokenRecord) => { + if (record.exp > now()) { + return false + } + + records.delete(token) + return true + } + + const issue = (input: ExtensionAssetTokenIssueInput) => { + const token = createOpaqueToken() + records.set(token, { + extensionId: input.extensionId, + version: input.version, + sessionId: input.sessionId, + pathPrefix: normalizePathPrefix(input.pathPrefix), + exp: now() + input.ttlMs, + }) + return token + } + + const unauthorized = (code: string, reason: string) => { + return { + ok: false as const, + error: new HttpError({ + status: 401, + code, + message: 'Unauthorized', + reason, + }), + } + } + + const validate = (token: string, input: ExtensionAssetTokenValidateInput): ExtensionAssetTokenValidationResult => { + const record = records.get(token) + if (!record) { + return unauthorized('EXTENSION_ASSET_TOKEN_NOT_FOUND', 'token was not found in token store') + } + + if (dropIfExpired(token, record)) { + return unauthorized('EXTENSION_ASSET_TOKEN_EXPIRED', 'token has expired') + } + + if (record.extensionId !== input.extensionId) { + return unauthorized('EXTENSION_ASSET_EXTENSION_MISMATCH', 'token extensionId does not match request extensionId') + } + + if (typeof input.version === 'string' && record.version !== input.version) { + return unauthorized('EXTENSION_ASSET_VERSION_MISMATCH', 'token version does not match request version') + } + + const normalizedAssetPath = input.assetPath.trim().replaceAll('\\', '/') + if (!normalizedAssetPath) { + return unauthorized('EXTENSION_ASSET_PATH_EMPTY', 'asset path is empty') + } + + if (record.pathPrefix && !normalizedAssetPath.startsWith(record.pathPrefix)) { + return unauthorized('EXTENSION_ASSET_PATH_PREFIX_MISMATCH', 'asset path is outside allowed prefix') + } + + return { ok: true } + } + + return { + issue, + validate, + revokeByExtensionId(extensionId) { + for (const [token, record] of records.entries()) { + if (record.extensionId === extensionId) { + records.delete(token) + } + } + }, + revokeAll() { + records.clear() + }, + } +} diff --git a/apps/stage-tamagotchi/src/main/services/airi/http-server/http/extension-static-assets/types.ts b/apps/stage-tamagotchi/src/main/services/airi/http-server/http/extension-static-assets/types.ts new file mode 100644 index 000000000..288ec88bf --- /dev/null +++ b/apps/stage-tamagotchi/src/main/services/airi/http-server/http/extension-static-assets/types.ts @@ -0,0 +1,36 @@ +import type { HttpError } from '../../errors' + +export interface ExtensionAssetTokenIssueInput { + extensionId: string + version: string + sessionId: string + pathPrefix: string + ttlMs: number +} + +export interface ExtensionAssetTokenValidateInput { + extensionId: string + version?: string + assetPath: string +} + +export type ExtensionAssetTokenValidationResult + = | { ok: true } + | { ok: false, error: HttpError } + +export interface ExtensionAssetTokenStore { + issue: (input: ExtensionAssetTokenIssueInput) => string + validate: (token: string, input: ExtensionAssetTokenValidateInput) => ExtensionAssetTokenValidationResult + revokeByExtensionId: (extensionId: string) => void + revokeAll: () => void +} + +export type ExtensionStaticAssetResolveResult + = | { ok: true, filePath: string, size: number, mtime: number } + | { ok: false, error: HttpError } + +export interface ExtensionStaticAssetRouteOptions { + authorize: (params: { token: string, extensionId: string, assetPath: string }) => Promise + resolveAsset: (params: { extensionId: string, assetPath: string }) => Promise + getType?: (ext: string) => string | undefined +} diff --git a/apps/stage-tamagotchi/src/main/services/airi/http-server/index.test.ts b/apps/stage-tamagotchi/src/main/services/airi/http-server/index.test.ts new file mode 100644 index 000000000..3aeb75d44 --- /dev/null +++ b/apps/stage-tamagotchi/src/main/services/airi/http-server/index.test.ts @@ -0,0 +1,20 @@ +import { describe, expect, it, vi } from 'vitest' + +import { setupBuiltInServer } from './index' + +describe('setupBuiltInServer', () => { + it('starts registered adapters', async () => { + const auth = { key: 'auth', start: vi.fn(async () => {}), stop: vi.fn(async () => {}) } + const assets = { key: 'assets', start: vi.fn(async () => {}), stop: vi.fn(async () => {}) } + + const service = setupBuiltInServer({ + authServer: auth, + extensionStaticAssetServer: assets, + }) + + await service.start() + + expect(auth.start).toHaveBeenCalledOnce() + expect(assets.start).toHaveBeenCalledOnce() + }) +}) diff --git a/apps/stage-tamagotchi/src/main/services/airi/http-server/index.ts b/apps/stage-tamagotchi/src/main/services/airi/http-server/index.ts new file mode 100644 index 000000000..40b31cdac --- /dev/null +++ b/apps/stage-tamagotchi/src/main/services/airi/http-server/index.ts @@ -0,0 +1,38 @@ +import type { ServerManager } from './server-manager/types' + +import { createHttpServerManager } from './server-manager' + +export interface BuiltInServer { + start: () => Promise + stop: () => Promise +} + +/** + * Composes AIRI local HTTP servers behind one lifecycle service. + * + * Use when: + * - Main process needs one start/stop entrypoint for local HTTP services + * + * Expects: + * - Each server follows the `ServerManager` lifecycle contract + * + * Returns: + * - A lifecycle service with ordered startup/shutdown behavior + */ +export function setupBuiltInServer(params: { + authServer?: ServerManager + extensionStaticAssetServer?: ServerManager + servers?: ServerManager[] +}): BuiltInServer { + const servers = [ + ...(params.authServer ? [params.authServer] : []), + ...(params.extensionStaticAssetServer ? [params.extensionStaticAssetServer] : []), + ...(params.servers ?? []), + ] + const manager = createHttpServerManager(servers) + + return { + start: manager.start, + stop: manager.stop, + } +} diff --git a/apps/stage-tamagotchi/src/main/services/airi/http-server/server-manager/index.test.ts b/apps/stage-tamagotchi/src/main/services/airi/http-server/server-manager/index.test.ts new file mode 100644 index 000000000..bffa77e41 --- /dev/null +++ b/apps/stage-tamagotchi/src/main/services/airi/http-server/server-manager/index.test.ts @@ -0,0 +1,61 @@ +import { describe, expect, it, vi } from 'vitest' + +import { createHttpServerManager } from './index' + +describe('createHttpServerManager', () => { + it('starts and stops registered servers in order', async () => { + const startA = vi.fn(async () => {}) + const stopA = vi.fn(async () => {}) + const startB = vi.fn(async () => {}) + const stopB = vi.fn(async () => {}) + + const manager = createHttpServerManager([ + { key: 'a', start: startA, stop: stopA }, + { key: 'b', start: startB, stop: stopB }, + ]) + + await manager.start() + await manager.stop() + + expect(startA).toHaveBeenCalledOnce() + expect(startB).toHaveBeenCalledOnce() + expect(stopB).toHaveBeenCalledOnce() + expect(stopA).toHaveBeenCalledOnce() + }) + + it('serializes concurrent start and stop calls', async () => { + let releaseStartA: (() => void) | undefined + + const startA = vi.fn(async () => { + await new Promise((resolve) => { + releaseStartA = resolve + }) + }) + const stopA = vi.fn(async () => {}) + const startB = vi.fn(async () => {}) + const stopB = vi.fn(async () => {}) + + const manager = createHttpServerManager([ + { key: 'a', start: startA, stop: stopA }, + { key: 'b', start: startB, stop: stopB }, + ]) + + const firstStart = manager.start() + const secondStart = manager.start() + await Promise.resolve() + + expect(startA).toHaveBeenCalledOnce() + expect(startB).toHaveBeenCalledTimes(0) + + releaseStartA?.() + await Promise.all([firstStart, secondStart]) + + expect(startA).toHaveBeenCalledOnce() + expect(startB).toHaveBeenCalledOnce() + + await Promise.all([manager.stop(), manager.stop()]) + + expect(stopB).toHaveBeenCalledOnce() + expect(stopA).toHaveBeenCalledOnce() + }) +}) diff --git a/apps/stage-tamagotchi/src/main/services/airi/http-server/server-manager/index.ts b/apps/stage-tamagotchi/src/main/services/airi/http-server/server-manager/index.ts new file mode 100644 index 000000000..f32ac4244 --- /dev/null +++ b/apps/stage-tamagotchi/src/main/services/airi/http-server/server-manager/index.ts @@ -0,0 +1,51 @@ +import type { ServerManager } from './types' + +import { Mutex } from 'async-mutex' + +/** + * Creates an ordered lifecycle manager for AIRI local HTTP servers. + * + * Use when: + * - Multiple standalone HTTP servers must boot together + * - Shutdown order must run in reverse startup order + * + * Expects: + * - The `servers` list order defines startup order + * - `start`/`stop` may be called multiple times safely + * + * Returns: + * - An idempotent manager with `start` and `stop` + */ +export function createHttpServerManager(servers: ServerManager[]) { + let started = false + const lifecycleMutex = new Mutex() + + return { + async start() { + await lifecycleMutex.runExclusive(async () => { + if (started) { + return + } + + for (const server of servers) { + await server.start() + } + + started = true + }) + }, + async stop() { + await lifecycleMutex.runExclusive(async () => { + if (!started) { + return + } + + for (const server of [...servers].reverse()) { + await server.stop() + } + + started = false + }) + }, + } +} diff --git a/apps/stage-tamagotchi/src/main/services/airi/http-server/server-manager/types.ts b/apps/stage-tamagotchi/src/main/services/airi/http-server/server-manager/types.ts new file mode 100644 index 000000000..deb29a7a1 --- /dev/null +++ b/apps/stage-tamagotchi/src/main/services/airi/http-server/server-manager/types.ts @@ -0,0 +1,19 @@ +/** + * Shared lifecycle contract for AIRI local HTTP sub-servers. + * + * Use when: + * - Registering standalone local HTTP services under `services/airi/http-server` + * - Composing startup/shutdown order in the server manager + * + * Expects: + * - `start` to be idempotent + * - `stop` to be safe to call after partial startup + * + * Returns: + * - Promise lifecycle completion for each server action + */ +export interface ServerManager { + key: string + start: () => Promise + stop: () => Promise +} diff --git a/apps/stage-tamagotchi/src/main/services/airi/http-server/server.ts b/apps/stage-tamagotchi/src/main/services/airi/http-server/server.ts new file mode 100644 index 000000000..e569bfd8b --- /dev/null +++ b/apps/stage-tamagotchi/src/main/services/airi/http-server/server.ts @@ -0,0 +1,73 @@ +import { Mutex } from 'async-mutex' +import { getRandomPort } from 'get-port-please' +import { serve } from 'h3' + +export interface BuiltInServerAddress { + host: string + port: number + baseUrl: string +} + +/** + * Creates a reusable local HTTP server lifecycle around an h3 app. + * + * Use when: + * - A local HTTP module needs host/port assignment and start/stop lifecycle + * - Callers may trigger concurrent start/stop operations + * + * Expects: + * - `app` is a valid h3 app/handler accepted by `serve` + * - Caller manages route registration before first `start` + * + * Returns: + * - Idempotent lifecycle with serialized start/stop and runtime address getter + */ +export function createH3Server(options: { + app: Parameters[0] + host?: string + port?: number + silent?: boolean +}) { + const host = options.host ?? '127.0.0.1' + const silent = options.silent ?? true + const lifecycleMutex = new Mutex() + + let server: ReturnType | undefined + let address: BuiltInServerAddress | undefined + + return { + async start(): Promise { + return await lifecycleMutex.runExclusive(async () => { + if (address) { + return address + } + + const port = options.port ?? await getRandomPort(host) + server = serve(options.app, { hostname: host, port, silent }) + + address = { + host, + port, + baseUrl: `http://${host}:${port}`, + } + + return address + }) + }, + async stop(): Promise { + await lifecycleMutex.runExclusive(async () => { + address = undefined + if (!server) { + return + } + + const activeServer = server + server = undefined + await activeServer.close().catch(() => {}) + }) + }, + getAddress() { + return address + }, + } +} diff --git a/apps/stage-tamagotchi/src/main/services/airi/plugins/asset-mount.test.ts b/apps/stage-tamagotchi/src/main/services/airi/plugins/asset-mount.test.ts new file mode 100644 index 000000000..518935413 --- /dev/null +++ b/apps/stage-tamagotchi/src/main/services/airi/plugins/asset-mount.test.ts @@ -0,0 +1,57 @@ +import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' + +import { afterEach, describe, expect, it } from 'vitest' + +import { + buildMountedPluginAssetPath, + normalizePluginAssetPath, + parsePluginAssetRequestPath, + resolvePluginAssetFilePath, +} from './asset-mount' + +describe('asset-mount', () => { + const tempRoots: string[] = [] + + afterEach(async () => { + for (const root of tempRoots) { + await rm(root, { recursive: true, force: true }) + } + tempRoots.length = 0 + }) + + it('normalizes valid asset paths and rejects traversal-like segments', () => { + expect(normalizePluginAssetPath('dist/ui/index.html')).toBe('dist/ui/index.html') + expect(normalizePluginAssetPath('./dist/ui/index.html')).toBeUndefined() + expect(normalizePluginAssetPath('../secret.txt')).toBeUndefined() + expect(normalizePluginAssetPath('dist/../ui/index.html')).toBeUndefined() + }) + + it('parses mounted plugin request path and rejects malformed routes', () => { + expect(parsePluginAssetRequestPath('/_airi/extensions/airi-plugin-game-chess/ui/dist/ui/index.html')).toEqual({ + extensionId: 'airi-plugin-game-chess', + assetPath: 'dist/ui/index.html', + }) + expect(parsePluginAssetRequestPath('/_airi/extensions/airi-plugin-game-chess/ui/../../etc/passwd')).toBeUndefined() + expect(parsePluginAssetRequestPath('/_airi/extensions//ui/index.html')).toBeUndefined() + }) + + it('builds mounted asset path with encoded segments', () => { + expect(buildMountedPluginAssetPath({ + extensionId: 'airi-plugin-game-chess', + assetPath: 'dist/ui/index.html', + })).toBe('/_airi/extensions/airi-plugin-game-chess/ui/dist/ui/index.html') + }) + + it('resolves only files inside plugin root', async () => { + const root = await mkdtemp(join(tmpdir(), 'airi-plugin-assets-')) + tempRoots.push(root) + + await mkdir(join(root, 'dist', 'ui'), { recursive: true }) + await writeFile(join(root, 'dist', 'ui', 'index.html'), '') + + await expect(resolvePluginAssetFilePath(root, 'dist/ui/index.html')).resolves.toContain('dist/ui/index.html') + await expect(resolvePluginAssetFilePath(root, '../outside.txt')).resolves.toBeUndefined() + }) +}) diff --git a/apps/stage-tamagotchi/src/main/services/airi/plugins/asset-mount.ts b/apps/stage-tamagotchi/src/main/services/airi/plugins/asset-mount.ts new file mode 100644 index 000000000..3e3bd6ece --- /dev/null +++ b/apps/stage-tamagotchi/src/main/services/airi/plugins/asset-mount.ts @@ -0,0 +1,100 @@ +import { realpath } from 'node:fs/promises' +import { resolve, sep } from 'node:path' + +export interface ParsedPluginAssetRequest { + extensionId: string + assetPath: string +} + +const pathPrefix = '/_airi/extensions/' +const segmentPattern = /^[\w.+-]+$/ + +export function normalizePluginAssetPath(value: string): string | undefined { + const normalized = value.trim().replaceAll('\\', '/') + if (!normalized) { + return undefined + } + + const segments = normalized + .split('/') + .map(segment => decodeURIComponent(segment).trim()) + .filter(Boolean) + + if (segments.length === 0) { + return undefined + } + + if (segments.some(segment => segment === '.' || segment === '..')) { + return undefined + } + + return segments.join('/') +} + +export function parsePluginAssetRequestPath(pathname: string): ParsedPluginAssetRequest | undefined { + if (!pathname.startsWith(pathPrefix)) { + return undefined + } + + const rawRemainder = pathname.slice(pathPrefix.length) + if (!rawRemainder) { + return undefined + } + + const segments = rawRemainder.split('/').filter(Boolean) + if (segments.length < 3) { + return undefined + } + + const extensionId = decodeURIComponent(segments[0] ?? '') + const mountSegment = decodeURIComponent(segments[1] ?? '') + const rawAssetPath = segments.slice(2).join('/') + + if (!segmentPattern.test(extensionId) || mountSegment !== 'ui') { + return undefined + } + + const assetPath = normalizePluginAssetPath(rawAssetPath) + if (!assetPath) { + return undefined + } + + return { + extensionId, + assetPath, + } +} + +export async function resolvePluginAssetFilePath(rootDir: string, assetPath: string) { + const normalizedAssetPath = normalizePluginAssetPath(assetPath) + if (!normalizedAssetPath) { + return undefined + } + + const resolvedRoot = await realpath(rootDir) + const resolvedCandidate = resolve(resolvedRoot, normalizedAssetPath) + const normalizedRootPrefix = `${resolvedRoot}${sep}` + if (resolvedCandidate !== resolvedRoot && !resolvedCandidate.startsWith(normalizedRootPrefix)) { + return undefined + } + + return resolvedCandidate +} + +export function buildMountedPluginAssetPath(input: { + extensionId: string + assetPath: string +}) { + const normalizedAssetPath = normalizePluginAssetPath(input.assetPath) + if (!normalizedAssetPath) { + return undefined + } + + const encodedExtensionId = encodeURIComponent(input.extensionId) + const encodedAssetPath = normalizedAssetPath + .split('/') + .map(segment => encodeURIComponent(segment)) + .join('/') + + return `${pathPrefix}${encodedExtensionId}/ui/${encodedAssetPath}` +} diff --git a/apps/stage-tamagotchi/src/main/services/electron/loopback-server.ts b/apps/stage-tamagotchi/src/main/services/electron/loopback-server.ts deleted file mode 100644 index d488d2bcd..000000000 --- a/apps/stage-tamagotchi/src/main/services/electron/loopback-server.ts +++ /dev/null @@ -1,146 +0,0 @@ -import http from 'node:http' - -import { useLogg } from '@guiiai/logg' - -const log = useLogg('loopback-server').useGlobalConfig() - -export interface LoopbackCallbackResult { - code: string - state: string -} - -/** - * Fixed ports for the loopback OIDC callback server. - * The server relay page (`/api/auth/oidc/electron-callback`) forwards the - * authorization code to `http://127.0.0.1:{port}/callback` via JS fetch(). - * The port is encoded in the `state` parameter, not in the redirect_uri. - * - * See RFC 8252 S7.3 for the loopback redirect pattern. - */ -const LOOPBACK_PORTS = [19721, 19722, 19723, 19724, 19725] - -/** - * Start a temporary HTTP server on 127.0.0.1 to receive an OIDC authorization - * callback. Tries ports from LOOPBACK_PORTS in order. The server handles - * exactly one request and then shuts down. - * - * This follows RFC 8252 S7.3 (Loopback Interface Redirection) and is the - * approach used by VS Code, GitHub Desktop, and Slack Desktop. - */ -export function startLoopbackServer(): Promise<{ - port: number - result: Promise - close: () => void -}> { - return new Promise((resolveStart, rejectStart) => { - let settled = false - let resultResolve: (value: LoopbackCallbackResult) => void - let resultReject: (reason: Error) => void - - const resultPromise = new Promise((resolve, reject) => { - resultResolve = resolve - resultReject = reject - }) - - const server = http.createServer((req, res) => { - if (settled) - return - - const url = new URL(req.url ?? '/', `http://127.0.0.1`) - - // CORS: the relay page on the server origin sends a cross-origin fetch() - // to the loopback. Allow all origins since this is a one-shot local server. - res.setHeader('Access-Control-Allow-Origin', '*') - res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS') - if (req.method === 'OPTIONS') { - res.writeHead(204) - res.end() - return - } - - if (url.pathname !== '/callback') { - res.writeHead(404) - res.end('Not found') - return - } - - const error = url.searchParams.get('error') - if (error) { - const description = url.searchParams.get('error_description') ?? error - settled = true - res.writeHead(200, { 'Content-Type': 'text/html' }) - res.end('

Authentication failed

You can close this window.

') - resultReject!(new Error(description)) - server.close() - return - } - - const code = url.searchParams.get('code') - const state = url.searchParams.get('state') - - if (!code || !state) { - res.writeHead(400, { 'Content-Type': 'text/html' }) - res.end('

Missing parameters

') - return - } - - settled = true - res.writeHead(200, { 'Content-Type': 'text/html' }) - res.end('

Authentication successful!

You can close this window and return to the app.

') - resultResolve!({ code, state }) - server.close() - }) - - // Safety timeout: close after 5 minutes - const timeout = setTimeout(() => { - if (!settled) { - settled = true - resultReject!(new Error('Sign-in timed out — no callback received')) - server.close() - } - }, 5 * 60 * 1000) - - server.on('close', () => clearTimeout(timeout)) - - // Try each port in order - let portIndex = 0 - - function tryListen(): void { - if (portIndex >= LOOPBACK_PORTS.length) { - rejectStart(new Error(`All loopback ports (${LOOPBACK_PORTS.join(', ')}) are in use`)) - return - } - - const port = LOOPBACK_PORTS[portIndex] - - server.once('error', (err: NodeJS.ErrnoException) => { - if (err.code === 'EADDRINUSE') { - log.withFields({ port }).log('Port in use, trying next') - portIndex++ - tryListen() - } - else { - rejectStart(err) - } - }) - - server.listen(port, '127.0.0.1', () => { - log.withFields({ port }).log('Loopback callback server started') - resolveStart({ - port, - result: resultPromise, - close: () => { - if (settled) - return - - settled = true - resultReject!(new Error('OIDC sign-in attempt cancelled')) - server.close() - }, - }) - }) - } - - tryListen() - }) -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e9194dcea..63d2a3dcc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,6 +6,9 @@ settings: catalogs: default: + '@ax-llm/ax': + specifier: ^19.0.43 + version: 19.0.45 '@better-auth/oauth-provider': specifier: 1.5.6 version: 1.5.6 @@ -123,6 +126,9 @@ catalogs: '@types/unist': specifier: ^3.0.3 version: 3.0.3 + '@types/whatwg-mimetype': + specifier: ^5.0.0 + version: 5.0.0 '@types/ws': specifier: ^8.18.1 version: 8.18.1 @@ -183,6 +189,9 @@ catalogs: capacitor-native-settings: specifier: ^8.1.0 version: 8.1.0 + chess.js: + specifier: ^1.4.0 + version: 1.4.0 crossws: specifier: ^0.4.5 version: 0.4.5 @@ -267,12 +276,18 @@ catalogs: posthog-js: specifier: 1.306.1 version: 1.306.1 + reka-ui: + specifier: ^2.9.2 + version: 2.9.6 splitpanes: specifier: ^4.0.4 version: 4.0.4 std-env: specifier: ^4.1.0 version: 4.1.0 + stockfish: + specifier: ^18.0.7 + version: 18.0.7 superjson: specifier: ^2.2.6 version: 2.2.6 @@ -297,6 +312,9 @@ catalogs: valibot: specifier: 1.2.0 version: 1.2.0 + vieval: + specifier: ^0.0.1 + version: 0.0.1 vite: specifier: ^8.0.8 version: 8.0.8 @@ -318,6 +336,9 @@ catalogs: web-haptics: specifier: ^0.0.6 version: 0.0.6 + whatwg-mimetype: + specifier: ^5.0.0 + version: 5.0.0 ws: specifier: ^8.20.0 version: 8.20.0 @@ -1243,6 +1264,9 @@ importers: async-mutex: specifier: 'catalog:' version: 0.5.0 + chess.js: + specifier: 'catalog:' + version: 1.4.0 colorjs.io: specifier: ^0.6.1 version: 0.6.1 @@ -1363,6 +1387,9 @@ importers: web-haptics: specifier: 'catalog:' version: 0.0.6(react@19.2.3)(vue@3.5.32(typescript@5.9.3)) + whatwg-mimetype: + specifier: 'catalog:' + version: 5.0.0 xsschema: specifier: 'catalog:' version: 0.5.0-beta.2(@valibot/to-json-schema@1.0.0-rc.0(valibot@1.3.1(typescript@5.9.3)))(zod-to-json-schema@3.25.2(zod@4.3.6))(zod@4.3.6) @@ -1375,7 +1402,7 @@ importers: version: 3.0.2(electron@41.2.1) '@electron-toolkit/tsconfig': specifier: ^2.0.0 - version: 2.0.0(@types/node@24.12.2) + version: 2.0.0(@types/node@25.6.0) '@electron-toolkit/utils': specifier: ^4.0.0 version: 4.0.0(electron@41.2.1) @@ -1414,7 +1441,7 @@ importers: version: 3.1.0 '@intlify/unplugin-vue-i18n': specifier: ^11.0.7 - version: 11.0.7(@vue/compiler-dom@3.5.32)(eslint@10.2.1(jiti@2.6.1))(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-i18n@11.3.2(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)) + version: 11.0.7(@vue/compiler-dom@3.5.32)(eslint@10.2.1(jiti@2.6.1))(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-i18n@11.3.2(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)) '@modelcontextprotocol/sdk': specifier: 'catalog:' version: 1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6) @@ -1450,10 +1477,10 @@ importers: version: link:../../packages/ui-transitions '@proj-airi/unplugin-fetch': specifier: 'catalog:' - version: 0.2.3(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 0.2.3(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) '@proj-airi/unplugin-live2d-sdk': specifier: ^0.1.7 - version: 0.1.7(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) + version: 0.1.7(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) '@types/audioworklet': specifier: 'catalog:' version: 0.0.97 @@ -1472,12 +1499,15 @@ importers: '@types/three': specifier: ^0.184.0 version: 0.184.0 + '@types/whatwg-mimetype': + specifier: 'catalog:' + version: 5.0.0 '@types/yauzl': specifier: ^2.10.3 version: 2.10.3 '@vitejs/plugin-vue': specifier: ^6.0.6 - version: 6.0.6(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3)) + version: 6.0.6(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3)) '@vue-macros/volar': specifier: ^3.1.2 version: 3.1.2(typescript@5.9.3)(vue-tsc@3.2.6(typescript@5.9.3))(vue@3.5.32(typescript@5.9.3)) @@ -1507,7 +1537,7 @@ importers: version: 6.8.3 electron-vite: specifier: ^5.0.0 - version: 5.0.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 5.0.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) get-port-please: specifier: 'catalog:' version: 3.2.0 @@ -1528,31 +1558,31 @@ importers: version: 2.2.6 unocss-preset-scrollbar: specifier: ^4.0.0 - version: 4.0.0(unocss@66.6.8(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))) + version: 4.0.0(unocss@66.6.8(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))) unplugin-info: specifier: ^1.3.2 - version: 1.3.2(esbuild@0.27.2)(rollup@4.60.1)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 1.3.2(esbuild@0.27.2)(rollup@4.60.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) unplugin-yaml: specifier: ^4.1.0 - version: 4.1.0(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.0(@nuxt/kit@3.20.2(magicast@0.5.2))(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) vite: specifier: 'catalog:' - version: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) + version: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) vite-bundle-visualizer: specifier: ^1.2.1 version: 1.2.1(rolldown@1.0.0-rc.16)(rollup@4.60.1) vite-plugin-mkcert: specifier: 'catalog:' - version: 2.0.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 2.0.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) vite-plugin-vue-devtools: specifier: ^8.1.1 - version: 8.1.1(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3)) + version: 8.1.1(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3)) vite-plugin-vue-layouts: specifier: ^0.11.0 - version: 0.11.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-router@5.0.4(@vue/compiler-sfc@3.5.32)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)) + version: 0.11.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-router@5.0.4(@vue/compiler-sfc@3.5.32)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)) vue-macros: specifier: ^3.1.2 - version: 3.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@vueuse/core@14.2.1(vue@3.5.32(typescript@5.9.3)))(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-tsc@3.2.6(typescript@5.9.3))(vue@3.5.32(typescript@5.9.3)) + version: 3.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@vueuse/core@14.2.1(vue@3.5.32(typescript@5.9.3)))(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-tsc@3.2.6(typescript@5.9.3))(vue@3.5.32(typescript@5.9.3)) vue-tsc: specifier: ^3.2.6 version: 3.2.6(typescript@5.9.3) @@ -3768,6 +3798,70 @@ importers: specifier: ^3.2.6 version: 3.2.6(typescript@5.9.3) + plugins/airi-plugin-game-chess: + dependencies: + '@moeru/std': + specifier: 'catalog:' + version: 0.1.0-beta.17 + '@proj-airi/ui': + specifier: workspace:^ + version: link:../../packages/ui + animejs: + specifier: ^4.3.6 + version: 4.3.6 + chess.js: + specifier: 'catalog:' + version: 1.4.0 + reka-ui: + specifier: 'catalog:' + version: 2.9.6(vue@3.5.32(typescript@5.9.3)) + stockfish: + specifier: 'catalog:' + version: 18.0.7 + vue: + specifier: 'catalog:' + version: 3.5.32(typescript@5.9.3) + devDependencies: + '@ax-llm/ax': + specifier: 'catalog:' + version: 19.0.45(zod@4.3.6) + '@proj-airi/stage-ui': + specifier: workspace:^ + version: link:../../packages/stage-ui + '@proj-airi/unocss-preset-chromatic': + specifier: ^1.0.2 + version: 1.1.1 + '@unocss/reset': + specifier: ^66.6.7 + version: 66.6.8 + '@vitejs/plugin-vue': + specifier: ^6.0.5 + version: 6.0.6(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3)) + '@xsai-ext/providers': + specifier: 'catalog:' + version: 0.5.0-beta.2 + '@xsai/generate-text': + specifier: 'catalog:' + version: 0.5.0-beta.2(patch_hash=306bfb723913596b334140f0d6fa48063f336e3b44024efc1d72bf60d54b15e6) + '@xsai/shared-chat': + specifier: 'catalog:' + version: 0.5.0-beta.2(patch_hash=26f2819b987245ec85f216b821ddf73aeb28fd7e611238a2d37250e42f838a01) + '@xsai/stream-text': + specifier: 'catalog:' + version: 0.5.0-beta.2(patch_hash=90dfe10d02f5946658508ec019937eab600745c93446ce7b2fdb1a0ed70e3e49) + pinia: + specifier: 'catalog:' + version: 3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)) + unocss: + specifier: ^66.6.7 + version: 66.6.8(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + vieval: + specifier: 'catalog:' + version: 0.0.1(@types/node@25.6.0)(chokidar@5.0.0)(dotenv@17.4.2)(esbuild@0.27.2)(giget@2.0.0)(jiti@2.6.1)(less@4.6.4)(magicast@0.5.2)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) + vue-tsc: + specifier: ^3.1.1 + version: 3.2.6(typescript@5.9.3) + plugins/airi-plugin-homeassistant: dependencies: '@guiiai/logg': @@ -4351,6 +4445,15 @@ packages: '@asamuzakjp/nwsapi@2.3.9': resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} + '@ax-llm/ax@19.0.45': + resolution: {integrity: sha512-zC/OqUutTV0omOuAxBgeCihmxP3e1eUcbA78FCaMF3pPXZ3/FgQDZf0R04LH/lVIxm5BH4/qDdNNYcIVGwii8Q==} + hasBin: true + peerDependencies: + zod: ^3.24.0 || ^4.0.0 + peerDependenciesMeta: + zod: + optional: true + '@azure/msal-common@14.16.1': resolution: {integrity: sha512-nyxsA6NA4SVKh5YyRpbSXiMr7oQbwark7JU9LMeg6tJYTSPyAGkdx61wPT4gyxZfxlSxMMEyAsWaubBlNyIa1w==} engines: {node: '>=0.8.0'} @@ -9526,6 +9629,9 @@ packages: '@types/webxr@0.5.24': resolution: {integrity: sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==} + '@types/whatwg-mimetype@5.0.0': + resolution: {integrity: sha512-YYiBDCoqBgDIF2ByYn4qDb4RaXZ46cOQ6j2We1Ni3bikFNI7YFeL8jxSiYowWsriZrb1mw09CLZ+b+ZkIhsLVw==} + '@types/ws@8.18.1': resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} @@ -11029,6 +11135,26 @@ packages: magicast: optional: true + c12@4.0.0-beta.4: + resolution: {integrity: sha512-gcWQAloC/SwGx4U7l3iQdalUQQLLXwYS1d3SqIwFj4UUrTXh8L9yGkBcA00B0gxELMwbxtsrt6VrAxtSgqZZoA==} + peerDependencies: + chokidar: ^5 + dotenv: '*' + giget: '*' + jiti: '*' + magicast: '*' + peerDependenciesMeta: + chokidar: + optional: true + dotenv: + optional: true + giget: + optional: true + jiti: + optional: true + magicast: + optional: true + cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} @@ -11112,6 +11238,9 @@ packages: character-entities@2.0.2: resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + chess.js@1.4.0: + resolution: {integrity: sha512-BBJgrrtKQOzFLonR0l+k64A98NLemPwNsCskwb+29bRwobUa4iTm51E1kwGPbWXAcfdDa18nad6vpPPKPWarqw==} + chevrotain@10.5.0: resolution: {integrity: sha512-Pkv5rBY3+CsHOYfV5g/Vs5JY9WTHHDEKOlohI2XeygaZhUeqhAlldZ8Hz9cRmxu709bvS08YzxHdTPHhffc13A==} @@ -11331,6 +11460,9 @@ packages: confbox@0.2.2: resolution: {integrity: sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==} + confbox@0.2.4: + resolution: {integrity: sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==} + config-chain@1.1.13: resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} @@ -11609,6 +11741,9 @@ packages: date-fns@4.1.0: resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==} + dayjs@1.11.20: + resolution: {integrity: sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==} + de-indent@1.0.2: resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==} @@ -12657,9 +12792,15 @@ packages: fast-string-truncated-width@1.2.1: resolution: {integrity: sha512-Q9acT/+Uu3GwGj+5w/zsGuQjh9O1TyywhIwAxHudtWrgF09nHOPrvTLhQevPbttcxjr/SNN7mJmfOw/B1bXgow==} + fast-string-truncated-width@3.0.3: + resolution: {integrity: sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==} + fast-string-width@1.1.0: resolution: {integrity: sha512-O3fwIVIH5gKB38QNbdg+3760ZmGz0SZMgvwJbA1b2TGXceKE6A2cOlfogh1iw8lr049zPyd7YADHy+B7U4W9bQ==} + fast-string-width@3.0.2: + resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==} + fast-uri@3.1.0: resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} @@ -15555,6 +15696,9 @@ packages: rc9@2.1.2: resolution: {integrity: sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==} + rc9@3.0.1: + resolution: {integrity: sha512-gMDyleLWVE+i6Sgtc0QbbY6pEKqYs97NGi6isHQPqYlLemPoO8dxQ3uGi0f4NiP98c+jMW6cG1Kx9dDwfvqARQ==} + rc@1.2.8: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} hasBin: true @@ -16216,6 +16360,10 @@ packages: std-env@4.1.0: resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} + stockfish@18.0.7: + resolution: {integrity: sha512-tJ+bfMAHs4fV7QYiLcHUicx1RzKOQwj8twx/z0iUTauMG+SE3l1rEtRSg+WER81ke3bc5tqKBWEWntUbydkgZg==} + hasBin: true + store2@2.14.4: resolution: {integrity: sha512-srTItn1GOvyvOycgxjAnPA63FZNwy0PTyUBFMHRM+hVFltAeoh0LmNBz9SZqUS9mMqGk8rfyWyXn3GH5ReJ8Zw==} @@ -17204,6 +17352,10 @@ packages: vfile@6.0.3: resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + vieval@0.0.1: + resolution: {integrity: sha512-JCvKwQP0rE/SvvZR/vPJFBqVE286+ZxGCQpohQBtmdMuwoiE38JbkDDPlT09qx2FCckCJYdEqP05DmCxJ8X0TA==} + hasBin: true + vite-bundle-visualizer@1.2.1: resolution: {integrity: sha512-cwz/Pg6+95YbgIDp+RPwEToc4TKxfsFWSG/tsl2DSZd9YZicUag1tQXjJ5xcL7ydvEoaC2FOZeaXOU60t9BRXw==} engines: {node: ^18.19.0 || >=20.6.0} @@ -17633,6 +17785,10 @@ packages: resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} engines: {node: '>=18'} + whatwg-mimetype@5.0.0: + resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==} + engines: {node: '>=20'} + whatwg-url@15.1.0: resolution: {integrity: sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==} engines: {node: '>=20'} @@ -18125,6 +18281,13 @@ snapshots: '@asamuzakjp/nwsapi@2.3.9': {} + '@ax-llm/ax@19.0.45(zod@4.3.6)': + dependencies: + '@opentelemetry/api': 1.9.1 + dayjs: 1.11.20 + optionalDependencies: + zod: 4.3.6 + '@azure/msal-common@14.16.1': {} '@azure/msal-node@2.16.3': @@ -19381,9 +19544,9 @@ snapshots: dependencies: electron: 41.2.1 - '@electron-toolkit/tsconfig@2.0.0(@types/node@24.12.2)': + '@electron-toolkit/tsconfig@2.0.0(@types/node@25.6.0)': dependencies: - '@types/node': 24.12.2 + '@types/node': 25.6.0 '@electron-toolkit/utils@4.0.0(electron@41.2.1)': dependencies: @@ -20269,31 +20432,6 @@ snapshots: - supports-color - typescript - '@intlify/unplugin-vue-i18n@11.0.7(@vue/compiler-dom@3.5.32)(eslint@10.2.1(jiti@2.6.1))(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-i18n@11.3.2(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))': - dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.1(jiti@2.6.1)) - '@intlify/bundle-utils': 11.0.7(vue-i18n@11.3.2(vue@3.5.32(typescript@5.9.3))) - '@intlify/shared': 11.3.2 - '@intlify/vue-i18n-extensions': 8.0.0(@intlify/shared@11.3.2)(@vue/compiler-dom@3.5.32)(vue-i18n@11.3.2(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)) - '@rollup/pluginutils': 5.3.0(rollup@4.60.1) - '@typescript-eslint/scope-manager': 8.58.1 - '@typescript-eslint/typescript-estree': 8.58.1(typescript@5.9.3) - debug: 4.4.3(supports-color@10.2.2) - fast-glob: 3.3.3 - pathe: 2.0.3 - picocolors: 1.1.1 - unplugin: 2.3.11 - vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) - vue: 3.5.32(typescript@5.9.3) - optionalDependencies: - vue-i18n: 11.3.2(vue@3.5.32(typescript@5.9.3)) - transitivePeerDependencies: - - '@vue/compiler-dom' - - eslint - - rollup - - supports-color - - typescript - '@intlify/unplugin-vue-i18n@11.0.7(@vue/compiler-dom@3.5.32)(eslint@10.2.1(jiti@2.6.1))(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-i18n@11.3.2(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))': dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.1(jiti@2.6.1)) @@ -22264,35 +22402,11 @@ snapshots: transitivePeerDependencies: - magicast - '@proj-airi/unplugin-fetch@0.2.3(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))': - dependencies: - ofetch: 1.5.1 - vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) - '@proj-airi/unplugin-fetch@0.2.3(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: ofetch: 1.5.1 vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) - '@proj-airi/unplugin-live2d-sdk@0.1.7(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)': - dependencies: - ofetch: 1.5.1 - vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) - yauzl: 3.3.0 - transitivePeerDependencies: - - '@types/node' - - '@vitejs/devtools' - - esbuild - - jiti - - less - - sass - - sass-embedded - - stylus - - sugarss - - terser - - tsx - - yaml - '@proj-airi/unplugin-live2d-sdk@0.1.7(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)': dependencies: ofetch: 1.5.1 @@ -23240,7 +23354,6 @@ snapshots: '@types/node@25.6.0': dependencies: undici-types: 7.19.2 - optional: true '@types/nprogress@0.2.3': {} @@ -23324,6 +23437,8 @@ snapshots: '@types/webxr@0.5.24': {} + '@types/whatwg-mimetype@5.0.0': {} + '@types/ws@8.18.1': dependencies: '@types/node': 24.12.2 @@ -23767,6 +23882,19 @@ snapshots: unplugin-utils: 0.3.1 vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) + '@unocss/vite@66.6.8(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))': + dependencies: + '@jridgewell/remapping': 2.3.5 + '@unocss/config': 66.6.8 + '@unocss/core': 66.6.8 + '@unocss/inspector': 66.6.8 + chokidar: 5.0.0 + magic-string: 0.30.21 + pathe: 2.0.3 + tinyglobby: 0.2.16 + unplugin-utils: 0.3.1 + vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) + '@unrteljs/eval@0.2.1': dependencies: builtin-modules: 5.0.0 @@ -23891,12 +24019,6 @@ snapshots: vite: 6.4.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) vue: 3.5.32(typescript@5.9.3) - '@vitejs/plugin-vue@6.0.6(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3))': - dependencies: - '@rolldown/pluginutils': 1.0.0-rc.13 - vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) - vue: 3.5.32(typescript@5.9.3) - '@vitejs/plugin-vue@6.0.6(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3))': dependencies: '@rolldown/pluginutils': 1.0.0-rc.13 @@ -24199,15 +24321,6 @@ snapshots: transitivePeerDependencies: - vue - '@vue-macros/devtools@3.1.2(typescript@5.9.3)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))': - dependencies: - sirv: 3.0.2 - vue: 3.5.32(typescript@5.9.3) - optionalDependencies: - vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) - transitivePeerDependencies: - - typescript - '@vue-macros/devtools@3.1.2(typescript@5.9.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: sirv: 3.0.2 @@ -25379,6 +25492,21 @@ snapshots: optionalDependencies: magicast: 0.5.2 + c12@4.0.0-beta.4(chokidar@5.0.0)(dotenv@17.4.2)(giget@2.0.0)(jiti@2.6.1)(magicast@0.5.2): + dependencies: + confbox: 0.2.4 + defu: 6.1.7 + exsolve: 1.0.8 + pathe: 2.0.3 + pkg-types: 2.3.0 + rc9: 3.0.1 + optionalDependencies: + chokidar: 5.0.0 + dotenv: 17.4.2 + giget: 2.0.0 + jiti: 2.6.1 + magicast: 0.5.2 + cac@6.7.14: {} cac@7.0.0: {} @@ -25459,6 +25587,8 @@ snapshots: character-entities@2.0.2: {} + chess.js@1.4.0: {} + chevrotain@10.5.0: dependencies: '@chevrotain/cst-dts-gen': 10.5.0 @@ -25672,6 +25802,8 @@ snapshots: confbox@0.2.2: {} + confbox@0.2.4: {} + config-chain@1.1.13: dependencies: ini: 1.3.8 @@ -25968,6 +26100,8 @@ snapshots: date-fns@4.1.0: {} + dayjs@1.11.20: {} + de-indent@1.0.2: {} debounce@1.2.1: {} @@ -26305,7 +26439,7 @@ snapshots: transitivePeerDependencies: - supports-color - electron-vite@5.0.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): + electron-vite@5.0.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): dependencies: '@babel/core': 7.29.0 '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.29.0) @@ -26313,7 +26447,7 @@ snapshots: esbuild: 0.25.12 magic-string: 0.30.21 picocolors: 1.1.1 - vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) + vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) transitivePeerDependencies: - supports-color @@ -27110,10 +27244,16 @@ snapshots: fast-string-truncated-width@1.2.1: {} + fast-string-truncated-width@3.0.3: {} + fast-string-width@1.1.0: dependencies: fast-string-truncated-width: 1.2.1 + fast-string-width@3.0.2: + dependencies: + fast-string-truncated-width: 3.0.3 + fast-uri@3.1.0: {} fast-wrap-ansi@0.1.6: @@ -30682,6 +30822,11 @@ snapshots: defu: 6.1.7 destr: 2.0.5 + rc9@3.0.1: + dependencies: + defu: 6.1.7 + destr: 2.0.5 + rc@1.2.8: dependencies: deep-extend: 0.6.0 @@ -31591,6 +31736,8 @@ snapshots: std-env@4.1.0: {} + stockfish@18.0.7: {} + store2@2.14.4: {} string-width@4.2.3: @@ -32162,8 +32309,7 @@ snapshots: undici-types@7.16.0: {} - undici-types@7.19.2: - optional: true + undici-types@7.19.2: {} undici@6.24.1: {} @@ -32273,6 +32419,11 @@ snapshots: '@unocss/preset-mini': 66.6.8 unocss: 66.6.8(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + unocss-preset-scrollbar@4.0.0(unocss@66.6.8(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))): + dependencies: + '@unocss/preset-mini': 66.6.8 + unocss: 66.6.8(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + unocss@66.6.8(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vite@6.4.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): dependencies: '@unocss/cli': 66.6.8 @@ -32321,6 +32472,30 @@ snapshots: - '@emnapi/runtime' - vite + unocss@66.6.8(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): + dependencies: + '@unocss/cli': 66.6.8 + '@unocss/core': 66.6.8 + '@unocss/preset-attributify': 66.6.8 + '@unocss/preset-icons': 66.6.8 + '@unocss/preset-mini': 66.6.8 + '@unocss/preset-tagify': 66.6.8 + '@unocss/preset-typography': 66.6.8 + '@unocss/preset-uno': 66.6.8 + '@unocss/preset-web-fonts': 66.6.8 + '@unocss/preset-wind': 66.6.8 + '@unocss/preset-wind3': 66.6.8 + '@unocss/preset-wind4': 66.6.8 + '@unocss/transformer-attributify-jsx': 66.6.8(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + '@unocss/transformer-compile-class': 66.6.8 + '@unocss/transformer-directives': 66.6.8 + '@unocss/transformer-variant-group': 66.6.8 + '@unocss/vite': 66.6.8(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + - vite + unpack-string@0.0.2: {} unpipe@1.0.0: {} @@ -32333,14 +32508,6 @@ snapshots: unplugin: 2.3.11 vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) - unplugin-combine@2.3.0(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(unplugin@2.3.11)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): - optionalDependencies: - esbuild: 0.27.2 - rolldown: 1.0.0-rc.16 - rollup: 4.60.1 - unplugin: 2.3.11 - vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) - unplugin-combine@2.3.0(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(unplugin@2.3.11)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): optionalDependencies: esbuild: 0.27.2 @@ -32375,19 +32542,6 @@ snapshots: transitivePeerDependencies: - supports-color - unplugin-info@1.3.2(esbuild@0.27.2)(rollup@4.60.1)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): - dependencies: - ci-info: 4.4.0 - git-url-parse: 16.1.0 - simple-git: 3.36.0 - unplugin: 2.3.11 - optionalDependencies: - esbuild: 0.27.2 - rollup: 4.60.1 - vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) - transitivePeerDependencies: - - supports-color - unplugin-info@1.3.2(esbuild@0.27.2)(rollup@4.60.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): dependencies: ci-info: 4.4.0 @@ -32497,17 +32651,6 @@ snapshots: rollup: 4.60.1 vite: 6.4.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) - unplugin-yaml@4.1.0(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): - dependencies: - '@rollup/pluginutils': 5.3.0(rollup@4.60.1) - unplugin: 3.0.0 - yaml: 2.8.3 - optionalDependencies: - esbuild: 0.27.2 - rolldown: 1.0.0-rc.16 - rollup: 4.60.1 - vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) - unplugin@2.3.11: dependencies: '@jridgewell/remapping': 2.3.5 @@ -32670,6 +32813,37 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 + vieval@0.0.1(@types/node@25.6.0)(chokidar@5.0.0)(dotenv@17.4.2)(esbuild@0.27.2)(giget@2.0.0)(jiti@2.6.1)(less@4.6.4)(magicast@0.5.2)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3): + dependencies: + '@moeru/std': 0.1.0-beta.17 + '@pnpm/find-workspace-dir': 1000.1.5 + '@xsai-ext/providers': 0.5.0-beta.2 + '@xsai/generate-text': 0.5.0-beta.2(patch_hash=306bfb723913596b334140f0d6fa48063f336e3b44024efc1d72bf60d54b15e6) + c12: 4.0.0-beta.4(chokidar@5.0.0)(dotenv@17.4.2)(giget@2.0.0)(jiti@2.6.1)(magicast@0.5.2) + es-toolkit: 1.43.0 + fast-string-width: 3.0.2 + meow: 14.1.0 + tinyglobby: 0.2.16 + tinyrainbow: 3.1.0 + vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) + transitivePeerDependencies: + - '@types/node' + - '@vitejs/devtools' + - chokidar + - dotenv + - esbuild + - giget + - jiti + - less + - magicast + - sass + - sass-embedded + - stylus + - sugarss + - terser + - tsx + - yaml + vite-bundle-visualizer@1.2.1(rolldown@1.0.0-rc.16)(rollup@2.80.0): dependencies: cac: 6.7.14 @@ -32692,22 +32866,12 @@ snapshots: - rollup - supports-color - vite-dev-rpc@1.1.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): - dependencies: - birpc: 2.9.0 - vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) - vite-hot-client: 2.1.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) - vite-dev-rpc@1.1.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): dependencies: birpc: 2.9.0 vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) vite-hot-client: 2.1.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) - vite-hot-client@2.1.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): - dependencies: - vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) - vite-hot-client@2.1.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): dependencies: vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) @@ -32753,21 +32917,6 @@ snapshots: - tsx - yaml - vite-plugin-inspect@11.3.3(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): - dependencies: - ansis: 4.2.0 - debug: 4.4.3(supports-color@10.2.2) - error-stack-parser-es: 1.0.5 - ohash: 2.0.11 - open: 10.2.0 - perfect-debounce: 2.1.0 - sirv: 3.0.2 - unplugin-utils: 0.3.1 - vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) - vite-dev-rpc: 1.1.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) - transitivePeerDependencies: - - supports-color - vite-plugin-inspect@11.3.3(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): dependencies: ansis: 4.2.0 @@ -32799,13 +32948,6 @@ snapshots: - typescript - ws - vite-plugin-mkcert@2.0.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): - dependencies: - debug: 4.4.3(supports-color@10.2.2) - supports-color: 10.2.2 - undici: 8.1.0 - vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) - vite-plugin-mkcert@2.0.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): dependencies: debug: 4.4.3(supports-color@10.2.2) @@ -32824,20 +32966,6 @@ snapshots: transitivePeerDependencies: - supports-color - vite-plugin-vue-devtools@8.1.1(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3)): - dependencies: - '@vue/devtools-core': 8.1.1(vue@3.5.32(typescript@5.9.3)) - '@vue/devtools-kit': 8.1.1 - '@vue/devtools-shared': 8.1.1 - sirv: 3.0.2 - vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) - vite-plugin-inspect: 11.3.3(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) - vite-plugin-vue-inspector: 5.3.2(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) - transitivePeerDependencies: - - '@nuxt/kit' - - supports-color - - vue - vite-plugin-vue-devtools@8.1.1(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3)): dependencies: '@vue/devtools-core': 8.1.1(vue@3.5.32(typescript@5.9.3)) @@ -32852,21 +32980,6 @@ snapshots: - supports-color - vue - vite-plugin-vue-inspector@5.3.2(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): - dependencies: - '@babel/core': 7.29.0 - '@babel/plugin-proposal-decorators': 7.28.0(@babel/core@7.29.0) - '@babel/plugin-syntax-import-attributes': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.29.0) - '@babel/plugin-transform-typescript': 7.28.5(@babel/core@7.29.0) - '@vue/babel-plugin-jsx': 1.5.0(@babel/core@7.29.0) - '@vue/compiler-dom': 3.5.32 - kolorist: 1.8.0 - magic-string: 0.30.21 - vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) - transitivePeerDependencies: - - supports-color - vite-plugin-vue-inspector@5.3.2(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): dependencies: '@babel/core': 7.29.0 @@ -32882,16 +32995,6 @@ snapshots: transitivePeerDependencies: - supports-color - vite-plugin-vue-layouts@0.11.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-router@5.0.4(@vue/compiler-sfc@3.5.32)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)): - dependencies: - debug: 4.4.3(supports-color@10.2.2) - fast-glob: 3.3.3 - vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) - vue: 3.5.32(typescript@5.9.3) - vue-router: 5.0.4(@vue/compiler-sfc@3.5.32)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)) - transitivePeerDependencies: - - supports-color - vite-plugin-vue-layouts@0.11.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-router@5.0.4(@vue/compiler-sfc@3.5.32)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)): dependencies: debug: 4.4.3(supports-color@10.2.2) @@ -33150,54 +33253,6 @@ snapshots: - vue-tsc - webpack - vue-macros@3.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@vueuse/core@14.2.1(vue@3.5.32(typescript@5.9.3)))(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-tsc@3.2.6(typescript@5.9.3))(vue@3.5.32(typescript@5.9.3)): - dependencies: - '@vue-macros/better-define': 3.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vue@3.5.32(typescript@5.9.3)) - '@vue-macros/boolean-prop': 3.1.2(vue@3.5.32(typescript@5.9.3)) - '@vue-macros/chain-call': 3.1.2(vue@3.5.32(typescript@5.9.3)) - '@vue-macros/common': 3.1.2(vue@3.5.32(typescript@5.9.3)) - '@vue-macros/config': 3.1.2(vue@3.5.32(typescript@5.9.3)) - '@vue-macros/define-emit': 3.1.2(vue@3.5.32(typescript@5.9.3)) - '@vue-macros/define-models': 3.1.2(@vueuse/core@14.2.1(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)) - '@vue-macros/define-prop': 3.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vue@3.5.32(typescript@5.9.3)) - '@vue-macros/define-props': 3.1.2(@vue-macros/reactivity-transform@3.1.2(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)) - '@vue-macros/define-props-refs': 3.1.2(vue@3.5.32(typescript@5.9.3)) - '@vue-macros/define-render': 3.1.2(vue@3.5.32(typescript@5.9.3)) - '@vue-macros/define-slots': 3.1.2(vue@3.5.32(typescript@5.9.3)) - '@vue-macros/define-stylex': 3.1.2(vue@3.5.32(typescript@5.9.3)) - '@vue-macros/devtools': 3.1.2(typescript@5.9.3)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) - '@vue-macros/export-expose': 3.1.2(vue@3.5.32(typescript@5.9.3)) - '@vue-macros/export-props': 3.1.2(vue@3.5.32(typescript@5.9.3)) - '@vue-macros/export-render': 3.1.2(vue@3.5.32(typescript@5.9.3)) - '@vue-macros/hoist-static': 3.1.2(vue@3.5.32(typescript@5.9.3)) - '@vue-macros/jsx-directive': 3.1.2(typescript@5.9.3) - '@vue-macros/named-template': 3.1.2(vue@3.5.32(typescript@5.9.3)) - '@vue-macros/reactivity-transform': 3.1.2(vue@3.5.32(typescript@5.9.3)) - '@vue-macros/script-lang': 3.1.2(vue@3.5.32(typescript@5.9.3)) - '@vue-macros/setup-block': 3.1.2(vue@3.5.32(typescript@5.9.3)) - '@vue-macros/setup-component': 3.1.2(vue@3.5.32(typescript@5.9.3)) - '@vue-macros/setup-sfc': 3.1.2(vue@3.5.32(typescript@5.9.3)) - '@vue-macros/short-bind': 3.1.2(vue@3.5.32(typescript@5.9.3)) - '@vue-macros/short-emits': 3.1.2(vue@3.5.32(typescript@5.9.3)) - '@vue-macros/short-vmodel': 3.1.2(vue@3.5.32(typescript@5.9.3)) - '@vue-macros/volar': 3.1.2(typescript@5.9.3)(vue-tsc@3.2.6(typescript@5.9.3))(vue@3.5.32(typescript@5.9.3)) - unplugin: 2.3.11 - unplugin-combine: 2.3.0(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(unplugin@2.3.11)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) - unplugin-vue-define-options: 3.1.2(vue@3.5.32(typescript@5.9.3)) - vue: 3.5.32(typescript@5.9.3) - transitivePeerDependencies: - - '@emnapi/core' - - '@emnapi/runtime' - - '@rspack/core' - - '@vueuse/core' - - esbuild - - rolldown - - rollup - - typescript - - vite - - vue-tsc - - webpack - vue-macros@3.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@vueuse/core@14.2.1(vue@3.5.32(typescript@5.9.3)))(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-tsc@3.2.6(typescript@5.9.3))(vue@3.5.32(typescript@5.9.3)): dependencies: '@vue-macros/better-define': 3.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vue@3.5.32(typescript@5.9.3)) @@ -33377,6 +33432,8 @@ snapshots: whatwg-mimetype@4.0.0: {} + whatwg-mimetype@5.0.0: {} + whatwg-url@15.1.0: dependencies: tr46: 6.0.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 244a440a9..44b52932a 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -28,6 +28,7 @@ patchedDependencies: mineflayer@4.33.0: patches/mineflayer@4.33.0.patch pixi-live2d-display: patches/pixi-live2d-display.patch catalog: + '@ax-llm/ax': ^19.0.43 '@better-auth/oauth-provider': 1.5.6 '@capacitor/android': ^8.3.1 '@capacitor/barcode-scanner': ^3.0.2 @@ -67,6 +68,7 @@ catalog: '@types/hast': ^3.0.4 '@types/splitpanes': ^2.2.6 '@types/unist': ^3.0.3 + '@types/whatwg-mimetype': ^5.0.0 '@types/ws': ^8.18.1 '@vueuse/core': 14.1.0 '@xsai-ext/providers': 0.5.0-beta.2 @@ -87,6 +89,7 @@ catalog: builder-util-runtime: ^9.5.1 cac: ^7.0.0 capacitor-native-settings: ^8.1.0 + chess.js: ^1.4.0 crossws: ^0.4.5 d3: 7.9.0 date-fns: ^4.1.0 @@ -115,8 +118,10 @@ catalog: oxc-minify: ^0.126.0 pinia: ^3.0.4 posthog-js: 1.306.1 + reka-ui: ^2.9.2 splitpanes: ^4.0.4 std-env: ^4.1.0 + stockfish: ^18.0.7 superjson: ^2.2.6 tsdown: ^0.21.9 tsx: ^4.21.0 @@ -125,6 +130,7 @@ catalog: unstorage: ^1.17.5 uqr: ^0.1.3 valibot: 1.2.0 + vieval: ^0.0.1 vite: ^8.0.8 vite-plugin-inspect: 12.0.0-beta.1 vite-plugin-mkcert: ^2.0.0 @@ -132,6 +138,7 @@ catalog: vue-router: ^5.0.4 vue-sonner: 2.0.9 web-haptics: ^0.0.6 + whatwg-mimetype: ^5.0.0 ws: ^8.20.0 xsschema: 0.5.0-beta.2 yaml: ^2.8.3 @@ -173,6 +180,7 @@ onlyBuiltDependencies: - protobufjs - sharp - spawn-sync + - stockfish - utf-8-validate - vue-demi packageExtensions: