@pixelbyte-software/pixcode 1.51.2 → 1.51.4

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 (331) hide show
  1. package/CODE_OF_CONDUCT.md +41 -41
  2. package/CONTRIBUTING.md +155 -155
  3. package/LICENSE +718 -718
  4. package/README.de.md +169 -169
  5. package/README.ja.md +167 -167
  6. package/README.ko.md +167 -167
  7. package/README.md +419 -419
  8. package/README.ru.md +169 -169
  9. package/README.tr.md +298 -298
  10. package/README.zh-CN.md +167 -167
  11. package/SECURITY.md +46 -46
  12. package/dist/api-automation.html +110 -110
  13. package/dist/api-docs.html +548 -548
  14. package/dist/assets/index-B9N-gfOQ.css +32 -0
  15. package/dist/assets/{index-EN9ngyxf.js → index-HfGHXhD6.js} +175 -175
  16. package/dist/clear-cache.html +85 -85
  17. package/dist/convert-icons.md +52 -52
  18. package/dist/docs.html +308 -308
  19. package/dist/favicon.svg +8 -8
  20. package/dist/features.html +133 -133
  21. package/dist/generate-icons.js +48 -48
  22. package/dist/humans.txt +15 -15
  23. package/dist/icons/codex-white.svg +3 -3
  24. package/dist/icons/codex.svg +3 -3
  25. package/dist/icons/cursor-white.svg +11 -11
  26. package/dist/icons/icon-128x128.svg +9 -9
  27. package/dist/icons/icon-144x144.svg +9 -9
  28. package/dist/icons/icon-152x152.svg +9 -9
  29. package/dist/icons/icon-192x192.svg +9 -9
  30. package/dist/icons/icon-384x384.svg +9 -9
  31. package/dist/icons/icon-512x512.svg +9 -9
  32. package/dist/icons/icon-72x72.svg +9 -9
  33. package/dist/icons/icon-96x96.svg +9 -9
  34. package/dist/icons/icon-template.svg +9 -9
  35. package/dist/icons/qwen-logo.svg +14 -14
  36. package/dist/index.html +59 -59
  37. package/dist/landing.html +268 -268
  38. package/dist/llms-full.txt +119 -119
  39. package/dist/llms.txt +53 -53
  40. package/dist/logo.svg +12 -12
  41. package/dist/manifest.json +60 -60
  42. package/dist/openapi.yaml +1696 -1696
  43. package/dist/orchestration.html +125 -125
  44. package/dist/robots.txt +4 -4
  45. package/dist/site.css +692 -692
  46. package/dist/sitemap.xml +51 -51
  47. package/dist/sw.js +132 -132
  48. package/dist-server/server/cli.js +96 -96
  49. package/dist-server/server/daemon/manager.js +33 -33
  50. package/dist-server/server/daemon-manager.js +64 -64
  51. package/dist-server/server/database/db.js +14 -2
  52. package/dist-server/server/database/db.js.map +1 -1
  53. package/dist-server/server/index.js +191 -31
  54. package/dist-server/server/index.js.map +1 -1
  55. package/dist-server/server/middleware/auth.js +16 -5
  56. package/dist-server/server/middleware/auth.js.map +1 -1
  57. package/dist-server/server/modules/orchestration/a2a/adapters/json-event.adapter.js +84 -0
  58. package/dist-server/server/modules/orchestration/a2a/adapters/json-event.adapter.js.map +1 -0
  59. package/dist-server/server/modules/orchestration/a2a/adapters/json-event.adapter.test.js +43 -0
  60. package/dist-server/server/modules/orchestration/a2a/adapters/json-event.adapter.test.js.map +1 -0
  61. package/dist-server/server/modules/orchestration/hermes/hermes.routes.js +55 -1
  62. package/dist-server/server/modules/orchestration/hermes/hermes.routes.js.map +1 -1
  63. package/dist-server/server/modules/orchestration/index.js +1 -0
  64. package/dist-server/server/modules/orchestration/index.js.map +1 -1
  65. package/dist-server/server/routes/auth.js +12 -5
  66. package/dist-server/server/routes/auth.js.map +1 -1
  67. package/dist-server/server/routes/commands.js +25 -25
  68. package/dist-server/server/routes/git.js +29 -17
  69. package/dist-server/server/routes/git.js.map +1 -1
  70. package/dist-server/server/routes/live-view.js +46 -46
  71. package/dist-server/server/routes/platformization.js +7 -6
  72. package/dist-server/server/routes/platformization.js.map +1 -1
  73. package/dist-server/server/services/hermes-gateway.js +310 -0
  74. package/dist-server/server/services/hermes-gateway.js.map +1 -1
  75. package/dist-server/server/services/platformization.js +58 -2
  76. package/dist-server/server/services/platformization.js.map +1 -1
  77. package/dist-server/server/services/public-api-manifest.js +59 -51
  78. package/dist-server/server/services/public-api-manifest.js.map +1 -1
  79. package/package.json +222 -222
  80. package/scripts/fix-node-pty.js +67 -67
  81. package/scripts/github/create-v1.38-issues.mjs +351 -351
  82. package/scripts/github/create-vscode-workbench-issues.mjs +121 -121
  83. package/scripts/hermes/configure-pixcode-mcp.mjs +165 -163
  84. package/scripts/hermes/pixcode-mcp-server.mjs +1009 -958
  85. package/scripts/smoke/changes-panel-layout.mjs +48 -48
  86. package/scripts/smoke/chat-composer-fixed-layout.mjs +55 -55
  87. package/scripts/smoke/chat-message-timeline-order.mjs +41 -41
  88. package/scripts/smoke/chat-realtime-hydration.mjs +44 -44
  89. package/scripts/smoke/chat-session-provider-pools.mjs +35 -35
  90. package/scripts/smoke/chat-session-state.mjs +19 -19
  91. package/scripts/smoke/code-editor-theme.mjs +55 -55
  92. package/scripts/smoke/code-editor-vscode-engine.mjs +91 -91
  93. package/scripts/smoke/command-center-agent-writes.mjs +79 -79
  94. package/scripts/smoke/command-center-non-git.mjs +46 -46
  95. package/scripts/smoke/context-packet.mjs +43 -43
  96. package/scripts/smoke/control-room-ux-redesign.mjs +91 -91
  97. package/scripts/smoke/daemon-entrypoint.mjs +20 -20
  98. package/scripts/smoke/default-landing-routing.mjs +33 -33
  99. package/scripts/smoke/desktop-native-notifications.mjs +30 -30
  100. package/scripts/smoke/desktop-tray-icon.mjs +33 -33
  101. package/scripts/smoke/discord-release-workflow.mjs +24 -24
  102. package/scripts/smoke/git-install-update.mjs +255 -255
  103. package/scripts/smoke/handoff-artifact-protocol.mjs +50 -50
  104. package/scripts/smoke/hermes-api-install.mjs +56 -56
  105. package/scripts/smoke/hermes-gateway-persistence.mjs +104 -104
  106. package/scripts/smoke/hermes-mcp-pixcode-roundtrip.mjs +426 -367
  107. package/scripts/smoke/hermes-rest-chat-api.mjs +162 -162
  108. package/scripts/smoke/hermes-rest-chat-live.mjs +45 -45
  109. package/scripts/smoke/hermes-rest-codex-launch.mjs +209 -209
  110. package/scripts/smoke/hermes-rest-gateway.mjs +79 -70
  111. package/scripts/smoke/hermes-rest-live.mjs +42 -42
  112. package/scripts/smoke/hermes-roundtrip.mjs +167 -167
  113. package/scripts/smoke/hermes-settings-commands.mjs +349 -346
  114. package/scripts/smoke/hermes-smoke-launcher-guard.mjs +34 -34
  115. package/scripts/smoke/live-view-diagnostics.mjs +53 -53
  116. package/scripts/smoke/live-view-environment.mjs +92 -92
  117. package/scripts/smoke/live-view-integration.mjs +450 -450
  118. package/scripts/smoke/mac-desktop-runtime.mjs +37 -37
  119. package/scripts/smoke/mobile-tunnel-guidance.mjs +29 -29
  120. package/scripts/smoke/model-registry.mjs +36 -36
  121. package/scripts/smoke/multi-project-ui.mjs +45 -45
  122. package/scripts/smoke/multi-worker-slots.mjs +42 -42
  123. package/scripts/smoke/notification-center.mjs +87 -87
  124. package/scripts/smoke/notification-inapp-preference.mjs +23 -23
  125. package/scripts/smoke/notification-taxonomy.mjs +58 -58
  126. package/scripts/smoke/orchestration-api.mjs +172 -172
  127. package/scripts/smoke/orchestration-execution-dashboard.mjs +33 -33
  128. package/scripts/smoke/orchestration-live-run.mjs +176 -176
  129. package/scripts/smoke/orchestration-mobile-scroll.mjs +29 -29
  130. package/scripts/smoke/orchestration-model-sync.mjs +30 -30
  131. package/scripts/smoke/orchestration-permission-fallback.mjs +34 -34
  132. package/scripts/smoke/orchestration-runtime-guards.mjs +48 -48
  133. package/scripts/smoke/orchestration-user-facing-output.mjs +25 -25
  134. package/scripts/smoke/permission-policy.mjs +50 -50
  135. package/scripts/smoke/pixcode-workbench-1-48.mjs +167 -167
  136. package/scripts/smoke/provider-models-opencode-live.mjs +66 -66
  137. package/scripts/smoke/provider-rest-api.mjs +124 -124
  138. package/scripts/smoke/provider-selection-status.mjs +52 -52
  139. package/scripts/smoke/run-state-refresh.mjs +52 -52
  140. package/scripts/smoke/runtime-manager.mjs +99 -99
  141. package/scripts/smoke/shell-manual-disconnect.mjs +30 -30
  142. package/scripts/smoke/side-panel-editor-layout.mjs +34 -34
  143. package/scripts/smoke/static-root-routing.mjs +21 -21
  144. package/scripts/smoke/strict-handoff-compact.mjs +60 -60
  145. package/scripts/smoke/taskmaster-config.mjs +24 -24
  146. package/scripts/smoke/taskmaster-execution-telegram.mjs +3 -3
  147. package/scripts/smoke/taskmaster-onboarding.mjs +3 -3
  148. package/scripts/smoke/taskmaster-run-graph.mjs +3 -3
  149. package/scripts/smoke/telegram-control.mjs +242 -242
  150. package/scripts/smoke/tunnel-persistence.mjs +56 -56
  151. package/scripts/smoke/update-issue-progress.mjs +69 -69
  152. package/scripts/smoke/update-ux.mjs +55 -55
  153. package/scripts/smoke/v138-completion.mjs +132 -132
  154. package/scripts/smoke/v138-desktop-release-hardening.mjs +69 -69
  155. package/scripts/smoke/v138-diagnostics.mjs +63 -63
  156. package/scripts/smoke/v138-issue-planner.mjs +33 -33
  157. package/scripts/smoke/v143-remote-control.mjs +76 -76
  158. package/scripts/smoke/v144-production-loop.mjs +47 -47
  159. package/scripts/smoke/v145-platformization.mjs +46 -46
  160. package/scripts/smoke/v146-control-room-ui.mjs +150 -150
  161. package/scripts/smoke/version-modal-autoshow.mjs +29 -29
  162. package/scripts/smoke/vscode-workbench-layout.mjs +63 -63
  163. package/scripts/smoke/vscode-workbench-polish.mjs +461 -436
  164. package/scripts/smoke/workflow-fallback-replay.mjs +56 -56
  165. package/scripts/smoke/workflow-templates.mjs +43 -43
  166. package/scripts/smoke/workflow-trace-timeline.mjs +46 -46
  167. package/scripts/update-git-install.mjs +293 -293
  168. package/server/claude-sdk.js +920 -920
  169. package/server/cli.js +1039 -1039
  170. package/server/constants/config.js +4 -4
  171. package/server/cursor-cli.js +344 -344
  172. package/server/daemon/manager.js +563 -563
  173. package/server/daemon-manager.js +964 -964
  174. package/server/database/db.js +908 -895
  175. package/server/database/json-store.js +197 -197
  176. package/server/gemini-cli.js +550 -550
  177. package/server/gemini-response-handler.js +79 -79
  178. package/server/index.js +201 -30
  179. package/server/load-env.js +35 -35
  180. package/server/middleware/auth.js +171 -156
  181. package/server/modules/orchestration/a2a/adapter-registry.ts +108 -108
  182. package/server/modules/orchestration/a2a/adapters/abstract-a2a.adapter.ts +63 -63
  183. package/server/modules/orchestration/a2a/adapters/claude-code.adapter.ts +286 -286
  184. package/server/modules/orchestration/a2a/adapters/codex.adapter.ts +244 -244
  185. package/server/modules/orchestration/a2a/adapters/cursor.adapter.ts +249 -249
  186. package/server/modules/orchestration/a2a/adapters/gemini.adapter.ts +248 -248
  187. package/server/modules/orchestration/a2a/adapters/json-event.adapter.test.ts +60 -0
  188. package/server/modules/orchestration/a2a/adapters/json-event.adapter.ts +101 -0
  189. package/server/modules/orchestration/a2a/adapters/opencode.adapter.ts +248 -248
  190. package/server/modules/orchestration/a2a/adapters/qwen.adapter.ts +248 -248
  191. package/server/modules/orchestration/a2a/agent-card.ts +55 -55
  192. package/server/modules/orchestration/a2a/routes.ts +590 -590
  193. package/server/modules/orchestration/a2a/task-store.ts +178 -178
  194. package/server/modules/orchestration/a2a/types.ts +126 -126
  195. package/server/modules/orchestration/a2a/validator.ts +113 -113
  196. package/server/modules/orchestration/hermes/hermes.routes.ts +642 -583
  197. package/server/modules/orchestration/index.ts +101 -100
  198. package/server/modules/orchestration/preview/port-watcher.ts +112 -112
  199. package/server/modules/orchestration/preview/preview-proxy.ts +60 -60
  200. package/server/modules/orchestration/preview/types.ts +19 -19
  201. package/server/modules/orchestration/security/permission-policy.ts +401 -401
  202. package/server/modules/orchestration/tasks/orchestration-task-store.ts +41 -41
  203. package/server/modules/orchestration/tasks/orchestration-task.routes.ts +64 -64
  204. package/server/modules/orchestration/tasks/orchestration-task.service.ts +209 -209
  205. package/server/modules/orchestration/tasks/orchestration-task.types.ts +40 -40
  206. package/server/modules/orchestration/tasks/task-run-graph.ts +155 -155
  207. package/server/modules/orchestration/workflows/approval-queue.ts +106 -106
  208. package/server/modules/orchestration/workflows/built-in-workflows.ts +127 -127
  209. package/server/modules/orchestration/workflows/context-packet.ts +186 -186
  210. package/server/modules/orchestration/workflows/handoff-artifact.ts +175 -175
  211. package/server/modules/orchestration/workflows/workflow-fallback-policy.ts +161 -161
  212. package/server/modules/orchestration/workflows/workflow-replay.ts +254 -254
  213. package/server/modules/orchestration/workflows/workflow-runner.ts +2070 -2070
  214. package/server/modules/orchestration/workflows/workflow-store.ts +97 -97
  215. package/server/modules/orchestration/workflows/workflow-templates.ts +272 -272
  216. package/server/modules/orchestration/workflows/workflow-trace.ts +424 -424
  217. package/server/modules/orchestration/workflows/workflow.routes.ts +586 -586
  218. package/server/modules/orchestration/workflows/workflow.types.ts +111 -111
  219. package/server/modules/orchestration/workflows/workspace-target.ts +122 -122
  220. package/server/modules/orchestration/workspace/docker-workspace.ts +136 -136
  221. package/server/modules/orchestration/workspace/path-safety.ts +55 -55
  222. package/server/modules/orchestration/workspace/types.ts +52 -52
  223. package/server/modules/orchestration/workspace/workspace-manager.ts +102 -102
  224. package/server/modules/orchestration/workspace/worktree-workspace.ts +126 -126
  225. package/server/modules/providers/index.ts +2 -2
  226. package/server/modules/providers/list/claude/claude-auth.provider.ts +146 -146
  227. package/server/modules/providers/list/claude/claude-mcp.provider.ts +135 -135
  228. package/server/modules/providers/list/claude/claude-sessions.provider.ts +306 -306
  229. package/server/modules/providers/list/claude/claude.provider.ts +15 -15
  230. package/server/modules/providers/list/codex/codex-auth.provider.ts +117 -117
  231. package/server/modules/providers/list/codex/codex-mcp.provider.ts +135 -135
  232. package/server/modules/providers/list/codex/codex-sessions.provider.ts +319 -319
  233. package/server/modules/providers/list/codex/codex.provider.ts +15 -15
  234. package/server/modules/providers/list/cursor/cursor-auth.provider.ts +147 -147
  235. package/server/modules/providers/list/cursor/cursor-mcp.provider.ts +108 -108
  236. package/server/modules/providers/list/cursor/cursor-sessions.provider.ts +421 -421
  237. package/server/modules/providers/list/cursor/cursor.provider.ts +15 -15
  238. package/server/modules/providers/list/gemini/gemini-auth.provider.ts +173 -173
  239. package/server/modules/providers/list/gemini/gemini-mcp.provider.ts +110 -110
  240. package/server/modules/providers/list/gemini/gemini-sessions.provider.ts +227 -227
  241. package/server/modules/providers/list/gemini/gemini.provider.ts +15 -15
  242. package/server/modules/providers/list/opencode/opencode-auth.provider.ts +131 -131
  243. package/server/modules/providers/list/opencode/opencode-mcp.provider.ts +126 -126
  244. package/server/modules/providers/list/opencode/opencode-sessions.provider.ts +286 -286
  245. package/server/modules/providers/list/opencode/opencode.provider.ts +29 -29
  246. package/server/modules/providers/list/qwen/qwen-auth.provider.ts +146 -146
  247. package/server/modules/providers/list/qwen/qwen-mcp.provider.ts +114 -114
  248. package/server/modules/providers/list/qwen/qwen-sessions.provider.ts +265 -265
  249. package/server/modules/providers/list/qwen/qwen.provider.ts +21 -21
  250. package/server/modules/providers/provider.registry.ts +40 -40
  251. package/server/modules/providers/provider.routes.ts +944 -944
  252. package/server/modules/providers/services/mcp.service.ts +86 -86
  253. package/server/modules/providers/services/provider-auth.service.ts +26 -26
  254. package/server/modules/providers/services/sessions.service.ts +45 -45
  255. package/server/modules/providers/shared/base/abstract.provider.ts +20 -20
  256. package/server/modules/providers/shared/mcp/mcp.provider.ts +151 -151
  257. package/server/modules/providers/shared/provider-configs.ts +142 -142
  258. package/server/modules/providers/tests/mcp.test.ts +293 -293
  259. package/server/openai-codex.js +462 -462
  260. package/server/opencode-cli.js +491 -491
  261. package/server/opencode-response-handler.js +111 -111
  262. package/server/projects.js +3008 -3008
  263. package/server/qwen-code-cli.js +410 -410
  264. package/server/qwen-response-handler.js +73 -73
  265. package/server/routes/agent.js +1435 -1435
  266. package/server/routes/auth.js +154 -146
  267. package/server/routes/codex.js +20 -20
  268. package/server/routes/commands.js +570 -570
  269. package/server/routes/cursor.js +61 -61
  270. package/server/routes/diagnostics.js +41 -41
  271. package/server/routes/gemini.js +25 -25
  272. package/server/routes/git.js +1650 -1635
  273. package/server/routes/live-view.js +411 -411
  274. package/server/routes/mcp-utils.js +13 -13
  275. package/server/routes/messages.js +62 -62
  276. package/server/routes/network.js +125 -125
  277. package/server/routes/platformization.js +198 -197
  278. package/server/routes/plugins.js +320 -320
  279. package/server/routes/production-agent-loop.js +90 -90
  280. package/server/routes/projects.js +917 -917
  281. package/server/routes/public-api.js +34 -34
  282. package/server/routes/qwen.js +27 -27
  283. package/server/routes/remote.js +55 -55
  284. package/server/routes/settings.js +321 -321
  285. package/server/routes/telegram.js +140 -140
  286. package/server/routes/user.js +125 -125
  287. package/server/routes/webhooks.js +63 -63
  288. package/server/services/control-room.js +102 -102
  289. package/server/services/diagnostics.js +165 -165
  290. package/server/services/external-access.js +375 -375
  291. package/server/services/hermes-gateway.js +1562 -1247
  292. package/server/services/hermes-install-jobs.js +729 -729
  293. package/server/services/install-jobs.js +715 -715
  294. package/server/services/live-view.js +956 -956
  295. package/server/services/managed-runtimes.js +493 -493
  296. package/server/services/model-registry.js +144 -144
  297. package/server/services/notification-orchestrator.js +365 -365
  298. package/server/services/notification-taxonomy.js +204 -204
  299. package/server/services/platformization.js +844 -779
  300. package/server/services/production-agent-loop.js +248 -248
  301. package/server/services/provider-cli-versions.js +149 -149
  302. package/server/services/provider-credentials.js +189 -189
  303. package/server/services/provider-models.js +396 -396
  304. package/server/services/public-api-manifest.js +190 -182
  305. package/server/services/remote-connection.js +127 -127
  306. package/server/services/runtime-manager.js +323 -323
  307. package/server/services/startup-update.js +234 -234
  308. package/server/services/telegram/bot.js +331 -331
  309. package/server/services/telegram/control-center.js +979 -979
  310. package/server/services/telegram/telegram-http-client.js +151 -151
  311. package/server/services/telegram/translations.js +340 -340
  312. package/server/services/vapid-keys.js +36 -36
  313. package/server/services/webhooks.js +216 -216
  314. package/server/sessionManager.js +225 -225
  315. package/server/shared/interfaces.ts +54 -54
  316. package/server/shared/types.ts +172 -172
  317. package/server/shared/utils.ts +193 -193
  318. package/server/tsconfig.json +36 -36
  319. package/server/utils/colors.js +21 -21
  320. package/server/utils/commandParser.js +305 -305
  321. package/server/utils/frontmatter.js +18 -18
  322. package/server/utils/gitConfig.js +34 -34
  323. package/server/utils/plugin-loader.js +457 -457
  324. package/server/utils/plugin-process-manager.js +185 -185
  325. package/server/utils/port-access.js +209 -209
  326. package/server/utils/runtime-paths.js +37 -37
  327. package/server/utils/url-detection.js +71 -71
  328. package/server/vite-daemon.js +79 -79
  329. package/shared/modelConstants.js +161 -161
  330. package/shared/networkHosts.js +22 -22
  331. package/dist/assets/index-DMz0zv6T.css +0 -32
@@ -1,815 +1,880 @@
1
- import crypto from 'node:crypto';
2
- import os from 'node:os';
3
- import { execFile } from 'node:child_process';
4
- import { promisify } from 'node:util';
5
-
6
- import bcrypt from 'bcryptjs';
7
-
8
- import { appConfigDb, userDb } from '../database/db.js';
9
-
10
- const CONFIG_KEY = 'platformization';
11
- const execFileAsync = promisify(execFile);
12
-
13
- export const TEAM_ROLES = {
14
- owner: [
15
- 'team:manage',
16
- 'project:admin',
17
- 'run:approve',
18
- 'secret:manage',
19
- 'marketplace:manage',
20
- 'eval:run',
21
- 'usage:view',
22
- 'security:audit',
23
- ],
24
- admin: [
25
- 'project:admin',
26
- 'run:approve',
27
- 'secret:manage',
28
- 'marketplace:manage',
29
- 'eval:run',
30
- 'usage:view',
31
- 'security:audit',
32
- ],
33
- member: [
34
- 'project:write',
35
- 'run:create',
36
- 'secret:use',
37
- 'eval:run',
38
- 'usage:view',
39
- ],
40
- project_partner: [
41
- 'project:write',
42
- 'run:create',
43
- 'run:approve',
44
- 'review:manage',
45
- 'usage:view',
46
- ],
47
- project_worker: [
48
- 'project:write',
49
- 'run:create',
50
- 'review:update',
51
- ],
52
- project_reviewer: [
53
- 'project:read',
54
- 'review:manage',
55
- 'usage:view',
56
- ],
57
- viewer: [
58
- 'project:read',
59
- 'usage:view',
60
- ],
61
- };
62
-
63
- export const SECRET_SCOPES = ['global', 'provider', 'project', 'workflow', 'telegram', 'api'];
64
-
65
- export const MARKETPLACE_PLUGIN_TYPES = ['mcp-server', 'workflow-template', 'provider-adapter', 'notification-channel'];
66
-
67
- export const SECURITY_AUDIT_CHECKS = [
68
- 'dependency_audit',
69
- 'secret_scan',
70
- 'permission_audit',
71
- 'agent_output_leak_detection',
72
- ];
73
-
74
- function nowIso() {
75
- return new Date().toISOString();
76
- }
77
-
78
- function emptyStore() {
79
- return {
80
- teamMembers: [],
81
- secrets: [],
82
- marketplacePlugins: [],
83
- evaluationSuites: [],
84
- evaluationRuns: [],
85
- usageEvents: [],
86
- securityAuditRuns: [],
87
- projectCollaborators: [],
88
- remoteAccessConfigs: [],
89
- auditLog: [],
90
- };
91
- }
92
-
93
- function readStore() {
94
- const raw = appConfigDb.get(CONFIG_KEY);
95
- if (!raw) return emptyStore();
96
- try {
97
- const parsed = JSON.parse(raw);
98
- return {
99
- teamMembers: Array.isArray(parsed.teamMembers) ? parsed.teamMembers : [],
100
- secrets: Array.isArray(parsed.secrets) ? parsed.secrets : [],
101
- marketplacePlugins: Array.isArray(parsed.marketplacePlugins) ? parsed.marketplacePlugins : [],
102
- evaluationSuites: Array.isArray(parsed.evaluationSuites) ? parsed.evaluationSuites : [],
103
- evaluationRuns: Array.isArray(parsed.evaluationRuns) ? parsed.evaluationRuns : [],
104
- usageEvents: Array.isArray(parsed.usageEvents) ? parsed.usageEvents : [],
105
- securityAuditRuns: Array.isArray(parsed.securityAuditRuns) ? parsed.securityAuditRuns : [],
106
- projectCollaborators: Array.isArray(parsed.projectCollaborators) ? parsed.projectCollaborators : [],
107
- remoteAccessConfigs: Array.isArray(parsed.remoteAccessConfigs) ? parsed.remoteAccessConfigs : [],
108
- auditLog: Array.isArray(parsed.auditLog) ? parsed.auditLog : [],
109
- };
110
- } catch {
111
- return emptyStore();
112
- }
113
- }
114
-
115
- function writeStore(store) {
116
- appConfigDb.set(CONFIG_KEY, JSON.stringify(store));
117
- }
118
-
1
+ import crypto from 'node:crypto';
2
+ import os from 'node:os';
3
+ import { execFile } from 'node:child_process';
4
+ import { promisify } from 'node:util';
5
+
6
+ import bcrypt from 'bcryptjs';
7
+
8
+ import { appConfigDb, userDb } from '../database/db.js';
9
+
10
+ const CONFIG_KEY = 'platformization';
11
+ const execFileAsync = promisify(execFile);
12
+
13
+ export const TEAM_ROLES = {
14
+ owner: [
15
+ 'team:manage',
16
+ 'project:admin',
17
+ 'run:approve',
18
+ 'secret:manage',
19
+ 'marketplace:manage',
20
+ 'eval:run',
21
+ 'usage:view',
22
+ 'security:audit',
23
+ ],
24
+ admin: [
25
+ 'project:admin',
26
+ 'run:approve',
27
+ 'secret:manage',
28
+ 'marketplace:manage',
29
+ 'eval:run',
30
+ 'usage:view',
31
+ 'security:audit',
32
+ ],
33
+ member: [
34
+ 'project:write',
35
+ 'run:create',
36
+ 'secret:use',
37
+ 'eval:run',
38
+ 'usage:view',
39
+ ],
40
+ project_partner: [
41
+ 'project:write',
42
+ 'run:create',
43
+ 'run:approve',
44
+ 'review:manage',
45
+ 'usage:view',
46
+ ],
47
+ project_worker: [
48
+ 'project:write',
49
+ 'run:create',
50
+ 'review:update',
51
+ ],
52
+ project_reviewer: [
53
+ 'project:read',
54
+ 'review:manage',
55
+ 'usage:view',
56
+ ],
57
+ viewer: [
58
+ 'project:read',
59
+ 'usage:view',
60
+ ],
61
+ };
62
+
63
+ export const SECRET_SCOPES = ['global', 'provider', 'project', 'workflow', 'telegram', 'api'];
64
+
65
+ export const MARKETPLACE_PLUGIN_TYPES = ['mcp-server', 'workflow-template', 'provider-adapter', 'notification-channel'];
66
+
67
+ export const SECURITY_AUDIT_CHECKS = [
68
+ 'dependency_audit',
69
+ 'secret_scan',
70
+ 'permission_audit',
71
+ 'agent_output_leak_detection',
72
+ ];
73
+
74
+ function nowIso() {
75
+ return new Date().toISOString();
76
+ }
77
+
78
+ function emptyStore() {
79
+ return {
80
+ teamMembers: [],
81
+ secrets: [],
82
+ marketplacePlugins: [],
83
+ evaluationSuites: [],
84
+ evaluationRuns: [],
85
+ usageEvents: [],
86
+ securityAuditRuns: [],
87
+ projectCollaborators: [],
88
+ remoteAccessConfigs: [],
89
+ auditLog: [],
90
+ };
91
+ }
92
+
93
+ function readStore() {
94
+ const raw = appConfigDb.get(CONFIG_KEY);
95
+ if (!raw) return emptyStore();
96
+ try {
97
+ const parsed = JSON.parse(raw);
98
+ return {
99
+ teamMembers: Array.isArray(parsed.teamMembers) ? parsed.teamMembers : [],
100
+ secrets: Array.isArray(parsed.secrets) ? parsed.secrets : [],
101
+ marketplacePlugins: Array.isArray(parsed.marketplacePlugins) ? parsed.marketplacePlugins : [],
102
+ evaluationSuites: Array.isArray(parsed.evaluationSuites) ? parsed.evaluationSuites : [],
103
+ evaluationRuns: Array.isArray(parsed.evaluationRuns) ? parsed.evaluationRuns : [],
104
+ usageEvents: Array.isArray(parsed.usageEvents) ? parsed.usageEvents : [],
105
+ securityAuditRuns: Array.isArray(parsed.securityAuditRuns) ? parsed.securityAuditRuns : [],
106
+ projectCollaborators: Array.isArray(parsed.projectCollaborators) ? parsed.projectCollaborators : [],
107
+ remoteAccessConfigs: Array.isArray(parsed.remoteAccessConfigs) ? parsed.remoteAccessConfigs : [],
108
+ auditLog: Array.isArray(parsed.auditLog) ? parsed.auditLog : [],
109
+ };
110
+ } catch {
111
+ return emptyStore();
112
+ }
113
+ }
114
+
115
+ function writeStore(store) {
116
+ appConfigDb.set(CONFIG_KEY, JSON.stringify(store));
117
+ }
118
+
119
119
  function compact(text, max = 120) {
120
120
  const value = String(text || '').replace(/\s+/g, ' ').trim();
121
121
  return value.length > max ? value.slice(0, max).replace(/[-_\s]+$/g, '') : value;
122
122
  }
123
123
 
124
- function slugify(value) {
125
- const slug = compact(value, 72)
126
- .toLowerCase()
127
- .replace(/[^a-z0-9]+/g, '-')
128
- .replace(/^-+|-+$/g, '');
129
- return slug || crypto.randomUUID();
130
- }
131
-
132
- function addAudit(store, action, actorId, details = {}) {
133
- store.auditLog.unshift({
134
- id: crypto.randomUUID(),
135
- action,
136
- actorId: actorId || null,
137
- createdAt: nowIso(),
138
- details,
139
- });
140
- store.auditLog = store.auditLog.slice(0, 250);
141
- }
142
-
124
+ function compactProjectIdentifier(value) {
125
+ return String(value || '').replace(/\s+/g, ' ').trim();
126
+ }
127
+
128
+ function slugify(value) {
129
+ const slug = compact(value, 72)
130
+ .toLowerCase()
131
+ .replace(/[^a-z0-9]+/g, '-')
132
+ .replace(/^-+|-+$/g, '');
133
+ return slug || crypto.randomUUID();
134
+ }
135
+
136
+ function addAudit(store, action, actorId, details = {}) {
137
+ store.auditLog.unshift({
138
+ id: crypto.randomUUID(),
139
+ action,
140
+ actorId: actorId || null,
141
+ createdAt: nowIso(),
142
+ details,
143
+ });
144
+ store.auditLog = store.auditLog.slice(0, 250);
145
+ }
146
+
143
147
  function normalizeRole(role) {
144
148
  return TEAM_ROLES[role] ? role : 'viewer';
145
149
  }
146
150
 
147
- function normalizeScope(scope) {
148
- return SECRET_SCOPES.includes(scope) ? scope : 'project';
151
+ export function isAdminUser(user = {}) {
152
+ return user?.role === 'admin' || user?.role === 'owner';
149
153
  }
150
154
 
151
- function vaultKey() {
152
- const material = process.env.PIXCODE_SECRET_KEY || process.env.JWT_SECRET || appConfigDb.getOrCreateJwtSecret();
153
- return crypto.createHash('sha256').update(material).digest();
154
- }
155
-
156
- function sealSecret(value) {
157
- const iv = crypto.randomBytes(12);
158
- const cipher = crypto.createCipheriv('aes-256-gcm', vaultKey(), iv);
159
- const encrypted = Buffer.concat([cipher.update(String(value || ''), 'utf8'), cipher.final()]);
160
- return {
161
- algorithm: 'aes-256-gcm',
162
- iv: iv.toString('base64'),
163
- tag: cipher.getAuthTag().toString('base64'),
164
- ciphertext: encrypted.toString('base64'),
165
- };
166
- }
167
-
168
- function openSecret(sealed) {
169
- const decipher = crypto.createDecipheriv('aes-256-gcm', vaultKey(), Buffer.from(sealed.iv, 'base64'));
170
- decipher.setAuthTag(Buffer.from(sealed.tag, 'base64'));
171
- return Buffer.concat([
172
- decipher.update(Buffer.from(sealed.ciphertext, 'base64')),
173
- decipher.final(),
174
- ]).toString('utf8');
175
- }
155
+ function resolveUser(input = {}) {
156
+ const users = userDb.listUsers();
157
+ const userId = Number(input.userId);
158
+ if (Number.isFinite(userId)) {
159
+ return users.find((user) => user.id === userId && user.is_active) || null;
160
+ }
176
161
 
177
- function redactSecret(secret) {
178
- return {
179
- ...secret,
180
- sealedValue: undefined,
181
- redacted: '********',
182
- };
162
+ const userRef = compact(input.userRef || input.email || input.username || '').toLowerCase();
163
+ if (!userRef) return null;
164
+ return users.find((user) => user.is_active && String(user.username).toLowerCase() === userRef) || null;
183
165
  }
184
166
 
185
- function scopeMatches(secret, input = {}) {
186
- if (secret.scope === 'global') return true;
187
- if (secret.scope === 'provider') return !input.provider || secret.target === input.provider;
188
- if (secret.scope === 'project') return !input.projectPath || secret.target === input.projectPath || secret.target === input.projectName;
189
- if (secret.scope === 'workflow') return !input.workflowId || secret.target === input.workflowId;
190
- if (secret.scope === 'telegram') return input.channel === 'telegram';
191
- if (secret.scope === 'api') return input.channel === 'api';
192
- return false;
193
- }
167
+ function projectMatches(collaborator, project = {}) {
168
+ const projectName = compactProjectIdentifier(project.name || project.projectName || project);
169
+ const projectPath = compactProjectIdentifier(project.fullPath || project.path || project.projectPath || '');
194
170
 
195
- export function getPlatformizationState() {
196
- const store = readStore();
197
- return {
198
- roles: TEAM_ROLES,
199
- secretScopes: SECRET_SCOPES,
200
- marketplacePluginTypes: MARKETPLACE_PLUGIN_TYPES,
201
- securityAuditChecks: SECURITY_AUDIT_CHECKS,
202
- teamMembers: store.teamMembers,
203
- secrets: store.secrets.map(redactSecret),
204
- marketplacePlugins: store.marketplacePlugins,
205
- evaluationSuites: store.evaluationSuites,
206
- evaluationRuns: store.evaluationRuns,
207
- usageSummary: summarizeUsageEvents(store.usageEvents),
208
- securityAuditRuns: store.securityAuditRuns,
209
- adminUsers: listAdminUsers(),
210
- projectCollaborators: store.projectCollaborators,
211
- remoteAccessConfigs: store.remoteAccessConfigs,
212
- auditLog: store.auditLog,
213
- };
171
+ return Boolean(
172
+ (projectName && collaborator.projectName === projectName) ||
173
+ (projectPath && collaborator.projectPath === projectPath)
174
+ );
214
175
  }
215
176
 
216
- export function createTeamMember(input = {}, actorId = null) {
217
- const email = compact(input.email || input.username || '');
218
- if (!email) throw new Error('Team member email or username is required.');
219
- const store = readStore();
220
- const member = {
221
- id: crypto.randomUUID(),
222
- email,
223
- displayName: compact(input.displayName || email, 80),
224
- role: normalizeRole(input.role || 'viewer'),
225
- projectScopes: Array.isArray(input.projectScopes) ? input.projectScopes : [],
226
- status: input.status || 'active',
227
- createdAt: nowIso(),
228
- updatedAt: nowIso(),
229
- };
230
- member.permissions = TEAM_ROLES[member.role];
231
- store.teamMembers.unshift(member);
232
- addAudit(store, 'team.member.created', actorId, { memberId: member.id, role: member.role });
233
- writeStore(store);
234
- return member;
235
- }
177
+ export function userHasProjectAccess(user, project, capability = 'viewFiles') {
178
+ if (isAdminUser(user)) return true;
179
+ if (!user?.id && !user?.userId) return false;
236
180
 
237
- export function updateTeamMember(memberId, patch = {}, actorId = null) {
181
+ const userId = Number(user.id ?? user.userId);
182
+ const username = String(user.username || '').toLowerCase();
238
183
  const store = readStore();
239
- let updated = null;
240
- store.teamMembers = store.teamMembers.map((member) => {
241
- if (member.id !== memberId) return member;
242
- updated = {
243
- ...member,
244
- ...patch,
245
- id: member.id,
246
- role: normalizeRole(patch.role || member.role),
247
- updatedAt: nowIso(),
248
- };
249
- updated.permissions = TEAM_ROLES[updated.role];
250
- return updated;
251
- });
252
- if (updated) {
253
- addAudit(store, 'team.member.updated', actorId, { memberId, role: updated.role });
254
- writeStore(store);
255
- }
256
- return updated;
257
- }
258
-
259
- export function listAdminUsers() {
260
- return userDb.listUsers().map((user) => ({
261
- id: user.id,
262
- username: user.username,
263
- role: user.role || 'member',
264
- status: user.is_active ? 'active' : 'disabled',
265
- isActive: Boolean(user.is_active),
266
- createdAt: user.created_at,
267
- lastLogin: user.last_login,
268
- }));
269
- }
270
184
 
271
- export async function createAdminUser(input = {}, actorId = null) {
272
- const username = compact(input.username || input.email || '');
273
- const password = String(input.password || '');
274
- if (!username || password.length < 6) {
275
- throw new Error('Admin user creation requires a username and a password with at least 6 characters.');
276
- }
185
+ return store.projectCollaborators.some((collaborator) => {
186
+ if (collaborator.status === 'disabled') return false;
187
+ if (!projectMatches(collaborator, project)) return false;
277
188
 
278
- const role = normalizeRole(input.role || 'member');
279
- const passwordHash = await bcrypt.hash(password, 12);
280
- const user = userDb.createManagedUser(username, passwordHash, {
281
- role,
282
- is_active: input.status !== 'disabled',
283
- });
189
+ const sameUser = Number(collaborator.userId) === userId ||
190
+ String(collaborator.userRef || '').toLowerCase() === username;
191
+ if (!sameUser) return false;
284
192
 
285
- const store = readStore();
286
- const member = {
287
- id: crypto.randomUUID(),
288
- userId: user.id,
289
- email: input.email || username,
290
- displayName: compact(input.displayName || username, 80),
291
- role,
292
- projectScopes: Array.isArray(input.projectScopes) ? input.projectScopes : [],
293
- status: input.status || 'active',
294
- createdAt: nowIso(),
295
- updatedAt: nowIso(),
296
- permissions: TEAM_ROLES[role],
297
- };
298
- store.teamMembers.unshift(member);
299
- addAudit(store, 'admin.user.created', actorId, { userId: user.id, username, role });
300
- writeStore(store);
301
- return {
302
- ...user,
303
- status: member.status,
304
- permissions: member.permissions,
305
- };
306
- }
193
+ if (capability === 'viewFiles') {
194
+ return collaborator.capabilities?.viewFiles !== false;
195
+ }
307
196
 
308
- export function updateAdminUser(userId, patch = {}, actorId = null) {
309
- const numericUserId = Number(userId);
310
- const role = patch.role ? normalizeRole(patch.role) : undefined;
311
- const isActive = patch.status === 'disabled' ? false : patch.status === 'active' ? true : undefined;
312
- const user = userDb.updateUser(numericUserId, {
313
- username: patch.username,
314
- role,
315
- is_active: isActive,
197
+ return collaborator.capabilities?.[capability] === true;
316
198
  });
317
- if (!user) return null;
318
-
319
- const store = readStore();
320
- store.teamMembers = store.teamMembers.map((member) => {
321
- if (member.userId !== numericUserId) return member;
322
- const nextRole = role || member.role;
323
- const nextStatus = patch.status || member.status;
324
- return {
325
- ...member,
326
- role: nextRole,
327
- status: nextStatus,
328
- permissions: TEAM_ROLES[nextRole] || TEAM_ROLES.viewer,
329
- updatedAt: nowIso(),
330
- };
331
- });
332
- addAudit(store, 'admin.user.updated', actorId, { userId: numericUserId, role: role || user.role, status: patch.status });
333
- writeStore(store);
334
- return {
335
- ...user,
336
- role: role || user.role || 'member',
337
- status: user.is_active ? 'active' : 'disabled',
338
- };
339
199
  }
340
200
 
201
+ export function filterProjectsForUser(projects = [], user) {
202
+ if (isAdminUser(user)) return projects;
203
+ return projects.filter((project) => userHasProjectAccess(user, project, 'viewFiles'));
204
+ }
205
+
206
+ function normalizeScope(scope) {
207
+ return SECRET_SCOPES.includes(scope) ? scope : 'project';
208
+ }
209
+
210
+ function vaultKey() {
211
+ const material = process.env.PIXCODE_SECRET_KEY || process.env.JWT_SECRET || appConfigDb.getOrCreateJwtSecret();
212
+ return crypto.createHash('sha256').update(material).digest();
213
+ }
214
+
215
+ function sealSecret(value) {
216
+ const iv = crypto.randomBytes(12);
217
+ const cipher = crypto.createCipheriv('aes-256-gcm', vaultKey(), iv);
218
+ const encrypted = Buffer.concat([cipher.update(String(value || ''), 'utf8'), cipher.final()]);
219
+ return {
220
+ algorithm: 'aes-256-gcm',
221
+ iv: iv.toString('base64'),
222
+ tag: cipher.getAuthTag().toString('base64'),
223
+ ciphertext: encrypted.toString('base64'),
224
+ };
225
+ }
226
+
227
+ function openSecret(sealed) {
228
+ const decipher = crypto.createDecipheriv('aes-256-gcm', vaultKey(), Buffer.from(sealed.iv, 'base64'));
229
+ decipher.setAuthTag(Buffer.from(sealed.tag, 'base64'));
230
+ return Buffer.concat([
231
+ decipher.update(Buffer.from(sealed.ciphertext, 'base64')),
232
+ decipher.final(),
233
+ ]).toString('utf8');
234
+ }
235
+
236
+ function redactSecret(secret) {
237
+ return {
238
+ ...secret,
239
+ sealedValue: undefined,
240
+ redacted: '********',
241
+ };
242
+ }
243
+
244
+ function scopeMatches(secret, input = {}) {
245
+ if (secret.scope === 'global') return true;
246
+ if (secret.scope === 'provider') return !input.provider || secret.target === input.provider;
247
+ if (secret.scope === 'project') return !input.projectPath || secret.target === input.projectPath || secret.target === input.projectName;
248
+ if (secret.scope === 'workflow') return !input.workflowId || secret.target === input.workflowId;
249
+ if (secret.scope === 'telegram') return input.channel === 'telegram';
250
+ if (secret.scope === 'api') return input.channel === 'api';
251
+ return false;
252
+ }
253
+
254
+ export function getPlatformizationState() {
255
+ const store = readStore();
256
+ return {
257
+ roles: TEAM_ROLES,
258
+ secretScopes: SECRET_SCOPES,
259
+ marketplacePluginTypes: MARKETPLACE_PLUGIN_TYPES,
260
+ securityAuditChecks: SECURITY_AUDIT_CHECKS,
261
+ teamMembers: store.teamMembers,
262
+ secrets: store.secrets.map(redactSecret),
263
+ marketplacePlugins: store.marketplacePlugins,
264
+ evaluationSuites: store.evaluationSuites,
265
+ evaluationRuns: store.evaluationRuns,
266
+ usageSummary: summarizeUsageEvents(store.usageEvents),
267
+ securityAuditRuns: store.securityAuditRuns,
268
+ adminUsers: listAdminUsers(),
269
+ projectCollaborators: store.projectCollaborators,
270
+ remoteAccessConfigs: store.remoteAccessConfigs,
271
+ auditLog: store.auditLog,
272
+ };
273
+ }
274
+
275
+ export function createTeamMember(input = {}, actorId = null) {
276
+ const email = compact(input.email || input.username || '');
277
+ if (!email) throw new Error('Team member email or username is required.');
278
+ const store = readStore();
279
+ const member = {
280
+ id: crypto.randomUUID(),
281
+ email,
282
+ displayName: compact(input.displayName || email, 80),
283
+ role: normalizeRole(input.role || 'viewer'),
284
+ projectScopes: Array.isArray(input.projectScopes) ? input.projectScopes : [],
285
+ status: input.status || 'active',
286
+ createdAt: nowIso(),
287
+ updatedAt: nowIso(),
288
+ };
289
+ member.permissions = TEAM_ROLES[member.role];
290
+ store.teamMembers.unshift(member);
291
+ addAudit(store, 'team.member.created', actorId, { memberId: member.id, role: member.role });
292
+ writeStore(store);
293
+ return member;
294
+ }
295
+
296
+ export function updateTeamMember(memberId, patch = {}, actorId = null) {
297
+ const store = readStore();
298
+ let updated = null;
299
+ store.teamMembers = store.teamMembers.map((member) => {
300
+ if (member.id !== memberId) return member;
301
+ updated = {
302
+ ...member,
303
+ ...patch,
304
+ id: member.id,
305
+ role: normalizeRole(patch.role || member.role),
306
+ updatedAt: nowIso(),
307
+ };
308
+ updated.permissions = TEAM_ROLES[updated.role];
309
+ return updated;
310
+ });
311
+ if (updated) {
312
+ addAudit(store, 'team.member.updated', actorId, { memberId, role: updated.role });
313
+ writeStore(store);
314
+ }
315
+ return updated;
316
+ }
317
+
318
+ export function listAdminUsers() {
319
+ return userDb.listUsers().map((user) => ({
320
+ id: user.id,
321
+ username: user.username,
322
+ role: user.role || 'member',
323
+ status: user.is_active ? 'active' : 'disabled',
324
+ isActive: Boolean(user.is_active),
325
+ createdAt: user.created_at,
326
+ lastLogin: user.last_login,
327
+ }));
328
+ }
329
+
330
+ export async function createAdminUser(input = {}, actorId = null) {
331
+ const username = compact(input.username || input.email || '');
332
+ const password = String(input.password || '');
333
+ if (!username || password.length < 6) {
334
+ throw new Error('Admin user creation requires a username and a password with at least 6 characters.');
335
+ }
336
+
337
+ const role = normalizeRole(input.role || 'member');
338
+ const passwordHash = await bcrypt.hash(password, 12);
339
+ const user = userDb.createManagedUser(username, passwordHash, {
340
+ role,
341
+ is_active: input.status !== 'disabled',
342
+ });
343
+
344
+ const store = readStore();
345
+ const member = {
346
+ id: crypto.randomUUID(),
347
+ userId: user.id,
348
+ email: input.email || username,
349
+ displayName: compact(input.displayName || username, 80),
350
+ role,
351
+ projectScopes: Array.isArray(input.projectScopes) ? input.projectScopes : [],
352
+ status: input.status || 'active',
353
+ createdAt: nowIso(),
354
+ updatedAt: nowIso(),
355
+ permissions: TEAM_ROLES[role],
356
+ };
357
+ store.teamMembers.unshift(member);
358
+ addAudit(store, 'admin.user.created', actorId, { userId: user.id, username, role });
359
+ writeStore(store);
360
+ return {
361
+ ...user,
362
+ status: member.status,
363
+ permissions: member.permissions,
364
+ };
365
+ }
366
+
367
+ export function updateAdminUser(userId, patch = {}, actorId = null) {
368
+ const numericUserId = Number(userId);
369
+ const role = patch.role ? normalizeRole(patch.role) : undefined;
370
+ const isActive = patch.status === 'disabled' ? false : patch.status === 'active' ? true : undefined;
371
+ const user = userDb.updateUser(numericUserId, {
372
+ username: patch.username,
373
+ role,
374
+ is_active: isActive,
375
+ });
376
+ if (!user) return null;
377
+
378
+ const store = readStore();
379
+ store.teamMembers = store.teamMembers.map((member) => {
380
+ if (member.userId !== numericUserId) return member;
381
+ const nextRole = role || member.role;
382
+ const nextStatus = patch.status || member.status;
383
+ return {
384
+ ...member,
385
+ role: nextRole,
386
+ status: nextStatus,
387
+ permissions: TEAM_ROLES[nextRole] || TEAM_ROLES.viewer,
388
+ updatedAt: nowIso(),
389
+ };
390
+ });
391
+ addAudit(store, 'admin.user.updated', actorId, { userId: numericUserId, role: role || user.role, status: patch.status });
392
+ writeStore(store);
393
+ return {
394
+ ...user,
395
+ role: role || user.role || 'member',
396
+ status: user.is_active ? 'active' : 'disabled',
397
+ };
398
+ }
399
+
341
400
  export function createProjectCollaborator(input = {}, actorId = null) {
342
- const projectName = compact(input.projectName || input.project || '');
401
+ const projectName = compactProjectIdentifier(input.projectName || input.project || '');
343
402
  const projectPath = input.projectPath || null;
344
- const userRef = compact(input.userRef || input.email || input.username || '');
403
+ const targetUser = resolveUser(input);
404
+ const userRef = compact(input.userRef || input.email || input.username || targetUser?.username || '');
345
405
  if (!projectName || !userRef) {
346
406
  throw new Error('Project collaborator requires a project name and user reference.');
347
407
  }
348
408
 
349
- const role = ['partner', 'worker', 'reviewer', 'viewer'].includes(input.role) ? input.role : 'worker';
350
- const capabilities = {
351
- chatAgents: input.capabilities?.chatAgents !== false,
352
- viewFiles: true,
353
- editFiles: role === 'partner' || role === 'worker',
354
- useShell: role === 'partner',
355
- approveActions: role === 'partner' || role === 'reviewer',
356
- manageSecrets: role === 'partner',
357
- manageProjectSettings: role === 'partner',
358
- };
359
- const collaborator = {
409
+ if (!targetUser) {
410
+ throw new Error('Create the user account before assigning project access.');
411
+ }
412
+
413
+ const role = ['partner', 'worker', 'reviewer', 'viewer'].includes(input.role) ? input.role : 'worker';
414
+ const capabilities = {
415
+ chatAgents: input.capabilities?.chatAgents !== false,
416
+ viewFiles: true,
417
+ editFiles: role === 'partner' || role === 'worker',
418
+ useShell: role === 'partner',
419
+ approveActions: role === 'partner' || role === 'reviewer',
420
+ manageSecrets: role === 'partner',
421
+ manageProjectSettings: role === 'partner',
422
+ };
423
+ const collaborator = {
360
424
  id: crypto.randomUUID(),
361
425
  projectName,
362
426
  projectPath,
427
+ userId: targetUser.id,
363
428
  userRef,
364
- role,
365
- capabilities: {
366
- ...capabilities,
367
- ...(input.capabilities && typeof input.capabilities === 'object' ? input.capabilities : {}),
368
- },
369
- status: input.status || 'active',
370
- createdAt: nowIso(),
371
- updatedAt: nowIso(),
372
- };
373
- const store = readStore();
374
- store.projectCollaborators.unshift(collaborator);
375
- addAudit(store, 'project.collaborator.created', actorId, { collaboratorId: collaborator.id, projectName, userRef, role });
376
- writeStore(store);
377
- return collaborator;
378
- }
379
-
380
- export function updateProjectCollaborator(collaboratorId, patch = {}, actorId = null) {
381
- const store = readStore();
382
- let updated = null;
383
- store.projectCollaborators = store.projectCollaborators.map((collaborator) => {
384
- if (collaborator.id !== collaboratorId) return collaborator;
385
- updated = {
386
- ...collaborator,
387
- ...patch,
388
- id: collaborator.id,
389
- capabilities: {
390
- ...collaborator.capabilities,
391
- ...(patch.capabilities && typeof patch.capabilities === 'object' ? patch.capabilities : {}),
392
- },
393
- updatedAt: nowIso(),
394
- };
395
- return updated;
396
- });
397
- if (updated) {
398
- addAudit(store, 'project.collaborator.updated', actorId, { collaboratorId, role: updated.role, status: updated.status });
399
- writeStore(store);
400
- }
401
- return updated;
402
- }
403
-
404
- export function createSecret(input = {}, actorId = null) {
405
- const name = compact(input.name || input.envName || '');
406
- const value = input.value;
407
- if (!name || typeof value !== 'string') throw new Error('Secret name and string value are required.');
408
- const scope = normalizeScope(input.scope || 'project');
409
- const store = readStore();
410
- const secret = {
411
- id: crypto.randomUUID(),
412
- name,
413
- envName: compact(input.envName || name).replace(/[^A-Z0-9_]/gi, '_').toUpperCase(),
414
- scope,
415
- target: input.target || input.projectPath || input.provider || null,
416
- createdAt: nowIso(),
417
- updatedAt: nowIso(),
418
- fingerprint: crypto.createHash('sha256').update(value).digest('hex').slice(0, 12),
419
- sealedValue: sealSecret(value),
420
- };
421
- store.secrets = store.secrets.filter((existing) => !(existing.envName === secret.envName && existing.scope === secret.scope && existing.target === secret.target));
422
- store.secrets.unshift(secret);
423
- addAudit(store, 'secret.created', actorId, { secretId: secret.id, scope: secret.scope, envName: secret.envName });
424
- writeStore(store);
425
- return redactSecret(secret);
426
- }
427
-
428
- export function listSecrets() {
429
- return readStore().secrets.map(redactSecret);
430
- }
431
-
432
- export function materializeScopedEnv(input = {}, options = {}) {
433
- const store = readStore();
434
- const env = {};
435
- const included = [];
436
- for (const secret of store.secrets) {
437
- if (!scopeMatches(secret, input)) continue;
438
- included.push({
439
- id: secret.id,
440
- envName: secret.envName,
441
- scope: secret.scope,
442
- target: secret.target,
443
- redacted: '********',
444
- });
445
- if (options.reveal === true) {
446
- env[secret.envName] = openSecret(secret.sealedValue);
447
- }
448
- }
449
- return { env, included };
450
- }
451
-
452
- export function upsertMarketplacePlugin(input = {}, actorId = null) {
453
- const pluginId = input.id || slugify(input.name || input.packageName || 'plugin');
454
- const store = readStore();
455
- const existing = store.marketplacePlugins.find((plugin) => plugin.id === pluginId);
456
- const plugin = {
457
- id: pluginId,
458
- name: compact(input.name || pluginId, 100),
459
- type: MARKETPLACE_PLUGIN_TYPES.includes(input.type) ? input.type : 'mcp-server',
460
- source: input.source || input.packageName || input.repository || null,
461
- permissionScopes: Array.isArray(input.permissionScopes) ? input.permissionScopes : [],
462
- installCommand: input.installCommand || null,
463
- status: input.status || existing?.status || 'available',
464
- health: input.health || existing?.health || { status: 'unknown', checkedAt: null },
465
- updatedAt: nowIso(),
466
- createdAt: existing?.createdAt || nowIso(),
467
- };
468
- store.marketplacePlugins = [plugin, ...store.marketplacePlugins.filter((item) => item.id !== pluginId)];
469
- addAudit(store, 'marketplace.plugin.upserted', actorId, { pluginId, type: plugin.type });
470
- writeStore(store);
471
- return plugin;
472
- }
473
-
474
- export function updateMarketplacePluginHealth(pluginId, health = {}, actorId = null) {
475
- const store = readStore();
476
- let updated = null;
477
- store.marketplacePlugins = store.marketplacePlugins.map((plugin) => {
478
- if (plugin.id !== pluginId) return plugin;
479
- updated = {
480
- ...plugin,
481
- health: {
482
- status: health.status || 'unknown',
483
- message: health.message || '',
484
- checkedAt: nowIso(),
485
- },
486
- updatedAt: nowIso(),
487
- };
488
- return updated;
489
- });
490
- if (updated) {
491
- addAudit(store, 'marketplace.plugin.health_checked', actorId, { pluginId, status: updated.health.status });
492
- writeStore(store);
493
- }
494
- return updated;
495
- }
496
-
497
- export function createEvaluationSuite(input = {}, actorId = null) {
498
- const tasks = Array.isArray(input.tasks) ? input.tasks : [];
499
- const suite = {
500
- id: input.id || slugify(input.name || 'evaluation-suite'),
501
- name: compact(input.name || 'Evaluation suite', 100),
502
- description: compact(input.description || '', 240),
503
- tasks: tasks.map((task, index) => ({
504
- id: task.id || `task-${index + 1}`,
505
- title: compact(task.title || `Task ${index + 1}`, 120),
506
- acceptanceCriteria: Array.isArray(task.acceptanceCriteria) ? task.acceptanceCriteria : [],
507
- projectPath: task.projectPath || null,
508
- })),
509
- createdAt: nowIso(),
510
- updatedAt: nowIso(),
511
- };
512
- const store = readStore();
513
- store.evaluationSuites = [suite, ...store.evaluationSuites.filter((item) => item.id !== suite.id)];
514
- addAudit(store, 'eval.suite.upserted', actorId, { suiteId: suite.id, tasks: suite.tasks.length });
515
- writeStore(store);
516
- return suite;
517
- }
518
-
519
- export function createEvaluationRun(input = {}, actorId = null) {
520
- const results = Array.isArray(input.results) ? input.results : [];
521
- const passed = results.filter((result) => result.status === 'passed').length;
522
- const run = {
523
- id: crypto.randomUUID(),
524
- suiteId: input.suiteId || null,
525
- provider: input.provider || null,
526
- model: input.model || null,
527
- status: input.status || 'completed',
528
- createdAt: nowIso(),
529
- results,
530
- summary: {
531
- total: results.length,
532
- passed,
533
- failed: results.filter((result) => result.status === 'failed').length,
534
- passRate: results.length ? Math.round((passed / results.length) * 1000) / 10 : 0,
535
- averageLatencyMs: average(results.map((result) => Number(result.latencyMs || 0)).filter(Boolean)),
536
- },
537
- };
538
- const store = readStore();
539
- store.evaluationRuns.unshift(run);
540
- addAudit(store, 'eval.run.created', actorId, { runId: run.id, suiteId: run.suiteId, passRate: run.summary.passRate });
541
- writeStore(store);
542
- return run;
543
- }
544
-
545
- function average(values) {
546
- if (!values.length) return 0;
547
- return Math.round(values.reduce((sum, value) => sum + value, 0) / values.length);
548
- }
549
-
550
- export function recordUsageEvent(input = {}, actorId = null) {
551
- const event = {
552
- id: crypto.randomUUID(),
553
- createdAt: input.createdAt || nowIso(),
554
- provider: input.provider || 'unknown',
555
- model: input.model || 'unknown',
556
- workflow: input.workflow || input.source || 'manual',
557
- inputTokens: Number(input.inputTokens || 0),
558
- outputTokens: Number(input.outputTokens || 0),
559
- costUsd: Number(input.costUsd || 0),
560
- latencyMs: Number(input.latencyMs || 0),
561
- status: input.status || 'ok',
562
- };
563
- const store = readStore();
564
- store.usageEvents.unshift(event);
565
- store.usageEvents = store.usageEvents.slice(0, 2000);
566
- addAudit(store, 'usage.event.recorded', actorId, { provider: event.provider, model: event.model, status: event.status });
567
- writeStore(store);
568
- return event;
569
- }
570
-
571
- export function summarizeUsageEvents(events = readStore().usageEvents) {
572
- const groups = new Map();
573
- for (const event of events) {
574
- const key = `${event.provider}:${event.model}:${event.workflow}`;
575
- const current = groups.get(key) || {
576
- provider: event.provider,
577
- model: event.model,
578
- workflow: event.workflow,
579
- runs: 0,
580
- errors: 0,
581
- inputTokens: 0,
582
- outputTokens: 0,
583
- totalTokens: 0,
584
- costUsd: 0,
585
- latencyMs: 0,
586
- };
587
- current.runs += 1;
588
- current.errors += event.status === 'error' ? 1 : 0;
589
- current.inputTokens += event.inputTokens;
590
- current.outputTokens += event.outputTokens;
591
- current.totalTokens += event.inputTokens + event.outputTokens;
592
- current.costUsd += event.costUsd;
593
- current.latencyMs += event.latencyMs;
594
- groups.set(key, current);
595
- }
596
- return Array.from(groups.values()).map((group) => ({
597
- ...group,
598
- costUsd: Math.round(group.costUsd * 10000) / 10000,
599
- averageLatencyMs: group.runs ? Math.round(group.latencyMs / group.runs) : 0,
600
- errorRate: group.runs ? Math.round((group.errors / group.runs) * 1000) / 10 : 0,
601
- latencyMs: undefined,
602
- }));
603
- }
604
-
605
- export function createSecurityAuditRun(input = {}, actorId = null) {
606
- const checks = Array.isArray(input.checks) && input.checks.length
607
- ? input.checks.filter((check) => SECURITY_AUDIT_CHECKS.includes(check))
608
- : SECURITY_AUDIT_CHECKS;
609
- const findings = Array.isArray(input.findings) ? input.findings : [];
610
- const run = {
611
- id: crypto.randomUUID(),
612
- protocol: 'pixcode.security-audit.v1',
613
- status: input.status || 'queued',
614
- projectName: input.projectName || null,
615
- projectPath: input.projectPath || null,
616
- checks,
617
- createdAt: nowIso(),
618
- findings: findings.map((finding, index) => ({
619
- id: finding.id || `finding-${index + 1}`,
620
- severity: finding.severity || 'medium',
621
- title: compact(finding.title || 'Security finding', 140),
622
- file: finding.file || null,
623
- recommendation: finding.recommendation || null,
624
- })),
625
- checklist: checks.map((check) => ({
626
- check,
627
- status: 'pending',
628
- })),
629
- };
630
- const store = readStore();
631
- store.securityAuditRuns.unshift(run);
632
- addAudit(store, 'security.audit.created', actorId, { runId: run.id, checks });
633
- writeStore(store);
634
- return run;
635
- }
636
-
637
- export function getAuditLog(filters = {}) {
638
- const store = readStore();
639
- let entries = store.auditLog;
640
- if (filters.userId) {
641
- entries = entries.filter((entry) => String(entry.actorId) === String(filters.userId));
642
- }
643
- if (filters.eventType) {
644
- entries = entries.filter((entry) => entry.action === filters.eventType || entry.action.includes(filters.eventType));
645
- }
646
- if (filters.projectName) {
647
- entries = entries.filter((entry) => entry.details?.projectName === filters.projectName);
648
- }
649
- if (filters.severity) {
650
- entries = entries.filter((entry) => entry.details?.severity === filters.severity);
651
- }
652
- return entries.slice(0, Number(filters.limit || 200));
653
- }
654
-
655
- export function exportAuditLog(format = 'json', filters = {}) {
656
- const entries = getAuditLog(filters);
657
- if (format === 'csv') {
658
- const header = ['id', 'createdAt', 'actorId', 'action', 'details'];
659
- const lines = entries.map((entry) => header.map((field) => {
660
- const value = field === 'details' ? JSON.stringify(entry.details || {}) : entry[field];
661
- return `"${String(value ?? '').replace(/"/g, '""')}"`;
662
- }).join(','));
663
- return [header.join(','), ...lines].join('\n');
664
- }
665
- return JSON.stringify(entries, null, 2);
666
- }
667
-
668
- function normalizeAccessMode(mode) {
669
- return ['lan', 'tailscale', 'cloudflare_tunnel', 'custom_domain'].includes(mode) ? mode : 'lan';
670
- }
671
-
672
- function normalizePublicUrl(value) {
673
- const raw = typeof value === 'string' ? value.trim() : '';
674
- if (!raw) return null;
675
- const url = new URL(raw);
676
- if (!['http:', 'https:'].includes(url.protocol)) {
677
- throw new Error('Remote access URL must use http or https.');
678
- }
679
- url.pathname = url.pathname.replace(/\/+$/, '');
680
- url.search = '';
681
- url.hash = '';
682
- return url.toString().replace(/\/$/, '');
683
- }
684
-
685
- export function saveRemoteAccessConfig(input = {}, actorId = null) {
686
- const mode = normalizeAccessMode(input.mode);
687
- const id = input.id || mode;
688
- const config = {
689
- id,
690
- mode,
691
- label: compact(input.label || mode.replace(/_/g, ' '), 80),
692
- url: input.url ? normalizePublicUrl(input.url) : null,
693
- targetPort: Number(input.targetPort || process.env.SERVER_PORT || 3001),
694
- public: mode === 'cloudflare_tunnel' || mode === 'custom_domain',
695
- tlsRequired: mode === 'cloudflare_tunnel' || mode === 'custom_domain',
696
- privateOnly: mode === 'tailscale' || mode === 'lan',
697
- status: input.status || 'configured',
698
- notes: compact(input.notes || '', 240),
699
- updatedAt: nowIso(),
700
- createdAt: input.createdAt || nowIso(),
701
- lastHealth: input.lastHealth || null,
702
- };
703
- const store = readStore();
704
- store.remoteAccessConfigs = [config, ...store.remoteAccessConfigs.filter((item) => item.id !== id)];
705
- addAudit(store, 'remote.access.configured', actorId, { mode, url: config.url, public: config.public });
706
- writeStore(store);
707
- return config;
708
- }
709
-
710
- export function getRemoteAccessState() {
711
- const store = readStore();
712
- return {
713
- host: os.hostname(),
714
- platform: os.platform(),
715
- localUrl: `http://127.0.0.1:${process.env.SERVER_PORT || 3001}`,
716
- configs: store.remoteAccessConfigs,
717
- recommendations: [
718
- {
719
- mode: 'tailscale',
720
- label: 'Tailscale private network',
721
- recommendedWhen: 'No stable domain, no public IP, private team access.',
722
- },
723
- {
724
- mode: 'cloudflare_tunnel',
725
- label: 'Cloudflare Tunnel',
726
- recommendedWhen: 'Stable public HTTPS URL without opening inbound ports.',
727
- },
728
- {
729
- mode: 'custom_domain',
730
- label: 'Custom domain / reverse proxy',
731
- recommendedWhen: 'Existing domain, reverse proxy, and TLS termination.',
732
- },
733
- ],
734
- };
735
- }
736
-
737
- export async function detectTailscaleStatus() {
738
- try {
739
- const { stdout } = await execFileAsync('tailscale', ['status', '--json'], { timeout: 5000 });
740
- const status = JSON.parse(stdout || '{}');
741
- const self = status.Self || {};
742
- const tailscaleIps = Array.isArray(self.TailscaleIPs) ? self.TailscaleIPs : [];
743
- return {
744
- installed: true,
745
- loggedIn: Boolean(self.ID || self.DNSName || tailscaleIps.length),
746
- backendState: status.BackendState || null,
747
- deviceName: self.HostName || os.hostname(),
748
- magicDnsName: self.DNSName || null,
749
- tailscaleIp: tailscaleIps[0] || null,
750
- pixcodeUrl: tailscaleIps[0] ? `http://${tailscaleIps[0]}:${process.env.SERVER_PORT || 3001}` : null,
751
- installUrl: 'https://tailscale.com/download',
752
- checkedAt: nowIso(),
753
- message: tailscaleIps[0] ? 'Tailscale is ready for private Pixcode access.' : 'Tailscale CLI is installed but no device IP was detected.',
754
- };
755
- } catch (error) {
756
- const isMissing = error?.code === 'ENOENT';
757
- return {
758
- installed: false,
759
- loggedIn: false,
760
- backendState: 'missing',
761
- deviceName: os.hostname(),
762
- magicDnsName: null,
763
- tailscaleIp: null,
764
- pixcodeUrl: null,
765
- installUrl: 'https://tailscale.com/download',
766
- checkedAt: nowIso(),
767
- message: isMissing
768
- ? 'Tailscale is optional. Use the LAN links now, or install Tailscale from Settings > Access for private team access without a public domain.'
769
- : (error?.message || 'Tailscale status could not be read.'),
770
- };
771
- }
772
- }
773
-
774
- export async function checkRemoteAccessHealth(input = {}, actorId = null) {
775
- const url = normalizePublicUrl(input.url || input.remoteUrl || '');
776
- const checkedAt = nowIso();
777
- if (!url) {
778
- throw new Error('Remote access health check requires a URL.');
779
- }
780
- const parsed = new URL(url);
781
- const controller = new AbortController();
782
- const timeout = setTimeout(() => controller.abort(), Number(input.timeoutMs || 5000));
783
- try {
784
- const response = await fetch(`${url}/api/auth/status`, { signal: controller.signal });
785
- const health = {
786
- url,
787
- reachable: response.ok,
788
- checkedAt,
789
- statusCode: response.status,
790
- https: parsed.protocol === 'https:',
791
- websocketExpected: true,
792
- message: response.ok ? 'Pixcode auth endpoint is reachable.' : `Pixcode returned HTTP ${response.status}.`,
793
- };
794
- const store = readStore();
795
- addAudit(store, 'remote.access.health_checked', actorId, { url, reachable: health.reachable, https: health.https });
796
- writeStore(store);
797
- return health;
798
- } catch (error) {
799
- const health = {
800
- url,
801
- reachable: false,
802
- checkedAt,
803
- statusCode: null,
804
- https: parsed.protocol === 'https:',
805
- websocketExpected: true,
806
- message: error?.name === 'AbortError' ? 'Health check timed out.' : (error?.message || 'Remote access URL is unreachable.'),
807
- };
808
- const store = readStore();
809
- addAudit(store, 'remote.access.health_checked', actorId, { url, reachable: false, https: health.https });
810
- writeStore(store);
811
- return health;
812
- } finally {
813
- clearTimeout(timeout);
814
- }
815
- }
429
+ role,
430
+ capabilities: {
431
+ ...capabilities,
432
+ ...(input.capabilities && typeof input.capabilities === 'object' ? input.capabilities : {}),
433
+ },
434
+ status: input.status || 'active',
435
+ createdAt: nowIso(),
436
+ updatedAt: nowIso(),
437
+ };
438
+ const store = readStore();
439
+ store.projectCollaborators.unshift(collaborator);
440
+ addAudit(store, 'project.collaborator.created', actorId, { collaboratorId: collaborator.id, projectName, userRef, role });
441
+ writeStore(store);
442
+ return collaborator;
443
+ }
444
+
445
+ export function updateProjectCollaborator(collaboratorId, patch = {}, actorId = null) {
446
+ const store = readStore();
447
+ let updated = null;
448
+ store.projectCollaborators = store.projectCollaborators.map((collaborator) => {
449
+ if (collaborator.id !== collaboratorId) return collaborator;
450
+ updated = {
451
+ ...collaborator,
452
+ ...patch,
453
+ id: collaborator.id,
454
+ capabilities: {
455
+ ...collaborator.capabilities,
456
+ ...(patch.capabilities && typeof patch.capabilities === 'object' ? patch.capabilities : {}),
457
+ },
458
+ updatedAt: nowIso(),
459
+ };
460
+ return updated;
461
+ });
462
+ if (updated) {
463
+ addAudit(store, 'project.collaborator.updated', actorId, { collaboratorId, role: updated.role, status: updated.status });
464
+ writeStore(store);
465
+ }
466
+ return updated;
467
+ }
468
+
469
+ export function createSecret(input = {}, actorId = null) {
470
+ const name = compact(input.name || input.envName || '');
471
+ const value = input.value;
472
+ if (!name || typeof value !== 'string') throw new Error('Secret name and string value are required.');
473
+ const scope = normalizeScope(input.scope || 'project');
474
+ const store = readStore();
475
+ const secret = {
476
+ id: crypto.randomUUID(),
477
+ name,
478
+ envName: compact(input.envName || name).replace(/[^A-Z0-9_]/gi, '_').toUpperCase(),
479
+ scope,
480
+ target: input.target || input.projectPath || input.provider || null,
481
+ createdAt: nowIso(),
482
+ updatedAt: nowIso(),
483
+ fingerprint: crypto.createHash('sha256').update(value).digest('hex').slice(0, 12),
484
+ sealedValue: sealSecret(value),
485
+ };
486
+ store.secrets = store.secrets.filter((existing) => !(existing.envName === secret.envName && existing.scope === secret.scope && existing.target === secret.target));
487
+ store.secrets.unshift(secret);
488
+ addAudit(store, 'secret.created', actorId, { secretId: secret.id, scope: secret.scope, envName: secret.envName });
489
+ writeStore(store);
490
+ return redactSecret(secret);
491
+ }
492
+
493
+ export function listSecrets() {
494
+ return readStore().secrets.map(redactSecret);
495
+ }
496
+
497
+ export function materializeScopedEnv(input = {}, options = {}) {
498
+ const store = readStore();
499
+ const env = {};
500
+ const included = [];
501
+ for (const secret of store.secrets) {
502
+ if (!scopeMatches(secret, input)) continue;
503
+ included.push({
504
+ id: secret.id,
505
+ envName: secret.envName,
506
+ scope: secret.scope,
507
+ target: secret.target,
508
+ redacted: '********',
509
+ });
510
+ if (options.reveal === true) {
511
+ env[secret.envName] = openSecret(secret.sealedValue);
512
+ }
513
+ }
514
+ return { env, included };
515
+ }
516
+
517
+ export function upsertMarketplacePlugin(input = {}, actorId = null) {
518
+ const pluginId = input.id || slugify(input.name || input.packageName || 'plugin');
519
+ const store = readStore();
520
+ const existing = store.marketplacePlugins.find((plugin) => plugin.id === pluginId);
521
+ const plugin = {
522
+ id: pluginId,
523
+ name: compact(input.name || pluginId, 100),
524
+ type: MARKETPLACE_PLUGIN_TYPES.includes(input.type) ? input.type : 'mcp-server',
525
+ source: input.source || input.packageName || input.repository || null,
526
+ permissionScopes: Array.isArray(input.permissionScopes) ? input.permissionScopes : [],
527
+ installCommand: input.installCommand || null,
528
+ status: input.status || existing?.status || 'available',
529
+ health: input.health || existing?.health || { status: 'unknown', checkedAt: null },
530
+ updatedAt: nowIso(),
531
+ createdAt: existing?.createdAt || nowIso(),
532
+ };
533
+ store.marketplacePlugins = [plugin, ...store.marketplacePlugins.filter((item) => item.id !== pluginId)];
534
+ addAudit(store, 'marketplace.plugin.upserted', actorId, { pluginId, type: plugin.type });
535
+ writeStore(store);
536
+ return plugin;
537
+ }
538
+
539
+ export function updateMarketplacePluginHealth(pluginId, health = {}, actorId = null) {
540
+ const store = readStore();
541
+ let updated = null;
542
+ store.marketplacePlugins = store.marketplacePlugins.map((plugin) => {
543
+ if (plugin.id !== pluginId) return plugin;
544
+ updated = {
545
+ ...plugin,
546
+ health: {
547
+ status: health.status || 'unknown',
548
+ message: health.message || '',
549
+ checkedAt: nowIso(),
550
+ },
551
+ updatedAt: nowIso(),
552
+ };
553
+ return updated;
554
+ });
555
+ if (updated) {
556
+ addAudit(store, 'marketplace.plugin.health_checked', actorId, { pluginId, status: updated.health.status });
557
+ writeStore(store);
558
+ }
559
+ return updated;
560
+ }
561
+
562
+ export function createEvaluationSuite(input = {}, actorId = null) {
563
+ const tasks = Array.isArray(input.tasks) ? input.tasks : [];
564
+ const suite = {
565
+ id: input.id || slugify(input.name || 'evaluation-suite'),
566
+ name: compact(input.name || 'Evaluation suite', 100),
567
+ description: compact(input.description || '', 240),
568
+ tasks: tasks.map((task, index) => ({
569
+ id: task.id || `task-${index + 1}`,
570
+ title: compact(task.title || `Task ${index + 1}`, 120),
571
+ acceptanceCriteria: Array.isArray(task.acceptanceCriteria) ? task.acceptanceCriteria : [],
572
+ projectPath: task.projectPath || null,
573
+ })),
574
+ createdAt: nowIso(),
575
+ updatedAt: nowIso(),
576
+ };
577
+ const store = readStore();
578
+ store.evaluationSuites = [suite, ...store.evaluationSuites.filter((item) => item.id !== suite.id)];
579
+ addAudit(store, 'eval.suite.upserted', actorId, { suiteId: suite.id, tasks: suite.tasks.length });
580
+ writeStore(store);
581
+ return suite;
582
+ }
583
+
584
+ export function createEvaluationRun(input = {}, actorId = null) {
585
+ const results = Array.isArray(input.results) ? input.results : [];
586
+ const passed = results.filter((result) => result.status === 'passed').length;
587
+ const run = {
588
+ id: crypto.randomUUID(),
589
+ suiteId: input.suiteId || null,
590
+ provider: input.provider || null,
591
+ model: input.model || null,
592
+ status: input.status || 'completed',
593
+ createdAt: nowIso(),
594
+ results,
595
+ summary: {
596
+ total: results.length,
597
+ passed,
598
+ failed: results.filter((result) => result.status === 'failed').length,
599
+ passRate: results.length ? Math.round((passed / results.length) * 1000) / 10 : 0,
600
+ averageLatencyMs: average(results.map((result) => Number(result.latencyMs || 0)).filter(Boolean)),
601
+ },
602
+ };
603
+ const store = readStore();
604
+ store.evaluationRuns.unshift(run);
605
+ addAudit(store, 'eval.run.created', actorId, { runId: run.id, suiteId: run.suiteId, passRate: run.summary.passRate });
606
+ writeStore(store);
607
+ return run;
608
+ }
609
+
610
+ function average(values) {
611
+ if (!values.length) return 0;
612
+ return Math.round(values.reduce((sum, value) => sum + value, 0) / values.length);
613
+ }
614
+
615
+ export function recordUsageEvent(input = {}, actorId = null) {
616
+ const event = {
617
+ id: crypto.randomUUID(),
618
+ createdAt: input.createdAt || nowIso(),
619
+ provider: input.provider || 'unknown',
620
+ model: input.model || 'unknown',
621
+ workflow: input.workflow || input.source || 'manual',
622
+ inputTokens: Number(input.inputTokens || 0),
623
+ outputTokens: Number(input.outputTokens || 0),
624
+ costUsd: Number(input.costUsd || 0),
625
+ latencyMs: Number(input.latencyMs || 0),
626
+ status: input.status || 'ok',
627
+ };
628
+ const store = readStore();
629
+ store.usageEvents.unshift(event);
630
+ store.usageEvents = store.usageEvents.slice(0, 2000);
631
+ addAudit(store, 'usage.event.recorded', actorId, { provider: event.provider, model: event.model, status: event.status });
632
+ writeStore(store);
633
+ return event;
634
+ }
635
+
636
+ export function summarizeUsageEvents(events = readStore().usageEvents) {
637
+ const groups = new Map();
638
+ for (const event of events) {
639
+ const key = `${event.provider}:${event.model}:${event.workflow}`;
640
+ const current = groups.get(key) || {
641
+ provider: event.provider,
642
+ model: event.model,
643
+ workflow: event.workflow,
644
+ runs: 0,
645
+ errors: 0,
646
+ inputTokens: 0,
647
+ outputTokens: 0,
648
+ totalTokens: 0,
649
+ costUsd: 0,
650
+ latencyMs: 0,
651
+ };
652
+ current.runs += 1;
653
+ current.errors += event.status === 'error' ? 1 : 0;
654
+ current.inputTokens += event.inputTokens;
655
+ current.outputTokens += event.outputTokens;
656
+ current.totalTokens += event.inputTokens + event.outputTokens;
657
+ current.costUsd += event.costUsd;
658
+ current.latencyMs += event.latencyMs;
659
+ groups.set(key, current);
660
+ }
661
+ return Array.from(groups.values()).map((group) => ({
662
+ ...group,
663
+ costUsd: Math.round(group.costUsd * 10000) / 10000,
664
+ averageLatencyMs: group.runs ? Math.round(group.latencyMs / group.runs) : 0,
665
+ errorRate: group.runs ? Math.round((group.errors / group.runs) * 1000) / 10 : 0,
666
+ latencyMs: undefined,
667
+ }));
668
+ }
669
+
670
+ export function createSecurityAuditRun(input = {}, actorId = null) {
671
+ const checks = Array.isArray(input.checks) && input.checks.length
672
+ ? input.checks.filter((check) => SECURITY_AUDIT_CHECKS.includes(check))
673
+ : SECURITY_AUDIT_CHECKS;
674
+ const findings = Array.isArray(input.findings) ? input.findings : [];
675
+ const run = {
676
+ id: crypto.randomUUID(),
677
+ protocol: 'pixcode.security-audit.v1',
678
+ status: input.status || 'queued',
679
+ projectName: input.projectName || null,
680
+ projectPath: input.projectPath || null,
681
+ checks,
682
+ createdAt: nowIso(),
683
+ findings: findings.map((finding, index) => ({
684
+ id: finding.id || `finding-${index + 1}`,
685
+ severity: finding.severity || 'medium',
686
+ title: compact(finding.title || 'Security finding', 140),
687
+ file: finding.file || null,
688
+ recommendation: finding.recommendation || null,
689
+ })),
690
+ checklist: checks.map((check) => ({
691
+ check,
692
+ status: 'pending',
693
+ })),
694
+ };
695
+ const store = readStore();
696
+ store.securityAuditRuns.unshift(run);
697
+ addAudit(store, 'security.audit.created', actorId, { runId: run.id, checks });
698
+ writeStore(store);
699
+ return run;
700
+ }
701
+
702
+ export function getAuditLog(filters = {}) {
703
+ const store = readStore();
704
+ let entries = store.auditLog;
705
+ if (filters.userId) {
706
+ entries = entries.filter((entry) => String(entry.actorId) === String(filters.userId));
707
+ }
708
+ if (filters.eventType) {
709
+ entries = entries.filter((entry) => entry.action === filters.eventType || entry.action.includes(filters.eventType));
710
+ }
711
+ if (filters.projectName) {
712
+ entries = entries.filter((entry) => entry.details?.projectName === filters.projectName);
713
+ }
714
+ if (filters.severity) {
715
+ entries = entries.filter((entry) => entry.details?.severity === filters.severity);
716
+ }
717
+ return entries.slice(0, Number(filters.limit || 200));
718
+ }
719
+
720
+ export function exportAuditLog(format = 'json', filters = {}) {
721
+ const entries = getAuditLog(filters);
722
+ if (format === 'csv') {
723
+ const header = ['id', 'createdAt', 'actorId', 'action', 'details'];
724
+ const lines = entries.map((entry) => header.map((field) => {
725
+ const value = field === 'details' ? JSON.stringify(entry.details || {}) : entry[field];
726
+ return `"${String(value ?? '').replace(/"/g, '""')}"`;
727
+ }).join(','));
728
+ return [header.join(','), ...lines].join('\n');
729
+ }
730
+ return JSON.stringify(entries, null, 2);
731
+ }
732
+
733
+ function normalizeAccessMode(mode) {
734
+ return ['lan', 'tailscale', 'cloudflare_tunnel', 'custom_domain'].includes(mode) ? mode : 'lan';
735
+ }
736
+
737
+ function normalizePublicUrl(value) {
738
+ const raw = typeof value === 'string' ? value.trim() : '';
739
+ if (!raw) return null;
740
+ const url = new URL(raw);
741
+ if (!['http:', 'https:'].includes(url.protocol)) {
742
+ throw new Error('Remote access URL must use http or https.');
743
+ }
744
+ url.pathname = url.pathname.replace(/\/+$/, '');
745
+ url.search = '';
746
+ url.hash = '';
747
+ return url.toString().replace(/\/$/, '');
748
+ }
749
+
750
+ export function saveRemoteAccessConfig(input = {}, actorId = null) {
751
+ const mode = normalizeAccessMode(input.mode);
752
+ const id = input.id || mode;
753
+ const config = {
754
+ id,
755
+ mode,
756
+ label: compact(input.label || mode.replace(/_/g, ' '), 80),
757
+ url: input.url ? normalizePublicUrl(input.url) : null,
758
+ targetPort: Number(input.targetPort || process.env.SERVER_PORT || 3001),
759
+ public: mode === 'cloudflare_tunnel' || mode === 'custom_domain',
760
+ tlsRequired: mode === 'cloudflare_tunnel' || mode === 'custom_domain',
761
+ privateOnly: mode === 'tailscale' || mode === 'lan',
762
+ status: input.status || 'configured',
763
+ notes: compact(input.notes || '', 240),
764
+ updatedAt: nowIso(),
765
+ createdAt: input.createdAt || nowIso(),
766
+ lastHealth: input.lastHealth || null,
767
+ };
768
+ const store = readStore();
769
+ store.remoteAccessConfigs = [config, ...store.remoteAccessConfigs.filter((item) => item.id !== id)];
770
+ addAudit(store, 'remote.access.configured', actorId, { mode, url: config.url, public: config.public });
771
+ writeStore(store);
772
+ return config;
773
+ }
774
+
775
+ export function getRemoteAccessState() {
776
+ const store = readStore();
777
+ return {
778
+ host: os.hostname(),
779
+ platform: os.platform(),
780
+ localUrl: `http://127.0.0.1:${process.env.SERVER_PORT || 3001}`,
781
+ configs: store.remoteAccessConfigs,
782
+ recommendations: [
783
+ {
784
+ mode: 'tailscale',
785
+ label: 'Tailscale private network',
786
+ recommendedWhen: 'No stable domain, no public IP, private team access.',
787
+ },
788
+ {
789
+ mode: 'cloudflare_tunnel',
790
+ label: 'Cloudflare Tunnel',
791
+ recommendedWhen: 'Stable public HTTPS URL without opening inbound ports.',
792
+ },
793
+ {
794
+ mode: 'custom_domain',
795
+ label: 'Custom domain / reverse proxy',
796
+ recommendedWhen: 'Existing domain, reverse proxy, and TLS termination.',
797
+ },
798
+ ],
799
+ };
800
+ }
801
+
802
+ export async function detectTailscaleStatus() {
803
+ try {
804
+ const { stdout } = await execFileAsync('tailscale', ['status', '--json'], { timeout: 5000 });
805
+ const status = JSON.parse(stdout || '{}');
806
+ const self = status.Self || {};
807
+ const tailscaleIps = Array.isArray(self.TailscaleIPs) ? self.TailscaleIPs : [];
808
+ return {
809
+ installed: true,
810
+ loggedIn: Boolean(self.ID || self.DNSName || tailscaleIps.length),
811
+ backendState: status.BackendState || null,
812
+ deviceName: self.HostName || os.hostname(),
813
+ magicDnsName: self.DNSName || null,
814
+ tailscaleIp: tailscaleIps[0] || null,
815
+ pixcodeUrl: tailscaleIps[0] ? `http://${tailscaleIps[0]}:${process.env.SERVER_PORT || 3001}` : null,
816
+ installUrl: 'https://tailscale.com/download',
817
+ checkedAt: nowIso(),
818
+ message: tailscaleIps[0] ? 'Tailscale is ready for private Pixcode access.' : 'Tailscale CLI is installed but no device IP was detected.',
819
+ };
820
+ } catch (error) {
821
+ const isMissing = error?.code === 'ENOENT';
822
+ return {
823
+ installed: false,
824
+ loggedIn: false,
825
+ backendState: 'missing',
826
+ deviceName: os.hostname(),
827
+ magicDnsName: null,
828
+ tailscaleIp: null,
829
+ pixcodeUrl: null,
830
+ installUrl: 'https://tailscale.com/download',
831
+ checkedAt: nowIso(),
832
+ message: isMissing
833
+ ? 'Tailscale is optional. Use the LAN links now, or install Tailscale from Settings > Access for private team access without a public domain.'
834
+ : (error?.message || 'Tailscale status could not be read.'),
835
+ };
836
+ }
837
+ }
838
+
839
+ export async function checkRemoteAccessHealth(input = {}, actorId = null) {
840
+ const url = normalizePublicUrl(input.url || input.remoteUrl || '');
841
+ const checkedAt = nowIso();
842
+ if (!url) {
843
+ throw new Error('Remote access health check requires a URL.');
844
+ }
845
+ const parsed = new URL(url);
846
+ const controller = new AbortController();
847
+ const timeout = setTimeout(() => controller.abort(), Number(input.timeoutMs || 5000));
848
+ try {
849
+ const response = await fetch(`${url}/api/auth/status`, { signal: controller.signal });
850
+ const health = {
851
+ url,
852
+ reachable: response.ok,
853
+ checkedAt,
854
+ statusCode: response.status,
855
+ https: parsed.protocol === 'https:',
856
+ websocketExpected: true,
857
+ message: response.ok ? 'Pixcode auth endpoint is reachable.' : `Pixcode returned HTTP ${response.status}.`,
858
+ };
859
+ const store = readStore();
860
+ addAudit(store, 'remote.access.health_checked', actorId, { url, reachable: health.reachable, https: health.https });
861
+ writeStore(store);
862
+ return health;
863
+ } catch (error) {
864
+ const health = {
865
+ url,
866
+ reachable: false,
867
+ checkedAt,
868
+ statusCode: null,
869
+ https: parsed.protocol === 'https:',
870
+ websocketExpected: true,
871
+ message: error?.name === 'AbortError' ? 'Health check timed out.' : (error?.message || 'Remote access URL is unreachable.'),
872
+ };
873
+ const store = readStore();
874
+ addAudit(store, 'remote.access.health_checked', actorId, { url, reachable: false, https: health.https });
875
+ writeStore(store);
876
+ return health;
877
+ } finally {
878
+ clearTimeout(timeout);
879
+ }
880
+ }