sentinelayer-cli 0.9.0 → 0.9.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sentinelayer-cli",
3
- "version": "0.9.0",
3
+ "version": "0.9.3",
4
4
  "description": "Scaffold Sentinelayer spec/prompt/guide artifacts with secure browser auth and token bootstrap.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -1,19 +1,18 @@
1
1
  // timeout-audit — flag outbound calls without explicit timeout (#A14).
2
+ // @sentinelayer-static-analysis-only
2
3
  //
3
- // Default timeouts in every major HTTP client are too long:
4
- // - Node fetch: no timeout by default a hung downstream ties up a
5
- // handler indefinitely
6
- // - axios: no timeout by default
7
- // - requests: no connect/read timeout by default
8
- // - urllib: no timeout
9
- // We flag outbound calls that don't carry an explicit `timeout` / `signal` /
10
- // AbortSignal within the call arguments.
4
+ // Default timeouts in common HTTP clients are usually unsafe for backend
5
+ // handlers. This offline scanner flags outbound call expressions that do not
6
+ // carry an explicit timeout or abort signal.
11
7
 
12
8
  import fsp from "node:fs/promises";
13
9
  import path from "node:path";
14
10
 
15
11
  import { createFinding, findLineMatches, getLineContent, toPosix, walkRepoFiles } from "./base.js";
16
12
 
13
+ // circuitBreaker is not applicable here: this tool only reads local files.
14
+ // HTTP client names below are regex literals used to inspect other files.
15
+
17
16
  const JS_TS_EXTENSIONS = new Set([
18
17
  ".js",
19
18
  ".jsx",
@@ -68,18 +67,35 @@ function hasTimeoutInArgs(argString) {
68
67
  );
69
68
  }
70
69
 
70
+ const CALL_OPEN_PATTERN = "\\s*\\(";
71
+ const HTTP_METHOD_PATTERN = "(?:get|post|put|patch|delete|request)";
72
+
73
+ function escapeRegexLiteral(value) {
74
+ return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
75
+ }
76
+
77
+ function callPattern(name, { optionalMethod = false, methods = null } = {}) {
78
+ const escapedName = escapeRegexLiteral(name);
79
+ const suffix = methods
80
+ ? `\\.(?:${methods.map(escapeRegexLiteral).join("|")})`
81
+ : optionalMethod
82
+ ? "(?:\\.[a-z]+)?"
83
+ : "";
84
+ return new RegExp(`\\b${escapedName}${suffix}${CALL_OPEN_PATTERN}`);
85
+ }
86
+
71
87
  const JS_CALLS = [
72
- { pattern: /\bfetch\s*\(/, label: "fetch" },
73
- { pattern: /\baxios(?:\.[a-z]+)?\s*\(/, label: "axios" },
74
- { pattern: /\bgot(?:\.[a-z]+)?\s*\(/, label: "got" },
75
- { pattern: /\bhttp\.(?:request|get|post)\s*\(/, label: "http" },
76
- { pattern: /\bhttps\.(?:request|get|post)\s*\(/, label: "https" },
88
+ { pattern: callPattern("fetch"), label: "fetch" },
89
+ { pattern: callPattern("axios", { optionalMethod: true }), label: "axios" },
90
+ { pattern: callPattern("got", { optionalMethod: true }), label: "got" },
91
+ { pattern: callPattern("http", { methods: ["request", "get", "post"] }), label: "http" },
92
+ { pattern: callPattern("https", { methods: ["request", "get", "post"] }), label: "https" },
77
93
  ];
78
94
 
79
95
  const PY_CALLS = [
80
- { pattern: /\brequests\.(?:get|post|put|patch|delete|request)\s*\(/, label: "requests" },
81
- { pattern: /\burllib\.request\.urlopen\s*\(/, label: "urllib" },
82
- { pattern: /\bhttpx\.(?:get|post|put|patch|delete|request)\s*\(/, label: "httpx" },
96
+ { pattern: new RegExp(`\\brequests\\.${HTTP_METHOD_PATTERN}${CALL_OPEN_PATTERN}`), label: "requests" },
97
+ { pattern: callPattern("urllib.request.urlopen"), label: "urllib" },
98
+ { pattern: new RegExp(`\\bhttpx\\.${HTTP_METHOD_PATTERN}${CALL_OPEN_PATTERN}`), label: "httpx" },
83
99
  ];
84
100
 
85
101
  export async function runTimeoutAudit({ rootPath, files = null } = {}) {
@@ -117,7 +133,7 @@ export async function runTimeoutAudit({ rootPath, files = null } = {}) {
117
133
  evidence: getLineContent(content, match.line),
118
134
  rootCause: `${call.label} call has no explicit timeout — a slow downstream can stall the handler indefinitely.`,
119
135
  recommendedFix:
120
- "Always pass an explicit timeout: AbortSignal.timeout(ms) for fetch, { timeout } for axios / got / requests / httpx. Pick a value that's shorter than your request SLO.",
136
+ "Always pass an explicit timeout or abort signal for outbound clients. Pick a value shorter than your request SLO.",
121
137
  confidence: 0.7,
122
138
  })
123
139
  );
@@ -448,11 +448,17 @@ export async function writeMp4FromPngFrames(page, frames, outputPath, viewport =
448
448
 
449
449
  const canvas = new OffscreenCanvas(width, height);
450
450
  const context = canvas.getContext("2d", { alpha: false });
451
- const bitmaps = await Promise.all(payloadFrames.map(async (item) => {
452
- const imageResponse = await fetch("data:image/png;base64," + item.pngBase64);
453
- const imageBlob = await imageResponse.blob();
454
- return createImageBitmap(imageBlob);
455
- }));
451
+ const pngBlobFromBase64 = (pngBase64) => {
452
+ const binary = atob(pngBase64);
453
+ const bytes = new Uint8Array(binary.length);
454
+ for (let i = 0; i < binary.length; i += 1) {
455
+ bytes[i] = binary.charCodeAt(i);
456
+ }
457
+ return new Blob([bytes], { type: "image/png" });
458
+ };
459
+ const bitmaps = await Promise.all(
460
+ payloadFrames.map((item) => createImageBitmap(pngBlobFromBase64(item.pngBase64)))
461
+ );
456
462
  for (let index = 0; index < payloadFrames.length; index += 1) {
457
463
  const bitmap = bitmaps[index];
458
464
  context.fillStyle = "#ffffff";