@pixelbyte-software/pixcode 1.54.9 → 1.54.11
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.
- package/CODE_OF_CONDUCT.md +41 -41
- package/CONTRIBUTING.md +156 -156
- package/LICENSE +21 -718
- package/README.de.md +169 -169
- package/README.ja.md +167 -167
- package/README.ko.md +167 -167
- package/README.md +435 -418
- package/README.ru.md +169 -169
- package/README.tr.md +298 -298
- package/README.zh-CN.md +167 -167
- package/SECURITY.md +46 -46
- package/dist/api-automation.html +110 -110
- package/dist/api-docs.html +548 -548
- package/dist/assets/{index-Bm6uAqKU.js → index-Bcqs_1vc.js} +114 -114
- package/dist/clear-cache.html +85 -85
- package/dist/convert-icons.md +52 -52
- package/dist/docs.html +308 -308
- package/dist/features.html +133 -133
- package/dist/generate-icons.js +48 -48
- package/dist/humans.txt +15 -15
- package/dist/icons/codex-white.svg +3 -3
- package/dist/icons/codex.svg +3 -3
- package/dist/icons/cursor-white.svg +11 -11
- package/dist/icons/qwen-logo.svg +14 -14
- package/dist/index.html +58 -58
- package/dist/landing.html +268 -268
- package/dist/llms-full.txt +119 -119
- package/dist/llms.txt +53 -53
- package/dist/manifest.json +60 -60
- package/dist/openapi.yaml +1696 -1696
- package/dist/orchestration.html +125 -125
- package/dist/robots.txt +4 -4
- package/dist/site.css +692 -692
- package/dist/sitemap.xml +51 -51
- package/dist/sw.js +132 -132
- package/dist-server/server/cli.js +100 -100
- package/dist-server/server/daemon/manager.js +33 -33
- package/dist-server/server/daemon-manager.js +64 -64
- package/dist-server/server/index.js +13 -0
- package/dist-server/server/index.js.map +1 -1
- package/dist-server/server/routes/agent.js +10 -1
- package/dist-server/server/routes/agent.js.map +1 -1
- package/dist-server/server/routes/commands.js +25 -25
- package/dist-server/server/routes/git.js +17 -17
- package/dist-server/server/routes/live-view.js +46 -46
- package/dist-server/server/services/public-api-manifest.js +51 -51
- package/dist-server/server/sessionManager.js +5 -5
- package/dist-server/server/sessionManager.js.map +1 -1
- package/package.json +224 -224
- package/scripts/fix-node-pty.js +67 -67
- package/scripts/github/create-v1.38-issues.mjs +351 -351
- package/scripts/github/create-vscode-workbench-issues.mjs +121 -121
- package/scripts/smoke/backend-resource-bounds.mjs +152 -152
- package/scripts/smoke/changes-panel-layout.mjs +48 -48
- package/scripts/smoke/chat-composer-fixed-layout.mjs +55 -55
- package/scripts/smoke/chat-message-timeline-order.mjs +41 -41
- package/scripts/smoke/chat-realtime-hydration.mjs +44 -44
- package/scripts/smoke/chat-session-provider-pools.mjs +35 -35
- package/scripts/smoke/chat-session-state.mjs +19 -19
- package/scripts/smoke/code-editor-theme.mjs +55 -55
- package/scripts/smoke/code-editor-vscode-engine.mjs +91 -91
- package/scripts/smoke/command-center-agent-writes.mjs +79 -79
- package/scripts/smoke/command-center-non-git.mjs +46 -46
- package/scripts/smoke/context-packet.mjs +43 -43
- package/scripts/smoke/control-room-ux-redesign.mjs +91 -91
- package/scripts/smoke/daemon-entrypoint.mjs +20 -20
- package/scripts/smoke/default-landing-routing.mjs +33 -33
- package/scripts/smoke/desktop-native-notifications.mjs +30 -30
- package/scripts/smoke/desktop-tray-icon.mjs +33 -33
- package/scripts/smoke/discord-release-workflow.mjs +24 -24
- package/scripts/smoke/git-install-update.mjs +255 -255
- package/scripts/smoke/handoff-artifact-protocol.mjs +50 -50
- package/scripts/smoke/live-view-diagnostics.mjs +53 -53
- package/scripts/smoke/live-view-environment.mjs +92 -92
- package/scripts/smoke/live-view-integration.mjs +450 -450
- package/scripts/smoke/mac-desktop-runtime.mjs +37 -37
- package/scripts/smoke/mobile-tunnel-guidance.mjs +29 -29
- package/scripts/smoke/mobile-ux.mjs +76 -76
- package/scripts/smoke/model-registry.mjs +36 -36
- package/scripts/smoke/multi-project-ui.mjs +45 -45
- package/scripts/smoke/multi-worker-slots.mjs +42 -42
- package/scripts/smoke/notification-center.mjs +87 -87
- package/scripts/smoke/notification-inapp-preference.mjs +23 -23
- package/scripts/smoke/notification-taxonomy.mjs +58 -58
- package/scripts/smoke/orchestration-api.mjs +172 -172
- package/scripts/smoke/orchestration-execution-dashboard.mjs +33 -33
- package/scripts/smoke/orchestration-live-run.mjs +176 -176
- package/scripts/smoke/orchestration-mobile-scroll.mjs +29 -29
- package/scripts/smoke/orchestration-model-sync.mjs +30 -30
- package/scripts/smoke/orchestration-permission-fallback.mjs +34 -34
- package/scripts/smoke/orchestration-runtime-guards.mjs +48 -48
- package/scripts/smoke/orchestration-user-facing-output.mjs +25 -25
- package/scripts/smoke/permission-policy.mjs +50 -50
- package/scripts/smoke/pixcode-workbench-1-48.mjs +167 -167
- package/scripts/smoke/provider-models-opencode-live.mjs +66 -66
- package/scripts/smoke/provider-rest-api.mjs +124 -124
- package/scripts/smoke/provider-selection-status.mjs +52 -52
- package/scripts/smoke/run-state-refresh.mjs +52 -52
- package/scripts/smoke/runtime-manager.mjs +99 -99
- package/scripts/smoke/shell-manual-disconnect.mjs +30 -30
- package/scripts/smoke/side-panel-editor-layout.mjs +34 -34
- package/scripts/smoke/static-root-routing.mjs +21 -21
- package/scripts/smoke/strict-handoff-compact.mjs +60 -60
- package/scripts/smoke/taskmaster-config.mjs +24 -24
- package/scripts/smoke/taskmaster-execution-telegram.mjs +3 -3
- package/scripts/smoke/taskmaster-onboarding.mjs +3 -3
- package/scripts/smoke/taskmaster-run-graph.mjs +3 -3
- package/scripts/smoke/telegram-control.mjs +331 -331
- package/scripts/smoke/tunnel-persistence.mjs +56 -56
- package/scripts/smoke/update-issue-progress.mjs +69 -69
- package/scripts/smoke/update-ux.mjs +55 -55
- package/scripts/smoke/v138-completion.mjs +132 -132
- package/scripts/smoke/v138-desktop-release-hardening.mjs +69 -69
- package/scripts/smoke/v138-diagnostics.mjs +63 -63
- package/scripts/smoke/v138-issue-planner.mjs +33 -33
- package/scripts/smoke/v143-remote-control.mjs +76 -76
- package/scripts/smoke/v144-production-loop.mjs +47 -47
- package/scripts/smoke/v145-platformization.mjs +46 -46
- package/scripts/smoke/v146-control-room-ui.mjs +150 -150
- package/scripts/smoke/version-modal-autoshow.mjs +29 -29
- package/scripts/smoke/vscode-workbench-layout.mjs +63 -63
- package/scripts/smoke/vscode-workbench-polish.mjs +461 -461
- package/scripts/smoke/workflow-fallback-replay.mjs +56 -56
- package/scripts/smoke/workflow-templates.mjs +43 -43
- package/scripts/smoke/workflow-trace-timeline.mjs +46 -46
- package/scripts/update-git-install.mjs +298 -298
- package/server/claude-sdk.js +927 -927
- package/server/cli.js +1106 -1106
- package/server/constants/config.js +4 -4
- package/server/cursor-cli.js +344 -344
- package/server/daemon/manager.js +563 -563
- package/server/daemon-manager.js +964 -964
- package/server/database/db.js +988 -988
- package/server/database/json-store.js +197 -197
- package/server/gemini-cli.js +550 -550
- package/server/gemini-response-handler.js +79 -79
- package/server/index.js +5671 -5659
- package/server/load-env.js +35 -35
- package/server/middleware/account-lockout.js +112 -112
- package/server/middleware/auth.js +277 -277
- package/server/middleware/rate-limiter.js +87 -87
- package/server/modules/orchestration/a2a/adapter-registry.ts +108 -108
- package/server/modules/orchestration/a2a/adapters/abstract-a2a.adapter.ts +63 -63
- package/server/modules/orchestration/a2a/adapters/claude-code.adapter.ts +286 -286
- package/server/modules/orchestration/a2a/adapters/codex.adapter.ts +244 -244
- package/server/modules/orchestration/a2a/adapters/cursor.adapter.ts +249 -249
- package/server/modules/orchestration/a2a/adapters/gemini.adapter.ts +248 -248
- package/server/modules/orchestration/a2a/adapters/json-event.adapter.test.ts +60 -60
- package/server/modules/orchestration/a2a/adapters/json-event.adapter.ts +101 -101
- package/server/modules/orchestration/a2a/adapters/opencode.adapter.ts +248 -248
- package/server/modules/orchestration/a2a/adapters/qwen.adapter.ts +248 -248
- package/server/modules/orchestration/a2a/agent-card.ts +55 -55
- package/server/modules/orchestration/a2a/auth.middleware.ts +29 -29
- package/server/modules/orchestration/a2a/bus.ts +46 -46
- package/server/modules/orchestration/a2a/routes.ts +586 -586
- package/server/modules/orchestration/a2a/task-dispatcher.ts +345 -345
- package/server/modules/orchestration/a2a/task-store.ts +178 -178
- package/server/modules/orchestration/a2a/types.ts +126 -126
- package/server/modules/orchestration/a2a/validator.ts +113 -113
- package/server/modules/orchestration/index.ts +99 -99
- package/server/modules/orchestration/preview/port-watcher.ts +112 -112
- package/server/modules/orchestration/preview/preview-proxy.ts +60 -60
- package/server/modules/orchestration/preview/types.ts +19 -19
- package/server/modules/orchestration/security/permission-policy.ts +401 -401
- package/server/modules/orchestration/tasks/orchestration-task-store.ts +41 -41
- package/server/modules/orchestration/tasks/orchestration-task.routes.ts +81 -81
- package/server/modules/orchestration/tasks/orchestration-task.service.ts +201 -201
- package/server/modules/orchestration/tasks/orchestration-task.types.ts +40 -40
- package/server/modules/orchestration/tasks/task-run-graph.ts +155 -155
- package/server/modules/orchestration/workflows/approval-queue.ts +106 -106
- package/server/modules/orchestration/workflows/built-in-workflows.ts +127 -127
- package/server/modules/orchestration/workflows/context-packet.ts +186 -186
- package/server/modules/orchestration/workflows/handoff-artifact.ts +175 -175
- package/server/modules/orchestration/workflows/workflow-fallback-policy.ts +161 -161
- package/server/modules/orchestration/workflows/workflow-replay.ts +254 -254
- package/server/modules/orchestration/workflows/workflow-runner.ts +2062 -2062
- package/server/modules/orchestration/workflows/workflow-store.ts +97 -97
- package/server/modules/orchestration/workflows/workflow-templates.ts +272 -272
- package/server/modules/orchestration/workflows/workflow-trace.ts +424 -424
- package/server/modules/orchestration/workflows/workflow.routes.ts +586 -586
- package/server/modules/orchestration/workflows/workflow.types.ts +111 -111
- package/server/modules/orchestration/workflows/workspace-target.ts +122 -122
- package/server/modules/orchestration/workspace/docker-workspace.ts +136 -136
- package/server/modules/orchestration/workspace/path-safety.ts +55 -55
- package/server/modules/orchestration/workspace/types.ts +52 -52
- package/server/modules/orchestration/workspace/workspace-manager.ts +102 -102
- package/server/modules/orchestration/workspace/worktree-workspace.ts +126 -126
- package/server/modules/providers/index.ts +2 -2
- package/server/modules/providers/list/claude/claude-auth.provider.ts +146 -146
- package/server/modules/providers/list/claude/claude-mcp.provider.ts +135 -135
- package/server/modules/providers/list/claude/claude-sessions.provider.ts +306 -306
- package/server/modules/providers/list/claude/claude.provider.ts +15 -15
- package/server/modules/providers/list/codex/codex-auth.provider.ts +117 -117
- package/server/modules/providers/list/codex/codex-mcp.provider.ts +135 -135
- package/server/modules/providers/list/codex/codex-sessions.provider.ts +319 -319
- package/server/modules/providers/list/codex/codex.provider.ts +15 -15
- package/server/modules/providers/list/cursor/cursor-auth.provider.ts +147 -147
- package/server/modules/providers/list/cursor/cursor-mcp.provider.ts +108 -108
- package/server/modules/providers/list/cursor/cursor-sessions.provider.ts +421 -421
- package/server/modules/providers/list/cursor/cursor.provider.ts +15 -15
- package/server/modules/providers/list/gemini/gemini-auth.provider.ts +173 -173
- package/server/modules/providers/list/gemini/gemini-mcp.provider.ts +110 -110
- package/server/modules/providers/list/gemini/gemini-sessions.provider.ts +227 -227
- package/server/modules/providers/list/gemini/gemini.provider.ts +15 -15
- package/server/modules/providers/list/opencode/opencode-auth.provider.ts +131 -131
- package/server/modules/providers/list/opencode/opencode-sessions.provider.ts +286 -286
- package/server/modules/providers/list/qwen/qwen-auth.provider.ts +146 -146
- package/server/modules/providers/list/qwen/qwen-sessions.provider.ts +265 -265
- package/server/modules/providers/provider.registry.ts +40 -40
- package/server/modules/providers/provider.routes.ts +982 -982
- package/server/modules/providers/services/mcp.service.ts +86 -86
- package/server/modules/providers/services/provider-auth.service.ts +26 -26
- package/server/modules/providers/services/sessions.service.ts +45 -45
- package/server/modules/providers/shared/base/abstract.provider.ts +20 -20
- package/server/modules/providers/shared/mcp/mcp.provider.ts +151 -151
- package/server/modules/providers/tests/mcp.test.ts +293 -293
- package/server/openai-codex.js +468 -468
- package/server/opencode-cli.js +491 -491
- package/server/opencode-response-handler.js +111 -111
- package/server/projects.js +3135 -3135
- package/server/qwen-code-cli.js +410 -410
- package/server/routes/agent.js +1462 -1451
- package/server/routes/auth.js +298 -298
- package/server/routes/codex.js +20 -20
- package/server/routes/commands.js +581 -581
- package/server/routes/cursor.js +61 -61
- package/server/routes/diagnostics.js +44 -44
- package/server/routes/gemini.js +25 -25
- package/server/routes/git.js +1822 -1822
- package/server/routes/live-view.js +434 -434
- package/server/routes/mcp-utils.js +13 -13
- package/server/routes/messages.js +97 -97
- package/server/routes/network.js +143 -143
- package/server/routes/platformization.js +231 -231
- package/server/routes/plugins.js +690 -690
- package/server/routes/production-agent-loop.js +90 -90
- package/server/routes/projects.js +918 -918
- package/server/routes/public-api.js +34 -34
- package/server/routes/qwen.js +27 -27
- package/server/routes/remote.js +56 -56
- package/server/routes/settings.js +321 -321
- package/server/routes/telegram.js +163 -163
- package/server/routes/user.js +125 -125
- package/server/routes/webhooks.js +63 -63
- package/server/services/control-room.js +102 -102
- package/server/services/diagnostics.js +165 -165
- package/server/services/external-access.js +403 -403
- package/server/services/install-jobs.js +715 -715
- package/server/services/live-view.js +956 -956
- package/server/services/managed-runtimes.js +493 -493
- package/server/services/model-registry.js +144 -144
- package/server/services/notification-orchestrator.js +365 -365
- package/server/services/notification-taxonomy.js +204 -204
- package/server/services/platformization.js +1052 -1052
- package/server/services/production-agent-loop.js +248 -248
- package/server/services/provider-cli-versions.js +149 -149
- package/server/services/provider-credentials.js +189 -189
- package/server/services/provider-models.js +396 -396
- package/server/services/public-api-manifest.js +181 -181
- package/server/services/qr-login.js +126 -126
- package/server/services/remote-connection.js +127 -127
- package/server/services/runtime-manager.js +323 -323
- package/server/services/startup-update.js +259 -259
- package/server/services/telegram/bot.js +393 -393
- package/server/services/telegram/control-center.js +2453 -2453
- package/server/services/telegram/telegram-gateway.js +314 -314
- package/server/services/telegram/telegram-http-client.js +201 -201
- package/server/services/telegram/translations.js +470 -470
- package/server/services/webhooks.js +216 -216
- package/server/sessionManager.js +225 -225
- package/server/setup-wizard.js +283 -283
- package/server/shared/interfaces.ts +54 -54
- package/server/shared/types.ts +172 -172
- package/server/shared/utils.ts +193 -193
- package/server/tsconfig.json +36 -36
- package/server/utils/colors.js +21 -21
- package/server/utils/commandParser.js +305 -305
- package/server/utils/frontmatter.js +18 -18
- package/server/utils/gitConfig.js +34 -34
- package/server/utils/plugin-loader.js +461 -461
- package/server/utils/plugin-process-manager.js +185 -185
- package/server/utils/port-access.js +227 -227
- package/server/utils/runtime-paths.js +37 -37
- package/server/utils/security-log.js +84 -84
- package/server/utils/url-detection.js +71 -71
- package/server/vite-daemon.js +79 -79
- package/shared/modelConstants.js +161 -161
- package/shared/networkHosts.js +22 -22
|
@@ -1,181 +1,181 @@
|
|
|
1
|
-
const API_GROUPS = [
|
|
2
|
-
{ id: 'auth', title: 'Authentication', basePath: '/api/auth', scopes: ['auth:read', 'auth:write'] },
|
|
3
|
-
{ id: 'projects', title: 'Projects', basePath: '/api/projects', scopes: ['projects:read', 'projects:write'] },
|
|
4
|
-
{ id: 'sessions', title: 'Sessions and messages', basePath: '/api/sessions', scopes: ['sessions:read', 'sessions:write'] },
|
|
5
|
-
{ id: 'providers', title: 'CLI providers', basePath: '/api/providers', scopes: ['providers:read', 'providers:write'] },
|
|
6
|
-
{ id: 'terminal', title: 'Visible terminal sessions', basePath: '/api/shell/sessions', scopes: ['terminal:launch'] },
|
|
7
|
-
{ id: 'orchestration', title: 'Orchestration runs', basePath: '/api/orchestration', scopes: ['orchestration:read', 'orchestration:write'] },
|
|
8
|
-
{ id: 'notifications', title: 'Notifications', basePath: '/api/settings/notifications', scopes: ['notifications:read', 'notifications:write'] },
|
|
9
|
-
{ id: 'files', title: 'Files', basePath: '/api/projects/:projectName/files', scopes: ['files:read', 'files:write'] },
|
|
10
|
-
{ id: 'git', title: 'Source control', basePath: '/api/git', scopes: ['git:read', 'git:write'] },
|
|
11
|
-
{ id: 'settings', title: 'Settings and API keys', basePath: '/api/settings', scopes: ['settings:read', 'settings:write'] },
|
|
12
|
-
{ id: 'updates', title: 'Update status', basePath: '/api/update', scopes: ['updates:read', 'updates:write'] },
|
|
13
|
-
{ id: 'diagnostics', title: 'Diagnostics', basePath: '/api/diagnostics', scopes: ['diagnostics:read'] },
|
|
14
|
-
{ id: 'remote', title: 'Remote connection', basePath: '/api/remote', scopes: ['remote:read', 'remote:write'] },
|
|
15
|
-
{ id: 'telegram', title: 'Telegram control', basePath: '/api/telegram', scopes: ['telegram:read', 'telegram:write'] },
|
|
16
|
-
{ id: 'webhooks', title: 'Outbound webhooks', basePath: '/api/webhooks', scopes: ['webhooks:read', 'webhooks:write'] },
|
|
17
|
-
{ id: 'plugins', title: 'Plugins and MCP tools', basePath: '/api/plugins', scopes: ['plugins:read', 'plugins:write'] },
|
|
18
|
-
];
|
|
19
|
-
|
|
20
|
-
const API_SCOPES = Array.from(new Set(API_GROUPS.flatMap((group) => group.scopes))).sort();
|
|
21
|
-
|
|
22
|
-
export function buildPublicApiManifest({ baseUrl = '' } = {}) {
|
|
23
|
-
const origin = String(baseUrl || '').replace(/\/+$/, '');
|
|
24
|
-
return {
|
|
25
|
-
name: 'Pixcode Public API',
|
|
26
|
-
version: '1.38',
|
|
27
|
-
baseUrl: origin || null,
|
|
28
|
-
auth: {
|
|
29
|
-
transports: ['Authorization: Bearer <px_api_key>', 'X-API-Key: <px_api_key>', '?apiKey=<px_api_key>'],
|
|
30
|
-
websocket: 'Pass the same px_ API key as the token query parameter.',
|
|
31
|
-
},
|
|
32
|
-
apiKey: {
|
|
33
|
-
prefix: 'px_',
|
|
34
|
-
scopes: API_SCOPES,
|
|
35
|
-
revocable: true,
|
|
36
|
-
manageableAt: '/api/settings/api-keys',
|
|
37
|
-
},
|
|
38
|
-
groups: API_GROUPS,
|
|
39
|
-
examples: [
|
|
40
|
-
{
|
|
41
|
-
title: 'List projects',
|
|
42
|
-
curl: `curl -H "X-API-Key: px_your_key" ${origin || 'http://127.0.0.1:3001'}/api/projects`,
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
title: 'Fetch diagnostics bundle',
|
|
46
|
-
curl: `curl -H "X-API-Key: px_your_key" ${origin || 'http://127.0.0.1:3001'}/api/diagnostics/bundle`,
|
|
47
|
-
},
|
|
48
|
-
{
|
|
49
|
-
title: 'Read the mobile remote control room',
|
|
50
|
-
curl: `curl -H "X-API-Key: px_your_key" ${origin || 'http://127.0.0.1:3001'}/api/remote/control-room`,
|
|
51
|
-
},
|
|
52
|
-
{
|
|
53
|
-
title: 'Register an outbound webhook',
|
|
54
|
-
curl: `curl -X POST -H "Content-Type: application/json" -H "X-API-Key: px_your_key" -d '{"name":"CI listener","url":"https://example.com/pixcode","events":["run.completed","approval.needed"]}' ${origin || 'http://127.0.0.1:3001'}/api/webhooks`,
|
|
55
|
-
},
|
|
56
|
-
],
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export function buildTypeScriptSdkStarter({ baseUrl = '' } = {}) {
|
|
61
|
-
const origin = String(baseUrl || 'http://127.0.0.1:3001').replace(/\/+$/, '');
|
|
62
|
-
return `export type PixcodeRun = {
|
|
63
|
-
id: string;
|
|
64
|
-
workflowId: string;
|
|
65
|
-
status: 'queued' | 'running' | 'completed' | 'failed' | 'canceled';
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
export class PixcodeClient {
|
|
69
|
-
constructor(
|
|
70
|
-
private readonly apiKey: string,
|
|
71
|
-
private readonly baseUrl = '${origin}',
|
|
72
|
-
) {}
|
|
73
|
-
|
|
74
|
-
private async request<T>(path: string, init: RequestInit = {}): Promise<T> {
|
|
75
|
-
const response = await fetch(new URL(path, this.baseUrl), {
|
|
76
|
-
...init,
|
|
77
|
-
headers: {
|
|
78
|
-
'Content-Type': 'application/json',
|
|
79
|
-
'X-API-Key': this.apiKey,
|
|
80
|
-
...(init.headers || {}),
|
|
81
|
-
},
|
|
82
|
-
});
|
|
83
|
-
if (!response.ok) throw new Error(\`Pixcode API \${response.status}: \${await response.text()}\`);
|
|
84
|
-
return response.json() as Promise<T>;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
projects() {
|
|
88
|
-
return this.request<{ projects: unknown[] }>('/api/projects');
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
controlRoom() {
|
|
92
|
-
return this.request<{ success: true; controlRoom: unknown }>('/api/remote/control-room');
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
approvals() {
|
|
96
|
-
return this.request<{ pendingApprovals: unknown[] }>('/api/orchestration/workflows/approvals');
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
decideApproval(approvalId: string, allow: boolean) {
|
|
100
|
-
return this.request(\`/api/orchestration/workflows/approvals/\${encodeURIComponent(approvalId)}\`, {
|
|
101
|
-
method: 'POST',
|
|
102
|
-
body: JSON.stringify({ allow, source: 'api' }),
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
startWorkflow(workflowId: string, input: string, metadata: Record<string, unknown> = {}) {
|
|
107
|
-
return this.request<PixcodeRun>(\`/api/orchestration/workflows/\${encodeURIComponent(workflowId)}/runs\`, {
|
|
108
|
-
method: 'POST',
|
|
109
|
-
body: JSON.stringify({ input, metadata }),
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
`;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
export function buildCurlCookbook({ baseUrl = '' } = {}) {
|
|
117
|
-
const origin = String(baseUrl || 'http://127.0.0.1:3001').replace(/\/+$/, '');
|
|
118
|
-
return {
|
|
119
|
-
title: 'Pixcode Public API Cookbook',
|
|
120
|
-
variables: {
|
|
121
|
-
PIXCODE_URL: origin,
|
|
122
|
-
PIXCODE_API_KEY: 'px_your_key',
|
|
123
|
-
},
|
|
124
|
-
examples: [
|
|
125
|
-
{
|
|
126
|
-
title: 'List projects',
|
|
127
|
-
command: `curl -H "X-API-Key: $PIXCODE_API_KEY" "$PIXCODE_URL/api/projects"`,
|
|
128
|
-
},
|
|
129
|
-
{
|
|
130
|
-
title: 'Read the mobile control room',
|
|
131
|
-
command: `curl -H "X-API-Key: $PIXCODE_API_KEY" "$PIXCODE_URL/api/remote/control-room"`,
|
|
132
|
-
},
|
|
133
|
-
{
|
|
134
|
-
title: 'List pending approvals',
|
|
135
|
-
command: `curl -H "X-API-Key: $PIXCODE_API_KEY" "$PIXCODE_URL/api/orchestration/workflows/approvals"`,
|
|
136
|
-
},
|
|
137
|
-
{
|
|
138
|
-
title: 'Approve a pending action',
|
|
139
|
-
command: `curl -X POST -H "Content-Type: application/json" -H "X-API-Key: $PIXCODE_API_KEY" -d '{"allow":true,"source":"api"}' "$PIXCODE_URL/api/orchestration/workflows/approvals/approval_id"`,
|
|
140
|
-
},
|
|
141
|
-
{
|
|
142
|
-
title: 'Create a webhook',
|
|
143
|
-
command: `curl -X POST -H "Content-Type: application/json" -H "X-API-Key: $PIXCODE_API_KEY" -d '{"name":"CI listener","url":"https://example.com/pixcode","events":["run.completed","run.failed","approval.needed"]}' "$PIXCODE_URL/api/webhooks"`,
|
|
144
|
-
},
|
|
145
|
-
],
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
export function buildOpenApiFragment(options = {}) {
|
|
150
|
-
const manifest = buildPublicApiManifest(options);
|
|
151
|
-
return {
|
|
152
|
-
openapi: '3.1.0',
|
|
153
|
-
info: {
|
|
154
|
-
title: manifest.name,
|
|
155
|
-
version: manifest.version,
|
|
156
|
-
},
|
|
157
|
-
security: [{ PixcodeApiKey: [] }],
|
|
158
|
-
components: {
|
|
159
|
-
securitySchemes: {
|
|
160
|
-
PixcodeApiKey: {
|
|
161
|
-
type: 'apiKey',
|
|
162
|
-
in: 'header',
|
|
163
|
-
name: 'X-API-Key',
|
|
164
|
-
description: 'Pixcode px_ API key. Keys are revocable and can carry scopes.',
|
|
165
|
-
},
|
|
166
|
-
},
|
|
167
|
-
},
|
|
168
|
-
paths: Object.fromEntries(
|
|
169
|
-
manifest.groups.map((group) => [
|
|
170
|
-
group.basePath,
|
|
171
|
-
{
|
|
172
|
-
get: {
|
|
173
|
-
summary: group.title,
|
|
174
|
-
'x-pixcode-group': group.id,
|
|
175
|
-
'x-pixcode-scopes': group.scopes,
|
|
176
|
-
},
|
|
177
|
-
},
|
|
178
|
-
]),
|
|
179
|
-
),
|
|
180
|
-
};
|
|
181
|
-
}
|
|
1
|
+
const API_GROUPS = [
|
|
2
|
+
{ id: 'auth', title: 'Authentication', basePath: '/api/auth', scopes: ['auth:read', 'auth:write'] },
|
|
3
|
+
{ id: 'projects', title: 'Projects', basePath: '/api/projects', scopes: ['projects:read', 'projects:write'] },
|
|
4
|
+
{ id: 'sessions', title: 'Sessions and messages', basePath: '/api/sessions', scopes: ['sessions:read', 'sessions:write'] },
|
|
5
|
+
{ id: 'providers', title: 'CLI providers', basePath: '/api/providers', scopes: ['providers:read', 'providers:write'] },
|
|
6
|
+
{ id: 'terminal', title: 'Visible terminal sessions', basePath: '/api/shell/sessions', scopes: ['terminal:launch'] },
|
|
7
|
+
{ id: 'orchestration', title: 'Orchestration runs', basePath: '/api/orchestration', scopes: ['orchestration:read', 'orchestration:write'] },
|
|
8
|
+
{ id: 'notifications', title: 'Notifications', basePath: '/api/settings/notifications', scopes: ['notifications:read', 'notifications:write'] },
|
|
9
|
+
{ id: 'files', title: 'Files', basePath: '/api/projects/:projectName/files', scopes: ['files:read', 'files:write'] },
|
|
10
|
+
{ id: 'git', title: 'Source control', basePath: '/api/git', scopes: ['git:read', 'git:write'] },
|
|
11
|
+
{ id: 'settings', title: 'Settings and API keys', basePath: '/api/settings', scopes: ['settings:read', 'settings:write'] },
|
|
12
|
+
{ id: 'updates', title: 'Update status', basePath: '/api/update', scopes: ['updates:read', 'updates:write'] },
|
|
13
|
+
{ id: 'diagnostics', title: 'Diagnostics', basePath: '/api/diagnostics', scopes: ['diagnostics:read'] },
|
|
14
|
+
{ id: 'remote', title: 'Remote connection', basePath: '/api/remote', scopes: ['remote:read', 'remote:write'] },
|
|
15
|
+
{ id: 'telegram', title: 'Telegram control', basePath: '/api/telegram', scopes: ['telegram:read', 'telegram:write'] },
|
|
16
|
+
{ id: 'webhooks', title: 'Outbound webhooks', basePath: '/api/webhooks', scopes: ['webhooks:read', 'webhooks:write'] },
|
|
17
|
+
{ id: 'plugins', title: 'Plugins and MCP tools', basePath: '/api/plugins', scopes: ['plugins:read', 'plugins:write'] },
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const API_SCOPES = Array.from(new Set(API_GROUPS.flatMap((group) => group.scopes))).sort();
|
|
21
|
+
|
|
22
|
+
export function buildPublicApiManifest({ baseUrl = '' } = {}) {
|
|
23
|
+
const origin = String(baseUrl || '').replace(/\/+$/, '');
|
|
24
|
+
return {
|
|
25
|
+
name: 'Pixcode Public API',
|
|
26
|
+
version: '1.38',
|
|
27
|
+
baseUrl: origin || null,
|
|
28
|
+
auth: {
|
|
29
|
+
transports: ['Authorization: Bearer <px_api_key>', 'X-API-Key: <px_api_key>', '?apiKey=<px_api_key>'],
|
|
30
|
+
websocket: 'Pass the same px_ API key as the token query parameter.',
|
|
31
|
+
},
|
|
32
|
+
apiKey: {
|
|
33
|
+
prefix: 'px_',
|
|
34
|
+
scopes: API_SCOPES,
|
|
35
|
+
revocable: true,
|
|
36
|
+
manageableAt: '/api/settings/api-keys',
|
|
37
|
+
},
|
|
38
|
+
groups: API_GROUPS,
|
|
39
|
+
examples: [
|
|
40
|
+
{
|
|
41
|
+
title: 'List projects',
|
|
42
|
+
curl: `curl -H "X-API-Key: px_your_key" ${origin || 'http://127.0.0.1:3001'}/api/projects`,
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
title: 'Fetch diagnostics bundle',
|
|
46
|
+
curl: `curl -H "X-API-Key: px_your_key" ${origin || 'http://127.0.0.1:3001'}/api/diagnostics/bundle`,
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
title: 'Read the mobile remote control room',
|
|
50
|
+
curl: `curl -H "X-API-Key: px_your_key" ${origin || 'http://127.0.0.1:3001'}/api/remote/control-room`,
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
title: 'Register an outbound webhook',
|
|
54
|
+
curl: `curl -X POST -H "Content-Type: application/json" -H "X-API-Key: px_your_key" -d '{"name":"CI listener","url":"https://example.com/pixcode","events":["run.completed","approval.needed"]}' ${origin || 'http://127.0.0.1:3001'}/api/webhooks`,
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function buildTypeScriptSdkStarter({ baseUrl = '' } = {}) {
|
|
61
|
+
const origin = String(baseUrl || 'http://127.0.0.1:3001').replace(/\/+$/, '');
|
|
62
|
+
return `export type PixcodeRun = {
|
|
63
|
+
id: string;
|
|
64
|
+
workflowId: string;
|
|
65
|
+
status: 'queued' | 'running' | 'completed' | 'failed' | 'canceled';
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export class PixcodeClient {
|
|
69
|
+
constructor(
|
|
70
|
+
private readonly apiKey: string,
|
|
71
|
+
private readonly baseUrl = '${origin}',
|
|
72
|
+
) {}
|
|
73
|
+
|
|
74
|
+
private async request<T>(path: string, init: RequestInit = {}): Promise<T> {
|
|
75
|
+
const response = await fetch(new URL(path, this.baseUrl), {
|
|
76
|
+
...init,
|
|
77
|
+
headers: {
|
|
78
|
+
'Content-Type': 'application/json',
|
|
79
|
+
'X-API-Key': this.apiKey,
|
|
80
|
+
...(init.headers || {}),
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
if (!response.ok) throw new Error(\`Pixcode API \${response.status}: \${await response.text()}\`);
|
|
84
|
+
return response.json() as Promise<T>;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
projects() {
|
|
88
|
+
return this.request<{ projects: unknown[] }>('/api/projects');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
controlRoom() {
|
|
92
|
+
return this.request<{ success: true; controlRoom: unknown }>('/api/remote/control-room');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
approvals() {
|
|
96
|
+
return this.request<{ pendingApprovals: unknown[] }>('/api/orchestration/workflows/approvals');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
decideApproval(approvalId: string, allow: boolean) {
|
|
100
|
+
return this.request(\`/api/orchestration/workflows/approvals/\${encodeURIComponent(approvalId)}\`, {
|
|
101
|
+
method: 'POST',
|
|
102
|
+
body: JSON.stringify({ allow, source: 'api' }),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
startWorkflow(workflowId: string, input: string, metadata: Record<string, unknown> = {}) {
|
|
107
|
+
return this.request<PixcodeRun>(\`/api/orchestration/workflows/\${encodeURIComponent(workflowId)}/runs\`, {
|
|
108
|
+
method: 'POST',
|
|
109
|
+
body: JSON.stringify({ input, metadata }),
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function buildCurlCookbook({ baseUrl = '' } = {}) {
|
|
117
|
+
const origin = String(baseUrl || 'http://127.0.0.1:3001').replace(/\/+$/, '');
|
|
118
|
+
return {
|
|
119
|
+
title: 'Pixcode Public API Cookbook',
|
|
120
|
+
variables: {
|
|
121
|
+
PIXCODE_URL: origin,
|
|
122
|
+
PIXCODE_API_KEY: 'px_your_key',
|
|
123
|
+
},
|
|
124
|
+
examples: [
|
|
125
|
+
{
|
|
126
|
+
title: 'List projects',
|
|
127
|
+
command: `curl -H "X-API-Key: $PIXCODE_API_KEY" "$PIXCODE_URL/api/projects"`,
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
title: 'Read the mobile control room',
|
|
131
|
+
command: `curl -H "X-API-Key: $PIXCODE_API_KEY" "$PIXCODE_URL/api/remote/control-room"`,
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
title: 'List pending approvals',
|
|
135
|
+
command: `curl -H "X-API-Key: $PIXCODE_API_KEY" "$PIXCODE_URL/api/orchestration/workflows/approvals"`,
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
title: 'Approve a pending action',
|
|
139
|
+
command: `curl -X POST -H "Content-Type: application/json" -H "X-API-Key: $PIXCODE_API_KEY" -d '{"allow":true,"source":"api"}' "$PIXCODE_URL/api/orchestration/workflows/approvals/approval_id"`,
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
title: 'Create a webhook',
|
|
143
|
+
command: `curl -X POST -H "Content-Type: application/json" -H "X-API-Key: $PIXCODE_API_KEY" -d '{"name":"CI listener","url":"https://example.com/pixcode","events":["run.completed","run.failed","approval.needed"]}' "$PIXCODE_URL/api/webhooks"`,
|
|
144
|
+
},
|
|
145
|
+
],
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function buildOpenApiFragment(options = {}) {
|
|
150
|
+
const manifest = buildPublicApiManifest(options);
|
|
151
|
+
return {
|
|
152
|
+
openapi: '3.1.0',
|
|
153
|
+
info: {
|
|
154
|
+
title: manifest.name,
|
|
155
|
+
version: manifest.version,
|
|
156
|
+
},
|
|
157
|
+
security: [{ PixcodeApiKey: [] }],
|
|
158
|
+
components: {
|
|
159
|
+
securitySchemes: {
|
|
160
|
+
PixcodeApiKey: {
|
|
161
|
+
type: 'apiKey',
|
|
162
|
+
in: 'header',
|
|
163
|
+
name: 'X-API-Key',
|
|
164
|
+
description: 'Pixcode px_ API key. Keys are revocable and can carry scopes.',
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
paths: Object.fromEntries(
|
|
169
|
+
manifest.groups.map((group) => [
|
|
170
|
+
group.basePath,
|
|
171
|
+
{
|
|
172
|
+
get: {
|
|
173
|
+
summary: group.title,
|
|
174
|
+
'x-pixcode-group': group.id,
|
|
175
|
+
'x-pixcode-scopes': group.scopes,
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
]),
|
|
179
|
+
),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
@@ -1,126 +1,126 @@
|
|
|
1
|
-
import crypto from 'node:crypto';
|
|
2
|
-
|
|
3
|
-
import { appConfigDb, userDb } from '../database/db.js';
|
|
4
|
-
|
|
5
|
-
const CONFIG_KEY = 'qr_login_settings';
|
|
6
|
-
const DEFAULT_SETTINGS = Object.freeze({
|
|
7
|
-
enabled: false,
|
|
8
|
-
ttlSeconds: 300,
|
|
9
|
-
});
|
|
10
|
-
const MIN_TTL_SECONDS = 60;
|
|
11
|
-
const MAX_TTL_SECONDS = 15 * 60;
|
|
12
|
-
const qrLoginTokens = new Map();
|
|
13
|
-
|
|
14
|
-
function clampTtl(value) {
|
|
15
|
-
const parsed = Number.parseInt(String(value || ''), 10);
|
|
16
|
-
if (!Number.isFinite(parsed)) return DEFAULT_SETTINGS.ttlSeconds;
|
|
17
|
-
return Math.min(MAX_TTL_SECONDS, Math.max(MIN_TTL_SECONDS, parsed));
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function normalizeSettings(value = {}) {
|
|
21
|
-
const raw = value && typeof value === 'object' ? value : {};
|
|
22
|
-
return {
|
|
23
|
-
enabled: raw.enabled === true,
|
|
24
|
-
ttlSeconds: clampTtl(raw.ttlSeconds),
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function readStoredSettings() {
|
|
29
|
-
const raw = appConfigDb.get(CONFIG_KEY);
|
|
30
|
-
if (!raw) return { ...DEFAULT_SETTINGS };
|
|
31
|
-
try {
|
|
32
|
-
return normalizeSettings(JSON.parse(raw));
|
|
33
|
-
} catch {
|
|
34
|
-
return { ...DEFAULT_SETTINGS };
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function cleanupExpiredTokens() {
|
|
39
|
-
const now = Date.now();
|
|
40
|
-
for (const [token, entry] of qrLoginTokens.entries()) {
|
|
41
|
-
if (!entry || entry.expiresAtMs <= now) {
|
|
42
|
-
qrLoginTokens.delete(token);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function appendQrLoginToken(baseUrl, token) {
|
|
48
|
-
const url = new URL(baseUrl);
|
|
49
|
-
url.searchParams.set('qrLoginToken', token);
|
|
50
|
-
return url.toString();
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export function getQrLoginSettings() {
|
|
54
|
-
return readStoredSettings();
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export function saveQrLoginSettings(input = {}) {
|
|
58
|
-
const settings = normalizeSettings(input);
|
|
59
|
-
appConfigDb.set(CONFIG_KEY, JSON.stringify(settings));
|
|
60
|
-
if (!settings.enabled) {
|
|
61
|
-
qrLoginTokens.clear();
|
|
62
|
-
}
|
|
63
|
-
return settings;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export function createQrLoginToken({ userId, baseUrl }) {
|
|
67
|
-
const settings = readStoredSettings();
|
|
68
|
-
if (!settings.enabled) {
|
|
69
|
-
const error = new Error('QR login is disabled.');
|
|
70
|
-
error.code = 'QR_LOGIN_DISABLED';
|
|
71
|
-
throw error;
|
|
72
|
-
}
|
|
73
|
-
const user = userDb.getUserById(userId);
|
|
74
|
-
if (!user) {
|
|
75
|
-
const error = new Error('QR login user was not found.');
|
|
76
|
-
error.code = 'QR_LOGIN_USER_NOT_FOUND';
|
|
77
|
-
throw error;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
cleanupExpiredTokens();
|
|
81
|
-
const token = crypto.randomBytes(32).toString('base64url');
|
|
82
|
-
const expiresAtMs = Date.now() + settings.ttlSeconds * 1000;
|
|
83
|
-
qrLoginTokens.set(token, {
|
|
84
|
-
userId: user.id,
|
|
85
|
-
createdAtMs: Date.now(),
|
|
86
|
-
expiresAtMs,
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
return {
|
|
90
|
-
token,
|
|
91
|
-
expiresAt: new Date(expiresAtMs).toISOString(),
|
|
92
|
-
ttlSeconds: settings.ttlSeconds,
|
|
93
|
-
qrUrl: baseUrl ? appendQrLoginToken(baseUrl, token) : null,
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
export function consumeQrLoginToken(token) {
|
|
98
|
-
const settings = readStoredSettings();
|
|
99
|
-
if (!settings.enabled) {
|
|
100
|
-
const error = new Error('QR login is disabled.');
|
|
101
|
-
error.code = 'QR_LOGIN_DISABLED';
|
|
102
|
-
throw error;
|
|
103
|
-
}
|
|
104
|
-
if (!token || typeof token !== 'string') {
|
|
105
|
-
const error = new Error('QR login token is required.');
|
|
106
|
-
error.code = 'QR_LOGIN_TOKEN_REQUIRED';
|
|
107
|
-
throw error;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
cleanupExpiredTokens();
|
|
111
|
-
const entry = qrLoginTokens.get(token);
|
|
112
|
-
qrLoginTokens.delete(token);
|
|
113
|
-
if (!entry) {
|
|
114
|
-
const error = new Error('QR login token is invalid or expired.');
|
|
115
|
-
error.code = 'QR_LOGIN_INVALID';
|
|
116
|
-
throw error;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const user = userDb.getUserById(entry.userId);
|
|
120
|
-
if (!user) {
|
|
121
|
-
const error = new Error('QR login user was not found.');
|
|
122
|
-
error.code = 'QR_LOGIN_USER_NOT_FOUND';
|
|
123
|
-
throw error;
|
|
124
|
-
}
|
|
125
|
-
return user;
|
|
126
|
-
}
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
import { appConfigDb, userDb } from '../database/db.js';
|
|
4
|
+
|
|
5
|
+
const CONFIG_KEY = 'qr_login_settings';
|
|
6
|
+
const DEFAULT_SETTINGS = Object.freeze({
|
|
7
|
+
enabled: false,
|
|
8
|
+
ttlSeconds: 300,
|
|
9
|
+
});
|
|
10
|
+
const MIN_TTL_SECONDS = 60;
|
|
11
|
+
const MAX_TTL_SECONDS = 15 * 60;
|
|
12
|
+
const qrLoginTokens = new Map();
|
|
13
|
+
|
|
14
|
+
function clampTtl(value) {
|
|
15
|
+
const parsed = Number.parseInt(String(value || ''), 10);
|
|
16
|
+
if (!Number.isFinite(parsed)) return DEFAULT_SETTINGS.ttlSeconds;
|
|
17
|
+
return Math.min(MAX_TTL_SECONDS, Math.max(MIN_TTL_SECONDS, parsed));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function normalizeSettings(value = {}) {
|
|
21
|
+
const raw = value && typeof value === 'object' ? value : {};
|
|
22
|
+
return {
|
|
23
|
+
enabled: raw.enabled === true,
|
|
24
|
+
ttlSeconds: clampTtl(raw.ttlSeconds),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function readStoredSettings() {
|
|
29
|
+
const raw = appConfigDb.get(CONFIG_KEY);
|
|
30
|
+
if (!raw) return { ...DEFAULT_SETTINGS };
|
|
31
|
+
try {
|
|
32
|
+
return normalizeSettings(JSON.parse(raw));
|
|
33
|
+
} catch {
|
|
34
|
+
return { ...DEFAULT_SETTINGS };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function cleanupExpiredTokens() {
|
|
39
|
+
const now = Date.now();
|
|
40
|
+
for (const [token, entry] of qrLoginTokens.entries()) {
|
|
41
|
+
if (!entry || entry.expiresAtMs <= now) {
|
|
42
|
+
qrLoginTokens.delete(token);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function appendQrLoginToken(baseUrl, token) {
|
|
48
|
+
const url = new URL(baseUrl);
|
|
49
|
+
url.searchParams.set('qrLoginToken', token);
|
|
50
|
+
return url.toString();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function getQrLoginSettings() {
|
|
54
|
+
return readStoredSettings();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function saveQrLoginSettings(input = {}) {
|
|
58
|
+
const settings = normalizeSettings(input);
|
|
59
|
+
appConfigDb.set(CONFIG_KEY, JSON.stringify(settings));
|
|
60
|
+
if (!settings.enabled) {
|
|
61
|
+
qrLoginTokens.clear();
|
|
62
|
+
}
|
|
63
|
+
return settings;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function createQrLoginToken({ userId, baseUrl }) {
|
|
67
|
+
const settings = readStoredSettings();
|
|
68
|
+
if (!settings.enabled) {
|
|
69
|
+
const error = new Error('QR login is disabled.');
|
|
70
|
+
error.code = 'QR_LOGIN_DISABLED';
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
const user = userDb.getUserById(userId);
|
|
74
|
+
if (!user) {
|
|
75
|
+
const error = new Error('QR login user was not found.');
|
|
76
|
+
error.code = 'QR_LOGIN_USER_NOT_FOUND';
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
cleanupExpiredTokens();
|
|
81
|
+
const token = crypto.randomBytes(32).toString('base64url');
|
|
82
|
+
const expiresAtMs = Date.now() + settings.ttlSeconds * 1000;
|
|
83
|
+
qrLoginTokens.set(token, {
|
|
84
|
+
userId: user.id,
|
|
85
|
+
createdAtMs: Date.now(),
|
|
86
|
+
expiresAtMs,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
token,
|
|
91
|
+
expiresAt: new Date(expiresAtMs).toISOString(),
|
|
92
|
+
ttlSeconds: settings.ttlSeconds,
|
|
93
|
+
qrUrl: baseUrl ? appendQrLoginToken(baseUrl, token) : null,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function consumeQrLoginToken(token) {
|
|
98
|
+
const settings = readStoredSettings();
|
|
99
|
+
if (!settings.enabled) {
|
|
100
|
+
const error = new Error('QR login is disabled.');
|
|
101
|
+
error.code = 'QR_LOGIN_DISABLED';
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
if (!token || typeof token !== 'string') {
|
|
105
|
+
const error = new Error('QR login token is required.');
|
|
106
|
+
error.code = 'QR_LOGIN_TOKEN_REQUIRED';
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
cleanupExpiredTokens();
|
|
111
|
+
const entry = qrLoginTokens.get(token);
|
|
112
|
+
qrLoginTokens.delete(token);
|
|
113
|
+
if (!entry) {
|
|
114
|
+
const error = new Error('QR login token is invalid or expired.');
|
|
115
|
+
error.code = 'QR_LOGIN_INVALID';
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const user = userDb.getUserById(entry.userId);
|
|
120
|
+
if (!user) {
|
|
121
|
+
const error = new Error('QR login user was not found.');
|
|
122
|
+
error.code = 'QR_LOGIN_USER_NOT_FOUND';
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
return user;
|
|
126
|
+
}
|