@juspay/neurolink 13.8.17 → 13.8.18

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 (192) hide show
  1. package/CHANGELOG.md +2 -2
  2. package/README.md +1 -1
  3. package/dist/action/actionExecutor.d.ts +33 -3
  4. package/dist/action/actionExecutor.js +71 -15
  5. package/dist/action/actionInputs.js +4 -1
  6. package/dist/action/cliVersion.d.ts +25 -0
  7. package/dist/action/cliVersion.js +26 -0
  8. package/dist/action/index.d.ts +2 -1
  9. package/dist/action/index.js +2 -1
  10. package/dist/adapters/video/directorPipeline.js +46 -36
  11. package/dist/adapters/video/ffmpegAdapter.d.ts +17 -0
  12. package/dist/adapters/video/ffmpegAdapter.js +32 -5
  13. package/dist/adapters/video/frameExtractor.js +1 -1
  14. package/dist/adapters/video/videoMerger.js +5 -2
  15. package/dist/agent/arithmeticEvaluator.d.ts +32 -0
  16. package/dist/agent/arithmeticEvaluator.js +349 -0
  17. package/dist/agent/directTools.d.ts +6 -18
  18. package/dist/agent/directTools.js +17 -56
  19. package/dist/artifacts/artifactStore.d.ts +22 -1
  20. package/dist/artifacts/artifactStore.js +66 -6
  21. package/dist/auth/anthropicOAuth.d.ts +26 -1
  22. package/dist/auth/anthropicOAuth.js +81 -5
  23. package/dist/auth/secureFiles.d.ts +37 -0
  24. package/dist/auth/secureFiles.js +95 -0
  25. package/dist/auth/tokenStore.d.ts +43 -6
  26. package/dist/auth/tokenStore.js +260 -79
  27. package/dist/avatar/providers/DIDAvatar.js +49 -45
  28. package/dist/avatar/providers/ReplicateAvatar.js +33 -40
  29. package/dist/browser/neurolink.min.js +485 -558
  30. package/dist/cli/commands/auth.js +112 -70
  31. package/dist/cli/commands/proxy.d.ts +4 -0
  32. package/dist/cli/commands/proxy.js +128 -13
  33. package/dist/cli/commands/serve.d.ts +10 -1
  34. package/dist/cli/commands/serve.js +98 -17
  35. package/dist/cli/commands/server.js +17 -5
  36. package/dist/cli/index.js +146 -5
  37. package/dist/cli/loop/optionsSchema.d.ts +1 -1
  38. package/dist/cli/utils/envManager.js +17 -3
  39. package/dist/cli/utils/serverUtils.d.ts +39 -3
  40. package/dist/cli/utils/serverUtils.js +63 -6
  41. package/dist/core/baseProvider.js +7 -1
  42. package/dist/core/modules/MessageBuilder.js +5 -0
  43. package/dist/core/modules/ToolsManager.d.ts +46 -1
  44. package/dist/core/modules/ToolsManager.js +190 -43
  45. package/dist/files/fileReferenceRegistry.d.ts +2 -23
  46. package/dist/files/fileReferenceRegistry.js +55 -5
  47. package/dist/hitl/hitlManager.js +1 -0
  48. package/dist/index.d.ts +2 -1
  49. package/dist/index.js +6 -1
  50. package/dist/mcp/auth/tokenStorage.d.ts +7 -0
  51. package/dist/mcp/auth/tokenStorage.js +23 -4
  52. package/dist/mcp/externalServerManager.d.ts +13 -3
  53. package/dist/mcp/externalServerManager.js +154 -136
  54. package/dist/mcp/index.d.ts +2 -0
  55. package/dist/mcp/index.js +3 -0
  56. package/dist/mcp/mcpClientFactory.d.ts +14 -0
  57. package/dist/mcp/mcpClientFactory.js +70 -7
  58. package/dist/mcp/projectConfigTrust.d.ts +72 -0
  59. package/dist/mcp/projectConfigTrust.js +237 -0
  60. package/dist/mcp/transportUrlPolicy.d.ts +39 -0
  61. package/dist/mcp/transportUrlPolicy.js +172 -0
  62. package/dist/music/providers/BeatovenMusic.js +35 -46
  63. package/dist/music/providers/ReplicateMusic.js +33 -40
  64. package/dist/neurolink.d.ts +16 -1
  65. package/dist/neurolink.js +64 -11
  66. package/dist/processors/archive/ArchiveProcessor.js +15 -1
  67. package/dist/processors/archive/zipEntryIndex.d.ts +22 -0
  68. package/dist/processors/archive/zipEntryIndex.js +33 -0
  69. package/dist/processors/base/BaseFileProcessor.d.ts +9 -21
  70. package/dist/processors/base/BaseFileProcessor.js +40 -111
  71. package/dist/processors/data/YamlProcessor.d.ts +13 -1
  72. package/dist/processors/data/YamlProcessor.js +93 -2
  73. package/dist/processors/document/OpenDocumentProcessor.js +7 -2
  74. package/dist/processors/document/PptxProcessor.js +12 -7
  75. package/dist/processors/media/VideoProcessor.js +93 -15
  76. package/dist/proxy/inboundAuth.d.ts +89 -0
  77. package/dist/proxy/inboundAuth.js +219 -0
  78. package/dist/proxy/tokenRefresh.d.ts +6 -0
  79. package/dist/proxy/tokenRefresh.js +13 -1
  80. package/dist/proxy/updateState.js +4 -4
  81. package/dist/rag/document/loaders.d.ts +0 -6
  82. package/dist/rag/document/loaders.js +16 -10
  83. package/dist/rag/ragIntegration.d.ts +2 -2
  84. package/dist/rag/ragIntegration.js +35 -10
  85. package/dist/rag/retrieval/vectorQueryTool.d.ts +7 -0
  86. package/dist/rag/retrieval/vectorQueryTool.js +54 -17
  87. package/dist/server/abstract/baseServerAdapter.d.ts +43 -0
  88. package/dist/server/abstract/baseServerAdapter.js +119 -6
  89. package/dist/server/adapters/expressAdapter.d.ts +3 -0
  90. package/dist/server/adapters/expressAdapter.js +78 -6
  91. package/dist/server/adapters/fastifyAdapter.d.ts +3 -0
  92. package/dist/server/adapters/fastifyAdapter.js +165 -70
  93. package/dist/server/adapters/honoAdapter.d.ts +9 -1
  94. package/dist/server/adapters/honoAdapter.js +99 -21
  95. package/dist/server/adapters/koaAdapter.d.ts +1 -0
  96. package/dist/server/adapters/koaAdapter.js +153 -74
  97. package/dist/server/index.d.ts +2 -0
  98. package/dist/server/index.js +4 -0
  99. package/dist/server/middleware/auth.js +6 -2
  100. package/dist/server/middleware/cache.js +6 -1
  101. package/dist/server/middleware/rateLimit.js +29 -4
  102. package/dist/server/routes/agentRoutes.js +24 -11
  103. package/dist/server/routes/claudeProxyRoutes.d.ts +1 -0
  104. package/dist/server/routes/claudeProxyRoutes.js +273 -84
  105. package/dist/server/routes/codexProxyRoutes.d.ts +2 -2
  106. package/dist/server/routes/codexProxyRoutes.js +125 -40
  107. package/dist/server/routes/index.d.ts +4 -0
  108. package/dist/server/routes/index.js +35 -4
  109. package/dist/server/routes/mcpRoutes.d.ts +6 -2
  110. package/dist/server/routes/mcpRoutes.js +21 -7
  111. package/dist/server/routes/memoryRoutes.js +7 -6
  112. package/dist/server/routes/toolRoutes.d.ts +12 -2
  113. package/dist/server/routes/toolRoutes.js +33 -3
  114. package/dist/server/routes/transcribeRoutes.js +60 -13
  115. package/dist/server/utils/clientAddress.d.ts +23 -0
  116. package/dist/server/utils/clientAddress.js +65 -0
  117. package/dist/server/utils/clientError.d.ts +35 -0
  118. package/dist/server/utils/clientError.js +129 -0
  119. package/dist/server/utils/pathMatch.d.ts +21 -0
  120. package/dist/server/utils/pathMatch.js +41 -0
  121. package/dist/server/utils/validation.d.ts +9 -0
  122. package/dist/server/utils/validation.js +9 -0
  123. package/dist/server/websocket/WebSocketHandler.js +22 -3
  124. package/dist/server/websocket/frameQueue.d.ts +19 -0
  125. package/dist/server/websocket/frameQueue.js +41 -3
  126. package/dist/server/websocket/transcribeWebSocket.d.ts +8 -1
  127. package/dist/server/websocket/transcribeWebSocket.js +223 -34
  128. package/dist/skills/skillStores.js +5 -0
  129. package/dist/tasks/store/fileTaskStore.d.ts +2 -0
  130. package/dist/tasks/store/fileTaskStore.js +11 -4
  131. package/dist/types/attachments.d.ts +117 -0
  132. package/dist/types/attachments.js +15 -0
  133. package/dist/types/auth.d.ts +6 -0
  134. package/dist/types/cli.d.ts +4 -0
  135. package/dist/types/config.d.ts +56 -0
  136. package/dist/types/file.d.ts +8 -0
  137. package/dist/types/hitl.d.ts +8 -0
  138. package/dist/types/index.d.ts +1 -0
  139. package/dist/types/index.js +1 -0
  140. package/dist/types/mcp.d.ts +98 -0
  141. package/dist/types/multimodal.js +0 -41
  142. package/dist/types/processor.d.ts +14 -1
  143. package/dist/types/proxy.d.ts +62 -3
  144. package/dist/types/rag.d.ts +9 -0
  145. package/dist/types/safeFetch.d.ts +40 -0
  146. package/dist/types/server.d.ts +206 -10
  147. package/dist/types/tools.d.ts +57 -0
  148. package/dist/types/transcribe.d.ts +6 -0
  149. package/dist/utils/attachmentPolicy.d.ts +89 -0
  150. package/dist/utils/attachmentPolicy.js +327 -0
  151. package/dist/utils/dotenvBootstrap.d.ts +48 -19
  152. package/dist/utils/dotenvBootstrap.js +172 -31
  153. package/dist/utils/fileDetector.js +115 -65
  154. package/dist/utils/json/coerce.js +11 -1
  155. package/dist/utils/logSanitize.d.ts +5 -0
  156. package/dist/utils/logSanitize.js +8 -0
  157. package/dist/utils/logger.js +73 -1
  158. package/dist/utils/messageBuilder.d.ts +2 -2
  159. package/dist/utils/messageBuilder.js +118 -105
  160. package/dist/utils/multimodalOptionsBuilder.d.ts +1 -0
  161. package/dist/utils/multimodalOptionsBuilder.js +2 -0
  162. package/dist/utils/redis.d.ts +12 -0
  163. package/dist/utils/redis.js +72 -0
  164. package/dist/utils/safeFetch.d.ts +22 -1
  165. package/dist/utils/safeFetch.js +132 -38
  166. package/dist/utils/safeStoreId.d.ts +19 -0
  167. package/dist/utils/safeStoreId.js +29 -0
  168. package/dist/utils/schemaConversion.d.ts +30 -3
  169. package/dist/utils/schemaConversion.js +457 -84
  170. package/dist/utils/sizeGuard.js +2 -2
  171. package/dist/utils/ssrfGuard.d.ts +3 -3
  172. package/dist/utils/ssrfGuard.js +71 -39
  173. package/dist/voice/eval/sttEvalStore.d.ts +26 -3
  174. package/dist/voice/eval/sttEvalStore.js +77 -12
  175. package/dist/voice/livekit/agentDefinition.d.ts +18 -0
  176. package/dist/voice/livekit/agentDefinition.js +22 -0
  177. package/dist/voice/livekit/realtimeMcpTools.d.ts +1 -1
  178. package/dist/voice/livekit/realtimeMcpTools.js +8 -7
  179. package/dist/voice/livekit/realtimeVoiceAgent.d.ts +2 -4
  180. package/dist/voice/livekit/realtimeVoiceAgent.js +18 -7
  181. package/dist/voice/livekit/voiceAgent.d.ts +2 -2
  182. package/dist/voice/livekit/voiceAgent.js +2 -1
  183. package/dist/voice/livekit/xaiSessionWatch.d.ts +0 -5
  184. package/dist/voice/livekit/xaiSessionWatch.js +30 -0
  185. package/dist/voice/providers/GeminiLive.js +21 -6
  186. package/dist/voice/providers/GoogleSTT.js +8 -5
  187. package/dist/voice/transcribe.d.ts +2 -2
  188. package/dist/voice/transcribe.js +34 -18
  189. package/dist/voice/whistle/audio.js +11 -5
  190. package/docs-site/static/search-index.json +71 -49
  191. package/package.json +171 -156
  192. package/scripts/observability/import-openobserve-dashboard.mjs +11 -3
package/CHANGELOG.md CHANGED
@@ -1,8 +1,8 @@
1
- ## [13.8.17](https://github.com/juspay/neurolink/compare/v13.8.16...v13.8.17) (2026-10-26)
1
+ ## [13.8.18](https://github.com/juspay/neurolink/compare/v13.8.17...v13.8.18) (2026-10-27)
2
2
 
3
3
  ### Bug Fixes
4
4
 
5
- - **(server):** stop reading an over-limit JSON or form body on Express at the limit ([349a15d](https://github.com/juspay/neurolink/commit/349a15d63c7fdd221a92b92ae3ea6be9d95a6400))
5
+ - **(security):** close the 2026-10 audit findings and pin the toolchain ([f7e105d](https://github.com/juspay/neurolink/commit/f7e105d21e649a05481647aebc80aa767617bcab))
6
6
 
7
7
  ## [11.2.3](https://github.com/juspay/neurolink/compare/v11.2.2...v11.2.3) (2026-08-19)
8
8
 
package/README.md CHANGED
@@ -1167,7 +1167,7 @@ node your-app.js
1167
1167
  Run AI-powered workflows directly in GitHub Actions with broad provider support and automatic PR/issue commenting.
1168
1168
 
1169
1169
  ```yaml
1170
- - uses: juspay/neurolink@v1
1170
+ - uses: juspay/neurolink@e42b4db89fa87946e2dd766cf2ab43929c689b8b # v13.8.16
1171
1171
  with:
1172
1172
  anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
1173
1173
  prompt: "Review this PR for security issues and code quality"
@@ -12,17 +12,47 @@ export declare function transformCliResponse(cliResponse: CliResponse): Omit<Act
12
12
  */
13
13
  export declare function buildCliArgs(inputs: ActionInputs): string[];
14
14
  /**
15
- * Install NeuroLink CLI
15
+ * Install the NeuroLink CLI into a private directory under RUNNER_TEMP and
16
+ * return the path of its `neurolink` executable.
17
+ *
18
+ * Three things changed from the original `npm install -g @juspay/neurolink`:
19
+ *
20
+ * - The version is exact unless the consumer opts into `latest`. The action
21
+ * runs with the consumer's provider keys in its environment, so a moving tag
22
+ * would hand one bad npm publish to every consumer within minutes; pinning
23
+ * the action itself by SHA cannot protect against that, only pinning what it
24
+ * installs can. `latest` still works, with a warning that names the trade.
25
+ * - `--ignore-scripts`: no lifecycle script of any package in the tree runs on
26
+ * the consumer's runner. NeuroLink has none of its own, and the packages
27
+ * whose scripts matter (ffmpeg-static's binary download, sharp's prebuilt
28
+ * lookup) are optional dependencies the action's text generation path does
29
+ * not use. Video inputs need an ffmpeg on the runner's PATH, which GitHub's
30
+ * Ubuntu images provide.
31
+ * - `npm audit signatures` after the install: every package in the tree must
32
+ * carry a valid registry signature, and the ones published with provenance
33
+ * (this package is) must carry an attestation that matches. A tarball that
34
+ * was swapped or re-signed fails here instead of running.
35
+ *
36
+ * A project directory rather than `-g` because `npm audit signatures` reads a
37
+ * project's lockfile, and because it keeps the install out of the runner's
38
+ * global prefix, where a later step could find a stale copy.
16
39
  */
17
- export declare function installNeurolink(version: string): Promise<void>;
40
+ export declare function installNeurolink(version: string, installRoot?: string): Promise<string>;
41
+ /**
42
+ * The exact `npm install` spec for a `neurolink_version` input. Anything that
43
+ * is neither `latest` nor an exact `x.y.z` is refused: a range or a dist-tag
44
+ * would quietly reintroduce the moving target the pin exists to remove.
45
+ */
46
+ export declare function resolveInstallSpec(version: string): string;
18
47
  /**
19
48
  * Execute NeuroLink CLI command
20
49
  * @param args - CLI arguments
21
50
  * @param env - Environment variables
22
51
  * @param workingDirectory - Working directory for execution
23
52
  * @param timeout - Timeout in milliseconds (defaults to 5 minutes)
53
+ * @param cliPath - The executable to run; defaults to `neurolink` on PATH
24
54
  */
25
- export declare function executeNeurolink(args: string[], env: Record<string, string>, workingDirectory?: string, timeout?: number): Promise<ActionExecutionResult>;
55
+ export declare function executeNeurolink(args: string[], env: Record<string, string>, workingDirectory?: string, timeout?: number, cliPath?: string): Promise<ActionExecutionResult>;
26
56
  /**
27
57
  * Run complete NeuroLink action
28
58
  */
@@ -8,9 +8,12 @@ import * as core from "@actions/core";
8
8
  import * as fs from "fs";
9
9
  import * as path from "path";
10
10
  import { buildEnvironmentVariables } from "./actionInputs.js";
11
+ import { EXACT_VERSION_PATTERN } from "./cliVersion.js";
11
12
  import { withTimeout } from "../utils/errorHandling.js";
12
13
  /** Default timeout for CLI execution (5 minutes) */
13
14
  const DEFAULT_EXECUTION_TIMEOUT_MS = 300000;
15
+ /** The package the action installs and runs. */
16
+ const CLI_PACKAGE = "@juspay/neurolink";
14
17
  /**
15
18
  * Transform CLI token usage format to action format
16
19
  */
@@ -130,17 +133,69 @@ export function buildCliArgs(inputs) {
130
133
  return args;
131
134
  }
132
135
  /**
133
- * Install NeuroLink CLI
136
+ * Install the NeuroLink CLI into a private directory under RUNNER_TEMP and
137
+ * return the path of its `neurolink` executable.
138
+ *
139
+ * Three things changed from the original `npm install -g @juspay/neurolink`:
140
+ *
141
+ * - The version is exact unless the consumer opts into `latest`. The action
142
+ * runs with the consumer's provider keys in its environment, so a moving tag
143
+ * would hand one bad npm publish to every consumer within minutes; pinning
144
+ * the action itself by SHA cannot protect against that, only pinning what it
145
+ * installs can. `latest` still works, with a warning that names the trade.
146
+ * - `--ignore-scripts`: no lifecycle script of any package in the tree runs on
147
+ * the consumer's runner. NeuroLink has none of its own, and the packages
148
+ * whose scripts matter (ffmpeg-static's binary download, sharp's prebuilt
149
+ * lookup) are optional dependencies the action's text generation path does
150
+ * not use. Video inputs need an ffmpeg on the runner's PATH, which GitHub's
151
+ * Ubuntu images provide.
152
+ * - `npm audit signatures` after the install: every package in the tree must
153
+ * carry a valid registry signature, and the ones published with provenance
154
+ * (this package is) must carry an attestation that matches. A tarball that
155
+ * was swapped or re-signed fails here instead of running.
156
+ *
157
+ * A project directory rather than `-g` because `npm audit signatures` reads a
158
+ * project's lockfile, and because it keeps the install out of the runner's
159
+ * global prefix, where a later step could find a stale copy.
134
160
  */
135
- export async function installNeurolink(version) {
136
- core.info(`Installing @juspay/neurolink@${version}...`);
137
- const installArgs = version === "latest"
138
- ? ["install", "-g", "@juspay/neurolink"]
139
- : ["install", "-g", `@juspay/neurolink@${version}`];
140
- await exec.exec("npm", installArgs, {
141
- silent: !core.isDebug(),
142
- });
143
- core.info("NeuroLink CLI installed successfully");
161
+ export async function installNeurolink(version, installRoot = process.env.RUNNER_TEMP || "/tmp") {
162
+ const spec = resolveInstallSpec(version);
163
+ const installDir = path.join(installRoot, `neurolink-cli-${process.pid}-${Date.now()}`);
164
+ fs.mkdirSync(installDir, { recursive: true, mode: 0o700 });
165
+ fs.writeFileSync(path.join(installDir, "package.json"), JSON.stringify({ name: "neurolink-action-cli", private: true }, null, 2), { mode: 0o600 });
166
+ core.info(`Installing ${spec}...`);
167
+ const npmOptions = { cwd: installDir, silent: !core.isDebug() };
168
+ await exec.exec("npm", [
169
+ "install",
170
+ "--ignore-scripts",
171
+ "--no-fund",
172
+ "--no-audit",
173
+ "--save-exact",
174
+ spec,
175
+ ], npmOptions);
176
+ core.info("Verifying registry signatures and provenance attestations...");
177
+ await exec.exec("npm", ["audit", "signatures"], npmOptions);
178
+ const binPath = path.join(installDir, "node_modules", ".bin", "neurolink");
179
+ if (!fs.existsSync(binPath)) {
180
+ throw new Error(`${spec} installed but exposes no neurolink executable at ${binPath}`);
181
+ }
182
+ core.info(`NeuroLink CLI ${version} installed at ${installDir}`);
183
+ return binPath;
184
+ }
185
+ /**
186
+ * The exact `npm install` spec for a `neurolink_version` input. Anything that
187
+ * is neither `latest` nor an exact `x.y.z` is refused: a range or a dist-tag
188
+ * would quietly reintroduce the moving target the pin exists to remove.
189
+ */
190
+ export function resolveInstallSpec(version) {
191
+ if (version === "latest") {
192
+ core.warning(`neurolink_version is "latest": the action will install whatever ${CLI_PACKAGE} version the registry serves at run time. Pin an exact version (the default) for a reproducible, review-gated install.`);
193
+ return `${CLI_PACKAGE}@latest`;
194
+ }
195
+ if (!EXACT_VERSION_PATTERN.test(version)) {
196
+ throw new Error(`neurolink_version must be an exact version such as 13.8.16, or the literal "latest"; received "${version}"`);
197
+ }
198
+ return `${CLI_PACKAGE}@${version}`;
144
199
  }
145
200
  /**
146
201
  * Execute NeuroLink CLI command
@@ -148,8 +203,9 @@ export async function installNeurolink(version) {
148
203
  * @param env - Environment variables
149
204
  * @param workingDirectory - Working directory for execution
150
205
  * @param timeout - Timeout in milliseconds (defaults to 5 minutes)
206
+ * @param cliPath - The executable to run; defaults to `neurolink` on PATH
151
207
  */
152
- export async function executeNeurolink(args, env, workingDirectory, timeout = DEFAULT_EXECUTION_TIMEOUT_MS) {
208
+ export async function executeNeurolink(args, env, workingDirectory, timeout = DEFAULT_EXECUTION_TIMEOUT_MS, cliPath = "neurolink") {
153
209
  const startTime = Date.now();
154
210
  try {
155
211
  // Filter out undefined values from process.env
@@ -159,7 +215,7 @@ export async function executeNeurolink(args, env, workingDirectory, timeout = DE
159
215
  processEnv[key] = value;
160
216
  }
161
217
  }
162
- const execPromise = exec.getExecOutput("neurolink", args, {
218
+ const execPromise = exec.getExecOutput(cliPath, args, {
163
219
  cwd: workingDirectory,
164
220
  env: { ...processEnv, ...env },
165
221
  silent: false,
@@ -251,8 +307,8 @@ export async function executeNeurolink(args, env, workingDirectory, timeout = DE
251
307
  export async function runNeurolink(inputs) {
252
308
  let credPath;
253
309
  try {
254
- // Install CLI
255
- await installNeurolink(inputs.neurolinkVersion);
310
+ // Install CLI (exact version unless `latest` was requested)
311
+ const cliPath = await installNeurolink(inputs.neurolinkVersion);
256
312
  // Build environment
257
313
  const env = buildEnvironmentVariables(inputs);
258
314
  // Handle Google Cloud credentials (base64 decode)
@@ -273,7 +329,7 @@ export async function runNeurolink(inputs) {
273
329
  }
274
330
  // Execute with timeout (use input timeout or default)
275
331
  const executionTimeout = inputs.timeout || DEFAULT_EXECUTION_TIMEOUT_MS;
276
- return await executeNeurolink(args, env, inputs.workingDirectory, executionTimeout);
332
+ return await executeNeurolink(args, env, inputs.workingDirectory, executionTimeout, cliPath);
277
333
  }
278
334
  finally {
279
335
  // Clean up credential file
@@ -6,6 +6,7 @@
6
6
  import * as core from "@actions/core";
7
7
  import { AIProviderName } from "../constants/enums.js";
8
8
  import { ErrorFactory } from "../utils/errorHandling.js";
9
+ import { DEFAULT_NEUROLINK_CLI_VERSION } from "./cliVersion.js";
9
10
  const PROVIDER_KEY_MAP = {
10
11
  [AIProviderName.OPENAI]: ["openaiApiKey"],
11
12
  [AIProviderName.ANTHROPIC]: ["anthropicApiKey"],
@@ -183,7 +184,9 @@ export function parseActionInputs() {
183
184
  // Advanced
184
185
  timeout: parseNumericInput("timeout", core.getInput("timeout"), 300, parseInt, 10),
185
186
  debug: core.getBooleanInput("debug"),
186
- neurolinkVersion: core.getInput("neurolink_version") || "latest",
187
+ // Empty means the pinned version this action was released with; `latest`
188
+ // is an explicit opt-in to a moving target (see ./cliVersion.ts).
189
+ neurolinkVersion: core.getInput("neurolink_version") || DEFAULT_NEUROLINK_CLI_VERSION,
187
190
  workingDirectory: core.getInput("working_directory") || ".",
188
191
  };
189
192
  }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * The NeuroLink CLI version the GitHub Action installs when the
3
+ * `neurolink_version` input is left empty.
4
+ *
5
+ * This is a deliberate, reviewed pin and not `latest`. The action runs with the
6
+ * consumer's provider keys and `github.token` in its environment, so an
7
+ * unpinned install would hand a single bad npm publish of `@juspay/neurolink`
8
+ * to every consumer's workflow within minutes — and pinning the ACTION by SHA
9
+ * would buy them nothing, because the code that actually runs is whatever the
10
+ * install fetched. pnpm's `minimumReleaseAge` cooldown does not apply here
11
+ * either: the action installs with npm.
12
+ *
13
+ * Why a committed constant and not the version in package.json: semantic-release
14
+ * derives the published version from git tags and no longer commits the bump,
15
+ * so `package.json#version` in this repository lags the registry by major
16
+ * versions and would pin consumers to an ancient CLI.
17
+ *
18
+ * Bump it in a reviewed change (`pnpm run build:action` afterwards — the
19
+ * committed bundle is diffed against a fresh build in CI). The supply-chain
20
+ * suite checks that it is an exact `x.y.z` and that the version exists on the
21
+ * registry with a provenance attestation.
22
+ */
23
+ export declare const DEFAULT_NEUROLINK_CLI_VERSION = "13.8.16";
24
+ /** An exact `x.y.z` (optionally pre-release) — the only shape the installer accepts besides `latest`. */
25
+ export declare const EXACT_VERSION_PATTERN: RegExp;
@@ -0,0 +1,26 @@
1
+ // src/lib/action/cliVersion.ts
2
+ /**
3
+ * The NeuroLink CLI version the GitHub Action installs when the
4
+ * `neurolink_version` input is left empty.
5
+ *
6
+ * This is a deliberate, reviewed pin and not `latest`. The action runs with the
7
+ * consumer's provider keys and `github.token` in its environment, so an
8
+ * unpinned install would hand a single bad npm publish of `@juspay/neurolink`
9
+ * to every consumer's workflow within minutes — and pinning the ACTION by SHA
10
+ * would buy them nothing, because the code that actually runs is whatever the
11
+ * install fetched. pnpm's `minimumReleaseAge` cooldown does not apply here
12
+ * either: the action installs with npm.
13
+ *
14
+ * Why a committed constant and not the version in package.json: semantic-release
15
+ * derives the published version from git tags and no longer commits the bump,
16
+ * so `package.json#version` in this repository lags the registry by major
17
+ * versions and would pin consumers to an ancient CLI.
18
+ *
19
+ * Bump it in a reviewed change (`pnpm run build:action` afterwards — the
20
+ * committed bundle is diffed against a fresh build in CI). The supply-chain
21
+ * suite checks that it is an exact `x.y.z` and that the version exists on the
22
+ * registry with a provenance attestation.
23
+ */
24
+ export const DEFAULT_NEUROLINK_CLI_VERSION = "13.8.16";
25
+ /** An exact `x.y.z` (optionally pre-release) — the only shape the installer accepts besides `latest`. */
26
+ export const EXACT_VERSION_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
@@ -3,5 +3,6 @@
3
3
  * @module action
4
4
  */
5
5
  export { parseActionInputs, validateProviderKey, buildEnvironmentVariables, validateActionInputs, maskSecrets, } from "./actionInputs.js";
6
- export { buildCliArgs, installNeurolink, executeNeurolink, runNeurolink, transformCliResponse, } from "./actionExecutor.js";
6
+ export { buildCliArgs, installNeurolink, resolveInstallSpec, executeNeurolink, runNeurolink, transformCliResponse, } from "./actionExecutor.js";
7
+ export { DEFAULT_NEUROLINK_CLI_VERSION, EXACT_VERSION_PATTERN, } from "./cliVersion.js";
7
8
  export { postResultComment, writeJobSummary, setActionOutputs, getActionOutputs, } from "./githubIntegration.js";
@@ -6,7 +6,8 @@
6
6
  // Input handling
7
7
  export { parseActionInputs, validateProviderKey, buildEnvironmentVariables, validateActionInputs, maskSecrets, } from "./actionInputs.js";
8
8
  // Execution
9
- export { buildCliArgs, installNeurolink, executeNeurolink, runNeurolink, transformCliResponse, } from "./actionExecutor.js";
9
+ export { buildCliArgs, installNeurolink, resolveInstallSpec, executeNeurolink, runNeurolink, transformCliResponse, } from "./actionExecutor.js";
10
+ export { DEFAULT_NEUROLINK_CLI_VERSION, EXACT_VERSION_PATTERN, } from "./cliVersion.js";
10
11
  // GitHub integration
11
12
  export { postResultComment, writeJobSummary, setActionOutputs, getActionOutputs, } from "./githubIntegration.js";
12
13
  // Re-export types for convenience
@@ -14,6 +14,8 @@
14
14
  import pLimit from "p-limit";
15
15
  import { ErrorCategory, ErrorSeverity } from "../../constants/enums.js";
16
16
  import { logger } from "../../utils/logger.js";
17
+ import { downloadAttachment, getAttachmentPolicy, readAttachmentFile, } from "../../utils/attachmentPolicy.js";
18
+ import { MAX_IMAGE_BYTES } from "../../utils/sizeGuard.js";
17
19
  import { validateImageForVideo } from "../../utils/parameterValidation.js";
18
20
  import { VIDEO_ERROR_CODES } from "../../constants/videoErrors.js";
19
21
  import { extractFirstFrame, extractLastFrame } from "./frameExtractor.js";
@@ -44,14 +46,14 @@ const DEFAULT_TRANSITION_DURATION = 4;
44
46
  *
45
47
  * @throws {VideoError} If the image cannot be resolved
46
48
  */
47
- async function resolveImageToBuffer(image, segmentIndex) {
49
+ async function resolveImageToBuffer(image, segmentIndex, policy) {
48
50
  if (Buffer.isBuffer(image)) {
49
51
  return image;
50
52
  }
51
53
  if (typeof image === "string") {
52
54
  return image.startsWith("http://") || image.startsWith("https://")
53
- ? fetchImageFromUrl(image, segmentIndex)
54
- : readImageFromDisk(image, segmentIndex);
55
+ ? fetchImageFromUrl(image, segmentIndex, policy)
56
+ : readImageFromDisk(image, segmentIndex, policy);
55
57
  }
56
58
  // ImageWithAltText
57
59
  if (typeof image === "object" && "data" in image) {
@@ -62,7 +64,7 @@ async function resolveImageToBuffer(image, segmentIndex) {
62
64
  if (typeof imgData === "string") {
63
65
  // Handle HTTP(S) URLs
64
66
  if (imgData.startsWith("http://") || imgData.startsWith("https://")) {
65
- return fetchImageFromUrl(imgData, segmentIndex);
67
+ return fetchImageFromUrl(imgData, segmentIndex, policy);
66
68
  }
67
69
  // Handle data URIs (e.g., "data:image/png;base64,iVBORw0KG...")
68
70
  if (imgData.startsWith("data:")) {
@@ -82,7 +84,7 @@ async function resolveImageToBuffer(image, segmentIndex) {
82
84
  }
83
85
  // Try as file path
84
86
  try {
85
- return await readImageFromDisk(imgData, segmentIndex);
87
+ return await readImageFromDisk(imgData, segmentIndex, policy);
86
88
  }
87
89
  catch (fileError) {
88
90
  // Not a valid file path - throw clear error instead of silently treating as base64
@@ -114,47 +116,52 @@ async function resolveImageToBuffer(image, segmentIndex) {
114
116
  context: { segmentIndex },
115
117
  });
116
118
  }
117
- /** Fetch an image from an HTTP(S) URL with timeout. */
118
- async function fetchImageFromUrl(url, segmentIndex) {
119
- const controller = new AbortController();
120
- const timeout = setTimeout(() => controller.abort(), IMAGE_IO_TIMEOUT_MS);
119
+ /**
120
+ * Fetch an image from an HTTP(S) URL under the attachment policy: HTTPS to a
121
+ * public host unless the policy says otherwise, DNS-pinned, redirects
122
+ * re-validated per hop, body bounded while streaming.
123
+ */
124
+ async function fetchImageFromUrl(url, segmentIndex, policy) {
121
125
  try {
122
- const response = await fetch(url, { signal: controller.signal });
123
- if (!response.ok) {
124
- throw new VideoError({
125
- code: VIDEO_ERROR_CODES.INVALID_INPUT,
126
- message: `Failed to fetch image for segment ${segmentIndex}: HTTP ${response.status}`,
127
- category: ErrorCategory.EXECUTION,
128
- severity: ErrorSeverity.HIGH,
129
- retriable: response.status >= 500,
130
- context: { segmentIndex, url: url.substring(0, 100) },
131
- });
132
- }
133
- return Buffer.from(await response.arrayBuffer());
126
+ const downloaded = await downloadAttachment(url, {
127
+ policy,
128
+ maxBytes: MAX_IMAGE_BYTES,
129
+ label: `segment ${segmentIndex} image URL`,
130
+ timeoutMs: IMAGE_IO_TIMEOUT_MS,
131
+ });
132
+ return downloaded.buffer;
134
133
  }
135
134
  catch (error) {
136
135
  if (error instanceof VideoError) {
137
136
  throw error;
138
137
  }
138
+ const message = error instanceof Error ? error.message : String(error);
139
+ const status = /HTTP (\d{3})/.exec(message)?.[1];
140
+ const refused = error?.code === "ATTACHMENT_URL_REFUSED";
139
141
  throw new VideoError({
140
142
  code: VIDEO_ERROR_CODES.INVALID_INPUT,
141
- message: `Failed to fetch image for segment ${segmentIndex}: ${error instanceof Error ? error.message : String(error)}`,
143
+ message: `Failed to fetch image for segment ${segmentIndex}: ${message}`,
142
144
  category: ErrorCategory.EXECUTION,
143
145
  severity: ErrorSeverity.HIGH,
144
- retriable: true,
145
- context: { segmentIndex },
146
+ retriable: !refused && (status === undefined || Number(status) >= 500),
147
+ context: { segmentIndex, url: url.substring(0, 100) },
146
148
  originalError: error instanceof Error ? error : undefined,
147
149
  });
148
150
  }
149
- finally {
150
- clearTimeout(timeout);
151
- }
152
151
  }
153
- /** Read an image from the local filesystem. */
154
- async function readImageFromDisk(filePath, segmentIndex) {
155
- const { readFile } = await import("node:fs/promises");
152
+ /**
153
+ * Read an image from the local filesystem under the attachment policy: roots
154
+ * containment when configured, O_NOFOLLOW open of the real path, regular
155
+ * files only, bounded at the image cap.
156
+ */
157
+ async function readImageFromDisk(filePath, segmentIndex, policy) {
156
158
  try {
157
- return await readFile(filePath);
159
+ const read = await readAttachmentFile(filePath, {
160
+ policy,
161
+ maxBytes: MAX_IMAGE_BYTES,
162
+ label: `segment ${segmentIndex} image file`,
163
+ });
164
+ return read.buffer;
158
165
  }
159
166
  catch (error) {
160
167
  throw new VideoError({
@@ -206,7 +213,7 @@ function processOrderedCompletions(state) {
206
213
  *
207
214
  * @throws {VideoError} On generation failure or circuit breaker trip
208
215
  */
209
- async function generateSingleClip(segment, index, videoOptions, region, state) {
216
+ async function generateSingleClip(segment, index, videoOptions, region, state, policy) {
210
217
  if (state.circuitOpen) {
211
218
  throw new VideoError({
212
219
  code: VIDEO_ERROR_CODES.DIRECTOR_CLIP_FAILED,
@@ -219,7 +226,7 @@ async function generateSingleClip(segment, index, videoOptions, region, state) {
219
226
  }
220
227
  const clipStart = Date.now();
221
228
  try {
222
- const imageBuffer = await resolveImageToBuffer(segment.image, index);
229
+ const imageBuffer = await resolveImageToBuffer(segment.image, index, policy);
223
230
  // Validate image buffer (type, dimensions, size limits) before generation
224
231
  const imageValidation = validateImageForVideo(imageBuffer);
225
232
  if (imageValidation) {
@@ -273,7 +280,7 @@ async function generateSingleClip(segment, index, videoOptions, region, state) {
273
280
  * @returns Ordered array of clip results (indexed by segment number)
274
281
  * @throws {VideoError} On any clip failure (all clips are mandatory)
275
282
  */
276
- async function generateClips(segments, videoOptions, region) {
283
+ async function generateClips(segments, videoOptions, region, policy) {
277
284
  const limit = pLimit(CLIP_CONCURRENCY);
278
285
  const state = {
279
286
  consecutiveFailures: 0,
@@ -282,7 +289,7 @@ async function generateClips(segments, videoOptions, region) {
282
289
  completions: new Array(segments.length).fill({ status: "pending" }),
283
290
  nextExpectedIndex: 0,
284
291
  };
285
- const clipPromises = segments.map((segment, i) => limit(() => generateSingleClip(segment, i, videoOptions, region, state)));
292
+ const clipPromises = segments.map((segment, i) => limit(() => generateSingleClip(segment, i, videoOptions, region, state, policy)));
286
293
  // Collect results — any failure is fatal
287
294
  const settled = await Promise.allSettled(clipPromises);
288
295
  const failures = settled.filter((r) => r.status === "rejected");
@@ -449,8 +456,11 @@ export async function executeDirectorPipeline(segments, videoOptions = {}, direc
449
456
  clipDuration,
450
457
  resolution: videoOptions.resolution ?? "720p",
451
458
  });
459
+ // The instance's attachment policy for segment images given as URLs or
460
+ // paths; the environment-derived default when the caller stamped none.
461
+ const attachmentPolicy = getAttachmentPolicy(directorOptions);
452
462
  // Phase 1: Generate all clips
453
- const clipResults = await generateClips(segments, videoOptions, region);
463
+ const clipResults = await generateClips(segments, videoOptions, region, attachmentPolicy);
454
464
  logger.info("Phase 1 complete — all clips generated", {
455
465
  clipCount: clipResults.length,
456
466
  elapsedMs: Date.now() - pipelineStart,
@@ -25,6 +25,23 @@ export declare const JPEG_QUALITY = "2";
25
25
  export declare const LAST_FRAME_SEEK_OFFSET = "0.5";
26
26
  /** Minimum valid MP4 buffer size in bytes (ftyp header = 8 bytes minimum) */
27
27
  export declare const MIN_VIDEO_BUFFER_SIZE = 12;
28
+ /**
29
+ * Input-side options every ffmpeg run here gets, placed before the first
30
+ * `-i` so they apply to the inputs.
31
+ *
32
+ * `-protocol_whitelist file,crypto,data` pins the demuxer to local files: a
33
+ * playlist or container that references `http://`, `concat:` or `subfile:`
34
+ * cannot make ffmpeg fetch or read anything else, whatever the installed
35
+ * build's defaults are. `-nostdin` keeps a child from ever waiting on a
36
+ * terminal. Passed explicitly rather than relied on, because the whitelist a
37
+ * build ships with is the build's choice, not ours.
38
+ */
39
+ export declare const FFMPEG_INPUT_HARDENING: readonly string[];
40
+ /**
41
+ * Prepend {@link FFMPEG_INPUT_HARDENING} to an argv that names an input, once.
42
+ * An argv without `-i` (a version probe) is returned as given.
43
+ */
44
+ export declare function withInputHardening(args: readonly string[]): string[];
28
45
  /**
29
46
  * Create a tracked temporary directory for FFmpeg operations.
30
47
  *
@@ -9,9 +9,8 @@
9
9
  *
10
10
  * @module adapters/video/ffmpegAdapter
11
11
  */
12
- import { randomUUID } from "node:crypto";
13
12
  import { readdirSync, rmdirSync, unlinkSync } from "node:fs";
14
- import { mkdir, readFile, rm, unlink, writeFile } from "node:fs/promises";
13
+ import { mkdtemp, readFile, rm, unlink, writeFile } from "node:fs/promises";
15
14
  import { tmpdir } from "node:os";
16
15
  import { join } from "node:path";
17
16
  import { logger } from "../../utils/logger.js";
@@ -34,6 +33,32 @@ export const LAST_FRAME_SEEK_OFFSET = "0.5";
34
33
  export const MIN_VIDEO_BUFFER_SIZE = 12;
35
34
  /** MP4 ftyp box magic bytes at offset 4: "ftyp" */
36
35
  const FTYP_MAGIC = Buffer.from([0x66, 0x74, 0x79, 0x70]);
36
+ /**
37
+ * Input-side options every ffmpeg run here gets, placed before the first
38
+ * `-i` so they apply to the inputs.
39
+ *
40
+ * `-protocol_whitelist file,crypto,data` pins the demuxer to local files: a
41
+ * playlist or container that references `http://`, `concat:` or `subfile:`
42
+ * cannot make ffmpeg fetch or read anything else, whatever the installed
43
+ * build's defaults are. `-nostdin` keeps a child from ever waiting on a
44
+ * terminal. Passed explicitly rather than relied on, because the whitelist a
45
+ * build ships with is the build's choice, not ours.
46
+ */
47
+ export const FFMPEG_INPUT_HARDENING = [
48
+ "-nostdin",
49
+ "-protocol_whitelist",
50
+ "file,crypto,data",
51
+ ];
52
+ /**
53
+ * Prepend {@link FFMPEG_INPUT_HARDENING} to an argv that names an input, once.
54
+ * An argv without `-i` (a version probe) is returned as given.
55
+ */
56
+ export function withInputHardening(args) {
57
+ if (!args.includes("-i") || args.includes("-protocol_whitelist")) {
58
+ return [...args];
59
+ }
60
+ return [...FFMPEG_INPUT_HARDENING, ...args];
61
+ }
37
62
  // ============================================================================
38
63
  // TEMP DIRECTORY MANAGEMENT
39
64
  // ============================================================================
@@ -80,8 +105,9 @@ function ensureCleanupRegistered() {
80
105
  */
81
106
  export async function createTrackedTempDir(prefix) {
82
107
  ensureCleanupRegistered();
83
- const dir = join(tmpdir(), `neurolink-${prefix}-${randomUUID()}`);
84
- await mkdir(dir, { recursive: true });
108
+ // mkdtemp: an unguessable name AND 0o700, atomically — nothing else on the
109
+ // host can read the frames or clips that land here.
110
+ const dir = await mkdtemp(join(tmpdir(), `neurolink-${prefix}-`));
85
111
  activeTempDirs.add(dir);
86
112
  return dir;
87
113
  }
@@ -172,8 +198,9 @@ export async function runFfmpeg(args, options = {}) {
172
198
  const ffmpegPath = options.binaryPath ?? (await getFfmpegPath());
173
199
  const timeoutMs = options.timeoutMs ?? FFMPEG_FRAME_TIMEOUT_MS;
174
200
  const maxBuffer = options.maxBuffer ?? FFMPEG_FRAME_MAX_BUFFER;
201
+ const hardenedArgs = withInputHardening(args);
175
202
  return new Promise((resolve, reject) => {
176
- const proc = execFile(ffmpegPath, args, { timeout: timeoutMs, maxBuffer }, (error, stdout, stderr) => {
203
+ const proc = execFile(ffmpegPath, hardenedArgs, { timeout: timeoutMs, maxBuffer }, (error, stdout, stderr) => {
177
204
  if (error) {
178
205
  // execFile's callback error carries no output, but ffmpeg prints a
179
206
  // stream's input report before it exits non-zero on a stream it
@@ -59,7 +59,7 @@ async function extractFrame(videoBuffer, position) {
59
59
  const inputPath = join(tempDir, "input.mp4");
60
60
  const outputPath = join(tempDir, `${position}_frame.jpg`);
61
61
  try {
62
- await writeFile(inputPath, videoBuffer);
62
+ await writeFile(inputPath, videoBuffer, { mode: 0o600, flag: "wx" });
63
63
  const args = position === "first"
64
64
  ? [
65
65
  "-y",
@@ -27,13 +27,16 @@ async function writeClipsAndBuildConcatList(videoBuffers, tempDir, concatListPat
27
27
  const concatLines = [];
28
28
  for (let i = 0; i < videoBuffers.length; i++) {
29
29
  const inputPath = join(tempDir, `clip_${i}.mp4`);
30
- await writeFile(inputPath, videoBuffers[i]);
30
+ await writeFile(inputPath, videoBuffers[i], { mode: 0o600, flag: "wx" });
31
31
  inputPaths.push(inputPath);
32
32
  // Normalize backslashes to forward slashes for FFmpeg, then escape single quotes
33
33
  const safePath = inputPath.replace(/\\/g, "/").replace(/'/g, "'\\''");
34
34
  concatLines.push(`file '${safePath}'`);
35
35
  }
36
- await writeFile(concatListPath, concatLines.join("\n"));
36
+ await writeFile(concatListPath, concatLines.join("\n"), {
37
+ mode: 0o600,
38
+ flag: "wx",
39
+ });
37
40
  return inputPaths;
38
41
  }
39
42
  /**
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Arithmetic evaluator for the built-in `calculateMath` tool.
3
+ *
4
+ * The expression is MODEL output. It is parsed by a small recursive-descent
5
+ * parser and evaluated over numbers only: there is no `Function`, no `eval`,
6
+ * no property access and no identifier that is not on the fixed allow-list
7
+ * below. Anything the grammar does not cover is reported as an error.
8
+ *
9
+ * Grammar (precedence low → high):
10
+ *
11
+ * expression := term (("+" | "-") term)*
12
+ * term := unary (("*" | "/" | "%") unary)*
13
+ * unary := ("-" | "+") unary | power
14
+ * power := primary ("**" unary)? right-associative
15
+ * primary := number | constant | function "(" args ")" | "(" expression ")"
16
+ *
17
+ * Numbers are decimal literals (`12`, `3.5`, `.5`, `1e-3`). Functions and
18
+ * constants are the listed `Math` members, accepted with or without the
19
+ * `Math.` prefix. Unary minus binds looser than `**`, so `-2 ** 2` is `-4`,
20
+ * as in mathematics (JavaScript rejects that spelling outright).
21
+ */
22
+ import type { ArithmeticEvaluation } from "../types/index.js";
23
+ /** The names the model may use, for the error text and the tool description. */
24
+ export declare const ARITHMETIC_FUNCTION_NAMES: readonly string[];
25
+ export declare const ARITHMETIC_CONSTANT_NAMES: readonly string[];
26
+ /**
27
+ * Evaluate an arithmetic expression without executing it as code.
28
+ *
29
+ * Total: every failure — unsupported syntax, an unknown name, a result that is
30
+ * not a finite number — comes back as `{ ok: false, error }`, never a throw.
31
+ */
32
+ export declare function evaluateArithmetic(expression: string): ArithmeticEvaluation;