sentinelayer-cli 0.1.2 → 0.3.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.
- package/README.md +996 -996
- package/bin/create-sentinelayer.js +5 -5
- package/bin/sentinelayer-cli.js +4 -4
- package/bin/sl.js +5 -5
- package/package.json +62 -54
- package/src/agents/jules/config/definition.js +209 -209
- package/src/agents/jules/config/system-prompt.js +175 -175
- package/src/agents/jules/error-intake.js +51 -51
- package/src/agents/jules/fix-cycle.js +377 -377
- package/src/agents/jules/loop.js +367 -367
- package/src/agents/jules/pulse.js +319 -319
- package/src/agents/jules/stream.js +186 -186
- package/src/agents/jules/swarm/file-scanner.js +74 -74
- package/src/agents/jules/swarm/index.js +11 -11
- package/src/agents/jules/swarm/orchestrator.js +362 -362
- package/src/agents/jules/swarm/pattern-hunter.js +123 -123
- package/src/agents/jules/swarm/sub-agent.js +308 -308
- package/src/agents/jules/tools/auth-audit.js +226 -222
- package/src/agents/jules/tools/dispatch.js +327 -327
- package/src/agents/jules/tools/file-edit.js +180 -180
- package/src/agents/jules/tools/file-read.js +100 -100
- package/src/agents/jules/tools/frontend-analyze.js +570 -570
- package/src/agents/jules/tools/glob.js +168 -168
- package/src/agents/jules/tools/grep.js +228 -228
- package/src/agents/jules/tools/index.js +29 -29
- package/src/agents/jules/tools/path-guards.js +161 -161
- package/src/agents/jules/tools/runtime-audit.js +493 -493
- package/src/agents/jules/tools/shell.js +383 -383
- package/src/ai/aidenid.js +972 -945
- package/src/ai/client.js +508 -508
- package/src/ai/domain-target-store.js +268 -268
- package/src/ai/identity-store.js +270 -270
- package/src/ai/site-store.js +145 -145
- package/src/audit/agents/architecture.js +180 -180
- package/src/audit/agents/compliance.js +179 -179
- package/src/audit/agents/documentation.js +165 -165
- package/src/audit/agents/performance.js +145 -145
- package/src/audit/agents/security.js +215 -215
- package/src/audit/agents/testing.js +172 -172
- package/src/audit/orchestrator.js +557 -557
- package/src/audit/package.js +204 -204
- package/src/audit/registry.js +284 -284
- package/src/audit/replay.js +103 -103
- package/src/auth/http.js +113 -113
- package/src/auth/service.js +891 -848
- package/src/auth/session-store.js +359 -345
- package/src/cli.js +252 -252
- package/src/commands/ai/identity-lifecycle.js +1338 -1337
- package/src/commands/ai/provision-governance.js +1272 -1246
- package/src/commands/ai/shared.js +147 -147
- package/src/commands/ai.js +11 -11
- package/src/commands/apply.js +12 -12
- package/src/commands/audit.js +1166 -1166
- package/src/commands/auth.js +375 -366
- package/src/commands/chat.js +191 -191
- package/src/commands/config.js +184 -184
- package/src/commands/cost.js +311 -311
- package/src/commands/daemon/core.js +850 -850
- package/src/commands/daemon/extended.js +1048 -1048
- package/src/commands/daemon/shared.js +213 -213
- package/src/commands/daemon.js +11 -11
- package/src/commands/guide.js +174 -174
- package/src/commands/ingest.js +58 -58
- package/src/commands/init.js +55 -55
- package/src/commands/legacy-args.js +10 -10
- package/src/commands/mcp.js +461 -404
- package/src/commands/omargate.js +15 -15
- package/src/commands/persona.js +20 -20
- package/src/commands/plugin.js +260 -260
- package/src/commands/policy.js +132 -132
- package/src/commands/prompt.js +238 -238
- package/src/commands/review.js +704 -704
- package/src/commands/scan.js +866 -788
- package/src/commands/spec.js +716 -716
- package/src/commands/swarm.js +651 -651
- package/src/commands/telemetry.js +202 -202
- package/src/commands/watch.js +510 -510
- package/src/config/agent-dictionary.js +182 -182
- package/src/config/io.js +56 -56
- package/src/config/paths.js +18 -18
- package/src/config/schema.js +55 -55
- package/src/config/service.js +184 -184
- package/src/cost/budget.js +235 -235
- package/src/cost/history.js +188 -188
- package/src/cost/tracker.js +171 -171
- package/src/daemon/artifact-lineage.js +534 -534
- package/src/daemon/assignment-ledger.js +770 -770
- package/src/daemon/ast-parser-layer.js +258 -258
- package/src/daemon/budget-governor.js +633 -633
- package/src/daemon/callgraph-overlay.js +646 -646
- package/src/daemon/error-worker.js +626 -626
- package/src/daemon/hybrid-mapper.js +929 -929
- package/src/daemon/jira-lifecycle.js +632 -632
- package/src/daemon/operator-control.js +657 -657
- package/src/daemon/reliability-lane.js +471 -471
- package/src/daemon/watchdog.js +971 -971
- package/src/guide/generator.js +316 -316
- package/src/ingest/engine.js +918 -918
- package/src/legacy-cli.js +2548 -2435
- package/src/mcp/registry.js +695 -695
- package/src/memory/blackboard.js +301 -301
- package/src/memory/retrieval.js +581 -581
- package/src/plugin/manifest.js +553 -553
- package/src/policy/packs.js +144 -144
- package/src/prompt/generator.js +118 -106
- package/src/review/ai-review.js +669 -669
- package/src/review/local-review.js +1284 -1284
- package/src/review/replay.js +235 -235
- package/src/review/report.js +664 -664
- package/src/review/spec-binding.js +487 -487
- package/src/scaffold/generator.js +67 -0
- package/src/scaffold/templates.js +150 -0
- package/src/scan/generator.js +418 -351
- package/src/scan/gh-secrets.js +107 -0
- package/src/spec/generator.js +519 -519
- package/src/spec/regenerate.js +237 -237
- package/src/spec/templates.js +91 -91
- package/src/swarm/dashboard.js +247 -247
- package/src/swarm/factory.js +363 -363
- package/src/swarm/pentest.js +934 -934
- package/src/swarm/registry.js +419 -419
- package/src/swarm/report.js +158 -158
- package/src/swarm/runtime.js +576 -576
- package/src/swarm/scenario-dsl.js +272 -272
- package/src/telemetry/ledger.js +302 -302
- package/src/telemetry/sync.js +96 -59
- package/src/ui/markdown.js +220 -220
package/src/audit/replay.js
CHANGED
|
@@ -1,103 +1,103 @@
|
|
|
1
|
-
import fsp from "node:fs/promises";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
|
|
4
|
-
function normalizeString(value) {
|
|
5
|
-
return String(value || "").trim();
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
function toPosixPath(value) {
|
|
9
|
-
return String(value || "").replace(/\\/g, "/");
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function fingerprintFinding(finding = {}) {
|
|
13
|
-
return [
|
|
14
|
-
normalizeString(finding.severity).toUpperCase(),
|
|
15
|
-
toPosixPath(finding.file),
|
|
16
|
-
String(Number(finding.line || 0)),
|
|
17
|
-
normalizeString(finding.message).toLowerCase(),
|
|
18
|
-
].join("|");
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function collectFindings(report = {}) {
|
|
22
|
-
const flattened = [];
|
|
23
|
-
for (const agent of report.agentResults || []) {
|
|
24
|
-
for (const finding of agent.findings || []) {
|
|
25
|
-
flattened.push({
|
|
26
|
-
severity: normalizeString(finding.severity).toUpperCase(),
|
|
27
|
-
file: toPosixPath(finding.file),
|
|
28
|
-
line: Number(finding.line || 0),
|
|
29
|
-
message: normalizeString(finding.message),
|
|
30
|
-
ruleId: normalizeString(finding.ruleId),
|
|
31
|
-
ownerAgentId: agent.agentId,
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
return flattened;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function summaryDelta(base = {}, candidate = {}) {
|
|
39
|
-
const keys = ["P0", "P1", "P2", "P3"];
|
|
40
|
-
const delta = {};
|
|
41
|
-
for (const key of keys) {
|
|
42
|
-
delta[key] = Number(candidate[key] || 0) - Number(base[key] || 0);
|
|
43
|
-
}
|
|
44
|
-
delta.blockingChanged = Boolean(base.blocking) !== Boolean(candidate.blocking);
|
|
45
|
-
return delta;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export function compareAuditReports(baseReport = {}, candidateReport = {}) {
|
|
49
|
-
const baseFindings = collectFindings(baseReport);
|
|
50
|
-
const candidateFindings = collectFindings(candidateReport);
|
|
51
|
-
const baseByFingerprint = new Map(baseFindings.map((finding) => [fingerprintFinding(finding), finding]));
|
|
52
|
-
const candidateByFingerprint = new Map(
|
|
53
|
-
candidateFindings.map((finding) => [fingerprintFinding(finding), finding])
|
|
54
|
-
);
|
|
55
|
-
|
|
56
|
-
const added = [];
|
|
57
|
-
const removed = [];
|
|
58
|
-
|
|
59
|
-
for (const [fingerprint, finding] of candidateByFingerprint.entries()) {
|
|
60
|
-
if (!baseByFingerprint.has(fingerprint)) {
|
|
61
|
-
added.push(finding);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
for (const [fingerprint, finding] of baseByFingerprint.entries()) {
|
|
65
|
-
if (!candidateByFingerprint.has(fingerprint)) {
|
|
66
|
-
removed.push(finding);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const deterministicEquivalent = added.length === 0 && removed.length === 0;
|
|
71
|
-
return {
|
|
72
|
-
schemaVersion: "1.0.0",
|
|
73
|
-
generatedAt: new Date().toISOString(),
|
|
74
|
-
baseRunId: normalizeString(baseReport.runId),
|
|
75
|
-
candidateRunId: normalizeString(candidateReport.runId),
|
|
76
|
-
baseSummary: baseReport.summary || { P0: 0, P1: 0, P2: 0, P3: 0, blocking: false },
|
|
77
|
-
candidateSummary: candidateReport.summary || { P0: 0, P1: 0, P2: 0, P3: 0, blocking: false },
|
|
78
|
-
summaryDelta: summaryDelta(baseReport.summary || {}, candidateReport.summary || {}),
|
|
79
|
-
baseFindingCount: baseFindings.length,
|
|
80
|
-
candidateFindingCount: candidateFindings.length,
|
|
81
|
-
addedCount: added.length,
|
|
82
|
-
removedCount: removed.length,
|
|
83
|
-
deterministicEquivalent,
|
|
84
|
-
added: added.slice(0, 500),
|
|
85
|
-
removed: removed.slice(0, 500),
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export async function writeAuditComparisonArtifact({
|
|
90
|
-
baseReport = {},
|
|
91
|
-
candidateReport = {},
|
|
92
|
-
outputDirectory = "",
|
|
93
|
-
} = {}) {
|
|
94
|
-
const resolvedOutputDirectory = path.resolve(String(outputDirectory || "."));
|
|
95
|
-
const comparison = compareAuditReports(baseReport, candidateReport);
|
|
96
|
-
const fileName = `AUDIT_COMPARISON_${comparison.baseRunId}_vs_${comparison.candidateRunId}.json`;
|
|
97
|
-
const outputPath = path.join(resolvedOutputDirectory, fileName);
|
|
98
|
-
await fsp.writeFile(outputPath, `${JSON.stringify(comparison, null, 2)}\n`, "utf-8");
|
|
99
|
-
return {
|
|
100
|
-
comparison,
|
|
101
|
-
outputPath,
|
|
102
|
-
};
|
|
103
|
-
}
|
|
1
|
+
import fsp from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
function normalizeString(value) {
|
|
5
|
+
return String(value || "").trim();
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function toPosixPath(value) {
|
|
9
|
+
return String(value || "").replace(/\\/g, "/");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function fingerprintFinding(finding = {}) {
|
|
13
|
+
return [
|
|
14
|
+
normalizeString(finding.severity).toUpperCase(),
|
|
15
|
+
toPosixPath(finding.file),
|
|
16
|
+
String(Number(finding.line || 0)),
|
|
17
|
+
normalizeString(finding.message).toLowerCase(),
|
|
18
|
+
].join("|");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function collectFindings(report = {}) {
|
|
22
|
+
const flattened = [];
|
|
23
|
+
for (const agent of report.agentResults || []) {
|
|
24
|
+
for (const finding of agent.findings || []) {
|
|
25
|
+
flattened.push({
|
|
26
|
+
severity: normalizeString(finding.severity).toUpperCase(),
|
|
27
|
+
file: toPosixPath(finding.file),
|
|
28
|
+
line: Number(finding.line || 0),
|
|
29
|
+
message: normalizeString(finding.message),
|
|
30
|
+
ruleId: normalizeString(finding.ruleId),
|
|
31
|
+
ownerAgentId: agent.agentId,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return flattened;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function summaryDelta(base = {}, candidate = {}) {
|
|
39
|
+
const keys = ["P0", "P1", "P2", "P3"];
|
|
40
|
+
const delta = {};
|
|
41
|
+
for (const key of keys) {
|
|
42
|
+
delta[key] = Number(candidate[key] || 0) - Number(base[key] || 0);
|
|
43
|
+
}
|
|
44
|
+
delta.blockingChanged = Boolean(base.blocking) !== Boolean(candidate.blocking);
|
|
45
|
+
return delta;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function compareAuditReports(baseReport = {}, candidateReport = {}) {
|
|
49
|
+
const baseFindings = collectFindings(baseReport);
|
|
50
|
+
const candidateFindings = collectFindings(candidateReport);
|
|
51
|
+
const baseByFingerprint = new Map(baseFindings.map((finding) => [fingerprintFinding(finding), finding]));
|
|
52
|
+
const candidateByFingerprint = new Map(
|
|
53
|
+
candidateFindings.map((finding) => [fingerprintFinding(finding), finding])
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const added = [];
|
|
57
|
+
const removed = [];
|
|
58
|
+
|
|
59
|
+
for (const [fingerprint, finding] of candidateByFingerprint.entries()) {
|
|
60
|
+
if (!baseByFingerprint.has(fingerprint)) {
|
|
61
|
+
added.push(finding);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
for (const [fingerprint, finding] of baseByFingerprint.entries()) {
|
|
65
|
+
if (!candidateByFingerprint.has(fingerprint)) {
|
|
66
|
+
removed.push(finding);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const deterministicEquivalent = added.length === 0 && removed.length === 0;
|
|
71
|
+
return {
|
|
72
|
+
schemaVersion: "1.0.0",
|
|
73
|
+
generatedAt: new Date().toISOString(),
|
|
74
|
+
baseRunId: normalizeString(baseReport.runId),
|
|
75
|
+
candidateRunId: normalizeString(candidateReport.runId),
|
|
76
|
+
baseSummary: baseReport.summary || { P0: 0, P1: 0, P2: 0, P3: 0, blocking: false },
|
|
77
|
+
candidateSummary: candidateReport.summary || { P0: 0, P1: 0, P2: 0, P3: 0, blocking: false },
|
|
78
|
+
summaryDelta: summaryDelta(baseReport.summary || {}, candidateReport.summary || {}),
|
|
79
|
+
baseFindingCount: baseFindings.length,
|
|
80
|
+
candidateFindingCount: candidateFindings.length,
|
|
81
|
+
addedCount: added.length,
|
|
82
|
+
removedCount: removed.length,
|
|
83
|
+
deterministicEquivalent,
|
|
84
|
+
added: added.slice(0, 500),
|
|
85
|
+
removed: removed.slice(0, 500),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function writeAuditComparisonArtifact({
|
|
90
|
+
baseReport = {},
|
|
91
|
+
candidateReport = {},
|
|
92
|
+
outputDirectory = "",
|
|
93
|
+
} = {}) {
|
|
94
|
+
const resolvedOutputDirectory = path.resolve(String(outputDirectory || "."));
|
|
95
|
+
const comparison = compareAuditReports(baseReport, candidateReport);
|
|
96
|
+
const fileName = `AUDIT_COMPARISON_${comparison.baseRunId}_vs_${comparison.candidateRunId}.json`;
|
|
97
|
+
const outputPath = path.join(resolvedOutputDirectory, fileName);
|
|
98
|
+
await fsp.writeFile(outputPath, `${JSON.stringify(comparison, null, 2)}\n`, "utf-8");
|
|
99
|
+
return {
|
|
100
|
+
comparison,
|
|
101
|
+
outputPath,
|
|
102
|
+
};
|
|
103
|
+
}
|
package/src/auth/http.js
CHANGED
|
@@ -1,113 +1,113 @@
|
|
|
1
|
-
import { setTimeout as sleep } from "node:timers/promises";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Default timeout applied to Sentinelayer API requests when no override is provided.
|
|
5
|
-
* @type {number}
|
|
6
|
-
*/
|
|
7
|
-
export const DEFAULT_REQUEST_TIMEOUT_MS = 20_000;
|
|
8
|
-
|
|
9
|
-
function normalizeApiError(errorPayload = {}) {
|
|
10
|
-
if (!errorPayload || typeof errorPayload !== "object" || Array.isArray(errorPayload)) {
|
|
11
|
-
return {
|
|
12
|
-
code: "UNKNOWN",
|
|
13
|
-
message: "Unknown API error",
|
|
14
|
-
requestId: null,
|
|
15
|
-
};
|
|
16
|
-
}
|
|
17
|
-
return {
|
|
18
|
-
code: String(errorPayload.code || "UNKNOWN"),
|
|
19
|
-
message: String(errorPayload.message || "Unknown API error"),
|
|
20
|
-
requestId: errorPayload.request_id ? String(errorPayload.request_id) : null,
|
|
21
|
-
};
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export class SentinelayerApiError extends Error {
|
|
25
|
-
/**
|
|
26
|
-
* @param {string} message
|
|
27
|
-
* @param {{ status?: number, code?: string, requestId?: string | null }} [options]
|
|
28
|
-
*/
|
|
29
|
-
constructor(message, { status = 500, code = "UNKNOWN", requestId = null } = {}) {
|
|
30
|
-
super(String(message || "Sentinelayer API error"));
|
|
31
|
-
this.name = "SentinelayerApiError";
|
|
32
|
-
this.status = Number(status || 500);
|
|
33
|
-
this.code = String(code || "UNKNOWN");
|
|
34
|
-
this.requestId = requestId ? String(requestId) : null;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Execute an HTTP request against the Sentinelayer API and parse a JSON response.
|
|
40
|
-
* Throws `SentinelayerApiError` for transport errors, timeouts, API failures, and invalid JSON.
|
|
41
|
-
*
|
|
42
|
-
* @param {string} url
|
|
43
|
-
* @param {{
|
|
44
|
-
* method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE",
|
|
45
|
-
* headers?: Record<string, string>,
|
|
46
|
-
* body?: unknown,
|
|
47
|
-
* timeoutMs?: number
|
|
48
|
-
* }} [options]
|
|
49
|
-
* @returns {Promise<any>}
|
|
50
|
-
*/
|
|
51
|
-
export async function requestJson(
|
|
52
|
-
url,
|
|
53
|
-
{ method = "GET", headers = {}, body, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS } = {}
|
|
54
|
-
) {
|
|
55
|
-
const controller = new AbortController();
|
|
56
|
-
const timeout = setTimeout(() => controller.abort(), Number(timeoutMs || DEFAULT_REQUEST_TIMEOUT_MS));
|
|
57
|
-
|
|
58
|
-
try {
|
|
59
|
-
const response = await fetch(String(url), {
|
|
60
|
-
method,
|
|
61
|
-
headers: {
|
|
62
|
-
"Content-Type": "application/json",
|
|
63
|
-
...headers,
|
|
64
|
-
},
|
|
65
|
-
body: body === undefined ? undefined : JSON.stringify(body),
|
|
66
|
-
signal: controller.signal,
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
const rawBody = await response.text();
|
|
70
|
-
let json = {};
|
|
71
|
-
if (rawBody.trim()) {
|
|
72
|
-
try {
|
|
73
|
-
json = JSON.parse(rawBody);
|
|
74
|
-
} catch {
|
|
75
|
-
throw new SentinelayerApiError("Invalid JSON returned by API.", {
|
|
76
|
-
status: response.status,
|
|
77
|
-
code: "INVALID_JSON",
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (!response.ok) {
|
|
83
|
-
const apiError = normalizeApiError(json && typeof json === "object" ? json.error : {});
|
|
84
|
-
throw new SentinelayerApiError(apiError.message, {
|
|
85
|
-
status: response.status,
|
|
86
|
-
code: apiError.code,
|
|
87
|
-
requestId: apiError.requestId,
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
return json;
|
|
92
|
-
} catch (error) {
|
|
93
|
-
if (error instanceof SentinelayerApiError) {
|
|
94
|
-
throw error;
|
|
95
|
-
}
|
|
96
|
-
if (error && typeof error === "object" && error.name === "AbortError") {
|
|
97
|
-
throw new SentinelayerApiError("Request timed out.", {
|
|
98
|
-
status: 408,
|
|
99
|
-
code: "TIMEOUT",
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
throw new SentinelayerApiError(
|
|
103
|
-
error instanceof Error ? error.message : String(error || "Request failed"),
|
|
104
|
-
{
|
|
105
|
-
status: 503,
|
|
106
|
-
code: "NETWORK_ERROR",
|
|
107
|
-
}
|
|
108
|
-
);
|
|
109
|
-
} finally {
|
|
110
|
-
clearTimeout(timeout);
|
|
111
|
-
await sleep(0);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
1
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Default timeout applied to Sentinelayer API requests when no override is provided.
|
|
5
|
+
* @type {number}
|
|
6
|
+
*/
|
|
7
|
+
export const DEFAULT_REQUEST_TIMEOUT_MS = 20_000;
|
|
8
|
+
|
|
9
|
+
function normalizeApiError(errorPayload = {}) {
|
|
10
|
+
if (!errorPayload || typeof errorPayload !== "object" || Array.isArray(errorPayload)) {
|
|
11
|
+
return {
|
|
12
|
+
code: "UNKNOWN",
|
|
13
|
+
message: "Unknown API error",
|
|
14
|
+
requestId: null,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
return {
|
|
18
|
+
code: String(errorPayload.code || "UNKNOWN"),
|
|
19
|
+
message: String(errorPayload.message || "Unknown API error"),
|
|
20
|
+
requestId: errorPayload.request_id ? String(errorPayload.request_id) : null,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class SentinelayerApiError extends Error {
|
|
25
|
+
/**
|
|
26
|
+
* @param {string} message
|
|
27
|
+
* @param {{ status?: number, code?: string, requestId?: string | null }} [options]
|
|
28
|
+
*/
|
|
29
|
+
constructor(message, { status = 500, code = "UNKNOWN", requestId = null } = {}) {
|
|
30
|
+
super(String(message || "Sentinelayer API error"));
|
|
31
|
+
this.name = "SentinelayerApiError";
|
|
32
|
+
this.status = Number(status || 500);
|
|
33
|
+
this.code = String(code || "UNKNOWN");
|
|
34
|
+
this.requestId = requestId ? String(requestId) : null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Execute an HTTP request against the Sentinelayer API and parse a JSON response.
|
|
40
|
+
* Throws `SentinelayerApiError` for transport errors, timeouts, API failures, and invalid JSON.
|
|
41
|
+
*
|
|
42
|
+
* @param {string} url
|
|
43
|
+
* @param {{
|
|
44
|
+
* method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE",
|
|
45
|
+
* headers?: Record<string, string>,
|
|
46
|
+
* body?: unknown,
|
|
47
|
+
* timeoutMs?: number
|
|
48
|
+
* }} [options]
|
|
49
|
+
* @returns {Promise<any>}
|
|
50
|
+
*/
|
|
51
|
+
export async function requestJson(
|
|
52
|
+
url,
|
|
53
|
+
{ method = "GET", headers = {}, body, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS } = {}
|
|
54
|
+
) {
|
|
55
|
+
const controller = new AbortController();
|
|
56
|
+
const timeout = setTimeout(() => controller.abort(), Number(timeoutMs || DEFAULT_REQUEST_TIMEOUT_MS));
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const response = await fetch(String(url), {
|
|
60
|
+
method,
|
|
61
|
+
headers: {
|
|
62
|
+
"Content-Type": "application/json",
|
|
63
|
+
...headers,
|
|
64
|
+
},
|
|
65
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
66
|
+
signal: controller.signal,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const rawBody = await response.text();
|
|
70
|
+
let json = {};
|
|
71
|
+
if (rawBody.trim()) {
|
|
72
|
+
try {
|
|
73
|
+
json = JSON.parse(rawBody);
|
|
74
|
+
} catch {
|
|
75
|
+
throw new SentinelayerApiError("Invalid JSON returned by API.", {
|
|
76
|
+
status: response.status,
|
|
77
|
+
code: "INVALID_JSON",
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (!response.ok) {
|
|
83
|
+
const apiError = normalizeApiError(json && typeof json === "object" ? json.error : {});
|
|
84
|
+
throw new SentinelayerApiError(apiError.message, {
|
|
85
|
+
status: response.status,
|
|
86
|
+
code: apiError.code,
|
|
87
|
+
requestId: apiError.requestId,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return json;
|
|
92
|
+
} catch (error) {
|
|
93
|
+
if (error instanceof SentinelayerApiError) {
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
if (error && typeof error === "object" && error.name === "AbortError") {
|
|
97
|
+
throw new SentinelayerApiError("Request timed out.", {
|
|
98
|
+
status: 408,
|
|
99
|
+
code: "TIMEOUT",
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
throw new SentinelayerApiError(
|
|
103
|
+
error instanceof Error ? error.message : String(error || "Request failed"),
|
|
104
|
+
{
|
|
105
|
+
status: 503,
|
|
106
|
+
code: "NETWORK_ERROR",
|
|
107
|
+
}
|
|
108
|
+
);
|
|
109
|
+
} finally {
|
|
110
|
+
clearTimeout(timeout);
|
|
111
|
+
await sleep(0);
|
|
112
|
+
}
|
|
113
|
+
}
|