ima2-gen 2.0.15 → 2.0.16
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/README.md +30 -0
- package/bin/commands/skill.js +336 -17
- package/bin/ima2.js +22 -2
- package/bin/lib/doctor-checks.js +6 -4
- package/config.js +1 -0
- package/docs/CLI.md +25 -3
- package/docs/PROMPT_STUDIO.ko.md +29 -0
- package/docs/PROMPT_STUDIO.md +31 -0
- package/docs/migration/runtime-test-inventory.md +7 -1
- package/lib/agentImageVideoGen.js +8 -0
- package/lib/agentQueueStore.js +36 -10
- package/lib/agentQueueWorker.js +79 -7
- package/lib/agentRuntime.js +2 -0
- package/lib/assetLifecycle.js +40 -8
- package/lib/canvasVersionStore.js +86 -0
- package/lib/cardNewsGenerator.js +37 -3
- package/lib/cardNewsJobStore.js +10 -0
- package/lib/cardNewsManifestStore.js +6 -12
- package/lib/cardNewsPath.js +25 -0
- package/lib/db.js +2 -0
- package/lib/generatePipeline.js +474 -0
- package/lib/generationInputValidation.js +32 -0
- package/lib/grokImageCore.js +18 -1
- package/lib/historyIndex.js +11 -4
- package/lib/historyList.js +3 -0
- package/lib/imageMetadata.js +3 -0
- package/lib/inflight.js +30 -3
- package/lib/multimodePipeline.js +462 -0
- package/lib/nodeGeneration.js +440 -0
- package/lib/nodeValidation.js +23 -0
- package/lib/oauthProxy/generators.js +3 -246
- package/lib/oauthProxy/multimodeGenerators.js +251 -0
- package/lib/routeHelpers.js +9 -2
- package/package.json +2 -2
- package/routes/agent.js +4 -4
- package/routes/canvasVersions.js +21 -1
- package/routes/cardNews.js +2 -1
- package/routes/edit.js +25 -9
- package/routes/events.js +5 -1
- package/routes/generate.js +2 -481
- package/routes/keys.js +31 -39
- package/routes/multimode.js +2 -502
- package/routes/nodes.js +3 -448
- package/routes/video.js +12 -3
- package/routes/videoExtended.js +19 -4
- package/server.js +53 -1
- package/skills/ima2/SKILL.md +204 -0
- package/skills/ima2-front/SKILL.md +611 -0
- package/skills/ima2-front/references/a11y-patterns.md +134 -0
- package/skills/ima2-front/references/aesthetics.md +327 -0
- package/skills/ima2-front/references/anti-slop.md +241 -0
- package/skills/ima2-front/references/asset-requirements.md +431 -0
- package/skills/ima2-front/references/brand-asset-sourcing.md +122 -0
- package/skills/ima2-front/references/color-system.md +132 -0
- package/skills/ima2-front/references/consistency-locks.md +20 -0
- package/skills/ima2-front/references/crud-ui.md +53 -0
- package/skills/ima2-front/references/dropdown-layer.md +110 -0
- package/skills/ima2-front/references/i18n-global.md +104 -0
- package/skills/ima2-front/references/iterative-design.md +90 -0
- package/skills/ima2-front/references/korea-2026.md +174 -0
- package/skills/ima2-front/references/layout-discipline.md +161 -0
- package/skills/ima2-front/references/liquid-glass.md +181 -0
- package/skills/ima2-front/references/logo-sections.md +194 -0
- package/skills/ima2-front/references/mobile-ux.md +144 -0
- package/skills/ima2-front/references/performance-budget.md +106 -0
- package/skills/ima2-front/references/preflight-full.md +58 -0
- package/skills/ima2-front/references/product-density.md +53 -0
- package/skills/ima2-front/references/prototype-variants.md +54 -0
- package/skills/ima2-front/references/reference-capture.md +120 -0
- package/skills/ima2-front/references/responsive-viewport.md +139 -0
- package/skills/ima2-front/references/seo-baseline.md +123 -0
- package/skills/ima2-front/references/soft-3d-asset-gates.md +70 -0
- package/skills/ima2-front/references/stacks/astro.md +204 -0
- package/skills/ima2-front/references/stacks/mobile-native.md +281 -0
- package/skills/ima2-front/references/stacks/nextjs.md +219 -0
- package/skills/ima2-front/references/stacks/react.md +264 -0
- package/skills/ima2-front/references/stacks/svelte.md +235 -0
- package/skills/ima2-front/references/stacks/vanilla.md +226 -0
- package/skills/ima2-front/references/theme-switching.md +82 -0
- package/skills/ima2-front/references/top-bar.md +126 -0
- package/skills/ima2-front/references/typography-wrapping.md +270 -0
- package/skills/ima2-front/references/ux-writing-ko.md +62 -0
- package/skills/ima2-front/references/visual-verification.md +102 -0
- package/skills/ima2-uiux/SKILL.md +477 -0
- package/skills/ima2-uiux/references/anti-slop-judgment.md +39 -0
- package/skills/ima2-uiux/references/color-system.md +80 -0
- package/skills/ima2-uiux/references/design-isms.md +194 -0
- package/skills/ima2-uiux/references/design-read-example.md +33 -0
- package/skills/ima2-uiux/references/design-system-bootstrap.md +133 -0
- package/skills/ima2-uiux/references/favicon-logo.md +355 -0
- package/skills/ima2-uiux/references/form-patterns.md +118 -0
- package/skills/ima2-uiux/references/intent-discovery-ladder.md +86 -0
- package/skills/ima2-uiux/references/korean-design-vocabulary.md +52 -0
- package/skills/ima2-uiux/references/layout-macrostructures.md +54 -0
- package/skills/ima2-uiux/references/logo-trust-sections.md +96 -0
- package/skills/ima2-uiux/references/mobile-native-ux.md +207 -0
- package/skills/ima2-uiux/references/product-personalities.md +118 -0
- package/skills/ima2-uiux/references/responsive-nav.md +66 -0
- package/skills/ima2-uiux/references/typography-line-breaks.md +193 -0
- package/skills/ima2-uiux/references/ux-preflight.md +65 -0
- package/skills/ima2-uiux/references/ux-states.md +197 -0
- package/skills/ima2-uiux/references/visual-hierarchy.md +115 -0
- package/ui/dist/.vite/manifest.json +13 -13
- package/ui/dist/assets/AgentWorkspace-CtYt4SF5.js +3 -0
- package/ui/dist/assets/{CardNewsWorkspace-Cdxn1ohU.js → CardNewsWorkspace-DN5cVqG5.js} +1 -1
- package/ui/dist/assets/GenerationRequestLogPanel-DnmUQdKR.js +1 -0
- package/ui/dist/assets/{NodeCanvas-C67sLboA.js → NodeCanvas-CxpfIBI0.js} +1 -1
- package/ui/dist/assets/PromptBuilderPanel-C2R3ObtR.js +2 -0
- package/ui/dist/assets/{PromptImportDialog-DeQXsEEv.js → PromptImportDialog-BIxz_yEx.js} +2 -2
- package/ui/dist/assets/PromptImportDiscoverySection-BQTlPMME.js +1 -0
- package/ui/dist/assets/PromptImportFolderSection-Ds_Th9O3.js +1 -0
- package/ui/dist/assets/{PromptLibraryPanel-Db3xEPM_.js → PromptLibraryPanel-Rs4l45pV.js} +2 -2
- package/ui/dist/assets/{SettingsWorkspace-BDk_1W-V.js → SettingsWorkspace-p1AZ6uT9.js} +1 -1
- package/ui/dist/assets/index-0-_vgFGs.js +4 -0
- package/ui/dist/assets/index-Dm3pFxV4.js +23 -0
- package/ui/dist/assets/index-J8yDF3Ch.css +1 -0
- package/ui/dist/index.html +2 -2
- package/ui/dist/assets/AgentWorkspace-DAg_7zpY.js +0 -3
- package/ui/dist/assets/GenerationRequestLogPanel-C_qvEUi2.js +0 -1
- package/ui/dist/assets/PromptBuilderPanel-DjvVdAie.js +0 -2
- package/ui/dist/assets/PromptImportDiscoverySection-CrwyUKYI.js +0 -1
- package/ui/dist/assets/PromptImportFolderSection-CToGMqYE.js +0 -1
- package/ui/dist/assets/index-2oG6in1i.css +0 -1
- package/ui/dist/assets/index-CoxMOXAU.js +0 -23
- package/ui/dist/assets/index-p8egnGHk.js +0 -4
package/routes/nodes.js
CHANGED
|
@@ -1,455 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { newNodeId, saveNode, loadNodeMeta, loadAssetB64, } from "../lib/nodeStore.js";
|
|
3
|
-
import { startJob, finishJob, registerJobAbortController, isJobCanceled, isStartJobFailure, INFLIGHT_RETRY_AFTER_SECONDS } from "../lib/inflight.js";
|
|
4
|
-
import { isGenerationCanceledError, makeGenerationCanceledError, throwIfJobCanceled, } from "../lib/generationCancel.js";
|
|
5
|
-
import { detectImageMimeFromB64, summarizeReferencePayload, validateAndNormalizeRefs } from "../lib/refs.js";
|
|
6
|
-
import { classifyUpstreamError } from "../lib/errorClassify.js";
|
|
7
|
-
import { normalizeOAuthParams } from "../lib/oauthNormalize.js";
|
|
8
|
-
import { resolveProviderOptions } from "../lib/providerOptions.js";
|
|
9
|
-
import { generateViaResponses, editViaResponses } from "../lib/responsesImageAdapter.js";
|
|
10
|
-
import { generateViaGrok } from "../lib/grokImageAdapter.js";
|
|
11
|
-
import { generateViaAgy } from "../lib/agyImageAdapter.js";
|
|
12
|
-
import { generateViaGeminiApi } from "../lib/geminiApiImageAdapter.js";
|
|
13
|
-
import { isNonRetryableGenerationError, normalizeGenerationFailure } from "../lib/generationErrors.js";
|
|
14
|
-
import { logEvent, logError } from "../lib/logger.js";
|
|
1
|
+
import { loadNodeMeta } from "../lib/nodeStore.js";
|
|
15
2
|
import { errInfo } from "../lib/errInfo.js";
|
|
3
|
+
import { runNodeGeneration } from "../lib/nodeGeneration.js";
|
|
16
4
|
import { requireRuntimeContext } from "../lib/runtimeContext.js";
|
|
17
|
-
import { validateModeration, imageFormatFromMime, writeSse, dataUrlFromB64 } from "../lib/routeHelpers.js";
|
|
18
|
-
import { publish } from "../lib/eventBus.js";
|
|
19
|
-
import { publishJobEvent } from "../lib/ssePublish.js";
|
|
20
|
-
import { asUpstream, wantsSse, writeNodeError, loadParentNodeB64, toGrokReferences, nodeErrorDetails, } from "../lib/nodeHelpers.js";
|
|
21
5
|
export function registerNodeRoutes(app, ctxRaw) {
|
|
22
6
|
const ctx = requireRuntimeContext(ctxRaw);
|
|
23
|
-
app.post("/api/node/generate",
|
|
24
|
-
const body = (req.body ?? {});
|
|
25
|
-
const asyncMode = body.async === true;
|
|
26
|
-
const streamResponse = !asyncMode && wantsSse(req);
|
|
27
|
-
const parentNodeId = (typeof body.parentNodeId === "string" ? body.parentNodeId : null);
|
|
28
|
-
const requestId = typeof body.requestId === "string" ? body.requestId : (req.id ?? "");
|
|
29
|
-
const sessionId = typeof body.sessionId === "string" ? body.sessionId : null;
|
|
30
|
-
const clientNodeId = typeof body.clientNodeId === "string" ? body.clientNodeId : null;
|
|
31
|
-
let finishMeta = {};
|
|
32
|
-
let finishStatus = "completed";
|
|
33
|
-
let finishHttpStatus;
|
|
34
|
-
let finishErrorCode;
|
|
35
|
-
let finishCanceled = false;
|
|
36
|
-
const cancelController = new AbortController();
|
|
37
|
-
const referencePayload = summarizeReferencePayload(body.references);
|
|
38
|
-
try {
|
|
39
|
-
const { prompt, quality: rawQuality = "medium", size = "1024x1024", format = "png", moderation = "low", references = [], externalSrc = null, mode: promptMode = "auto", contextMode: rawContextMode = "parent-plus-refs", searchMode: rawSearchMode = "on", model: rawModel, reasoningEffort: rawReasoningEffort, } = body;
|
|
40
|
-
const { provider = "oauth" } = body;
|
|
41
|
-
const { quality, warnings: qualityWarnings } = normalizeOAuthParams({ provider, quality: rawQuality });
|
|
42
|
-
const normalizedPromptMode = promptMode === "direct" ? "direct" : "auto";
|
|
43
|
-
const contextMode = ["parent-plus-refs", "parent-only", "ancestry"].includes(rawContextMode)
|
|
44
|
-
? rawContextMode
|
|
45
|
-
: "parent-plus-refs";
|
|
46
|
-
const searchMode = ["off", "auto", "on"].includes(rawSearchMode) ? rawSearchMode : "on";
|
|
47
|
-
const providerOptions = resolveProviderOptions(ctx, {
|
|
48
|
-
provider,
|
|
49
|
-
rawModel,
|
|
50
|
-
rawReasoningEffort,
|
|
51
|
-
rawSize: size,
|
|
52
|
-
rawWebSearchEnabled: body.webSearchEnabled,
|
|
53
|
-
searchMode,
|
|
54
|
-
});
|
|
55
|
-
if (providerOptions.error) {
|
|
56
|
-
finishStatus = "error";
|
|
57
|
-
finishHttpStatus = providerOptions.status;
|
|
58
|
-
finishErrorCode = providerOptions.code;
|
|
59
|
-
return res.status(providerOptions.status).json({
|
|
60
|
-
error: { code: providerOptions.code, message: providerOptions.error },
|
|
61
|
-
parentNodeId,
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
const imageModel = providerOptions.model;
|
|
65
|
-
const reasoningEffort = providerOptions.reasoningEffort;
|
|
66
|
-
const effectiveSize = providerOptions.size;
|
|
67
|
-
const webSearchEnabled = providerOptions.webSearchEnabled;
|
|
68
|
-
const activeProvider = providerOptions.provider;
|
|
69
|
-
const effectiveImageModel = (activeProvider === "grok" || activeProvider === "grok-api") && quality === "high"
|
|
70
|
-
? "grok-imagine-image-quality"
|
|
71
|
-
: imageModel;
|
|
72
|
-
if (contextMode === "ancestry") {
|
|
73
|
-
finishStatus = "error";
|
|
74
|
-
finishHttpStatus = 400;
|
|
75
|
-
finishErrorCode = "CONTEXT_MODE_UNSUPPORTED";
|
|
76
|
-
return res.status(400).json({
|
|
77
|
-
error: { code: "CONTEXT_MODE_UNSUPPORTED", message: "Ancestry context is not supported yet." },
|
|
78
|
-
parentNodeId,
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
if (!prompt || typeof prompt !== "string") {
|
|
82
|
-
finishStatus = "error";
|
|
83
|
-
finishHttpStatus = 400;
|
|
84
|
-
finishErrorCode = "INVALID_PROMPT";
|
|
85
|
-
return res.status(400).json({
|
|
86
|
-
error: { code: "INVALID_PROMPT", message: "Prompt is required" },
|
|
87
|
-
parentNodeId,
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
const refCheckResult = validateAndNormalizeRefs(references);
|
|
91
|
-
if (refCheckResult.error) {
|
|
92
|
-
finishStatus = "error";
|
|
93
|
-
finishHttpStatus = 400;
|
|
94
|
-
finishErrorCode = refCheckResult.code;
|
|
95
|
-
return res.status(400).json({
|
|
96
|
-
error: { code: refCheckResult.code, message: refCheckResult.error },
|
|
97
|
-
code: refCheckResult.code,
|
|
98
|
-
parentNodeId,
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
const refCheck = refCheckResult;
|
|
102
|
-
const moderationCheck = validateModeration(ctx, moderation);
|
|
103
|
-
if (moderationCheck.error) {
|
|
104
|
-
finishStatus = "error";
|
|
105
|
-
finishHttpStatus = 400;
|
|
106
|
-
finishErrorCode = "INVALID_MODERATION";
|
|
107
|
-
return res.status(400).json({
|
|
108
|
-
error: { code: "INVALID_MODERATION", message: moderationCheck.error },
|
|
109
|
-
parentNodeId,
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
const startTime = Date.now();
|
|
113
|
-
let parentB64 = null;
|
|
114
|
-
if (parentNodeId) {
|
|
115
|
-
parentB64 = await loadParentNodeB64(ctx, parentNodeId);
|
|
116
|
-
}
|
|
117
|
-
else if (typeof externalSrc === "string" && externalSrc.length > 0) {
|
|
118
|
-
parentB64 = await loadAssetB64(ctx.rootDir, externalSrc, ctx.config.storage.generatedDir);
|
|
119
|
-
}
|
|
120
|
-
const operation = parentB64 ? "edit" : "generate";
|
|
121
|
-
const referenceDiagnostics = refCheck.referenceDiagnostics || [];
|
|
122
|
-
const generateReferenceDiagnostics = operation === "generate" ? referenceDiagnostics : [];
|
|
123
|
-
const referenceMismatchCount = generateReferenceDiagnostics.filter((ref) => ref.warnings?.includes("mime_mismatch")).length;
|
|
124
|
-
const refsForRequest = contextMode === "parent-only" ? [] : (refCheck.refDetails || refCheck.refs);
|
|
125
|
-
const parentImagePresent = !!parentB64;
|
|
126
|
-
const inputImageCount = (parentImagePresent ? 1 : 0) + refsForRequest.length;
|
|
127
|
-
if ((activeProvider === "grok" || activeProvider === "agy" || activeProvider === "grok-api" || activeProvider === "gemini-api") && inputImageCount > 3) {
|
|
128
|
-
finishStatus = "error";
|
|
129
|
-
finishHttpStatus = 400;
|
|
130
|
-
const code = activeProvider === "agy" ? "AGY_REF_TOO_MANY" : "GROK_REF_TOO_MANY";
|
|
131
|
-
return res.status(400).json({
|
|
132
|
-
error: {
|
|
133
|
-
code,
|
|
134
|
-
message: `${activeProvider === "agy" ? "Agy" : "Grok"} image editing supports up to 3 reference images.`,
|
|
135
|
-
},
|
|
136
|
-
code,
|
|
137
|
-
parentNodeId,
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
const started = startJob({
|
|
141
|
-
requestId,
|
|
142
|
-
kind: "node",
|
|
143
|
-
prompt: body.prompt,
|
|
144
|
-
meta: {
|
|
145
|
-
kind: "node",
|
|
146
|
-
sessionId,
|
|
147
|
-
parentNodeId,
|
|
148
|
-
clientNodeId,
|
|
149
|
-
refsCount: referencePayload.refsCount,
|
|
150
|
-
referenceBytes: referencePayload.referenceBytes,
|
|
151
|
-
referenceB64Chars: referencePayload.referenceB64Chars,
|
|
152
|
-
},
|
|
153
|
-
});
|
|
154
|
-
if (started && isStartJobFailure(started)) {
|
|
155
|
-
finishStatus = "error";
|
|
156
|
-
finishHttpStatus = started.code === "TOO_MANY_JOBS" ? 429 : 409;
|
|
157
|
-
finishErrorCode = started.code;
|
|
158
|
-
if (started.code === "TOO_MANY_JOBS") {
|
|
159
|
-
res.setHeader("Retry-After", String(INFLIGHT_RETRY_AFTER_SECONDS));
|
|
160
|
-
}
|
|
161
|
-
return writeNodeError(res, finishHttpStatus, started.code, started.code === "TOO_MANY_JOBS"
|
|
162
|
-
? "Too many concurrent generation jobs"
|
|
163
|
-
: "Request ID already in use", parentNodeId, {}, requestId);
|
|
164
|
-
}
|
|
165
|
-
registerJobAbortController(requestId, cancelController);
|
|
166
|
-
if (asyncMode)
|
|
167
|
-
res.status(202).json({ requestId });
|
|
168
|
-
logEvent("node", "request", {
|
|
169
|
-
requestId,
|
|
170
|
-
operation,
|
|
171
|
-
sessionId,
|
|
172
|
-
parentNodeId,
|
|
173
|
-
clientNodeId,
|
|
174
|
-
quality,
|
|
175
|
-
model: effectiveImageModel,
|
|
176
|
-
size: effectiveSize,
|
|
177
|
-
moderation,
|
|
178
|
-
refs: refsForRequest.length,
|
|
179
|
-
referenceBytes: referencePayload.referenceBytes,
|
|
180
|
-
referenceMismatchCount,
|
|
181
|
-
refDetectedMimes: [...new Set(generateReferenceDiagnostics.map((ref) => ref.detectedMime).filter(Boolean))].join(","),
|
|
182
|
-
refDeclaredMimes: [...new Set(generateReferenceDiagnostics.map((ref) => ref.declaredMime).filter(Boolean))].join(","),
|
|
183
|
-
inputImageCount,
|
|
184
|
-
parentImagePresent,
|
|
185
|
-
contextMode,
|
|
186
|
-
searchMode,
|
|
187
|
-
webSearchEnabled,
|
|
188
|
-
promptChars: prompt.length,
|
|
189
|
-
promptMode: normalizedPromptMode,
|
|
190
|
-
});
|
|
191
|
-
const emitProgress = streamResponse || asyncMode;
|
|
192
|
-
if (streamResponse) {
|
|
193
|
-
res.writeHead(200, {
|
|
194
|
-
"Content-Type": "text/event-stream; charset=utf-8",
|
|
195
|
-
"Cache-Control": "no-cache, no-transform",
|
|
196
|
-
Connection: "keep-alive",
|
|
197
|
-
});
|
|
198
|
-
writeSse(res, "phase", { requestId, phase: "streaming" });
|
|
199
|
-
publish(requestId, "phase", { requestId, phase: "streaming" });
|
|
200
|
-
}
|
|
201
|
-
else if (asyncMode) {
|
|
202
|
-
publish(requestId, "phase", { requestId, phase: "streaming" });
|
|
203
|
-
}
|
|
204
|
-
let b64, usage, webSearchCalls = 0, revisedPrompt = null;
|
|
205
|
-
const grokDirectApiKey = activeProvider === "grok-api" ? ctx.xaiApiKey : undefined;
|
|
206
|
-
let resultFormat = activeProvider === "grok" || activeProvider === "agy" || activeProvider === "grok-api" || activeProvider === "gemini-api" ? "jpeg" : format;
|
|
207
|
-
const maxAttempts = inputImageCount > 0 ? 1 : 2;
|
|
208
|
-
let lastErr = null;
|
|
209
|
-
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
210
|
-
try {
|
|
211
|
-
logEvent("node", "attempt", {
|
|
212
|
-
requestId,
|
|
213
|
-
attempt,
|
|
214
|
-
operation,
|
|
215
|
-
sessionId,
|
|
216
|
-
parentNodeId,
|
|
217
|
-
clientNodeId,
|
|
218
|
-
model: effectiveImageModel,
|
|
219
|
-
moderation,
|
|
220
|
-
quality,
|
|
221
|
-
size: effectiveSize,
|
|
222
|
-
refs: refsForRequest.length,
|
|
223
|
-
inputImageCount,
|
|
224
|
-
parentImagePresent,
|
|
225
|
-
contextMode,
|
|
226
|
-
searchMode,
|
|
227
|
-
webSearchEnabled,
|
|
228
|
-
});
|
|
229
|
-
const r = activeProvider === "gemini-api"
|
|
230
|
-
? await generateViaGeminiApi(parentB64 ? `Edit this image: ${prompt}` : prompt, requireRuntimeContext(ctx), {
|
|
231
|
-
model: effectiveImageModel,
|
|
232
|
-
size: effectiveSize,
|
|
233
|
-
signal: cancelController.signal,
|
|
234
|
-
requestId,
|
|
235
|
-
references: parentB64
|
|
236
|
-
? [{ b64: parentB64, declaredMime: null, detectedMime: null }, ...(refCheck.refDetails || [])]
|
|
237
|
-
: refCheck.refDetails,
|
|
238
|
-
})
|
|
239
|
-
: activeProvider === "agy"
|
|
240
|
-
? await generateViaAgy(parentB64 ? `Edit this image: ${prompt}` : prompt, {
|
|
241
|
-
references: parentB64
|
|
242
|
-
? [{ b64: parentB64, declaredMime: null, detectedMime: null }]
|
|
243
|
-
: undefined,
|
|
244
|
-
signal: cancelController.signal,
|
|
245
|
-
requestId,
|
|
246
|
-
})
|
|
247
|
-
: activeProvider === "grok" || activeProvider === "grok-api"
|
|
248
|
-
? await generateViaGrok(prompt, ctx, {
|
|
249
|
-
model: effectiveImageModel,
|
|
250
|
-
size: effectiveSize,
|
|
251
|
-
requestId,
|
|
252
|
-
signal: cancelController.signal,
|
|
253
|
-
references: toGrokReferences(parentB64, refsForRequest),
|
|
254
|
-
directApiKey: grokDirectApiKey,
|
|
255
|
-
})
|
|
256
|
-
: parentB64
|
|
257
|
-
? await editViaResponses(activeProvider, prompt, parentB64, quality, effectiveSize, moderation, normalizedPromptMode, ctx, requestId, {
|
|
258
|
-
model: effectiveImageModel,
|
|
259
|
-
references: refsForRequest,
|
|
260
|
-
searchMode,
|
|
261
|
-
reasoningEffort,
|
|
262
|
-
webSearchEnabled,
|
|
263
|
-
signal: cancelController.signal,
|
|
264
|
-
})
|
|
265
|
-
: await generateViaResponses(activeProvider, prompt, quality, effectiveSize, moderation, refsForRequest, requestId, normalizedPromptMode, ctx, {
|
|
266
|
-
model: effectiveImageModel,
|
|
267
|
-
reasoningEffort,
|
|
268
|
-
webSearchEnabled,
|
|
269
|
-
signal: cancelController.signal,
|
|
270
|
-
partialImages: emitProgress ? 2 : 0,
|
|
271
|
-
onPartialImage: emitProgress
|
|
272
|
-
? (partial) => {
|
|
273
|
-
if (isJobCanceled(requestId))
|
|
274
|
-
return;
|
|
275
|
-
const pd = { requestId, image: dataUrlFromB64(format, partial.b64), index: partial.index };
|
|
276
|
-
if (streamResponse)
|
|
277
|
-
writeSse(res, "partial", pd);
|
|
278
|
-
publish(requestId, "partial", pd);
|
|
279
|
-
}
|
|
280
|
-
: null,
|
|
281
|
-
});
|
|
282
|
-
throwIfJobCanceled(requestId);
|
|
283
|
-
if (r.b64) {
|
|
284
|
-
b64 = r.b64;
|
|
285
|
-
usage = r.usage;
|
|
286
|
-
webSearchCalls = r.webSearchCalls || 0;
|
|
287
|
-
revisedPrompt = r.revisedPrompt || null;
|
|
288
|
-
if (activeProvider === "grok" || activeProvider === "grok-api" || activeProvider === "gemini-api") {
|
|
289
|
-
resultFormat = imageFormatFromMime(("mime" in r ? r.mime : undefined) || detectImageMimeFromB64(r.b64) || "image/jpeg");
|
|
290
|
-
}
|
|
291
|
-
break;
|
|
292
|
-
}
|
|
293
|
-
lastErr = { message: "Empty response (safety refusal)" };
|
|
294
|
-
}
|
|
295
|
-
catch (e) {
|
|
296
|
-
lastErr = asUpstream(e);
|
|
297
|
-
if (isNonRetryableGenerationError(lastErr))
|
|
298
|
-
break;
|
|
299
|
-
}
|
|
300
|
-
if (attempt + 1 < maxAttempts) {
|
|
301
|
-
logEvent("node", "retry", {
|
|
302
|
-
requestId,
|
|
303
|
-
attempt: attempt + 1,
|
|
304
|
-
operation,
|
|
305
|
-
parentNodeId,
|
|
306
|
-
clientNodeId,
|
|
307
|
-
errorCode: lastErr?.code,
|
|
308
|
-
errorEventType: lastErr?.eventType,
|
|
309
|
-
errorEventCount: lastErr?.eventCount,
|
|
310
|
-
});
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
if (!b64) {
|
|
314
|
-
const finalErr = normalizeGenerationFailure(lastErr, {
|
|
315
|
-
safetyMessage: lastErr?.message || "Empty response after generation attempt",
|
|
316
|
-
});
|
|
317
|
-
finishStatus = "error";
|
|
318
|
-
finishHttpStatus = finalErr.status || 500;
|
|
319
|
-
finishErrorCode = finalErr.code || "NODE_GEN_FAILED";
|
|
320
|
-
logEvent("node", "final_error", {
|
|
321
|
-
requestId,
|
|
322
|
-
operation,
|
|
323
|
-
finalCode: finishErrorCode,
|
|
324
|
-
upstreamCode: lastErr?.upstreamCode || lastErr?.code,
|
|
325
|
-
errorEventType: lastErr?.eventType,
|
|
326
|
-
errorEventCount: lastErr?.eventCount,
|
|
327
|
-
diagnosticReason: lastErr?.diagnosticReason,
|
|
328
|
-
retryKind: lastErr?.retryKind,
|
|
329
|
-
referencesDroppedOnRetry: lastErr?.referencesDroppedOnRetry,
|
|
330
|
-
attempts: maxAttempts,
|
|
331
|
-
outerHttpAlreadyCommitted: res.headersSent,
|
|
332
|
-
sseErrorSent: streamResponse,
|
|
333
|
-
});
|
|
334
|
-
return writeNodeError(res, finishHttpStatus ?? 500, finishErrorCode ?? "NODE_GEN_FAILED", finalErr.message, parentNodeId, nodeErrorDetails(finalErr, lastErr), requestId);
|
|
335
|
-
}
|
|
336
|
-
const nodeId = newNodeId();
|
|
337
|
-
throwIfJobCanceled(requestId);
|
|
338
|
-
const elapsed = +((Date.now() - startTime) / 1000).toFixed(1);
|
|
339
|
-
const meta = {
|
|
340
|
-
nodeId,
|
|
341
|
-
parentNodeId,
|
|
342
|
-
sessionId,
|
|
343
|
-
clientNodeId,
|
|
344
|
-
prompt,
|
|
345
|
-
userPrompt: prompt,
|
|
346
|
-
revisedPrompt,
|
|
347
|
-
promptMode: normalizedPromptMode,
|
|
348
|
-
options: { quality, size: effectiveSize, format: resultFormat, moderation },
|
|
349
|
-
model: effectiveImageModel,
|
|
350
|
-
reasoningEffort,
|
|
351
|
-
createdAt: Date.now(),
|
|
352
|
-
createdAtIso: new Date().toISOString(),
|
|
353
|
-
elapsed,
|
|
354
|
-
usage: usage || null,
|
|
355
|
-
webSearchCalls,
|
|
356
|
-
webSearchEnabled,
|
|
357
|
-
contextMode,
|
|
358
|
-
searchMode,
|
|
359
|
-
provider: activeProvider,
|
|
360
|
-
kind: parentB64 ? "edit" : "generate",
|
|
361
|
-
requestId,
|
|
362
|
-
refsCount: refsForRequest.length,
|
|
363
|
-
quality,
|
|
364
|
-
size: effectiveSize,
|
|
365
|
-
format: resultFormat,
|
|
366
|
-
moderation,
|
|
367
|
-
};
|
|
368
|
-
await mkdir(ctx.config.storage.generatedDir, { recursive: true });
|
|
369
|
-
throwIfJobCanceled(requestId);
|
|
370
|
-
const { filename } = await saveNode(ctx.rootDir, {
|
|
371
|
-
nodeId,
|
|
372
|
-
b64,
|
|
373
|
-
meta,
|
|
374
|
-
ext: resultFormat,
|
|
375
|
-
generatedDir: ctx.config.storage.generatedDir,
|
|
376
|
-
});
|
|
377
|
-
finishMeta = { nodeId, filename, imageChars: b64.length };
|
|
378
|
-
finishHttpStatus = 200;
|
|
379
|
-
logEvent("node", "saved", {
|
|
380
|
-
requestId,
|
|
381
|
-
nodeId,
|
|
382
|
-
filename,
|
|
383
|
-
imageChars: b64.length,
|
|
384
|
-
elapsedMs: Date.now() - startTime,
|
|
385
|
-
});
|
|
386
|
-
const payload = {
|
|
387
|
-
nodeId,
|
|
388
|
-
parentNodeId,
|
|
389
|
-
requestId,
|
|
390
|
-
image: dataUrlFromB64(resultFormat, b64),
|
|
391
|
-
filename,
|
|
392
|
-
url: `/generated/${filename}`,
|
|
393
|
-
elapsed,
|
|
394
|
-
usage,
|
|
395
|
-
webSearchCalls,
|
|
396
|
-
webSearchEnabled,
|
|
397
|
-
provider: activeProvider,
|
|
398
|
-
model: effectiveImageModel,
|
|
399
|
-
reasoningEffort,
|
|
400
|
-
size: effectiveSize,
|
|
401
|
-
format: resultFormat,
|
|
402
|
-
moderation,
|
|
403
|
-
refsCount: refsForRequest.length,
|
|
404
|
-
contextMode,
|
|
405
|
-
searchMode,
|
|
406
|
-
warnings: qualityWarnings,
|
|
407
|
-
revisedPrompt,
|
|
408
|
-
promptMode: normalizedPromptMode,
|
|
409
|
-
};
|
|
410
|
-
publishJobEvent(requestId, "done", payload);
|
|
411
|
-
if (res.writableEnded) {
|
|
412
|
-
// async mode — response already sent
|
|
413
|
-
}
|
|
414
|
-
else if (streamResponse) {
|
|
415
|
-
writeSse(res, "done", payload);
|
|
416
|
-
res.end();
|
|
417
|
-
}
|
|
418
|
-
else {
|
|
419
|
-
res.json(payload);
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
catch (e) {
|
|
423
|
-
const err = errInfo(e);
|
|
424
|
-
const ext = (err.raw && typeof err.raw === "object" ? err.raw : {});
|
|
425
|
-
const code = err.code || classifyUpstreamError(err.message) || "NODE_GEN_FAILED";
|
|
426
|
-
if (isGenerationCanceledError(err.raw) || isJobCanceled(requestId)) {
|
|
427
|
-
const canceled = makeGenerationCanceledError();
|
|
428
|
-
finishCanceled = true;
|
|
429
|
-
finishHttpStatus = canceled.status;
|
|
430
|
-
finishErrorCode = canceled.code;
|
|
431
|
-
return writeNodeError(res, canceled.status, canceled.code, canceled.message, parentNodeId, {}, requestId);
|
|
432
|
-
}
|
|
433
|
-
finishStatus = "error";
|
|
434
|
-
finishHttpStatus = err.status || 500;
|
|
435
|
-
finishErrorCode = code;
|
|
436
|
-
logError("node", "error", err.raw, { requestId, code, parentNodeId, sessionId, clientNodeId });
|
|
437
|
-
writeNodeError(res, err.status || 500, code, err.message, parentNodeId, {
|
|
438
|
-
upstreamCode: ext.upstreamCode || null,
|
|
439
|
-
upstreamType: ext.upstreamType || null,
|
|
440
|
-
upstreamParam: ext.upstreamParam || null,
|
|
441
|
-
}, requestId);
|
|
442
|
-
}
|
|
443
|
-
finally {
|
|
444
|
-
finishJob(requestId, {
|
|
445
|
-
canceled: finishCanceled,
|
|
446
|
-
status: finishStatus,
|
|
447
|
-
httpStatus: finishHttpStatus,
|
|
448
|
-
errorCode: finishErrorCode,
|
|
449
|
-
meta: finishMeta,
|
|
450
|
-
});
|
|
451
|
-
}
|
|
452
|
-
});
|
|
7
|
+
app.post("/api/node/generate", (req, res) => runNodeGeneration(req, res, ctx));
|
|
453
8
|
app.get("/api/node/:nodeId", async (req, res) => {
|
|
454
9
|
try {
|
|
455
10
|
const { nodeId } = req.params;
|
package/routes/video.js
CHANGED
|
@@ -21,8 +21,14 @@ import { generateVideoThumbnail } from "../lib/videoThumb.js";
|
|
|
21
21
|
import { publish } from "../lib/eventBus.js";
|
|
22
22
|
import { publishJobEvent } from "../lib/ssePublish.js";
|
|
23
23
|
function sendSse(res, event, data) {
|
|
24
|
-
res.
|
|
25
|
-
|
|
24
|
+
if (res.writableEnded || res.destroyed)
|
|
25
|
+
return;
|
|
26
|
+
try {
|
|
27
|
+
res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
/* client went away mid-write; bus emit still delivers */
|
|
31
|
+
}
|
|
26
32
|
}
|
|
27
33
|
function dualEmitVideo(res, requestId, event, data) {
|
|
28
34
|
if (!res.writableEnded)
|
|
@@ -107,6 +113,7 @@ export function registerVideoRoutes(app, ctxRaw) {
|
|
|
107
113
|
let finishErrorCode;
|
|
108
114
|
let finishMeta = {};
|
|
109
115
|
let finishCanceled = false;
|
|
116
|
+
let jobOwned = false;
|
|
110
117
|
const cancelController = new AbortController();
|
|
111
118
|
if (!asyncMode) {
|
|
112
119
|
res.setHeader("Content-Type", "text/event-stream; charset=utf-8");
|
|
@@ -249,6 +256,7 @@ export function registerVideoRoutes(app, ctxRaw) {
|
|
|
249
256
|
? "Too many concurrent generation jobs"
|
|
250
257
|
: "Request ID already in use");
|
|
251
258
|
}
|
|
259
|
+
jobOwned = true;
|
|
252
260
|
registerJobAbortController(requestId, cancelController);
|
|
253
261
|
if (asyncMode)
|
|
254
262
|
res.status(202).json({ requestId });
|
|
@@ -389,7 +397,8 @@ export function registerVideoRoutes(app, ctxRaw) {
|
|
|
389
397
|
}
|
|
390
398
|
}
|
|
391
399
|
finally {
|
|
392
|
-
|
|
400
|
+
if (jobOwned)
|
|
401
|
+
finishJob(requestId, { canceled: finishCanceled, status: finishStatus, httpStatus: finishHttpStatus, errorCode: finishErrorCode, meta: finishMeta });
|
|
393
402
|
if (!res.writableEnded)
|
|
394
403
|
res.end();
|
|
395
404
|
}
|
package/routes/videoExtended.js
CHANGED
|
@@ -15,6 +15,15 @@ function routeError(message, status = 400) {
|
|
|
15
15
|
return Object.assign(new Error(message), { status });
|
|
16
16
|
}
|
|
17
17
|
function sendError(res, err) {
|
|
18
|
+
if (err?.name === "TimeoutError") {
|
|
19
|
+
res.status(504).json({ error: "Video operation timed out", code: "VIDEO_TIMEOUT" });
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (err?.name === "AbortError") {
|
|
23
|
+
if (!res.headersSent)
|
|
24
|
+
res.status(499).json({ error: "Request canceled", code: "REQUEST_CANCELED" });
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
18
27
|
res.status(typeof err?.status === "number" ? err.status : 500).json({ error: err?.message || String(err) });
|
|
19
28
|
}
|
|
20
29
|
async function safeGeneratedFile(ctx, file, options = {}) {
|
|
@@ -102,7 +111,11 @@ async function saveVideoResult(ctx, options) {
|
|
|
102
111
|
invalidateHistoryIndex();
|
|
103
112
|
return { filename, url: `/generated/${encodeURIComponent(filename)}`, sourceUrl: options.videoUrl };
|
|
104
113
|
}
|
|
105
|
-
function
|
|
114
|
+
function envDeadline(name, fallbackMs) {
|
|
115
|
+
const value = Number(process.env[name]);
|
|
116
|
+
return Number.isFinite(value) && value >= 1000 ? value : fallbackMs;
|
|
117
|
+
}
|
|
118
|
+
function requestSignal(req, res, timeoutMs) {
|
|
106
119
|
const ac = new AbortController();
|
|
107
120
|
const abort = () => {
|
|
108
121
|
if (!res.writableEnded)
|
|
@@ -110,7 +123,7 @@ function requestSignal(req, res) {
|
|
|
110
123
|
};
|
|
111
124
|
req.on("aborted", abort);
|
|
112
125
|
res.on("close", abort);
|
|
113
|
-
return ac.signal;
|
|
126
|
+
return AbortSignal.any([ac.signal, AbortSignal.timeout(timeoutMs)]);
|
|
114
127
|
}
|
|
115
128
|
function requirePrompt(value) {
|
|
116
129
|
return typeof value === "string" && value.trim() ? value : null;
|
|
@@ -143,7 +156,7 @@ export function registerVideoExtendedRoutes(app, ctxRaw) {
|
|
|
143
156
|
if (!videoUrl || typeof videoUrl !== "string")
|
|
144
157
|
return res.status(400).json({ error: "videoUrl required" });
|
|
145
158
|
const validModel = validateEditModel(model);
|
|
146
|
-
const signal = requestSignal(req, res);
|
|
159
|
+
const signal = requestSignal(req, res, envDeadline("IMA2_VIDEO_EDIT_TIMEOUT_MS", 10 * 60_000));
|
|
147
160
|
const { url, headers } = videoProxyUrl(ctx, "/v1/videos/edits");
|
|
148
161
|
const video = await resolveVideoInput(ctx, videoUrl);
|
|
149
162
|
const apiRes = await fetch(url, { method: "POST", headers, body: JSON.stringify({ model: validModel, prompt, video }), signal });
|
|
@@ -182,7 +195,7 @@ export function registerVideoExtendedRoutes(app, ctxRaw) {
|
|
|
182
195
|
const dur = Number(duration);
|
|
183
196
|
if (!Number.isInteger(dur) || dur < 2 || dur > 10)
|
|
184
197
|
return res.status(400).json({ error: "duration must be an integer between 2 and 10" });
|
|
185
|
-
const signal = requestSignal(req, res);
|
|
198
|
+
const signal = requestSignal(req, res, envDeadline("IMA2_VIDEO_EXTEND_TIMEOUT_MS", 10 * 60_000));
|
|
186
199
|
const { url, headers } = videoProxyUrl(ctx, "/v1/videos/extensions");
|
|
187
200
|
const video = await resolveVideoInput(ctx, videoUrl);
|
|
188
201
|
const apiRes = await fetch(url, { method: "POST", headers, body: JSON.stringify({ model: validModel, prompt, duration: dur, video }), signal });
|
|
@@ -238,6 +251,7 @@ export function registerVideoExtendedRoutes(app, ctxRaw) {
|
|
|
238
251
|
// --- Video Analysis (Grok 4.3 Vision) ---
|
|
239
252
|
app.post("/api/video/analyze", async (req, res) => {
|
|
240
253
|
try {
|
|
254
|
+
const signal = requestSignal(req, res, envDeadline("IMA2_VIDEO_ANALYZE_TIMEOUT_MS", 2 * 60_000));
|
|
241
255
|
const { videoUrl } = req.body ?? {};
|
|
242
256
|
if (!videoUrl || typeof videoUrl !== "string")
|
|
243
257
|
return res.status(400).json({ error: "videoUrl required" });
|
|
@@ -268,6 +282,7 @@ export function registerVideoExtendedRoutes(app, ctxRaw) {
|
|
|
268
282
|
],
|
|
269
283
|
}],
|
|
270
284
|
}),
|
|
285
|
+
signal,
|
|
271
286
|
});
|
|
272
287
|
if (!apiRes.ok) {
|
|
273
288
|
const t = await apiRes.text();
|
package/server.js
CHANGED
|
@@ -10,7 +10,7 @@ import { startGrokProxy } from "./lib/grokProxyLauncher.js";
|
|
|
10
10
|
import { startOAuthProxy } from "./lib/oauthLauncher.js";
|
|
11
11
|
import { migrateGeneratedStorage } from "./lib/storageMigration.js";
|
|
12
12
|
import { purgeStaleJobs } from "./lib/inflight.js";
|
|
13
|
-
import { configureLogger } from "./lib/logger.js";
|
|
13
|
+
import { configureLogger, logError } from "./lib/logger.js";
|
|
14
14
|
import { createRequestLogger } from "./lib/requestLogger.js";
|
|
15
15
|
import { configureApiCachePolicy } from "./lib/apiCachePolicy.js";
|
|
16
16
|
import { configureRoutes } from "./routes/index.js";
|
|
@@ -21,6 +21,7 @@ import { stopAgentQueueWorker } from "./lib/agentQueueWorker.js";
|
|
|
21
21
|
import { reapCardNewsJobs } from "./lib/cardNewsJobStore.js";
|
|
22
22
|
import { reapTerminalJobs } from "./lib/inflight.js";
|
|
23
23
|
import { errInfo } from "./lib/errInfo.js";
|
|
24
|
+
import { timingSafeEqual } from "node:crypto";
|
|
24
25
|
const rootDir = dirname(fileURLToPath(import.meta.url));
|
|
25
26
|
async function loadApiKey() {
|
|
26
27
|
if (process.env.OPENAI_API_KEY) {
|
|
@@ -152,11 +153,43 @@ function setUiStaticHeaders(res, filePath) {
|
|
|
152
153
|
res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
|
|
153
154
|
}
|
|
154
155
|
}
|
|
156
|
+
export function isLoopbackHost(host) {
|
|
157
|
+
const normalized = String(host || "").trim().toLowerCase();
|
|
158
|
+
return normalized === "127.0.0.1" || normalized === "::1" || normalized === "localhost";
|
|
159
|
+
}
|
|
160
|
+
export function assertLanAccessConfiguration(host, token) {
|
|
161
|
+
if (isLoopbackHost(host) || token)
|
|
162
|
+
return;
|
|
163
|
+
const message = `[server.security] Refusing non-loopback host ${host || "<empty>"}: set IMA2_LAN_TOKEN to enable LAN access.`;
|
|
164
|
+
console.error(message);
|
|
165
|
+
throw new Error(message);
|
|
166
|
+
}
|
|
167
|
+
function tokenMatches(actual, expected) {
|
|
168
|
+
if (typeof actual !== "string")
|
|
169
|
+
return false;
|
|
170
|
+
const actualBuffer = Buffer.from(actual);
|
|
171
|
+
const expectedBuffer = Buffer.from(expected);
|
|
172
|
+
return actualBuffer.length === expectedBuffer.length && timingSafeEqual(actualBuffer, expectedBuffer);
|
|
173
|
+
}
|
|
174
|
+
function createLanApiGuard(host, token) {
|
|
175
|
+
const requiredToken = isLoopbackHost(host) ? "" : String(token || "");
|
|
176
|
+
return function lanApiGuard(req, res, next) {
|
|
177
|
+
if (!requiredToken || !req.path.startsWith("/api"))
|
|
178
|
+
return next();
|
|
179
|
+
const supplied = req.get("x-ima2-token") ?? req.query.token;
|
|
180
|
+
if (tokenMatches(supplied, requiredToken))
|
|
181
|
+
return next();
|
|
182
|
+
return res.status(401).json({
|
|
183
|
+
error: { code: "LAN_TOKEN_REQUIRED", message: "A valid IMA2 LAN token is required" },
|
|
184
|
+
});
|
|
185
|
+
};
|
|
186
|
+
}
|
|
155
187
|
export function buildApp(ctx) {
|
|
156
188
|
const app = express();
|
|
157
189
|
configureApiCachePolicy(app);
|
|
158
190
|
configureLogger({ level: ctx.config.log.level });
|
|
159
191
|
app.use(createRequestLogger());
|
|
192
|
+
app.use(createLanApiGuard(ctx.config.server.host, ctx.config.server.lanToken));
|
|
160
193
|
app.use(express.json({ limit: ctx.config.server.bodyLimit }));
|
|
161
194
|
app.use(express.static(join(ctx.rootDir, "ui", "dist"), {
|
|
162
195
|
setHeaders: setUiStaticHeaders,
|
|
@@ -173,6 +206,24 @@ export function buildApp(ctx) {
|
|
|
173
206
|
immutable: true,
|
|
174
207
|
}));
|
|
175
208
|
configureRoutes(app, ctx);
|
|
209
|
+
app.use((_req, res) => {
|
|
210
|
+
res.status(404).json({ error: { code: "NOT_FOUND", message: "Route not found" } });
|
|
211
|
+
});
|
|
212
|
+
app.use((error, _req, res, _next) => {
|
|
213
|
+
const info = errInfo(error);
|
|
214
|
+
const candidateStatus = Number(info.status);
|
|
215
|
+
const operational = Boolean(error?.isOperational)
|
|
216
|
+
|| (Number.isInteger(candidateStatus) && candidateStatus >= 400 && candidateStatus < 500);
|
|
217
|
+
const status = operational ? candidateStatus : 500;
|
|
218
|
+
if (!operational)
|
|
219
|
+
logError("server", "unhandled:error", error);
|
|
220
|
+
res.status(status).json({
|
|
221
|
+
error: {
|
|
222
|
+
code: operational && info.code ? info.code : "INTERNAL_ERROR",
|
|
223
|
+
message: operational ? info.message : "Internal server error",
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
});
|
|
176
227
|
return app;
|
|
177
228
|
}
|
|
178
229
|
function runtimeHostUrl(host) {
|
|
@@ -309,6 +360,7 @@ export async function createRuntimeContext(overrides = {}) {
|
|
|
309
360
|
}
|
|
310
361
|
export async function startServer(overrides = {}) {
|
|
311
362
|
const ctx = await createRuntimeContext(overrides);
|
|
363
|
+
assertLanAccessConfiguration(ctx.config.server.host, ctx.config.server.lanToken);
|
|
312
364
|
await migrateGeneratedStorage(ctx);
|
|
313
365
|
purgeStaleJobs();
|
|
314
366
|
const app = buildApp(ctx);
|