@covro/covro 0.0.0-stage → 0.1.0-dev.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +173 -2
  3. package/README.zh-CN.md +174 -0
  4. package/bin/covro.mjs +8 -0
  5. package/dist/cli/control.js +66 -0
  6. package/dist/cli/control.js.map +7 -0
  7. package/dist/cli/daemon.js +118 -0
  8. package/dist/cli/daemon.js.map +7 -0
  9. package/dist/cli/main.js +152 -0
  10. package/dist/cli/main.js.map +7 -0
  11. package/dist/cli/paths.js +60 -0
  12. package/dist/cli/paths.js.map +7 -0
  13. package/dist/cli/service.js +87 -0
  14. package/dist/cli/service.js.map +7 -0
  15. package/dist/codex/adapter.js +1195 -0
  16. package/dist/codex/adapter.js.map +7 -0
  17. package/dist/codex/auto-review.js +102 -0
  18. package/dist/codex/auto-review.js.map +7 -0
  19. package/dist/codex/compatibility.js +7 -0
  20. package/dist/codex/compatibility.js.map +7 -0
  21. package/dist/codex/mapping.js +210 -0
  22. package/dist/codex/mapping.js.map +7 -0
  23. package/dist/codex/permissions.js +51 -0
  24. package/dist/codex/permissions.js.map +7 -0
  25. package/dist/codex/thread-title-policy.js +52 -0
  26. package/dist/codex/thread-title-policy.js.map +7 -0
  27. package/dist/codex/thread-titles.js +259 -0
  28. package/dist/codex/thread-titles.js.map +7 -0
  29. package/dist/codex/transport.js +236 -0
  30. package/dist/codex/transport.js.map +7 -0
  31. package/dist/codex/turn-token-usage.js +46 -0
  32. package/dist/codex/turn-token-usage.js.map +7 -0
  33. package/dist/codex/types.js +1 -0
  34. package/dist/codex/types.js.map +7 -0
  35. package/dist/drizzle/0000_past_wildside.sql +45 -0
  36. package/dist/drizzle/0001_shocking_ender_wiggin.sql +2 -0
  37. package/dist/drizzle/0002_allow_parallel_threads.sql +1 -0
  38. package/dist/drizzle/0003_project_registrations.sql +11 -0
  39. package/dist/drizzle/0004_turn_usage.sql +6 -0
  40. package/dist/drizzle/meta/0000_snapshot.json +309 -0
  41. package/dist/drizzle/meta/0001_snapshot.json +323 -0
  42. package/dist/drizzle/meta/0002_snapshot.json +315 -0
  43. package/dist/drizzle/meta/0003_snapshot.json +381 -0
  44. package/dist/drizzle/meta/0004_snapshot.json +420 -0
  45. package/dist/drizzle/meta/_journal.json +41 -0
  46. package/dist/server/auth.js +47 -0
  47. package/dist/server/auth.js.map +7 -0
  48. package/dist/server/compaction.js +40 -0
  49. package/dist/server/compaction.js.map +7 -0
  50. package/dist/server/config-store.js +147 -0
  51. package/dist/server/config-store.js.map +7 -0
  52. package/dist/server/config.js +124 -0
  53. package/dist/server/config.js.map +7 -0
  54. package/dist/server/db/index.js +21 -0
  55. package/dist/server/db/index.js.map +7 -0
  56. package/dist/server/db/migrate.js +26 -0
  57. package/dist/server/db/migrate.js.map +7 -0
  58. package/dist/server/db/operations.js +201 -0
  59. package/dist/server/db/operations.js.map +7 -0
  60. package/dist/server/db/schema.js +99 -0
  61. package/dist/server/db/schema.js.map +7 -0
  62. package/dist/server/db/turn-usage.js +31 -0
  63. package/dist/server/db/turn-usage.js.map +7 -0
  64. package/dist/server/events.js +123 -0
  65. package/dist/server/events.js.map +7 -0
  66. package/dist/server/http.js +305 -0
  67. package/dist/server/http.js.map +7 -0
  68. package/dist/server/images.js +49 -0
  69. package/dist/server/images.js.map +7 -0
  70. package/dist/server/instance-lock.js +25 -0
  71. package/dist/server/instance-lock.js.map +7 -0
  72. package/dist/server/local-files.js +100 -0
  73. package/dist/server/local-files.js.map +7 -0
  74. package/dist/server/main.js +125 -0
  75. package/dist/server/main.js.map +7 -0
  76. package/dist/server/markdown.js +28 -0
  77. package/dist/server/markdown.js.map +7 -0
  78. package/dist/server/projection.js +348 -0
  79. package/dist/server/projection.js.map +7 -0
  80. package/dist/server/projects.js +230 -0
  81. package/dist/server/projects.js.map +7 -0
  82. package/dist/server/service.js +1221 -0
  83. package/dist/server/service.js.map +7 -0
  84. package/dist/server/snapshot.js +98 -0
  85. package/dist/server/snapshot.js.map +7 -0
  86. package/dist/server/thread-export.js +97 -0
  87. package/dist/server/thread-export.js.map +7 -0
  88. package/dist/server/websocket.js +126 -0
  89. package/dist/server/websocket.js.map +7 -0
  90. package/dist/shared/async-questions.js +39 -0
  91. package/dist/shared/async-questions.js.map +7 -0
  92. package/dist/shared/codex-compatibility.js +56 -0
  93. package/dist/shared/codex-compatibility.js.map +7 -0
  94. package/dist/shared/elicitation.js +314 -0
  95. package/dist/shared/elicitation.js.map +7 -0
  96. package/dist/shared/errors.js +24 -0
  97. package/dist/shared/errors.js.map +7 -0
  98. package/dist/shared/gateway-config.js +1 -0
  99. package/dist/shared/gateway-config.js.map +7 -0
  100. package/dist/shared/images.js +21 -0
  101. package/dist/shared/images.js.map +7 -0
  102. package/dist/shared/package.js +5 -0
  103. package/dist/shared/package.js.map +7 -0
  104. package/dist/shared/permissions.js +63 -0
  105. package/dist/shared/permissions.js.map +7 -0
  106. package/dist/shared/thread-name.js +14 -0
  107. package/dist/shared/thread-name.js.map +7 -0
  108. package/dist/shared/thread-summary.js +24 -0
  109. package/dist/shared/thread-summary.js.map +7 -0
  110. package/dist/shared/types.js +1 -0
  111. package/dist/shared/types.js.map +7 -0
  112. package/dist/shared/updates.js +7 -0
  113. package/dist/shared/updates.js.map +7 -0
  114. package/dist/shared/validation.js +116 -0
  115. package/dist/shared/validation.js.map +7 -0
  116. package/dist/shared/versions.js +36 -0
  117. package/dist/shared/versions.js.map +7 -0
  118. package/dist/update-worker.cjs +21968 -0
  119. package/dist/updates/files.js +54 -0
  120. package/dist/updates/files.js.map +7 -0
  121. package/dist/updates/manager.js +315 -0
  122. package/dist/updates/manager.js.map +7 -0
  123. package/dist/updates/registry.js +71 -0
  124. package/dist/updates/registry.js.map +7 -0
  125. package/dist/updates/transaction.js +96 -0
  126. package/dist/updates/transaction.js.map +7 -0
  127. package/dist/updates/worker.js +345 -0
  128. package/dist/updates/worker.js.map +7 -0
  129. package/dist/web/assets/index-BW9afH_Y.js +83 -0
  130. package/dist/web/assets/index-L3S1ZiO0.css +1 -0
  131. package/dist/web/covro.svg +6 -0
  132. package/dist/web/index.html +15 -0
  133. package/package.json +88 -4
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/server/http.ts"],
4
+ "sourcesContent": ["import { Hono } from 'hono';\nimport { bodyLimit } from 'hono/body-limit';\nimport { serveStatic } from '@hono/node-server/serve-static';\nimport { upgradeWebSocket, type HttpBindings } from '@hono/node-server';\nimport type { ZodType } from 'zod';\nimport { ZodError } from 'zod';\nimport { AppError } from '../shared/errors.js';\nimport { IMAGE_REQUEST_BYTES } from '../shared/images.js';\nimport * as schemas from '../shared/validation.js';\nimport type { Operation } from '../shared/types.js';\nimport type { AppConfig } from './config.js';\nimport { ConfigStore } from './config-store.js';\nimport { createAuthenticator, type Authenticator, type Identity } from './auth.js';\nimport type { GatewayService } from './service.js';\nimport { socketHandlers } from './websocket.js';\nimport { z } from 'zod';\nimport { releaseChannels, type UpdateController } from '../shared/updates.js';\nimport { settingsSchema, updateRequestSchema } from '../updates/manager.js';\nexport interface HttpDependencies {\n config: AppConfig;\n service: GatewayService;\n staticDir: string;\n authenticate?: Authenticator;\n updates?: UpdateController;\n}\nexport function createApp({\n config,\n service,\n staticDir,\n authenticate = createAuthenticator(config),\n updates,\n}: HttpDependencies) {\n const app = new Hono<{\n Bindings: HttpBindings;\n Variables: { identity: Identity; operationId?: string };\n }>();\n app.onError((error, c) => {\n const failure =\n error instanceof AppError\n ? error\n : error instanceof ZodError || error instanceof SyntaxError\n ? new AppError('VALIDATION_FAILED', 'Invalid request data', 400)\n : new AppError('INTERNAL_ERROR', 'An internal error occurred', 500);\n return c.json({ error: failure.toJSON(c.get('operationId')) }, failure.status as 400);\n });\n app.get('/api/health', (c) => c.json({ ok: true }));\n app.use('*', async (c, next) => {\n c.set('identity', await authenticate(c.req.raw, c.req.path === '/ws'));\n await next();\n });\n app.use('/api/*', (c, next) =>\n bodyLimit({\n maxSize: /^\\/api\\/threads\\/[^/]+\\/(turns|steer)$/.test(c.req.path)\n ? IMAGE_REQUEST_BYTES\n : 512 * 1024,\n onError: () => {\n throw new AppError('BODY_TOO_LARGE', 'Request body exceeds limit', 413);\n },\n })(c, next),\n );\n app.use('/api/*', async (c, next) => {\n c.header('Cache-Control', 'no-store');\n if (\n service.isInMaintenance() &&\n !['GET', 'HEAD', 'OPTIONS'].includes(c.req.method) &&\n !c.req.path.startsWith('/api/updates')\n )\n throw new AppError('UPDATE_MAINTENANCE', '\u6B63\u5728\u5347\u7EA7\uFF0C\u8BF7\u7B49\u5F85\u670D\u52A1\u91CD\u65B0\u8FDE\u63A5', 503);\n await next();\n });\n if (updates) {\n app.get('/api/updates', async (c) => c.json(await updates.status()));\n app.post('/api/updates/check', async (c) => {\n const input = z\n .object({ channel: z.enum(releaseChannels).optional() })\n .strict()\n .parse(await c.req.json());\n return c.json(await updates.check(input.channel));\n });\n app.post('/api/updates/settings', async (c) =>\n c.json(await updates.configure(settingsSchema.parse(await c.req.json()))),\n );\n app.post('/api/updates/run', async (c) =>\n c.json(await updates.start(updateRequestSchema.parse(await c.req.json())), 202),\n );\n }\n const id = (value: string) => schemas.idSchema.parse(value);\n const parse = async <T>(\n c: { req: { json: () => Promise<unknown> }; set: (key: 'operationId', value: string) => void },\n schema: ZodType<T>,\n ): Promise<T> => {\n const value: unknown = await c.req.json();\n if (\n value &&\n typeof value === 'object' &&\n 'clientOperationId' in value &&\n typeof value.clientOperationId === 'string'\n )\n c.set('operationId', value.clientOperationId);\n return schema.parse(value);\n };\n const status = (op: Operation) =>\n op.status === 'dispatching' || op.status === 'accepted' ? (202 as const) : (200 as const);\n const configuration = new ConfigStore(config);\n app.use('/api/config*', async (c, next) => {\n c.header('Cache-Control', 'no-store');\n await next();\n });\n app.get('/api/config', async (c) => c.json(await configuration.snapshot()));\n app.post('/api/config', async (c) => c.json(await configuration.save(await c.req.json())));\n app.post('/api/config/import', async (c) =>\n c.json(await configuration.previewImport(await c.req.json())),\n );\n app.get('/api/config/export', async (c) => {\n c.header('Content-Disposition', 'attachment; filename=\"codex-web-config.json\"');\n return c.json(await configuration.export());\n });\n app.get('/api/system', (c) => c.json(service.getSystem()));\n app.post('/api/system/compatibility/confirm', async (c) => {\n const op = await service.confirmCompatibility(\n await parse(c, schemas.confirmCompatibilitySchema),\n );\n return c.json(op, status(op));\n });\n app.get('/api/projects', async (c) => c.json({ data: await service.listProjects() }));\n app.get('/api/projects/:projectId/runtime-settings', async (c) =>\n c.json(await service.readRuntimeSettings(id(c.req.param('projectId')))),\n );\n app.post('/api/projects/:projectId/model-defaults', async (c) => {\n const op = await service.saveModelDefaults(\n id(c.req.param('projectId')),\n await parse(c, schemas.saveModelDefaultsSchema),\n );\n return c.json(op, status(op));\n });\n app.post('/api/projects', async (c) =>\n c.json(service.addProject(await parse(c, schemas.createProjectSchema))),\n );\n app.post('/api/projects/:projectId/rename', async (c) =>\n c.json(\n service.renameProject(\n id(c.req.param('projectId')),\n await parse(c, schemas.renameProjectSchema),\n ),\n ),\n );\n app.post('/api/projects/:projectId/remove', async (c) => {\n await parse(c, schemas.removeProjectSchema);\n return c.json(service.removeProject(id(c.req.param('projectId'))));\n });\n app.get('/api/projects/:projectId/threads', async (c) =>\n c.json(\n await service.listThreadSummaries(\n id(c.req.param('projectId')),\n c.req.query('cursor') === undefined\n ? undefined\n : schemas.cursorSchema.parse(c.req.query('cursor')),\n schemas.archivedQuerySchema.parse(c.req.query('archived')),\n ),\n ),\n );\n app.get('/api/threads', async (c) =>\n c.json(\n await service.listThreads(\n id(c.req.query('projectId') ?? ''),\n c.req.query('cursor') === undefined\n ? undefined\n : schemas.cursorSchema.parse(c.req.query('cursor')),\n schemas.archivedQuerySchema.parse(c.req.query('archived')),\n ),\n ),\n );\n app.post('/api/threads/:threadId/archive', async (c) => {\n return c.json(\n await service.setThreadArchived(\n id(c.req.param('threadId')),\n await parse(c, schemas.archiveThreadSchema),\n ),\n );\n });\n app.get('/api/threads/:threadId', async (c) =>\n c.json(await service.getThread(id(c.req.param('threadId')))),\n );\n app.get('/api/threads/:threadId/metadata', async (c) =>\n c.json(await service.getThreadMetadata(id(c.req.param('threadId')))),\n );\n app.get('/api/threads/:threadId/export', async (c) => {\n const exported = await service.exportThread(id(c.req.param('threadId')));\n c.header('Content-Type', 'text/markdown; charset=utf-8');\n c.header('Content-Disposition', `attachment; filename=\"${exported.filename}\"`);\n c.header('Cache-Control', 'no-store');\n c.header('X-Content-Type-Options', 'nosniff');\n c.header('X-Export-Completeness', exported.partial ? 'partial' : 'saved-history');\n return c.body(exported.markdown);\n });\n app.get('/api/threads/:threadId/images', async (c) => {\n c.header('Cache-Control', 'private, no-store');\n c.header('X-Content-Type-Options', 'nosniff');\n c.header('Cross-Origin-Resource-Policy', 'same-origin');\n c.header('Content-Security-Policy', \"default-src 'none'; sandbox\");\n const image = await service.getThreadImage(id(c.req.param('threadId')), c.req.query('path'));\n return c.body(image.bytes, 200, {\n 'Content-Type': image.contentType,\n 'Content-Length': String(image.bytes.length),\n });\n });\n app.get('/api/threads/:threadId/markdown', async (c) => {\n c.header('Cache-Control', 'private, no-store');\n c.header('X-Content-Type-Options', 'nosniff');\n c.header('Cross-Origin-Resource-Policy', 'same-origin');\n return c.json(\n await service.getThreadMarkdown(id(c.req.param('threadId')), c.req.query('path')),\n );\n });\n app.get('/api/operations/:operationId', (c) =>\n c.json(service.getOperation(id(c.req.param('operationId')))),\n );\n app.post('/api/projects/:projectId/threads', async (c) => {\n const op = await service.createThread(\n id(c.req.param('projectId')),\n await parse(c, schemas.createThreadSchema),\n );\n return c.json(op, status(op));\n });\n app.post('/api/threads/:threadId/resume', async (c) => {\n const op = await service.resumeThread(\n id(c.req.param('threadId')),\n await parse(c, schemas.resumeThreadSchema),\n );\n return c.json(op, status(op));\n });\n app.post('/api/threads/:threadId/fork', async (c) => {\n const op = await service.forkThread(\n id(c.req.param('threadId')),\n await parse(c, schemas.forkThreadSchema),\n );\n return c.json(op, status(op));\n });\n app.post('/api/threads/:threadId/rename', async (c) => {\n const op = await service.renameThread(\n id(c.req.param('threadId')),\n await parse(c, schemas.renameThreadSchema),\n );\n return c.json(op, status(op));\n });\n app.post('/api/threads/:threadId/turns', async (c) => {\n const op = await service.startTurn(\n id(c.req.param('threadId')),\n await parse(c, schemas.startTurnSchema),\n );\n return c.json(op, status(op));\n });\n app.post('/api/threads/:threadId/review', async (c) => {\n const op = await service.startReview(\n id(c.req.param('threadId')),\n await parse(c, schemas.startReviewSchema),\n );\n return c.json(op, status(op));\n });\n app.post('/api/threads/:threadId/steer', async (c) => {\n const op = await service.steer(\n id(c.req.param('threadId')),\n await parse(c, schemas.steerSchema),\n );\n return c.json(op, status(op));\n });\n app.post('/api/threads/:threadId/compact', async (c) => {\n const op = await service.compactThread(\n id(c.req.param('threadId')),\n await parse(c, schemas.operationSchema),\n );\n return c.json(op, status(op));\n });\n app.post('/api/threads/:threadId/interrupt', async (c) => {\n const op = await service.interrupt(\n id(c.req.param('threadId')),\n await parse(c, schemas.interruptSchema),\n );\n return c.json(op, status(op));\n });\n app.post('/api/actions/:actionId/resolve', async (c) => {\n const op = await service.resolveAction(\n id(c.req.param('actionId')),\n await parse(c, schemas.resolveActionSchema),\n );\n return c.json(op, status(op));\n });\n app.post('/api/operations/:operationId/reconcile', async (c) => {\n const op = await service.reconcileOperation(\n id(c.req.param('operationId')),\n await parse(c, schemas.reconcileSchema),\n );\n return c.json(op, status(op));\n });\n app.get(\n '/ws',\n upgradeWebSocket((c) =>\n socketHandlers(service, config, c.get('identity'), c.req.query('overview') === '1'),\n ),\n );\n app.all('/api/*', () => {\n throw new AppError('NOT_FOUND', 'API route not found', 404);\n });\n app.all('/api', () => {\n throw new AppError('NOT_FOUND', 'API route not found', 404);\n });\n app.all('/ws/*', () => {\n throw new AppError('NOT_FOUND', 'WebSocket route not found', 404);\n });\n app.use('*', async (c, next) => {\n if (!c.req.path.startsWith('/assets/')) c.header('Cache-Control', 'no-store');\n c.header('X-Content-Type-Options', 'nosniff');\n c.header('Referrer-Policy', 'same-origin');\n c.header(\n 'Content-Security-Policy',\n // Remote Markdown images load directly in the browser, including HTTPS redirects.\n // Keep scripts, connections and all other resource policies restricted as before.\n \"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https:; connect-src 'self'; frame-ancestors 'none'; base-uri 'none'\",\n );\n await next();\n });\n app.get('*', serveStatic({ root: staticDir }));\n app.get('*', async (c, next) => {\n if (!c.req.header('accept')?.includes('text/html') || /\\.[^/]+$/.test(c.req.path))\n return c.json({ error: { code: 'NOT_FOUND', message: 'Resource not found' } }, 404);\n return serveStatic({ path: `${staticDir}/index.html` })(c, next);\n });\n app.notFound((c) => c.json({ error: { code: 'NOT_FOUND', message: 'Resource not found' } }, 404));\n return app;\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,49 @@
1
+ import { extname } from "node:path";
2
+ import { AppError } from "../shared/errors.js";
3
+ import { readLocalFile } from "./local-files.js";
4
+ import { temporaryFileRoots } from "./local-files.js";
5
+ const MAX_LOCAL_IMAGE_BYTES = 20 * 1024 * 1024;
6
+ const imageTypes = {
7
+ ".png": "image/png",
8
+ ".jpg": "image/jpeg",
9
+ ".jpeg": "image/jpeg",
10
+ ".webp": "image/webp",
11
+ ".gif": "image/gif"
12
+ };
13
+ function matchesSignature(data, contentType) {
14
+ switch (contentType) {
15
+ case "image/png":
16
+ return data.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]));
17
+ case "image/jpeg":
18
+ return data.length >= 3 && data[0] === 255 && data[1] === 216 && data[2] === 255;
19
+ case "image/gif":
20
+ return ["GIF87a", "GIF89a"].includes(data.subarray(0, 6).toString("latin1"));
21
+ case "image/webp":
22
+ return data.length >= 12 && data.subarray(0, 4).toString("latin1") === "RIFF" && data.subarray(8, 12).toString("latin1") === "WEBP";
23
+ default:
24
+ return false;
25
+ }
26
+ }
27
+ async function readLocalImage(path, scope) {
28
+ let contentType = "";
29
+ const { bytes } = await readLocalFile(path, scope, {
30
+ code: "IMAGE",
31
+ label: "\u56FE\u7247",
32
+ maxBytes: MAX_LOCAL_IMAGE_BYTES,
33
+ sizeError: "\u56FE\u7247\u8D85\u8FC7 20 MiB \u9884\u89C8\u9650\u5236",
34
+ validatePath(candidate) {
35
+ contentType = imageTypes[extname(candidate).toLowerCase()] ?? "";
36
+ if (!contentType)
37
+ throw new AppError("UNSUPPORTED_IMAGE_TYPE", "\u4EC5\u652F\u6301 PNG\u3001JPEG\u3001WebP \u548C GIF \u56FE\u7247", 415);
38
+ }
39
+ });
40
+ if (!matchesSignature(bytes, contentType))
41
+ throw new AppError("INVALID_IMAGE_CONTENT", "\u6587\u4EF6\u5185\u5BB9\u4E0E\u56FE\u7247\u683C\u5F0F\u4E0D\u7B26", 415);
42
+ return { bytes, contentType };
43
+ }
44
+ export {
45
+ MAX_LOCAL_IMAGE_BYTES,
46
+ readLocalImage,
47
+ temporaryFileRoots as temporaryImageRoots
48
+ };
49
+ //# sourceMappingURL=images.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/server/images.ts"],
4
+ "sourcesContent": ["import { extname } from 'node:path';\nimport { AppError } from '../shared/errors.js';\nimport { readLocalFile, type FileScope } from './local-files.js';\n\nexport { temporaryFileRoots as temporaryImageRoots } from './local-files.js';\nexport type { FileRoot as ImageRoot } from './local-files.js';\n\nexport const MAX_LOCAL_IMAGE_BYTES = 20 * 1024 * 1024;\n\nconst imageTypes: Record<string, string> = {\n '.png': 'image/png',\n '.jpg': 'image/jpeg',\n '.jpeg': 'image/jpeg',\n '.webp': 'image/webp',\n '.gif': 'image/gif',\n};\n\nfunction matchesSignature(data: Buffer, contentType: string): boolean {\n switch (contentType) {\n case 'image/png':\n return data.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]));\n case 'image/jpeg':\n return data.length >= 3 && data[0] === 0xff && data[1] === 0xd8 && data[2] === 0xff;\n case 'image/gif':\n return ['GIF87a', 'GIF89a'].includes(data.subarray(0, 6).toString('latin1'));\n case 'image/webp':\n return (\n data.length >= 12 &&\n data.subarray(0, 4).toString('latin1') === 'RIFF' &&\n data.subarray(8, 12).toString('latin1') === 'WEBP'\n );\n default:\n return false;\n }\n}\n\nexport async function readLocalImage(path: string | undefined, scope: FileScope) {\n let contentType = '';\n const { bytes } = await readLocalFile(path, scope, {\n code: 'IMAGE',\n label: '\u56FE\u7247',\n maxBytes: MAX_LOCAL_IMAGE_BYTES,\n sizeError: '\u56FE\u7247\u8D85\u8FC7 20 MiB \u9884\u89C8\u9650\u5236',\n validatePath(candidate) {\n contentType = imageTypes[extname(candidate).toLowerCase()] ?? '';\n if (!contentType)\n throw new AppError('UNSUPPORTED_IMAGE_TYPE', '\u4EC5\u652F\u6301 PNG\u3001JPEG\u3001WebP \u548C GIF \u56FE\u7247', 415);\n },\n });\n if (!matchesSignature(bytes, contentType))\n throw new AppError('INVALID_IMAGE_CONTENT', '\u6587\u4EF6\u5185\u5BB9\u4E0E\u56FE\u7247\u683C\u5F0F\u4E0D\u7B26', 415);\n return { bytes, contentType };\n}\n"],
5
+ "mappings": "AAAA,SAAS,eAAe;AACxB,SAAS,gBAAgB;AACzB,SAAS,qBAAqC;AAE9C,SAA+B,0BAA2B;AAGnD,MAAM,wBAAwB,KAAK,OAAO;AAEjD,MAAM,aAAqC;AAAA,EACzC,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,SAAS;AAAA,EACT,QAAQ;AACV;AAEA,SAAS,iBAAiB,MAAc,aAA8B;AACpE,UAAQ,aAAa;AAAA,IACnB,KAAK;AACH,aAAO,KAAK,SAAS,GAAG,CAAC,EAAE,OAAO,OAAO,KAAK,CAAC,KAAK,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,EAAE,CAAC,CAAC;AAAA,IAClF,KAAK;AACH,aAAO,KAAK,UAAU,KAAK,KAAK,CAAC,MAAM,OAAQ,KAAK,CAAC,MAAM,OAAQ,KAAK,CAAC,MAAM;AAAA,IACjF,KAAK;AACH,aAAO,CAAC,UAAU,QAAQ,EAAE,SAAS,KAAK,SAAS,GAAG,CAAC,EAAE,SAAS,QAAQ,CAAC;AAAA,IAC7E,KAAK;AACH,aACE,KAAK,UAAU,MACf,KAAK,SAAS,GAAG,CAAC,EAAE,SAAS,QAAQ,MAAM,UAC3C,KAAK,SAAS,GAAG,EAAE,EAAE,SAAS,QAAQ,MAAM;AAAA,IAEhD;AACE,aAAO;AAAA,EACX;AACF;AAEA,eAAsB,eAAe,MAA0B,OAAkB;AAC/E,MAAI,cAAc;AAClB,QAAM,EAAE,MAAM,IAAI,MAAM,cAAc,MAAM,OAAO;AAAA,IACjD,MAAM;AAAA,IACN,OAAO;AAAA,IACP,UAAU;AAAA,IACV,WAAW;AAAA,IACX,aAAa,WAAW;AACtB,oBAAc,WAAW,QAAQ,SAAS,EAAE,YAAY,CAAC,KAAK;AAC9D,UAAI,CAAC;AACH,cAAM,IAAI,SAAS,0BAA0B,sEAA8B,GAAG;AAAA,IAClF;AAAA,EACF,CAAC;AACD,MAAI,CAAC,iBAAiB,OAAO,WAAW;AACtC,UAAM,IAAI,SAAS,yBAAyB,sEAAe,GAAG;AAChE,SAAO,EAAE,OAAO,YAAY;AAC9B;",
6
+ "names": []
7
+ }
@@ -0,0 +1,25 @@
1
+ import { chmod, mkdir } from "node:fs/promises";
2
+ import lockfile from "proper-lockfile";
3
+ import { AppError } from "../shared/errors.js";
4
+ async function acquireInstanceLock(dataDir) {
5
+ await mkdir(dataDir, { recursive: true, mode: 448 });
6
+ await chmod(dataDir, 448);
7
+ try {
8
+ return await lockfile.lock(dataDir, {
9
+ realpath: true,
10
+ stale: 1e4,
11
+ update: 2e3,
12
+ retries: 0
13
+ });
14
+ } catch {
15
+ throw new AppError(
16
+ "INSTANCE_LOCKED",
17
+ "Another gateway or migration owns this dataDir; retry after a stale lock expires",
18
+ 409
19
+ );
20
+ }
21
+ }
22
+ export {
23
+ acquireInstanceLock
24
+ };
25
+ //# sourceMappingURL=instance-lock.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/server/instance-lock.ts"],
4
+ "sourcesContent": ["import { chmod, mkdir } from 'node:fs/promises';\nimport lockfile from 'proper-lockfile';\nimport { AppError } from '../shared/errors.js';\n/** Atomic mkdir + heartbeat handles stale locks without signalling saved PIDs. */\nexport async function acquireInstanceLock(dataDir: string): Promise<() => Promise<void>> {\n await mkdir(dataDir, { recursive: true, mode: 0o700 });\n await chmod(dataDir, 0o700);\n try {\n return await lockfile.lock(dataDir, {\n realpath: true,\n stale: 10_000,\n update: 2_000,\n retries: 0,\n });\n } catch {\n throw new AppError(\n 'INSTANCE_LOCKED',\n 'Another gateway or migration owns this dataDir; retry after a stale lock expires',\n 409,\n );\n }\n}\n"],
5
+ "mappings": "AAAA,SAAS,OAAO,aAAa;AAC7B,OAAO,cAAc;AACrB,SAAS,gBAAgB;AAEzB,eAAsB,oBAAoB,SAA+C;AACvF,QAAM,MAAM,SAAS,EAAE,WAAW,MAAM,MAAM,IAAM,CAAC;AACrD,QAAM,MAAM,SAAS,GAAK;AAC1B,MAAI;AACF,WAAO,MAAM,SAAS,KAAK,SAAS;AAAA,MAClC,UAAU;AAAA,MACV,OAAO;AAAA,MACP,QAAQ;AAAA,MACR,SAAS;AAAA,IACX,CAAC;AAAA,EACH,QAAQ;AACN,UAAM,IAAI;AAAA,MACR;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AACF;",
6
+ "names": []
7
+ }
@@ -0,0 +1,100 @@
1
+ import { constants, realpathSync, statSync } from "node:fs";
2
+ import { open, realpath, stat } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { isAbsolute, parse, resolve } from "node:path";
5
+ import { AppError } from "../shared/errors.js";
6
+ import { containsPath } from "./projects.js";
7
+ function temporaryFileRoots() {
8
+ const roots = /* @__PURE__ */ new Map();
9
+ for (const directory of [tmpdir(), ...process.platform === "win32" ? [] : ["/tmp"]]) {
10
+ if (!isAbsolute(directory)) continue;
11
+ const alias = resolve(directory);
12
+ if (alias === parse(alias).root) continue;
13
+ try {
14
+ const canonicalRoot = realpathSync(alias);
15
+ if (canonicalRoot === parse(canonicalRoot).root || !statSync(canonicalRoot).isDirectory())
16
+ continue;
17
+ const root = roots.get(canonicalRoot) ?? { canonicalRoot, aliases: [] };
18
+ if (!root.aliases.includes(alias)) root.aliases.push(alias);
19
+ roots.set(canonicalRoot, root);
20
+ } catch {
21
+ }
22
+ }
23
+ return [...roots.values()];
24
+ }
25
+ function outsideProject(policy) {
26
+ return new AppError(
27
+ `${policy.code}_OUTSIDE_PROJECT`,
28
+ `\u53EA\u80FD\u9884\u89C8\u5F53\u524D\u4F1A\u8BDD\u6240\u5C5E\u9879\u76EE\u6216\u670D\u52A1\u5668\u4E34\u65F6\u76EE\u5F55\u5185\u7684${policy.label}`,
29
+ 403
30
+ );
31
+ }
32
+ function changedFile(policy) {
33
+ return new AppError(`${policy.code}_CHANGED`, `${policy.label}\u6587\u4EF6\u5DF2\u53D8\u5316\uFF0C\u8BF7\u91CD\u65B0\u6253\u5F00\u9884\u89C8`, 409);
34
+ }
35
+ async function readLocalFile(path, scope, policy) {
36
+ if (!path || path.length > 4096 || [...path].some(
37
+ (character) => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127
38
+ ) || /^[a-z][a-z\d+.-]*:/i.test(path) || path.startsWith("//") || path.startsWith("\\\\"))
39
+ throw new AppError(`INVALID_${policy.code}_PATH`, `${policy.label}\u8DEF\u5F84\u65E0\u6548`, 400);
40
+ const candidate = resolve(scope.cwd, path);
41
+ if (!scope.roots.some(
42
+ (root) => containsPath(root.canonicalRoot, candidate) || root.aliases.some((alias) => containsPath(alias, candidate))
43
+ ))
44
+ throw outsideProject(policy);
45
+ const isAllowed = (path2) => scope.roots.some((root) => containsPath(root.canonicalRoot, path2));
46
+ policy.validatePath(candidate);
47
+ try {
48
+ const canonical = await realpath(candidate);
49
+ if (!isAllowed(canonical)) throw outsideProject(policy);
50
+ const file = await open(
51
+ canonical,
52
+ constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK
53
+ );
54
+ try {
55
+ const before = await file.stat();
56
+ if (!before.isFile())
57
+ throw new AppError(`${policy.code}_NOT_FILE`, `${policy.label}\u8DEF\u5F84\u4E0D\u662F\u666E\u901A\u6587\u4EF6`, 415);
58
+ if (before.size > policy.maxBytes)
59
+ throw new AppError(`${policy.code}_TOO_LARGE`, policy.sizeError, 413);
60
+ const verifyPath = async () => {
61
+ const current = await realpath(candidate);
62
+ if (!isAllowed(current)) throw outsideProject(policy);
63
+ if (current !== canonical) throw changedFile(policy);
64
+ const currentStat = await stat(current);
65
+ if (currentStat.dev !== before.dev || currentStat.ino !== before.ino)
66
+ throw changedFile(policy);
67
+ };
68
+ await verifyPath();
69
+ const data = Buffer.alloc(before.size + 1);
70
+ let length = 0;
71
+ while (length < data.length) {
72
+ const { bytesRead } = await file.read(data, length, data.length - length, length);
73
+ if (!bytesRead) break;
74
+ length += bytesRead;
75
+ }
76
+ if (length > policy.maxBytes)
77
+ throw new AppError(`${policy.code}_TOO_LARGE`, policy.sizeError, 413);
78
+ const after = await file.stat();
79
+ if (length !== before.size || after.size !== before.size || after.mtimeMs !== before.mtimeMs)
80
+ throw changedFile(policy);
81
+ await verifyPath();
82
+ const bytes = data.subarray(0, length);
83
+ return { bytes, path: canonical };
84
+ } finally {
85
+ await file.close();
86
+ }
87
+ } catch (error) {
88
+ if (error instanceof AppError) throw error;
89
+ const code = error.code;
90
+ if (code === "ENOENT" || code === "ENOTDIR")
91
+ throw new AppError(`${policy.code}_NOT_FOUND`, `${policy.label}\u6587\u4EF6\u4E0D\u5B58\u5728\u6216\u5DF2\u88AB\u5220\u9664`, 404);
92
+ if (code === "ELOOP") throw outsideProject(policy);
93
+ throw new AppError(`${policy.code}_UNREADABLE`, `${policy.label}\u6587\u4EF6\u4E0D\u53EF\u8BFB\u53D6`, 403);
94
+ }
95
+ }
96
+ export {
97
+ readLocalFile,
98
+ temporaryFileRoots
99
+ };
100
+ //# sourceMappingURL=local-files.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/server/local-files.ts"],
4
+ "sourcesContent": ["import { constants, realpathSync, statSync } from 'node:fs';\nimport { open, realpath, stat } from 'node:fs/promises';\nimport { tmpdir } from 'node:os';\nimport { isAbsolute, parse, resolve } from 'node:path';\nimport { AppError } from '../shared/errors.js';\nimport { containsPath } from './projects.js';\n\nexport interface FileRoot {\n canonicalRoot: string;\n aliases: string[];\n}\n\nexport interface FileScope {\n cwd: string;\n roots: readonly FileRoot[];\n}\n\n/** Capture server-owned roots once; clients cannot nominate a temporary directory. */\nexport function temporaryFileRoots(): FileRoot[] {\n const roots = new Map<string, FileRoot>();\n for (const directory of [tmpdir(), ...(process.platform === 'win32' ? [] : ['/tmp'])]) {\n if (!isAbsolute(directory)) continue;\n const alias = resolve(directory);\n if (alias === parse(alias).root) continue;\n try {\n const canonicalRoot = realpathSync(alias);\n if (canonicalRoot === parse(canonicalRoot).root || !statSync(canonicalRoot).isDirectory())\n continue;\n const root = roots.get(canonicalRoot) ?? { canonicalRoot, aliases: [] };\n if (!root.aliases.includes(alias)) root.aliases.push(alias);\n roots.set(canonicalRoot, root);\n } catch {\n // Unavailable temp locations do not prevent previews in the remaining roots.\n }\n }\n return [...roots.values()];\n}\n\ninterface FilePolicy {\n code: 'IMAGE' | 'MARKDOWN';\n label: string;\n maxBytes: number;\n sizeError: string;\n validatePath: (path: string) => void;\n}\n\nfunction outsideProject(policy: FilePolicy): AppError {\n return new AppError(\n `${policy.code}_OUTSIDE_PROJECT`,\n `\u53EA\u80FD\u9884\u89C8\u5F53\u524D\u4F1A\u8BDD\u6240\u5C5E\u9879\u76EE\u6216\u670D\u52A1\u5668\u4E34\u65F6\u76EE\u5F55\u5185\u7684${policy.label}`,\n 403,\n );\n}\n\nfunction changedFile(policy: FilePolicy): AppError {\n return new AppError(`${policy.code}_CHANGED`, `${policy.label}\u6587\u4EF6\u5DF2\u53D8\u5316\uFF0C\u8BF7\u91CD\u65B0\u6253\u5F00\u9884\u89C8`, 409);\n}\n\n/** Shared project/temp authorization and bounded, verified file reads for previews. */\nexport async function readLocalFile(\n path: string | undefined,\n scope: FileScope,\n policy: FilePolicy,\n) {\n if (\n !path ||\n path.length > 4096 ||\n [...path].some(\n (character) => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127,\n ) ||\n /^[a-z][a-z\\d+.-]*:/i.test(path) ||\n path.startsWith('//') ||\n path.startsWith('\\\\\\\\')\n )\n throw new AppError(`INVALID_${policy.code}_PATH`, `${policy.label}\u8DEF\u5F84\u65E0\u6548`, 400);\n\n const candidate = resolve(scope.cwd, path);\n // Both spellings matter on systems where /tmp or the registered directory is an alias.\n if (\n !scope.roots.some(\n (root) =>\n containsPath(root.canonicalRoot, candidate) ||\n root.aliases.some((alias) => containsPath(alias, candidate)),\n )\n )\n throw outsideProject(policy);\n const isAllowed = (path: string) =>\n scope.roots.some((root) => containsPath(root.canonicalRoot, path));\n policy.validatePath(candidate);\n\n try {\n const canonical = await realpath(candidate);\n if (!isAllowed(canonical)) throw outsideProject(policy);\n // NOFOLLOW rejects a last-component symlink swapped in after realpath. NONBLOCK\n // ensures opening a substituted FIFO cannot leave the request waiting forever.\n const file = await open(\n canonical,\n constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK,\n );\n try {\n const before = await file.stat();\n if (!before.isFile())\n throw new AppError(`${policy.code}_NOT_FILE`, `${policy.label}\u8DEF\u5F84\u4E0D\u662F\u666E\u901A\u6587\u4EF6`, 415);\n if (before.size > policy.maxBytes)\n throw new AppError(`${policy.code}_TOO_LARGE`, policy.sizeError, 413);\n\n // Recheck the path and opened inode to detect ordinary concurrent replacements.\n // Node's portable file API cannot make parent-directory traversal race-free.\n const verifyPath = async () => {\n const current = await realpath(candidate);\n if (!isAllowed(current)) throw outsideProject(policy);\n if (current !== canonical) throw changedFile(policy);\n const currentStat = await stat(current);\n if (currentStat.dev !== before.dev || currentStat.ino !== before.ino)\n throw changedFile(policy);\n };\n await verifyPath();\n // One extra byte detects growth without letting readFile allocate unbounded data.\n const data = Buffer.alloc(before.size + 1);\n let length = 0;\n while (length < data.length) {\n const { bytesRead } = await file.read(data, length, data.length - length, length);\n if (!bytesRead) break;\n length += bytesRead;\n }\n if (length > policy.maxBytes)\n throw new AppError(`${policy.code}_TOO_LARGE`, policy.sizeError, 413);\n const after = await file.stat();\n if (length !== before.size || after.size !== before.size || after.mtimeMs !== before.mtimeMs)\n throw changedFile(policy);\n await verifyPath();\n const bytes = data.subarray(0, length);\n return { bytes, path: canonical };\n } finally {\n await file.close();\n }\n } catch (error) {\n if (error instanceof AppError) throw error;\n const code = (error as NodeJS.ErrnoException).code;\n if (code === 'ENOENT' || code === 'ENOTDIR')\n throw new AppError(`${policy.code}_NOT_FOUND`, `${policy.label}\u6587\u4EF6\u4E0D\u5B58\u5728\u6216\u5DF2\u88AB\u5220\u9664`, 404);\n if (code === 'ELOOP') throw outsideProject(policy);\n throw new AppError(`${policy.code}_UNREADABLE`, `${policy.label}\u6587\u4EF6\u4E0D\u53EF\u8BFB\u53D6`, 403);\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["path"]
7
+ }
@@ -0,0 +1,125 @@
1
+ import { fileURLToPath } from "node:url";
2
+ import { join } from "node:path";
3
+ import { existsSync, realpathSync } from "node:fs";
4
+ import { serve } from "@hono/node-server";
5
+ import { WebSocketServer } from "ws";
6
+ import { CodexAdapter } from "../codex/adapter.js";
7
+ import { loadConfig } from "./config.js";
8
+ import { acquireInstanceLock } from "./instance-lock.js";
9
+ import { openDatabase } from "./db/index.js";
10
+ import { migrateDatabase } from "./db/migrate.js";
11
+ import { OperationStore } from "./db/operations.js";
12
+ import { ProjectRegistry } from "./projects.js";
13
+ import { GatewayService } from "./service.js";
14
+ import { createApp } from "./http.js";
15
+ import { startHeartbeat } from "./websocket.js";
16
+ import { UpdateManager } from "../updates/manager.js";
17
+ async function startGateway(config, overrides = {}) {
18
+ const release = await acquireInstanceLock(config.dataDir);
19
+ let closeDatabase;
20
+ let codex;
21
+ const updates = overrides.updates ?? new UpdateManager(void 0, void 0, () => codex?.diagnostics().codexVersion ?? null);
22
+ try {
23
+ const db = openDatabase(join(config.dataDir, "app.sqlite"));
24
+ closeDatabase = db.close;
25
+ const packagedMigrations = fileURLToPath(new URL("../drizzle", import.meta.url));
26
+ migrateDatabase(
27
+ db.database,
28
+ overrides.migrationsDir ?? (existsSync(packagedMigrations) ? packagedMigrations : void 0)
29
+ );
30
+ codex = overrides.codex ?? new CodexAdapter(config.codex);
31
+ const service = new GatewayService({
32
+ store: new OperationStore(db.database),
33
+ projects: new ProjectRegistry(config.projects, db.database),
34
+ codex,
35
+ limits: config.limits,
36
+ authMode: config.auth.mode,
37
+ demo: config.demo
38
+ });
39
+ service.setMaintenance(overrides.maintenance ?? false);
40
+ try {
41
+ await codex.start();
42
+ } catch {
43
+ }
44
+ const wss = new WebSocketServer({ noServer: true, maxPayload: 8192 });
45
+ const app = createApp({
46
+ config,
47
+ service,
48
+ staticDir: overrides.staticDir ?? fileURLToPath(new URL("../web", import.meta.url)),
49
+ updates
50
+ });
51
+ const server = serve({
52
+ fetch: app.fetch,
53
+ hostname: config.host,
54
+ port: config.port,
55
+ websocket: { server: wss }
56
+ });
57
+ await new Promise((resolveListening, reject) => {
58
+ server.once("listening", resolveListening);
59
+ server.once("error", reject);
60
+ });
61
+ const stopHeartbeat = startHeartbeat(wss);
62
+ let closing;
63
+ const close = () => closing ??= (async () => {
64
+ stopHeartbeat();
65
+ updates.close();
66
+ const closed = new Promise((resolveClosed) => server.close(() => resolveClosed()));
67
+ for (const ws of wss.clients) {
68
+ ws.close(1001, "Gateway stopping");
69
+ ws.terminate();
70
+ }
71
+ wss.close();
72
+ try {
73
+ await service.shutdown();
74
+ } finally {
75
+ if ("closeAllConnections" in server) server.closeAllConnections();
76
+ await closed;
77
+ try {
78
+ db.close();
79
+ } finally {
80
+ await release();
81
+ }
82
+ }
83
+ })();
84
+ return { app, server, service, codex, close, updates };
85
+ } catch (error) {
86
+ updates.close();
87
+ try {
88
+ await codex?.stop();
89
+ } finally {
90
+ try {
91
+ closeDatabase?.();
92
+ } finally {
93
+ await release();
94
+ }
95
+ }
96
+ throw error;
97
+ }
98
+ }
99
+ if (process.argv[1] && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)) {
100
+ try {
101
+ const config = await loadConfig();
102
+ const runtime = await startGateway(config);
103
+ console.log(`Codex Web UI listening on ${config.host}:${config.port}`);
104
+ console.log(`Allowed origins: ${config.publicOrigin.join(", ")}`);
105
+ const stop = () => {
106
+ void runtime.close().then(
107
+ () => {
108
+ process.exitCode = 0;
109
+ },
110
+ () => {
111
+ process.exitCode = 1;
112
+ }
113
+ );
114
+ };
115
+ process.once("SIGINT", stop);
116
+ process.once("SIGTERM", stop);
117
+ } catch (error) {
118
+ console.error(error instanceof Error ? error.message : "Startup failed");
119
+ process.exitCode = 1;
120
+ }
121
+ }
122
+ export {
123
+ startGateway
124
+ };
125
+ //# sourceMappingURL=main.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/server/main.ts"],
4
+ "sourcesContent": ["import { fileURLToPath } from 'node:url';\nimport { join } from 'node:path';\nimport { existsSync, realpathSync } from 'node:fs';\nimport { serve } from '@hono/node-server';\nimport { WebSocketServer } from 'ws';\nimport { CodexAdapter } from '../codex/adapter.js';\nimport type { CodexClient } from '../codex/types.js';\nimport { loadConfig, type AppConfig } from './config.js';\nimport { acquireInstanceLock } from './instance-lock.js';\nimport { openDatabase } from './db/index.js';\nimport { migrateDatabase } from './db/migrate.js';\nimport { OperationStore } from './db/operations.js';\nimport { ProjectRegistry } from './projects.js';\nimport { GatewayService } from './service.js';\nimport { createApp } from './http.js';\nimport { startHeartbeat } from './websocket.js';\nimport { UpdateManager } from '../updates/manager.js';\nimport type { UpdateController } from '../shared/updates.js';\nexport async function startGateway(\n config: AppConfig,\n overrides: {\n codex?: CodexClient;\n staticDir?: string;\n migrationsDir?: string;\n updates?: UpdateController;\n maintenance?: boolean;\n } = {},\n) {\n const release = await acquireInstanceLock(config.dataDir);\n let closeDatabase: (() => void) | undefined;\n let codex: CodexClient | undefined;\n const updates =\n overrides.updates ??\n new UpdateManager(undefined, undefined, () => codex?.diagnostics().codexVersion ?? null);\n try {\n const db = openDatabase(join(config.dataDir, 'app.sqlite'));\n closeDatabase = db.close;\n const packagedMigrations = fileURLToPath(new URL('../drizzle', import.meta.url));\n migrateDatabase(\n db.database,\n overrides.migrationsDir ?? (existsSync(packagedMigrations) ? packagedMigrations : undefined),\n );\n codex = overrides.codex ?? new CodexAdapter(config.codex);\n const service = new GatewayService({\n store: new OperationStore(db.database),\n projects: new ProjectRegistry(config.projects, db.database),\n codex,\n limits: config.limits,\n authMode: config.auth.mode,\n demo: config.demo,\n });\n service.setMaintenance(overrides.maintenance ?? false);\n try {\n await codex.start();\n } catch {\n /* Diagnostics remain available; never substitute a fixture. */\n }\n const wss = new WebSocketServer({ noServer: true, maxPayload: 8192 });\n const app = createApp({\n config,\n service,\n staticDir: overrides.staticDir ?? fileURLToPath(new URL('../web', import.meta.url)),\n updates,\n });\n const server = serve({\n fetch: app.fetch,\n hostname: config.host,\n port: config.port,\n websocket: { server: wss },\n });\n await new Promise<void>((resolveListening, reject) => {\n server.once('listening', resolveListening);\n server.once('error', reject);\n });\n const stopHeartbeat = startHeartbeat(wss);\n let closing: Promise<void> | undefined;\n const close = (): Promise<void> =>\n (closing ??= (async () => {\n stopHeartbeat();\n updates.close();\n const closed = new Promise<void>((resolveClosed) => server.close(() => resolveClosed()));\n for (const ws of wss.clients) {\n ws.close(1001, 'Gateway stopping');\n ws.terminate();\n }\n wss.close();\n try {\n await service.shutdown();\n } finally {\n if ('closeAllConnections' in server) server.closeAllConnections();\n await closed;\n try {\n db.close();\n } finally {\n await release();\n }\n }\n })());\n return { app, server, service, codex, close, updates };\n } catch (error) {\n updates.close();\n try {\n await codex?.stop();\n } finally {\n try {\n closeDatabase?.();\n } finally {\n await release();\n }\n }\n throw error;\n }\n}\nif (process.argv[1] && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)) {\n try {\n const config = await loadConfig();\n const runtime = await startGateway(config);\n console.log(`Codex Web UI listening on ${config.host}:${config.port}`);\n console.log(`Allowed origins: ${config.publicOrigin.join(', ')}`);\n const stop = () => {\n void runtime.close().then(\n () => {\n process.exitCode = 0;\n },\n () => {\n process.exitCode = 1;\n },\n );\n };\n process.once('SIGINT', stop);\n process.once('SIGTERM', stop);\n } catch (error) {\n console.error(error instanceof Error ? error.message : 'Startup failed');\n process.exitCode = 1;\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,28 @@
1
+ import { extname } from "node:path";
2
+ import { AppError } from "../shared/errors.js";
3
+ import { readLocalFile } from "./local-files.js";
4
+ const MAX_MARKDOWN_BYTES = 2 * 1024 * 1024;
5
+ async function readLocalMarkdown(path, scope) {
6
+ const file = await readLocalFile(path, scope, {
7
+ code: "MARKDOWN",
8
+ label: "Markdown",
9
+ maxBytes: MAX_MARKDOWN_BYTES,
10
+ sizeError: "Markdown \u6587\u4EF6\u8D85\u8FC7 2 MiB \u9884\u89C8\u9650\u5236",
11
+ validatePath(candidate) {
12
+ if (!/^(\.md|\.markdown)$/i.test(extname(candidate)))
13
+ throw new AppError("UNSUPPORTED_MARKDOWN_TYPE", "\u4EC5\u652F\u6301 .md \u548C .markdown \u6587\u4EF6", 415);
14
+ }
15
+ });
16
+ try {
17
+ const content = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(file.bytes);
18
+ if (content.includes("\0")) throw new Error("Binary content");
19
+ return { path: file.path, content };
20
+ } catch {
21
+ throw new AppError("INVALID_MARKDOWN_CONTENT", "\u6587\u4EF6\u4E0D\u662F\u6709\u6548\u7684 UTF-8 Markdown \u6587\u672C", 415);
22
+ }
23
+ }
24
+ export {
25
+ MAX_MARKDOWN_BYTES,
26
+ readLocalMarkdown
27
+ };
28
+ //# sourceMappingURL=markdown.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/server/markdown.ts"],
4
+ "sourcesContent": ["import { extname } from 'node:path';\nimport { AppError } from '../shared/errors.js';\nimport type { MarkdownFile } from '../shared/types.js';\nimport { readLocalFile, type FileScope } from './local-files.js';\n\nexport const MAX_MARKDOWN_BYTES = 2 * 1024 * 1024;\n\nexport async function readLocalMarkdown(\n path: string | undefined,\n scope: FileScope,\n): Promise<MarkdownFile> {\n const file = await readLocalFile(path, scope, {\n code: 'MARKDOWN',\n label: 'Markdown',\n maxBytes: MAX_MARKDOWN_BYTES,\n sizeError: 'Markdown \u6587\u4EF6\u8D85\u8FC7 2 MiB \u9884\u89C8\u9650\u5236',\n validatePath(candidate) {\n if (!/^(\\.md|\\.markdown)$/i.test(extname(candidate)))\n throw new AppError('UNSUPPORTED_MARKDOWN_TYPE', '\u4EC5\u652F\u6301 .md \u548C .markdown \u6587\u4EF6', 415);\n },\n });\n try {\n const content = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(file.bytes);\n if (content.includes('\\0')) throw new Error('Binary content');\n return { path: file.path, content };\n } catch {\n throw new AppError('INVALID_MARKDOWN_CONTENT', '\u6587\u4EF6\u4E0D\u662F\u6709\u6548\u7684 UTF-8 Markdown \u6587\u672C', 415);\n }\n}\n"],
5
+ "mappings": "AAAA,SAAS,eAAe;AACxB,SAAS,gBAAgB;AAEzB,SAAS,qBAAqC;AAEvC,MAAM,qBAAqB,IAAI,OAAO;AAE7C,eAAsB,kBACpB,MACA,OACuB;AACvB,QAAM,OAAO,MAAM,cAAc,MAAM,OAAO;AAAA,IAC5C,MAAM;AAAA,IACN,OAAO;AAAA,IACP,UAAU;AAAA,IACV,WAAW;AAAA,IACX,aAAa,WAAW;AACtB,UAAI,CAAC,uBAAuB,KAAK,QAAQ,SAAS,CAAC;AACjD,cAAM,IAAI,SAAS,6BAA6B,wDAA0B,GAAG;AAAA,IACjF;AAAA,EACF,CAAC;AACD,MAAI;AACF,UAAM,UAAU,IAAI,YAAY,SAAS,EAAE,OAAO,MAAM,WAAW,KAAK,CAAC,EAAE,OAAO,KAAK,KAAK;AAC5F,QAAI,QAAQ,SAAS,IAAI,EAAG,OAAM,IAAI,MAAM,gBAAgB;AAC5D,WAAO,EAAE,MAAM,KAAK,MAAM,QAAQ;AAAA,EACpC,QAAQ;AACN,UAAM,IAAI,SAAS,4BAA4B,0EAA6B,GAAG;AAAA,EACjF;AACF;",
6
+ "names": []
7
+ }