ima2-gen 3.13.1 → 3.14.0

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 (157) hide show
  1. package/.env.example +10 -0
  2. package/CHANGELOG.md +14 -0
  3. package/README.md +15 -2
  4. package/bin/commands/capabilities.js +5 -3
  5. package/bin/commands/config.js +15 -4
  6. package/bin/commands/defaults.js +7 -5
  7. package/bin/commands/doctor.js +88 -39
  8. package/bin/commands/gen.js +21 -4
  9. package/bin/commands/models.js +2 -2
  10. package/bin/commands/ping.js +1 -1
  11. package/bin/commands/prompt.js +5 -3
  12. package/bin/commands/service.js +32 -4
  13. package/bin/commands/tools.js +19 -3
  14. package/bin/commands/upscale.js +18 -4
  15. package/bin/commands/video.js +37 -50
  16. package/bin/ima2.js +28 -9
  17. package/bin/lib/characterResolve.js +2 -1
  18. package/bin/lib/client.js +155 -20
  19. package/bin/lib/config-store.js +40 -7
  20. package/bin/lib/doctor-bundle.js +6 -2
  21. package/bin/lib/doctor-checks.js +13 -29
  22. package/bin/lib/doctor-media.js +3 -3
  23. package/bin/lib/doctor-providers.js +48 -35
  24. package/bin/lib/doctor-report.js +78 -0
  25. package/bin/lib/doctor-runtime.js +173 -0
  26. package/bin/lib/mcpJob.js +19 -7
  27. package/bin/lib/output.js +3 -0
  28. package/bin/lib/platform.js +12 -9
  29. package/bin/lib/recover-output.js +9 -2
  30. package/bin/lib/sse.js +9 -1
  31. package/bin/lib/videoMcp.js +16 -8
  32. package/config.js +41 -5
  33. package/docs/API.md +154 -16
  34. package/docs/CLI.md +65 -1
  35. package/docs/DOCKER.md +19 -4
  36. package/docs/DOCKER.zh-CN.md +11 -4
  37. package/docs/DOCKER.zh-TW.md +11 -4
  38. package/docs/FAQ.md +13 -0
  39. package/docs/NPX_QUICKSTART.md +6 -0
  40. package/docs/README.ja.md +15 -2
  41. package/docs/README.ko.md +15 -2
  42. package/docs/README.zh-CN.md +15 -2
  43. package/docs/README.zh-TW.md +15 -2
  44. package/docs/migration/runtime-test-inventory.md +79 -8
  45. package/lib/agyArtifact.js +82 -0
  46. package/lib/agyArtifactRead.js +249 -0
  47. package/lib/agyImageAdapter.js +2 -329
  48. package/lib/agyProcess.js +122 -0
  49. package/lib/apiRequestBudget.js +48 -0
  50. package/lib/assetLifecycle.js +42 -9
  51. package/lib/canvasVersionStore.js +46 -14
  52. package/lib/capabilities.js +7 -1
  53. package/lib/configKeys.js +2 -0
  54. package/lib/errors/providerMap.js +26 -0
  55. package/lib/eventBus.js +10 -6
  56. package/lib/eventsPolicy.js +9 -0
  57. package/lib/geminiApiImageAdapter.js +1 -214
  58. package/lib/generatePipeline.js +39 -165
  59. package/lib/generatedMediaAccess.js +74 -0
  60. package/lib/generationErrors.js +3 -22
  61. package/lib/grokImageAdapter.js +2 -353
  62. package/lib/grokImageCore.js +1 -57
  63. package/lib/grokImageDownload.js +159 -0
  64. package/lib/grokImageDownloadPolicy.js +124 -0
  65. package/lib/grokImagePlanner.js +301 -0
  66. package/lib/grokMultimodeAdapter.js +1 -75
  67. package/lib/grokVideoDownload.js +137 -34
  68. package/lib/inflight.js +112 -112
  69. package/lib/jobs/terminalStore.js +45 -0
  70. package/lib/lanSessionStore.js +131 -0
  71. package/lib/localAccessPolicy.js +180 -0
  72. package/lib/localLanAccess.js +281 -0
  73. package/lib/logger.js +22 -13
  74. package/lib/mcp/downloadMediaResult.js +72 -78
  75. package/lib/multimodePipeline.js +39 -126
  76. package/lib/nodeGeneration.js +44 -101
  77. package/lib/nodeStore.js +25 -11
  78. package/lib/nodeValidation.js +5 -1
  79. package/lib/pinnedHttpGet.js +174 -0
  80. package/lib/promptImport/githubFolder.js +32 -18
  81. package/lib/promptImport/githubSource.js +14 -10
  82. package/lib/promptImport/parsePromptCandidates.js +32 -2
  83. package/lib/providers/adapters/agyOperations.js +179 -0
  84. package/lib/providers/adapters/comfy.js +1 -0
  85. package/lib/providers/adapters/geminiOperations.js +214 -0
  86. package/lib/providers/adapters/googleExecution.js +97 -0
  87. package/lib/providers/adapters/grokExecution.js +137 -0
  88. package/lib/providers/adapters/grokMultimodeOperations.js +83 -0
  89. package/lib/providers/adapters/grokOperations.js +63 -0
  90. package/lib/providers/adapters/openaiExecution.js +118 -0
  91. package/lib/providers/adapters/openaiOperations.js +201 -0
  92. package/lib/providers/adapters/openaiTypes.js +1 -0
  93. package/lib/providers/derive.js +4 -0
  94. package/lib/providers/deriveCore.js +2 -7
  95. package/lib/providers/execution/admission.js +25 -0
  96. package/lib/providers/execution/index.js +34 -0
  97. package/lib/providers/execution/legacy.js +17 -0
  98. package/lib/providers/execution/legacyClassic.js +66 -0
  99. package/lib/providers/execution/legacyEdit.js +30 -0
  100. package/lib/providers/execution/legacyMultimode.js +34 -0
  101. package/lib/providers/execution/legacyNode.js +41 -0
  102. package/lib/providers/execution/types.js +1 -0
  103. package/lib/providers/registry.js +19 -14
  104. package/lib/providers/surfaceSupport.js +22 -0
  105. package/lib/responsesImageAdapter.js +1 -389
  106. package/lib/responsesTransport.js +190 -0
  107. package/lib/spriteJobEvents.js +20 -2
  108. package/lib/ssePublish.js +5 -4
  109. package/lib/videoExtendI2vOperation.js +76 -0
  110. package/package.json +5 -2
  111. package/routes/edit.js +31 -123
  112. package/routes/events.js +140 -63
  113. package/routes/mcpMedia.js +10 -2
  114. package/routes/models.js +28 -25
  115. package/routes/videoExtended.js +9 -68
  116. package/server.js +24 -46
  117. package/ui/dist/.vite/manifest.json +90 -63
  118. package/ui/dist/assets/{AgentWorkspace-PbBytTeV.js → AgentWorkspace-CL3MLv3l.js} +2 -2
  119. package/ui/dist/assets/App-B9cEM6dJ.js +9 -0
  120. package/ui/dist/assets/AssetGenWorkspace-swWPvXrx.js +2 -0
  121. package/ui/dist/assets/AssetsWorkspace-CRVeYU2F.js +1 -0
  122. package/ui/dist/assets/{CardNewsWorkspace-BS3jKOja.js → CardNewsWorkspace-BAOxWLKf.js} +2 -2
  123. package/ui/dist/assets/GenerationRequestLogPanel-DZtYZm-D.js +1 -0
  124. package/ui/dist/assets/HomeWorkspace-BeZ1s0Jm.js +1 -0
  125. package/ui/dist/assets/KeyingPanel-ZyBSXioh.js +1 -0
  126. package/ui/dist/assets/NodeCanvas-BvvcCErf.js +7 -0
  127. package/ui/dist/assets/{PromptBuilderPanel-D97A4pwB.js → PromptBuilderPanel-DBycmShA.js} +2 -2
  128. package/ui/dist/assets/PromptImportDialog-ChvMv7ek.js +2 -0
  129. package/ui/dist/assets/PromptImportDiscoverySection-60py7whH.js +1 -0
  130. package/ui/dist/assets/PromptImportFolderSection-CCRh_-_7.js +1 -0
  131. package/ui/dist/assets/PromptLibraryPanel-BFWQO8OT.js +2 -0
  132. package/ui/dist/assets/SettingsWorkspace-B5Ghsgdj.js +1 -0
  133. package/ui/dist/assets/SpriteRecipeWorkspace-DRWdkmwU.js +1 -0
  134. package/ui/dist/assets/index-7ngEOVxt.js +5 -0
  135. package/ui/dist/assets/index-B5jy7ePg.css +1 -0
  136. package/ui/dist/assets/index-gxh_02DQ.js +22 -0
  137. package/ui/dist/assets/{pptxgen.es-D4pedbwM.js → pptxgen.es-OUNhQLEV.js} +1 -1
  138. package/ui/dist/assets/promptBuilderStore-BTUUzbBT.js +1 -0
  139. package/ui/dist/assets/useAgentDialogFocus-DSc2qY8p.js +1 -0
  140. package/ui/dist/index.html +3 -2
  141. package/ui/dist/assets/AssetGenWorkspace-B-TGlvVb.js +0 -2
  142. package/ui/dist/assets/AssetsWorkspace-LnGaW151.js +0 -1
  143. package/ui/dist/assets/GenerationRequestLogPanel-DuDWD_ic.js +0 -1
  144. package/ui/dist/assets/HomeWorkspace-DVP7UG6O.js +0 -1
  145. package/ui/dist/assets/KeyingPanel-5bRUFQaq.js +0 -1
  146. package/ui/dist/assets/NodeCanvas-BBEpERTb.js +0 -7
  147. package/ui/dist/assets/PromptImportDialog-C89MN5JY.js +0 -2
  148. package/ui/dist/assets/PromptImportDiscoverySection-BgRfDOs2.js +0 -1
  149. package/ui/dist/assets/PromptImportFolderSection-zTMyriT1.js +0 -1
  150. package/ui/dist/assets/PromptLibraryPanel-BD96qZu_.js +0 -2
  151. package/ui/dist/assets/SettingsWorkspace-BAOk3X_8.js +0 -1
  152. package/ui/dist/assets/SpriteRecipeWorkspace-ClIM5QRo.js +0 -1
  153. package/ui/dist/assets/index-0fpfY8vu.css +0 -1
  154. package/ui/dist/assets/index-BFciPGTn.js +0 -5
  155. package/ui/dist/assets/index-BbykeXR9.js +0 -30
  156. package/ui/dist/assets/promptBuilderStore-Do7M8g7J.js +0 -1
  157. package/ui/dist/assets/useAgentDialogFocus-B6-QgEVI.js +0 -1
@@ -0,0 +1,180 @@
1
+ import { isIP } from "node:net";
2
+ import { posix } from "node:path";
3
+ const MAX_PUBLIC_ORIGINS = 16;
4
+ const MESSAGES = {
5
+ INVALID_PUBLIC_ORIGINS: "Invalid public origins configuration",
6
+ LOCAL_HOST_REJECTED: "Request host is not allowed",
7
+ LOCAL_ORIGIN_REJECTED: "Request origin is not allowed",
8
+ LOCAL_PATH_REJECTED: "Request path is not allowed",
9
+ LAN_TOKEN_REQUIRED: "A valid IMA2 LAN token is required",
10
+ INVALID_LAN_TOKEN: "Invalid LAN token configuration",
11
+ LAN_SESSION_CAPACITY: "LAN session capacity reached",
12
+ LAN_SESSION_UNAVAILABLE: "LAN sessions are unavailable",
13
+ LAN_AUTH_RATE_LIMITED: "Too many LAN authentication attempts",
14
+ LAN_SESSION_BODY_INVALID: "Expected an empty JSON object",
15
+ LAN_SESSION_CONTENT_TYPE: "Content type must be application/json",
16
+ };
17
+ export function localAccessError(code, status = 403) {
18
+ return Object.assign(new Error(MESSAGES[code]), { code, status });
19
+ }
20
+ function parseOrigin(value) {
21
+ if (typeof value !== "string" || /[\s\\*@?#]/u.test(value) || !/^https?:\/\/[^/]+\/?$/i.test(value))
22
+ throw new Error();
23
+ const url = new URL(value);
24
+ if (!/^https?:\/\//i.test(value) || !["http:", "https:"].includes(url.protocol)
25
+ || url.username || url.password || !url.hostname || url.pathname !== "/")
26
+ throw new Error();
27
+ return url.origin;
28
+ }
29
+ export function parsePublicOrigins(value) {
30
+ try {
31
+ if (!Array.isArray(value) || value.length > MAX_PUBLIC_ORIGINS)
32
+ throw new Error();
33
+ const origins = [...new Set(value.map(parseOrigin))];
34
+ if (origins.some((origin, index) => origins.slice(index + 1).some(other => hostMatches(new URL(origin).host, other) || hostMatches(new URL(other).host, origin))))
35
+ throw new Error();
36
+ return Object.freeze(origins);
37
+ }
38
+ catch {
39
+ throw localAccessError("INVALID_PUBLIC_ORIGINS", 500);
40
+ }
41
+ }
42
+ export function isLocalBind(host) {
43
+ return ["127.0.0.1", "::1", "localhost"].includes(String(host || "").trim().toLowerCase());
44
+ }
45
+ /** rawHeaders is authoritative for duplicates Node may otherwise discard/join. */
46
+ export function singleHeader(req, name, code) {
47
+ const values = [];
48
+ for (let i = 0; i < req.rawHeaders.length; i += 2) {
49
+ if (req.rawHeaders[i]?.toLowerCase() === name)
50
+ values.push(req.rawHeaders[i + 1] ?? "");
51
+ }
52
+ const value = req.headers[name];
53
+ if (values.length > 1 || Array.isArray(value))
54
+ throw localAccessError(code, code === "LAN_TOKEN_REQUIRED" ? 401 : 403);
55
+ return values[0] ?? value;
56
+ }
57
+ function literalHost(address) {
58
+ const mapped = /^::ffff:(\d+\.\d+\.\d+\.\d+)$/i.exec(address);
59
+ if (mapped)
60
+ return mapped[1];
61
+ const hex = /^::ffff:([\da-f]{1,4}):([\da-f]{1,4})$/i.exec(address);
62
+ if (hex) {
63
+ const high = parseInt(hex[1], 16), low = parseInt(hex[2], 16);
64
+ return `${high >> 8}.${high & 255}.${low >> 8}.${low & 255}`;
65
+ }
66
+ return isIP(address) === 6 ? `[${address}]` : address;
67
+ }
68
+ function servingOrigins(req, host, publicOrigins) {
69
+ const origins = new Map();
70
+ const port = req.socket.localPort;
71
+ const scheme = req.socket.encrypted ? "https" : "http";
72
+ const add = (hostname) => {
73
+ if (!port || !hostname)
74
+ return;
75
+ const url = new URL(`${scheme}://${hostname}:${port}`);
76
+ origins.set(url.host, url.origin);
77
+ };
78
+ const address = req.socket.localAddress;
79
+ if (address && isIP(address))
80
+ add(literalHost(address));
81
+ if (isLocalBind(host) || ["0.0.0.0", "::"].includes(host)) {
82
+ for (const alias of ["localhost", "127.0.0.1", "[::1]"])
83
+ add(alias);
84
+ }
85
+ if (host && !["0.0.0.0", "::"].includes(host))
86
+ add(literalHost(host));
87
+ for (const origin of publicOrigins) {
88
+ for (const [authority, inferred] of origins) {
89
+ if (hostMatches(authority, origin) || hostMatches(new URL(origin).host, inferred))
90
+ origins.delete(authority);
91
+ }
92
+ origins.set(new URL(origin).host, origin);
93
+ }
94
+ return [...origins.values()];
95
+ }
96
+ function hostMatches(host, origin) {
97
+ const url = new URL(origin);
98
+ return new URL(`${url.protocol}//${host}`).host === url.host;
99
+ }
100
+ export function createLocalAccessPolicy(config) {
101
+ const publicOrigins = parsePublicOrigins(config.server.publicOrigins);
102
+ const host = config.server.host.trim().toLowerCase();
103
+ return {
104
+ resolveHost(req) {
105
+ const hostOnly = Object.create(req);
106
+ hostOnly.headers = { ...req.headers };
107
+ delete hostOnly.headers.origin;
108
+ hostOnly.rawHeaders = req.rawHeaders.flatMap((value, i, all) => i % 2 === 0 && value.toLowerCase() !== "origin" ? [value, all[i + 1]] : []);
109
+ return this.resolveOrigin(hostOnly);
110
+ },
111
+ resolveOrigin(req) {
112
+ const authority = singleHeader(req, "host", "LOCAL_HOST_REJECTED");
113
+ if (!authority || !/^(?:\[[\da-fA-F:]+\]|[a-zA-Z0-9.-]+)(?::\d+)?$/.test(authority)) {
114
+ throw localAccessError("LOCAL_HOST_REJECTED");
115
+ }
116
+ let candidates;
117
+ try {
118
+ candidates = servingOrigins(req, host, publicOrigins).filter(origin => hostMatches(authority, origin));
119
+ }
120
+ catch {
121
+ throw localAccessError("LOCAL_HOST_REJECTED");
122
+ }
123
+ if (!candidates.length)
124
+ throw localAccessError("LOCAL_HOST_REJECTED");
125
+ const supplied = singleHeader(req, "origin", "LOCAL_ORIGIN_REJECTED");
126
+ if (supplied === undefined)
127
+ return candidates[0];
128
+ let origin;
129
+ try {
130
+ origin = parseOrigin(supplied);
131
+ }
132
+ catch {
133
+ throw localAccessError("LOCAL_ORIGIN_REJECTED");
134
+ }
135
+ if (!candidates.includes(origin))
136
+ throw localAccessError("LOCAL_ORIGIN_REJECTED");
137
+ return origin;
138
+ },
139
+ checkBrowserRequest(req, origin, viaCookie) {
140
+ const site = singleHeader(req, "sec-fetch-site", "LOCAL_ORIGIN_REJECTED");
141
+ if (site && site !== "same-origin" && site !== "none")
142
+ throw localAccessError("LOCAL_ORIGIN_REJECTED");
143
+ const supplied = singleHeader(req, "origin", "LOCAL_ORIGIN_REJECTED");
144
+ if (supplied !== undefined) {
145
+ try {
146
+ if (parseOrigin(supplied) !== origin)
147
+ throw new Error();
148
+ }
149
+ catch {
150
+ throw localAccessError("LOCAL_ORIGIN_REJECTED");
151
+ }
152
+ }
153
+ else if (viaCookie && !["GET", "HEAD", "OPTIONS"].includes(req.method)) {
154
+ throw localAccessError("LOCAL_ORIGIN_REJECTED");
155
+ }
156
+ },
157
+ };
158
+ }
159
+ /** Inspect the original full path even when called under an Express mount. */
160
+ export function protectedRequestPath(req) {
161
+ const raw = (req.originalUrl || req.url).split("?")[0] || "/";
162
+ let decoded;
163
+ try {
164
+ decoded = decodeURIComponent(raw);
165
+ }
166
+ catch {
167
+ throw localAccessError("LOCAL_PATH_REJECTED", 400);
168
+ }
169
+ const path = posix.normalize(decoded.replace(/\\/g, "/")).toLowerCase();
170
+ // Keep every protected interpretation, including a raw protected path whose
171
+ // dot segments escape the mount after normalization.
172
+ const candidates = [raw, decoded, path];
173
+ const api = candidates.some(candidate => /^\/api(?:\/|$)/i.test(candidate));
174
+ const media = candidates.some(candidate => /^\/generated(?:\/|$)/i.test(candidate));
175
+ const canonical = /^\/(api|generated)(?:\/|$)/i.exec(raw);
176
+ const prefix = canonical?.[0];
177
+ const alias = (api || media) && (!prefix || path !== decoded.toLowerCase()
178
+ || /[%\\\0]/.test(raw.slice(0, prefix.length)));
179
+ return { api, media, alias, path };
180
+ }
@@ -0,0 +1,281 @@
1
+ import express from "express";
2
+ import { createHash, timingSafeEqual } from "node:crypto";
3
+ import { createLocalAccessPolicy, isLocalBind, localAccessError, protectedRequestPath, singleHeader } from "./localAccessPolicy.js";
4
+ import { createLanAuthThrottle, createLanSessionStore } from "./lanSessionStore.js";
5
+ const SESSION_PATH = /^\/api\/auth\/lan\/session$/i;
6
+ const CALLBACK_PATH = "/api/mcp/oauth/callback";
7
+ const BOOTSTRAP_BODY_BYTES = 1024;
8
+ const COMPAT_TOKEN_MAX_BYTES = 4096;
9
+ function tokenMatches(value, expected) {
10
+ if (value === undefined || !expected)
11
+ return false;
12
+ const actual = Buffer.from(value), wanted = Buffer.from(expected);
13
+ return actual.length === wanted.length && timingSafeEqual(actual, wanted);
14
+ }
15
+ function cookieName(origin) {
16
+ const suffix = createHash("sha256").update(origin).digest("hex").slice(0, 12);
17
+ return `${origin.startsWith("https:") ? "__Host-" : ""}ima2_lan_${suffix}`;
18
+ }
19
+ function cookieOptions(origin) {
20
+ return { path: "/", httpOnly: true, sameSite: "strict", secure: origin.startsWith("https:") };
21
+ }
22
+ function readCredentials(req, name, maxBytes) {
23
+ const header = singleHeader(req, "x-ima2-token", "LAN_TOKEN_REQUIRED");
24
+ const params = new URLSearchParams((req.originalUrl || req.url).split("?").slice(1).join("?"));
25
+ const queries = params.getAll("token");
26
+ const invalidQuery = [...params.keys()].some(key => /^token[\[.]/.test(key));
27
+ if (queries.length > 1 || invalidQuery)
28
+ throw localAccessError("LAN_TOKEN_REQUIRED", 401);
29
+ const query = queries[0];
30
+ const cookies = name ? req.headers.cookie?.split(";").map(part => part.trim()) ?? [] : [];
31
+ const matches = cookies.filter(part => part.split("=")[0] === name);
32
+ if (matches.length > 1)
33
+ throw localAccessError("LAN_TOKEN_REQUIRED", 401);
34
+ const cookie = matches[0]?.slice(name.length + 1);
35
+ for (const value of [header, query, cookie]) {
36
+ if (value !== undefined && Buffer.byteLength(value) > maxBytes)
37
+ throw localAccessError("LAN_TOKEN_REQUIRED", 401);
38
+ }
39
+ if (cookie !== undefined && !/^[\w-]{43}$/.test(cookie)) {
40
+ throw localAccessError("LAN_TOKEN_REQUIRED", 401);
41
+ }
42
+ return { header, query, cookie, explicit: header ?? query };
43
+ }
44
+ function sendAccessError(res, error) {
45
+ const safe = error;
46
+ const known = typeof safe?.code === "string" && typeof safe?.status === "number";
47
+ if (!res.getHeader("Cache-Control"))
48
+ res.setHeader("Cache-Control", "no-store");
49
+ res.status(known ? safe.status : 503).type("application/json").end(JSON.stringify({ error: {
50
+ code: known ? safe.code : "LAN_SESSION_UNAVAILABLE",
51
+ message: known ? safe.message : "LAN sessions are unavailable",
52
+ } }));
53
+ }
54
+ function privateMediaHeaders(res) {
55
+ res.setHeader("Cache-Control", "private, no-store, max-age=0");
56
+ res.vary("Cookie");
57
+ res.vary("X-Ima2-Token");
58
+ res.setHeader("Referrer-Policy", "no-referrer");
59
+ res.setHeader("Cross-Origin-Resource-Policy", "same-origin");
60
+ }
61
+ function trackResponse(store, value, res) {
62
+ const untrack = store.track(value, () => { res.end(); });
63
+ const cleanup = () => {
64
+ untrack();
65
+ res.off("finish", cleanup);
66
+ res.off("close", cleanup);
67
+ };
68
+ res.once("finish", cleanup);
69
+ res.once("close", cleanup);
70
+ if (res.writableEnded || res.destroyed)
71
+ cleanup();
72
+ }
73
+ /** Compatibility-only token guard. Production uses the shared policy instance. */
74
+ export function createLanApiGuard(host, token) {
75
+ const expected = isLocalBind(host) ? "" : token || "";
76
+ return (req, res, next) => {
77
+ try {
78
+ if (!expected || !/^\/api(?:\/|$)/i.test(req.path))
79
+ return next();
80
+ if (req.method === "GET" && req.path.toLowerCase() === CALLBACK_PATH)
81
+ return next();
82
+ const credentials = readCredentials(req, "", COMPAT_TOKEN_MAX_BYTES);
83
+ if (!tokenMatches(credentials.explicit, expected))
84
+ throw localAccessError("LAN_TOKEN_REQUIRED", 401);
85
+ next();
86
+ }
87
+ catch (error) {
88
+ sendAccessError(res, error);
89
+ }
90
+ };
91
+ }
92
+ class LocalLanAccess {
93
+ policy;
94
+ lan;
95
+ token;
96
+ bounds;
97
+ store;
98
+ throttle;
99
+ tracked = new WeakSet();
100
+ sessionState = new WeakMap();
101
+ constructor(ctx) {
102
+ this.policy = createLocalAccessPolicy(ctx.config);
103
+ this.lan = !isLocalBind(ctx.config.server.host);
104
+ this.token = ctx.config.server.lanToken;
105
+ this.bounds = ctx.config.security;
106
+ const { lan, token, bounds } = this;
107
+ if ((lan && !token) || Buffer.byteLength(token) > bounds.lanTokenMaxBytes)
108
+ throw localAccessError("INVALID_LAN_TOKEN", 500);
109
+ this.store = createLanSessionStore({ ttlMs: bounds.lanSessionTtlMs, maxSessions: bounds.lanMaxSessions });
110
+ this.throttle = createLanAuthThrottle({ windowMs: bounds.lanAuthWindowMs, maxFailures: bounds.lanAuthMaxFailures, maxBuckets: bounds.lanAuthMaxBuckets });
111
+ }
112
+ mediaHeaders = (req, res, next) => {
113
+ // The canonical raw prefix still receives headers when its tail has bad escapes.
114
+ try {
115
+ if (this.lan && protectedRequestPath(req).media)
116
+ privateMediaHeaders(res);
117
+ }
118
+ catch {
119
+ const prefix = (req.originalUrl || req.url).replace(/%([\da-f]{2})/ig, (_match, hex) => String.fromCharCode(parseInt(hex, 16)));
120
+ if (this.lan && /^\/generated(?:[\/\\]|$)/i.test(prefix))
121
+ privateMediaHeaders(res);
122
+ }
123
+ next();
124
+ };
125
+ guard = (req, res, next) => {
126
+ try {
127
+ const { policy, lan, bounds } = this;
128
+ const route = protectedRequestPath(req);
129
+ if (!route.api && !route.media)
130
+ return next();
131
+ if (route.alias)
132
+ throw localAccessError("LOCAL_PATH_REJECTED", 400);
133
+ const rawPath = (req.originalUrl || req.url).split("?")[0]?.toLowerCase();
134
+ if (req.method === "GET" && rawPath === CALLBACK_PATH) {
135
+ policy.resolveHost(req);
136
+ return next();
137
+ }
138
+ const origin = policy.resolveOrigin(req);
139
+ const credentials = lan ? readCredentials(req, cookieName(origin), bounds.lanTokenMaxBytes) : null;
140
+ const viaCookie = !!credentials?.cookie && credentials.explicit === undefined;
141
+ policy.checkBrowserRequest(req, origin, viaCookie);
142
+ if (credentials)
143
+ this.authorize(credentials, origin, res, req.socket.remoteAddress || "unknown");
144
+ if (!res.writableEnded && !res.destroyed)
145
+ next();
146
+ }
147
+ catch (error) {
148
+ sendAccessError(res, error);
149
+ }
150
+ };
151
+ checkTokenCooldown(peer, res) {
152
+ const retry = this.throttle.retryAfter(peer);
153
+ if (retry) {
154
+ res.setHeader("Retry-After", String(retry));
155
+ throw localAccessError("LAN_AUTH_RATE_LIMITED", 429);
156
+ }
157
+ }
158
+ validateToken(value, peer) {
159
+ if (tokenMatches(value, this.token))
160
+ return;
161
+ this.throttle.fail(peer);
162
+ throw localAccessError("LAN_TOKEN_REQUIRED", 401);
163
+ }
164
+ authorize(credentials, origin, res, peer) {
165
+ const { store, tracked } = this;
166
+ if (credentials.explicit !== undefined) {
167
+ this.checkTokenCooldown(peer, res);
168
+ this.validateToken(credentials.explicit, peer);
169
+ return;
170
+ }
171
+ if (!credentials.cookie || !store.validate(credentials.cookie, origin))
172
+ throw localAccessError("LAN_TOKEN_REQUIRED", 401);
173
+ if (!tracked.has(res)) {
174
+ tracked.add(res);
175
+ trackResponse(store, credentials.cookie, res);
176
+ }
177
+ }
178
+ sessionCheck(req, res) {
179
+ const { policy, lan, bounds, throttle } = this;
180
+ res.setHeader("Cache-Control", "no-store");
181
+ res.setHeader("Referrer-Policy", "no-referrer");
182
+ res.setHeader("Content-Security-Policy", "frame-ancestors 'none'");
183
+ const origin = policy.resolveOrigin(req);
184
+ policy.checkBrowserRequest(req, origin, false);
185
+ if (req.method !== "GET" && !singleHeader(req, "origin", "LOCAL_ORIGIN_REJECTED"))
186
+ throw localAccessError("LOCAL_ORIGIN_REJECTED");
187
+ const peer = req.socket.remoteAddress || "unknown";
188
+ if (lan && req.method === "POST")
189
+ this.checkTokenCooldown(peer, res);
190
+ let credentials;
191
+ try {
192
+ credentials = lan ? readCredentials(req, cookieName(origin), bounds.lanTokenMaxBytes) : { header: undefined, query: undefined, cookie: undefined, explicit: undefined };
193
+ }
194
+ catch (error) {
195
+ if (lan && req.method === "POST")
196
+ throttle.fail(peer);
197
+ throw error;
198
+ }
199
+ if (lan && req.method === "POST")
200
+ this.validateToken(credentials.header, peer);
201
+ if (lan && req.method === "GET" && credentials.explicit !== undefined) {
202
+ this.checkTokenCooldown(peer, res);
203
+ this.validateToken(credentials.explicit, peer);
204
+ }
205
+ if (lan && req.method === "DELETE" && !credentials.cookie)
206
+ throw localAccessError("LAN_TOKEN_REQUIRED", 401);
207
+ return { origin, credentials };
208
+ }
209
+ sessionResult(req, res) {
210
+ const { sessionState, lan, store } = this;
211
+ const { origin, credentials } = sessionState.get(req);
212
+ if (req.method === "GET") {
213
+ const session = credentials.cookie ? store.validate(credentials.cookie, origin) : null;
214
+ res.json({ mode: lan ? "lan" : "local", authenticated: !lan || credentials.explicit !== undefined || !!session,
215
+ expiresAt: lan && credentials.explicit === undefined ? session?.expiresAt ?? null : null });
216
+ return;
217
+ }
218
+ if (req.method === "DELETE") {
219
+ if (lan) {
220
+ store.revoke(credentials.cookie);
221
+ res.clearCookie(cookieName(origin), cookieOptions(origin));
222
+ }
223
+ res.status(204).end();
224
+ return;
225
+ }
226
+ if (!req.body || Array.isArray(req.body) || typeof req.body !== "object" || Object.keys(req.body).length)
227
+ throw localAccessError("LAN_SESSION_BODY_INVALID", 400);
228
+ if (req.aborted || res.destroyed)
229
+ return;
230
+ if (lan) {
231
+ if (credentials.cookie && store.validate(credentials.cookie, origin))
232
+ store.revoke(credentials.cookie);
233
+ const session = store.issue(origin);
234
+ res.once("close", () => { if (!res.writableFinished)
235
+ store.revoke(session.value); });
236
+ try {
237
+ res.cookie(cookieName(origin), session.value, cookieOptions(origin));
238
+ }
239
+ catch {
240
+ store.revoke(session.value);
241
+ throw localAccessError("LAN_SESSION_UNAVAILABLE", 503);
242
+ }
243
+ }
244
+ res.status(204).end();
245
+ }
246
+ registerSessionRoutes(app, budget) {
247
+ const check = (req, res, next) => {
248
+ try {
249
+ if (!["GET", "POST", "DELETE"].includes(req.method))
250
+ return next("route");
251
+ if (protectedRequestPath(req).alias)
252
+ throw localAccessError("LOCAL_PATH_REJECTED", 400);
253
+ this.sessionState.set(req, this.sessionCheck(req, res));
254
+ next();
255
+ }
256
+ catch (error) {
257
+ sendAccessError(res, error);
258
+ }
259
+ };
260
+ const parser = express.json({ limit: BOOTSTRAP_BODY_BYTES, strict: true, inflate: false });
261
+ const body = (req, res, next) => {
262
+ if (req.method !== "POST")
263
+ return next();
264
+ if (!req.is("application/json"))
265
+ return sendAccessError(res, localAccessError("LAN_SESSION_CONTENT_TYPE", 415));
266
+ parser(req, res, error => error ? sendAccessError(res, localAccessError("LAN_SESSION_BODY_INVALID", 400)) : next());
267
+ };
268
+ app.all(SESSION_PATH, check, budget, body, (req, res) => {
269
+ try {
270
+ this.sessionResult(req, res);
271
+ }
272
+ catch (error) {
273
+ sendAccessError(res, error);
274
+ }
275
+ });
276
+ }
277
+ dispose = () => { this.store.dispose(); this.throttle.dispose(); };
278
+ }
279
+ export function createLocalLanAccess(ctx) {
280
+ return new LocalLanAccess(ctx);
281
+ }
package/lib/logger.js CHANGED
@@ -48,6 +48,17 @@ function shouldRedactKey(key) {
48
48
  lower.includes("base64") ||
49
49
  lower.includes("dataurl"));
50
50
  }
51
+ function sanitizeLogString(value) {
52
+ const oneLine = value
53
+ .replace(/data:image\/[a-z0-9.+-]+;base64,[A-Za-z0-9+/=]+/gi, "data:image/[redacted]")
54
+ .replace(/\b[a-z][a-z0-9+.-]*:\/\/[^\s"'<>]+/gi, "[redacted-url]")
55
+ .replace(/(^|[\s("'<>=,\[{])\/\/[^\s"'<>]+/g, "$1[redacted-url]")
56
+ .replace(/Bearer\s+[^\s"'<>]+/gi, "Bearer [redacted]")
57
+ .replace(/([?&][^=\s?&#"'<>]+)=([^&\s"'<>]*)/g, "$1=[redacted]")
58
+ .replace(/\s+/g, " ")
59
+ .trim();
60
+ return oneLine.length > MAX_VALUE_LEN ? `${oneLine.slice(0, MAX_VALUE_LEN)}...` : oneLine;
61
+ }
51
62
  function sanitizeValue(value) {
52
63
  if (value == null)
53
64
  return value;
@@ -59,25 +70,23 @@ function sanitizeValue(value) {
59
70
  return `[buffer:${value.length}]`;
60
71
  if (typeof value === "object")
61
72
  return "[object]";
62
- if (typeof value === "string") {
63
- const oneLine = value
64
- .replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer [redacted]")
65
- .replace(/data:image\/[a-z0-9.+-]+;base64,[A-Za-z0-9+/=]+/gi, "data:image/[redacted]")
66
- .replace(/\s+/g, " ")
67
- .trim();
68
- return oneLine.length > MAX_VALUE_LEN ? `${oneLine.slice(0, MAX_VALUE_LEN)}...` : oneLine;
69
- }
73
+ if (typeof value === "string")
74
+ return sanitizeLogString(value);
70
75
  return value;
71
76
  }
72
77
  export function sanitizeError(err) {
73
78
  if (!err)
74
79
  return { message: "Unknown error" };
75
- const e = err;
80
+ const e = typeof err === "object"
81
+ ? err
82
+ : {};
83
+ const message = typeof err === "string" ? err
84
+ : typeof e.message === "string" ? e.message : "Unknown error";
76
85
  return {
77
- name: e.name || "Error",
78
- code: e.code || undefined,
79
- status: e.status || undefined,
80
- message: sanitizeValue(e.message || "Unknown error"),
86
+ name: typeof e.name === "string" && e.name ? sanitizeLogString(e.name) : "Error",
87
+ code: typeof e.code === "string" ? sanitizeLogString(e.code) : undefined,
88
+ status: typeof e.status === "number" && Number.isFinite(e.status) ? e.status : undefined,
89
+ message: sanitizeLogString(message),
81
90
  };
82
91
  }
83
92
  export function sanitizeFields(fields = {}) {