Jelajahi Sumber

发布 v0.2.9 并完善独立认证验证

LSamien 2 bulan lalu
induk
melakukan
d17cba7f5a

+ 1 - 1
README.md

@@ -26,7 +26,7 @@ iOS 默认连接 `http://主机:8800` 并使用 8800 用户登录;已有 9119
 插件安装接口安装。默认源为
 `https://git.samien.cn/samien/hermes-yaoyao.git#hermes-plugins/yaoyao`。
 
-当前发布版本:**Git `v0.2.7` / 夭夭 Web `0.2.7` / Hermes Dashboard 插件 `1.7.2`**。版本组合由仓库根目录的 `release.json` 唯一声明,并由 `npm run release:verify` 校验。
+当前发布版本:**Git `v0.2.9` / 夭夭 Web `0.2.9` / Hermes Dashboard 插件 `1.7.2`**。版本组合由仓库根目录的 `release.json` 唯一声明,并由 `npm run release:verify` 校验。
 
 需要由自动化 Agent 部署或升级时,请直接使用 [Agent 安装手册](docs/agent-install.md)。其中包含固定版本校验、备份、同步、单一 Dashboard 重载与验证步骤。
 

+ 5 - 5
docs/agent-install.md

@@ -6,8 +6,8 @@
 
 | 项目 | 版本 | 校验位置 |
 | --- | --- | --- |
-| Git 发布标签 | `v0.2.7` | `release.json` 的 `gitTag` 与 `git describe --tags --exact-match HEAD` |
-| 夭夭 Web | `0.2.7` | `release.json` 与 `package.json` 的 `version` |
+| Git 发布标签 | `v0.2.9` | `release.json` 的 `gitTag` 与 `git describe --tags --exact-match HEAD` |
+| 夭夭 Web | `0.2.9` | `release.json` 与 `package.json` 的 `version` |
 | Hermes Dashboard 插件 | `1.7.2` | `hermes-plugins/yaoyao/dashboard/manifest.json` 的 `version` |
 
 以下步骤以仓库根目录为工作目录。不要使用浮动分支替代发布标签。
@@ -83,12 +83,12 @@ curl --fail --silent --show-error http://127.0.0.1:9119/api/auth/providers
 ## 1. 获取并校验指定发布版本
 
 ```bash
-RELEASE_VERSION=v0.2.7
+RELEASE_VERSION=v0.2.9
 git fetch --tags
 git checkout "$RELEASE_VERSION"
 
 test "$(git describe --tags --exact-match HEAD)" = "$RELEASE_VERSION"
-test "$(node -p \"require('./package.json').version\")" = "0.2.7"
+test "$(node -p \"require('./package.json').version\")" = "0.2.9"
 test "$(node -e \"console.log(require('./hermes-plugins/yaoyao/dashboard/manifest.json').version)\")" = "1.7.2"
 npm run release:verify
 ```
@@ -229,7 +229,7 @@ launchctl load -w ~/Library/LaunchAgents/com.samien.hermes-yaoyao.plist
 
 ## 7. 通过受管服务进行配套升级
 
-首次按第 6 步安装 Web `0.2.7` 后,可在左下角 Agent 菜单打开“系统更新”。
+首次按第 6 步安装 Web `0.2.9` 后,可在左下角 Agent 菜单打开“系统更新”。
 检查更新只读取固定的 `HERMES_YAOYAO_RELEASE_SOURCE`,并锁定最新 `vX.Y.Z`
 标签解析到的 Git 提交;执行升级前仍会检查插件持久目录是否安全。
 

+ 2 - 2
package-lock.json

@@ -1,12 +1,12 @@
 {
   "name": "hermes-yaoyao",
-  "version": "0.2.8",
+  "version": "0.2.9",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "hermes-yaoyao",
-      "version": "0.2.8",
+      "version": "0.2.9",
       "license": "BSL-1.1",
       "dependencies": {
         "@koa/bodyparser": "^5.0.0",

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "hermes-yaoyao",
-  "version": "0.2.8",
+  "version": "0.2.9",
   "private": true,
   "license": "BSL-1.1",
   "description": "夭夭 Web:直连 Hermes 9119 的聊天、团队、文件库与产物工作台",

+ 1 - 1
playwright.config.ts

@@ -21,7 +21,7 @@ export default defineConfig({
       timeout: 15_000,
     },
     {
-      command: 'cross-env NODE_ENV=production HERMES_YAOYAO_HOST=127.0.0.1 HERMES_YAOYAO_PORT=18801 HERMES_YAOYAO_UPSTREAM=http://127.0.0.1:19119 node dist-server/server/index.js',
+      command: 'node tests/fixtures/e2e-web-server.mjs',
       url: 'http://127.0.0.1:18801/healthz',
       reuseExistingServer: true,
       timeout: 15_000,

+ 3 - 3
release.json

@@ -1,7 +1,7 @@
 {
   "schemaVersion": 1,
-  "releaseVersion": "0.2.8",
-  "webVersion": "0.2.8",
+  "releaseVersion": "0.2.9",
+  "webVersion": "0.2.9",
   "pluginVersion": "1.7.2",
-  "gitTag": "v0.2.8"
+  "gitTag": "v0.2.9"
 }

+ 1 - 1
src/server/routes.ts

@@ -1654,7 +1654,7 @@ export function createApiRouter(dependencies: RouteDependencies): Router {
     }
     await withJar(ctx, async (jar) => {
       await requireGatewayAuthentication(ctx, dependencies, jar)
-      const accountKey = accountKeyFromCookieHeader(jar.header, ctx.req.socket.remoteAddress)
+      const accountKey = requestAccountKey(ctx.req)
       const files = await receiveGroupUploads(ctx.req, dependencies.uploads, accountKey)
       json(ctx, 201, { files })
     })

+ 27 - 3
tests/e2e/workspace.spec.ts

@@ -2,11 +2,35 @@ import { expect, test } from '@playwright/test'
 
 const WIDE_DOCX_BASE64 = 'UEsDBAoAAAAIAKwmFF15bjPX6AAAAK0BAAATAAAAW0NvbnRlbnRfVHlwZXNdLnhtbH1QyU7DMBD9FWuuKHHggBCK0wPLETiUDxjZk8SqN3nc0v49Tlt6QIXjzFv1+tXeO7GjzDYGBbdtB4KCjsaGScHn+rV5AMEFg0EXAyk4EMNq6NeHRCyqNrCCuZT0KCXrmTxyGxOFiowxeyz1zJNMqDc4kbzrunupYygUSlMWDxj6Zxpx64p42df3qUcmxyCeTsQlSwGm5KzGUnG5C+ZXSnNOaKvyyOHZJr6pBJBXExbk74Cz7r0Ok60h8YG5vKGvLPkVs5Em6q2vyvZ/mys94zhaTRf94pZy1MRcF/euvSAebfjpL49zD99QSwMECgAAAAAArCYUXQAAAAAAAAAAAAAAAAYAAABfcmVscy9QSwMECgAAAAgArCYUXZv9N+qtAAAAKQEAAAsAAABfcmVscy8ucmVsc43POw7CMAwG4KtE3mlaBoRQ0y4IqSsqB7ASN61oHkrCo7cnAwNFDIy2f3+W6/ZpZnanECdnBVRFCYysdGqyWsClP232wGJCq3B2lgQsFKFt6jPNmPJKHCcfWTZsFDCm5A+cRzmSwVg4TzZPBhcMplwGzT3KK2ri27Lc8fBpwNpknRIQOlUB6xdP/9huGCZJRydvhmz6ceIrkWUMmpKAhwuKq3e7yCzwpuarF5sXUEsDBAoAAAAAAKwmFF0AAAAAAAAAAAAAAAAFAAAAd29yZC9QSwMECgAAAAgArCYUXalT+9hjAQAABwMAABEAAAB3b3JkL2RvY3VtZW50LnhtbKVSW0/CMBT+K03fpXMBJYRBFAaaYDSKwdeydluTrW3awsBfb7uVDYwmJr6cc75z+c6lHU8PZQH2VGkmeASvewEElCeCMJ5F8H29uBpCoA3mBBeC0wgeqYbTybgaEZHsSsoNsARcj6oI5sbIEUI6yWmJdU9Iym0sFarExkKVoUooIpVIqNaWvyxQGAQ3qMSMQ0e5FeTotHRCOWEmG0YomD/PPoBUdM9oNUbO7aSqZZ1stoVXL8obG1C5oa6DYWB3sq6jtOOTA4bIZ6zwUexMG0rZgRIXRJdES8WIMzOrZ6JoaAd9y4p+daOLStNQJY30xMnmrOT7fKhLPD/GKl6swfrufhWDeL6MfzgF6vr8u9vr4/Lhb+1QsyNqH0LTxHjC7O3Tv0QY9uveubUHw76/n8yesHITCRnB27DOUCzLTYu2whhRtrCgaRfLKSZURbAGqRCmBdnOeNBseBoJnf4Z6v7w5AtQSwECFAAKAAAACACsJhRdeW4z1+gAAACtAQAAEwAAAAAAAAAAAAAAAAAAAAAAW0NvbnRlbnRfVHlwZXNdLnhtbFBLAQIUAAoAAAAAAKwmFF0AAAAAAAAAAAAAAAAGAAAAAAAAAAAAEAAAABkBAABfcmVscy9QSwECFAAKAAAACACsJhRdm/036q0AAAApAQAACwAAAAAAAAAAAAAAAAA9AQAAX3JlbHMvLnJlbHNQSwECFAAKAAAAAACsJhRdAAAAAAAAAAAAAAAABQAAAAAAAAAAABAAAAATAgAAd29yZC9QSwECFAAKAAAACACsJhRdqVP72GMBAAAHAwAAEQAAAAAAAAAAAAAAAAA2AgAAd29yZC9kb2N1bWVudC54bWxQSwUGAAAAAAUABQAgAQAAyAMAAAAA'
 
+const E2E_PASSWORD = 'e2e-password'
+
+test.beforeAll(async ({ request }) => {
+  const signIn = async (password: string) => {
+    const initial = await request.get('/api/app/bootstrap')
+    const initialBody = await initial.json() as { csrfToken: string }
+    return request.post('/api/app/login', {
+      headers: { Origin: 'http://127.0.0.1:18801', 'X-CSRF-Token': initialBody.csrfToken },
+      data: { username: 'admin', password },
+    })
+  }
+  let login = await signIn(E2E_PASSWORD)
+  if (!login.ok()) login = await signIn('admin')
+  expect(login.ok()).toBe(true)
+  const loginBody = await login.json() as { csrfToken: string; user: { mustChangePassword?: boolean } }
+  if (loginBody.user.mustChangePassword) {
+    const changed = await request.put('/api/app/account/credentials', {
+      headers: { Origin: 'http://127.0.0.1:18801', 'X-CSRF-Token': loginBody.csrfToken },
+      data: { currentPassword: 'admin', newPassword: E2E_PASSWORD, username: 'admin' },
+    })
+    expect(changed.ok()).toBe(true)
+  }
+})
+
 test.beforeEach(async ({ page }) => {
   await page.goto('/chat')
-  if (await page.getByRole('heading', { name: '登录 Hermes' }).isVisible()) {
-    await page.getByLabel('账号').fill('test')
-    await page.getByLabel('密码').fill('test')
+  if (await page.getByRole('heading', { name: '登录夭夭' }).isVisible()) {
+    await page.getByLabel('账号').fill('admin')
+    await page.getByLabel('密码').fill(E2E_PASSWORD)
     await page.getByRole('button', { name: '登录' }).click()
   }
   await expect(page.getByRole('navigation')).toBeVisible()

+ 16 - 0
tests/fixtures/e2e-web-server.mjs

@@ -0,0 +1,16 @@
+import { rmSync } from 'node:fs'
+
+const testHome = '/tmp/hermes-yaoyao-e2e-home'
+rmSync(testHome, { recursive: true, force: true })
+
+Object.assign(process.env, {
+  NODE_ENV: 'production',
+  HERMES_YAOYAO_HOME: testHome,
+  HERMES_YAOYAO_HOST: '127.0.0.1',
+  HERMES_YAOYAO_PORT: '18801',
+  HERMES_YAOYAO_UPSTREAM: 'http://127.0.0.1:19119',
+  HERMES_YAOYAO_UPSTREAM_USERNAME: 'test',
+  HERMES_YAOYAO_UPSTREAM_PASSWORD: 'test',
+})
+
+await import('../../dist-server/server/index.js')

+ 1 - 0
tests/fixtures/fake-hermes.mjs

@@ -116,6 +116,7 @@ const server = createServer(async (request, response) => {
   if (url.pathname === '/api/auth/me') return json(response, 200, { user_id: 'demo-user', display_name: '验收用户', email: 'demo@example.invalid', provider: 'basic' })
   if (url.pathname === '/api/auth/ws-ticket' && request.method === 'POST') return json(response, 200, { ticket: 'fake-ticket' })
   if (url.pathname === '/api/profiles') return json(response, 200, { profiles })
+  if (url.pathname === '/api/dashboard/plugins') return json(response, 200, [{ name: 'yaoyao', version: '1.7.2' }])
   if (url.pathname === '/api/plugins/yaoyao/profiles') return json(response, 200, { profiles: [
     { name: 'yaoyao', label: '夭夭', botName: '夭夭', agentName: '旧夭夭名称', isDefault: true },
     { name: 'yaoer', label: '瑶儿', botName: '瑶儿', agentName: '旧瑶儿名称', isDefault: false },

+ 28 - 25
tests/server/app.test.ts

@@ -3,8 +3,9 @@ import { tmpdir } from 'node:os'
 import { join } from 'node:path'
 import request from 'supertest'
 import { afterEach, describe, expect, it } from 'vitest'
-import { createApplication, type ApplicationRuntime } from '../../src/server/app.js'
+import { createApplication as createUnauthenticatedApplication, type ApplicationRuntime } from '../../src/server/app.js'
 import type { ServerConfig } from '../../src/server/config.js'
+import { createAuthenticatedApplication as createApplication } from './authenticatedApplication.js'
 
 interface RecordedRequest {
   path: string
@@ -355,14 +356,17 @@ describe('8800 BFF', () => {
       .set('Host', '127.0.0.1:8800')
       .expect(200)
     expect(records.map((entry) => entry.path)).toEqual([
-      '/api/status', '/api/auth/me', '/api/profiles', '/api/plugins/yaoyao/profiles',
+      '/api/status', '/api/auth/me', '/api/status',
+      '/api/status', '/api/auth/me', '/api/profiles',
+      '/api/status', '/api/auth/me', '/api/plugins/yaoyao/profiles',
     ])
     expect(response.body).toMatchObject({
       authRequired: true,
       authenticated: true,
-      user: { user_id: 'user-1' },
+      user: { username: 'test-admin', role: 'admin' },
       profiles: [{ name: 'default', agentName: '竹儿' }],
       groupUploadsEnabled: true,
+      upstreamReady: true,
     })
     expect(response.body.csrfToken).toEqual(expect.any(String))
     expect(response.body.status).toEqual({
@@ -382,10 +386,10 @@ describe('8800 BFF', () => {
       .set('Host', '127.0.0.1:8800')
       .expect(200)
     expect(records.map(entry => entry.path)).toEqual([
-      '/api/profiles', '/api/plugins/yaoyao/profiles',
+      '/api/status', '/api/auth/me', '/api/profiles', '/api/plugins/yaoyao/profiles',
     ])
     expect(response.body.profiles).toEqual([
-      { name: 'default', is_default: true, agentName: '竹儿' },
+      { name: 'default', is_default: true, agentName: '竹儿', display_name: '竹儿', description: '竹儿' },
     ])
     expect(JSON.stringify(response.body)).not.toContain('旧插件名称')
   })
@@ -447,9 +451,9 @@ describe('8800 BFF', () => {
     expect(marked.body).toEqual({ ok: true, supported: false })
   })
 
-  it('requires exact Origin and CSRF on login, then sequences the Gateway login', async () => {
+  it('requires exact Origin and CSRF on local login without forwarding credentials upstream', async () => {
     const records: RecordedRequest[] = []
-    const runtime = createApplication({ config: makeConfig(), fetchImpl: fakeGateway(records) })
+    const runtime = createUnauthenticatedApplication({ config: makeConfig(), fetchImpl: fakeGateway(records) })
     runtimes.push(runtime)
     const bootstrap = await request(runtime.app.callback())
       .get('/api/app/bootstrap')
@@ -461,7 +465,7 @@ describe('8800 BFF', () => {
       .set('Host', '127.0.0.1:8800')
       .set('Cookie', cookies)
       .set('X-CSRF-Token', bootstrap.body.csrfToken)
-      .send({ username: 'user', password: 'secret' })
+      .send({ username: 'admin', password: 'admin' })
       .expect(403)
 
     records.length = 0
@@ -471,19 +475,13 @@ describe('8800 BFF', () => {
       .set('Origin', 'http://127.0.0.1:8800')
       .set('Cookie', cookies)
       .set('X-CSRF-Token', bootstrap.body.csrfToken)
-      .send({ username: 'user', password: 'secret' })
+      .send({ username: 'admin', password: 'admin' })
       .expect(200)
-    expect(records.map((entry) => entry.path)).toEqual([
-      '/api/auth/providers',
-      '/auth/password-login',
-      '/api/status',
-      '/api/auth/me',
-      '/api/profiles',
-      '/api/plugins/yaoyao/profiles',
-    ])
-    expect(records[1]?.body).toEqual({ provider: 'basic', username: 'user', password: 'secret', next: '' })
+    expect(records).toEqual([])
     expect(response.body.authenticated).toBe(true)
-    expect(JSON.stringify(response.body)).not.toContain('secret')
+    expect(response.body.user).toMatchObject({ username: 'admin', role: 'admin', mustChangePassword: true })
+    expect(response.body.profiles).toEqual([])
+    expect(JSON.stringify(response.body)).not.toContain('passwordHash')
   })
 
   it('forces visibility and compressed-history query invariants', async () => {
@@ -608,7 +606,7 @@ describe('8800 BFF', () => {
       .get('/api/anything')
       .set('Host', '127.0.0.1:8800')
       .expect(404)
-    expect(response.body.code).toBe('not_found')
+    expect(response.body.code).toBe('node_route_not_found')
   })
 
   it('reuses a valid CSRF token across tabs instead of invalidating the first tab', async () => {
@@ -633,7 +631,7 @@ describe('8800 BFF', () => {
       if (url.pathname === '/api/auth/me') return jsonResponse({ detail: 'Unauthorized' }, { status: 401 })
       return jsonResponse({ profiles: [] })
     }) as typeof fetch
-    const runtime = createApplication({ config: makeConfig(), fetchImpl })
+    const runtime = createUnauthenticatedApplication({ config: makeConfig(), fetchImpl })
     runtimes.push(runtime)
     const bootstrap = await request(runtime.app.callback())
       .get('/api/app/bootstrap')
@@ -650,10 +648,15 @@ describe('8800 BFF', () => {
   })
 
   it('forces active file content to download instead of executing on the 8800 origin', async () => {
-    const fetchImpl = (async () => new Response('<script>window.pwned=true</script>', {
-      status: 200,
-      headers: { 'content-type': 'text/html; charset=utf-8' },
-    })) as typeof fetch
+    const fetchImpl = (async (input: string | URL | Request) => {
+      const path = new URL(input instanceof Request ? input.url : String(input)).pathname
+      if (path === '/api/status') return jsonResponse({ auth_required: true })
+      if (path === '/api/auth/me') return jsonResponse({ user_id: 'service' })
+      return new Response('<script>window.pwned=true</script>', {
+        status: 200,
+        headers: { 'content-type': 'text/html; charset=utf-8' },
+      })
+    }) as typeof fetch
     const runtime = createApplication({ config: makeConfig(), fetchImpl })
     runtimes.push(runtime)
     const response = await request(runtime.app.callback())

+ 33 - 0
tests/server/authenticatedApplication.ts

@@ -0,0 +1,33 @@
+import type Koa from 'koa'
+import {
+  createApplication,
+  type ApplicationOptions,
+  type ApplicationRuntime,
+} from '../../src/server/app.js'
+import type { ServerConfig } from '../../src/server/config.js'
+import { LocalAuthStore, type LocalUser } from '../../src/server/localAuth.js'
+
+const TEST_ADMIN: LocalUser = {
+  id: 'test-admin',
+  username: 'test-admin',
+  role: 'admin',
+  enabled: true,
+  mustChangePassword: false,
+  createdAt: 1,
+  updatedAt: 1,
+}
+
+class AuthenticatedTestStore extends LocalAuthStore {
+  override current(_ctx: Koa.Context): LocalUser { return TEST_ADMIN }
+  override require(_ctx: Koa.Context, _allowPasswordChange = false): LocalUser { return TEST_ADMIN }
+  override requireAdmin(_ctx: Koa.Context): LocalUser { return TEST_ADMIN }
+}
+
+export function createAuthenticatedApplication(
+  options: ApplicationOptions & { config: ServerConfig },
+): ApplicationRuntime {
+  return createApplication({
+    ...options,
+    auth: new AuthenticatedTestStore(options.config.home, Boolean(options.config.tlsCert)),
+  })
+}

+ 5 - 6
tests/server/pairing.test.ts

@@ -3,9 +3,10 @@ import { tmpdir } from 'node:os'
 import { join } from 'node:path'
 import request from 'supertest'
 import { afterEach, describe, expect, it } from 'vitest'
-import { createApplication, type ApplicationRuntime } from '../../src/server/app.js'
+import type { ApplicationRuntime } from '../../src/server/app.js'
 import type { ServerConfig } from '../../src/server/config.js'
 import { NodePairingStore } from '../../src/server/pairing.js'
+import { createAuthenticatedApplication as createApplication } from './authenticatedApplication.js'
 
 const homes: string[] = []
 const runtimes: ApplicationRuntime[] = []
@@ -153,7 +154,7 @@ describe('Hermes node pairing', () => {
       .set('Origin', 'http://127.0.0.1:8800')
       .set('Cookie', cookie)
       .set('X-CSRF-Token', bootstrap.body.csrfToken)
-      .send({ scopes: ['agents.read'] })
+      .send({ scopes: ['unknown.scope'] })
       .expect(400)
     const pairing = await request(runtime.app.callback())
       .post('/api/app/pairings')
@@ -163,8 +164,6 @@ describe('Hermes node pairing', () => {
       .set('X-CSRF-Token', bootstrap.body.csrfToken)
       .send({
         scopes: ['agents.read', 'history.read', 'sessions.execute', 'groups.execute'],
-        username: 'paired-user',
-        password: 'paired-password',
       })
       .expect(201)
 
@@ -182,7 +181,7 @@ describe('Hermes node pairing', () => {
       .expect(201)
     expect(claim.body.serverUrl).toBe(`http://127.0.0.1:8800/node/${claim.body.deviceId}`)
     expect(readFileSync(join(home, 'paired-devices.json'), 'utf8'))
-      .not.toContain('paired-password')
+      .not.toContain('paired-access')
 
     const profiles = await request(runtime.app.callback())
       .get(`/node/${claim.body.deviceId}/api/profiles`)
@@ -195,7 +194,7 @@ describe('Hermes node pairing', () => {
       authorization: '',
     })
     expect(upstreamRequests.at(-1)?.cookie).toContain('hermes_session_at=paired-access')
-    expect(upstreamRequests.at(-1)?.cookie).toContain('hermes_session_rt=independent-refresh')
+    expect(upstreamRequests.at(-1)?.cookie).not.toContain('hermes_session_rt=independent-refresh')
 
     const attachment = Buffer.from('remote attachment')
     await request(runtime.app.callback())

+ 2 - 1
tests/server/systemUpdateRoutes.test.ts

@@ -3,10 +3,11 @@ import { tmpdir } from 'node:os'
 import { join } from 'node:path'
 import request from 'supertest'
 import { afterEach, describe, expect, it } from 'vitest'
-import { createApplication, type ApplicationRuntime } from '../../src/server/app.js'
+import type { ApplicationRuntime } from '../../src/server/app.js'
 import type { ServerConfig } from '../../src/server/config.js'
 import { systemUpdateRequestAllowed } from '../../src/server/routes.js'
 import { SystemUpdateManager } from '../../src/server/updateManager.js'
+import { createAuthenticatedApplication as createApplication } from './authenticatedApplication.js'
 
 const runtimes: ApplicationRuntime[] = []
 const roots: string[] = []

+ 2 - 1
tests/server/websocket.test.ts

@@ -2,11 +2,12 @@ import { createServer, type Server } from 'node:http'
 import type { AddressInfo } from 'node:net'
 import WebSocket, { WebSocketServer } from 'ws'
 import { afterEach, describe, expect, it } from 'vitest'
-import { createApplication, createNodeServer, type NodeServerRuntime } from '../../src/server/app.js'
+import { createNodeServer, type NodeServerRuntime } from '../../src/server/app.js'
 import type { ServerConfig } from '../../src/server/config.js'
 import { mkdtempSync, rmSync } from 'node:fs'
 import { tmpdir } from 'node:os'
 import { join } from 'node:path'
+import { createAuthenticatedApplication as createApplication } from './authenticatedApplication.js'
 
 const closers: Array<() => Promise<void>> = []
 const homes: string[] = []