@coworker-jp/aidr 0.0.3 → 0.0.4
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 +1 -1
- package/src/binary-fetcher.mjs +32 -0
- package/src/cli.mjs +49 -5
- package/src/sentinel.mjs +239 -0
- package/src/templates.mjs +4 -4
- package/src/verify.mjs +4 -0
package/package.json
CHANGED
package/src/binary-fetcher.mjs
CHANGED
|
@@ -155,3 +155,35 @@ export async function fetchOpengrep(destPath) {
|
|
|
155
155
|
|
|
156
156
|
return { path: destPath, platform, sha256, verified: true };
|
|
157
157
|
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Download coworker-sentinel for the current platform into `destPath`.
|
|
161
|
+
* Uses the presigned S3 URL from the verify response when available,
|
|
162
|
+
* otherwise falls back to the licensed download endpoint.
|
|
163
|
+
* Returns { path, platform, sha256, verified }.
|
|
164
|
+
*/
|
|
165
|
+
export async function fetchSentinel(destPath, { accessKey, presignedUrl, env = "prod" } = {}) {
|
|
166
|
+
const platform = detectPlatform();
|
|
167
|
+
|
|
168
|
+
let binUrl;
|
|
169
|
+
if (presignedUrl) {
|
|
170
|
+
// Use presigned URL from verify response (no auth header needed)
|
|
171
|
+
binUrl = presignedUrl;
|
|
172
|
+
accessKey = undefined;
|
|
173
|
+
} else {
|
|
174
|
+
const base = downloadBase(env);
|
|
175
|
+
binUrl = `${base}/sentinel/${platform}`;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
await fsp.mkdir(path.dirname(destPath), { recursive: true });
|
|
179
|
+
const buf = await fetchBuffer(binUrl, accessKey);
|
|
180
|
+
const sha256 = crypto.createHash("sha256").update(buf).digest("hex");
|
|
181
|
+
|
|
182
|
+
const tmp = `${destPath}.tmp-${process.pid}`;
|
|
183
|
+
await fsp.writeFile(tmp, buf);
|
|
184
|
+
await fsp.chmod(tmp, 0o755);
|
|
185
|
+
await fsp.rename(tmp, destPath);
|
|
186
|
+
await clearQuarantine(destPath);
|
|
187
|
+
|
|
188
|
+
return { path: destPath, platform, sha256, verified: false };
|
|
189
|
+
}
|
package/src/cli.mjs
CHANGED
|
@@ -89,16 +89,42 @@ async function cmdInstall(opts) {
|
|
|
89
89
|
const base = scope === "user" ? home : process.cwd();
|
|
90
90
|
validateKey(opts.key);
|
|
91
91
|
|
|
92
|
+
let verifyInfo = null;
|
|
92
93
|
if (opts.verify !== false) {
|
|
93
94
|
try {
|
|
94
|
-
|
|
95
|
-
console.log(`verified: plan=${info.plan} expires_at=${info.expires_at || "n/a"}`);
|
|
95
|
+
verifyInfo = await verifyKey(opts.key, opts.env || DEFAULT_ENV);
|
|
96
96
|
} catch (e) {
|
|
97
97
|
console.error(`verify failed: ${e.message}`);
|
|
98
98
|
process.exit(1);
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
// --with-sentinel requires pro or trial plan
|
|
103
|
+
if (opts.withSentinel && verifyInfo && !verifyInfo.edr_enabled) {
|
|
104
|
+
console.error("Error: --with-sentinel requires a Pro or Trial plan.");
|
|
105
|
+
console.error(`Current plan: ${verifyInfo.plan}`);
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// --with-sentinel requires root (same as --scheduled)
|
|
110
|
+
if (opts.withSentinel && !opts.dryRun
|
|
111
|
+
&& process.getuid && process.getuid() !== 0
|
|
112
|
+
&& process.env.AI_SCANNER_FAKE_ROOT !== "1") {
|
|
113
|
+
const interactive = process.stdin.isTTY && process.stdout.isTTY && process.platform !== "win32";
|
|
114
|
+
if (interactive) {
|
|
115
|
+
console.error("--with-sentinel requires root. Re-running via sudo (you may be prompted for your password)...");
|
|
116
|
+
const result = spawnSync(
|
|
117
|
+
"sudo",
|
|
118
|
+
["-E", process.execPath, ...process.argv.slice(1)],
|
|
119
|
+
{ stdio: "inherit" }
|
|
120
|
+
);
|
|
121
|
+
process.exit(result.status ?? 1);
|
|
122
|
+
}
|
|
123
|
+
console.error("Error: --with-sentinel requires root privileges.");
|
|
124
|
+
console.error(` sudo -E npx @coworker-jp/aidr install --with-sentinel --key <ak_xxx>`);
|
|
125
|
+
process.exit(2);
|
|
126
|
+
}
|
|
127
|
+
|
|
102
128
|
let agents;
|
|
103
129
|
if (opts.all) {
|
|
104
130
|
agents = detectInstalled(home);
|
|
@@ -246,8 +272,6 @@ async function cmdInstall(opts) {
|
|
|
246
272
|
})
|
|
247
273
|
);
|
|
248
274
|
await Promise.all(copyOps);
|
|
249
|
-
|
|
250
|
-
console.log("Includes Opengrep (LGPL-2.1).");
|
|
251
275
|
} catch (e) {
|
|
252
276
|
console.error(`binary fetch failed: ${e.message}`);
|
|
253
277
|
if (!opts.binaryOnly) console.error("continuing with hook install (use --skip-binary to suppress)");
|
|
@@ -298,6 +322,25 @@ async function cmdInstall(opts) {
|
|
|
298
322
|
}
|
|
299
323
|
}
|
|
300
324
|
|
|
325
|
+
if (opts.withSentinel) {
|
|
326
|
+
const { installSentinel } = await import("./sentinel.mjs");
|
|
327
|
+
try {
|
|
328
|
+
const platform = (await import("./binary-fetcher.mjs")).detectPlatform();
|
|
329
|
+
const presignedUrl = verifyInfo?.sentinel_download_urls?.[platform] || undefined;
|
|
330
|
+
await installSentinel({
|
|
331
|
+
accessKey: opts.key,
|
|
332
|
+
dryRun: opts.dryRun,
|
|
333
|
+
skipBinary: opts.skipBinary,
|
|
334
|
+
presignedUrl,
|
|
335
|
+
env: opts.env || DEFAULT_ENV,
|
|
336
|
+
noActivate: process.env.AI_SCANNER_NO_SCHEDULED_ACTIVATE === "1",
|
|
337
|
+
});
|
|
338
|
+
} catch (e) {
|
|
339
|
+
console.error(`sentinel install failed: ${e.message}`);
|
|
340
|
+
process.exit(1);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
301
344
|
if (opts.browserExtension && !opts.dryRun) {
|
|
302
345
|
const firstAgentName = agents[0];
|
|
303
346
|
const firstAgent = await loadAgent(firstAgentName);
|
|
@@ -517,7 +560,7 @@ export async function run(argv) {
|
|
|
517
560
|
program
|
|
518
561
|
.name("aidr")
|
|
519
562
|
.description("AIDR setup CLI — installs ai-scanner hooks for AI coding agents")
|
|
520
|
-
.version("0.0.
|
|
563
|
+
.version("0.0.4");
|
|
521
564
|
|
|
522
565
|
program
|
|
523
566
|
.command("install")
|
|
@@ -536,6 +579,7 @@ export async function run(argv) {
|
|
|
536
579
|
.option("--uninstall", "remove all ai-scanner hooks/scripts/binaries for the selected agents (see 'uninstall' command)", false)
|
|
537
580
|
.option("--scheduled", "enable scheduled endpoint-info collection (creates a cron/systemd/launchd entry; without this flag endpoint-info is never collected)", false)
|
|
538
581
|
.option("--interval <hours>", "scheduled scan interval in hours (min 1, default 4; requires --scheduled)", "4")
|
|
582
|
+
.option("--with-sentinel", "install coworker-sentinel host-level EDR daemon (Pro/Trial plan; requires sudo)", false)
|
|
539
583
|
.option("--browser-extension", "install browser extension native messaging host for Shadow IT network monitoring (default: disabled)", false)
|
|
540
584
|
.action(cmdInstall);
|
|
541
585
|
|
package/src/sentinel.mjs
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* coworker-sentinel install/uninstall helpers.
|
|
3
|
+
*
|
|
4
|
+
* Requires root (sudo -E) because the daemon runs as a LaunchDaemon (macOS)
|
|
5
|
+
* or systemd service (Linux) to monitor OS-level events system-wide.
|
|
6
|
+
*
|
|
7
|
+
* Install paths (shared with scheduled.mjs):
|
|
8
|
+
* Binary: /opt/coworker/aidr/bin/coworker-sentinel
|
|
9
|
+
* macOS: /Library/LaunchDaemons/jp.coworker.sentinel.plist
|
|
10
|
+
* Linux: /etc/systemd/system/coworker-sentinel.service
|
|
11
|
+
* Env: /etc/aidr/aidr.env (shared with scheduler)
|
|
12
|
+
*/
|
|
13
|
+
import fs from "node:fs/promises";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
import { execFile, spawnSync } from "node:child_process";
|
|
16
|
+
import { promisify } from "node:util";
|
|
17
|
+
import { fetchSentinel, detectPlatform } from "./binary-fetcher.mjs";
|
|
18
|
+
import { SYSTEM_BIN_DIR, SYSTEM_ENV_FILE } from "./scheduled.mjs";
|
|
19
|
+
|
|
20
|
+
const execFileP = promisify(execFile);
|
|
21
|
+
|
|
22
|
+
const ROOT_PREFIX = process.env.AIDR_SCHEDULED_ROOT || "";
|
|
23
|
+
|
|
24
|
+
export const SENTINEL_BIN = path.join(SYSTEM_BIN_DIR, "coworker-sentinel");
|
|
25
|
+
export const SENTINEL_PLIST = ROOT_PREFIX + "/Library/LaunchDaemons/jp.coworker.sentinel.plist";
|
|
26
|
+
export const SENTINEL_SERVICE = ROOT_PREFIX + "/etc/systemd/system/coworker-sentinel.service";
|
|
27
|
+
export const SENTINEL_LOG_DIR = ROOT_PREFIX + "/var/log/coworker-sentinel";
|
|
28
|
+
|
|
29
|
+
function isRoot() {
|
|
30
|
+
if (process.env.AI_SCANNER_FAKE_ROOT === "1") return true;
|
|
31
|
+
return Boolean(process.getuid && process.getuid() === 0);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function exists(p) {
|
|
35
|
+
try { await fs.access(p); return true; } catch { return false; }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function isSentinelInstalled() {
|
|
39
|
+
return (
|
|
40
|
+
(await exists(SENTINEL_PLIST)) ||
|
|
41
|
+
(await exists(SENTINEL_SERVICE))
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function buildLaunchdPlist(envFile) {
|
|
46
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
47
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
|
|
48
|
+
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
49
|
+
<plist version="1.0">
|
|
50
|
+
<dict>
|
|
51
|
+
<key>Label</key>
|
|
52
|
+
<string>jp.coworker.sentinel</string>
|
|
53
|
+
<key>ProgramArguments</key>
|
|
54
|
+
<array>
|
|
55
|
+
<string>/bin/sh</string>
|
|
56
|
+
<string>-c</string>
|
|
57
|
+
<string>. ${envFile} && exec ${SENTINEL_BIN}</string>
|
|
58
|
+
</array>
|
|
59
|
+
<key>RunAtLoad</key>
|
|
60
|
+
<true/>
|
|
61
|
+
<key>KeepAlive</key>
|
|
62
|
+
<true/>
|
|
63
|
+
<key>StandardOutPath</key>
|
|
64
|
+
<string>${SENTINEL_LOG_DIR}/sentinel.log</string>
|
|
65
|
+
<key>StandardErrorPath</key>
|
|
66
|
+
<string>${SENTINEL_LOG_DIR}/sentinel.log</string>
|
|
67
|
+
<key>ThrottleInterval</key>
|
|
68
|
+
<integer>10</integer>
|
|
69
|
+
</dict>
|
|
70
|
+
</plist>
|
|
71
|
+
`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function buildSystemdService(envFile) {
|
|
75
|
+
return `[Unit]
|
|
76
|
+
Description=coworker-sentinel Host EDR Daemon
|
|
77
|
+
After=network.target
|
|
78
|
+
StartLimitIntervalSec=60
|
|
79
|
+
StartLimitBurst=5
|
|
80
|
+
|
|
81
|
+
[Service]
|
|
82
|
+
Type=simple
|
|
83
|
+
EnvironmentFile=${envFile}
|
|
84
|
+
ExecStart=${SENTINEL_BIN}
|
|
85
|
+
Restart=on-failure
|
|
86
|
+
RestartSec=5
|
|
87
|
+
StandardOutput=journal
|
|
88
|
+
StandardError=journal
|
|
89
|
+
SyslogIdentifier=coworker-sentinel
|
|
90
|
+
|
|
91
|
+
[Install]
|
|
92
|
+
WantedBy=multi-user.target
|
|
93
|
+
`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Install the sentinel daemon.
|
|
98
|
+
*
|
|
99
|
+
* opts:
|
|
100
|
+
* accessKey — ak_xxx (written to env file if not already present)
|
|
101
|
+
* dryRun — log actions without executing
|
|
102
|
+
* noActivate — skip launchctl/systemctl activation
|
|
103
|
+
* presignedUrl — presigned S3 URL from verify response (optional)
|
|
104
|
+
* env — "dev" | "prod"
|
|
105
|
+
*/
|
|
106
|
+
export async function installSentinel(opts = {}) {
|
|
107
|
+
const { accessKey, dryRun = false, skipBinary = false, noActivate = false, presignedUrl, env = "prod" } = opts;
|
|
108
|
+
|
|
109
|
+
if (!isRoot()) {
|
|
110
|
+
throw new Error(
|
|
111
|
+
"installSentinel requires root privileges.\n" +
|
|
112
|
+
"Re-run with: sudo -E npx @coworker-jp/aidr install --key <key> --with-sentinel"
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const platform = detectPlatform();
|
|
117
|
+
console.error(`[sentinel] Installing coworker-sentinel for ${platform}…`);
|
|
118
|
+
|
|
119
|
+
// 1. Download binary
|
|
120
|
+
if (!dryRun && !skipBinary) {
|
|
121
|
+
await fs.mkdir(SYSTEM_BIN_DIR, { recursive: true });
|
|
122
|
+
console.error(`[sentinel] Downloading binary → ${SENTINEL_BIN}`);
|
|
123
|
+
await fetchSentinel(SENTINEL_BIN, { accessKey, presignedUrl, env });
|
|
124
|
+
} else if (skipBinary) {
|
|
125
|
+
console.error(`[sentinel] Skipping binary download (--skip-binary)`);
|
|
126
|
+
} else {
|
|
127
|
+
console.error(`[sentinel] DRY-RUN: would download → ${SENTINEL_BIN}`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// 2. Create log directory
|
|
131
|
+
if (!dryRun) {
|
|
132
|
+
await fs.mkdir(SENTINEL_LOG_DIR, { recursive: true, mode: 0o755 });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// 3. Write env file (append AI_SCANNER_ACCESS_KEY if missing)
|
|
136
|
+
if (accessKey && !dryRun) {
|
|
137
|
+
let content = "";
|
|
138
|
+
try { content = await fs.readFile(SYSTEM_ENV_FILE, "utf8"); } catch { /* first run */ }
|
|
139
|
+
if (!content.includes("AI_SCANNER_ACCESS_KEY")) {
|
|
140
|
+
await fs.mkdir(path.dirname(SYSTEM_ENV_FILE), { recursive: true });
|
|
141
|
+
await fs.appendFile(SYSTEM_ENV_FILE, `\nAI_SCANNER_ACCESS_KEY=${accessKey}\n`);
|
|
142
|
+
await fs.chmod(SYSTEM_ENV_FILE, 0o600);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// 4. Write service/plist
|
|
147
|
+
if (process.platform === "darwin") {
|
|
148
|
+
await _installMacos(dryRun, noActivate);
|
|
149
|
+
} else {
|
|
150
|
+
await _installLinux(dryRun, noActivate);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
console.error("[sentinel] Installation complete.");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function _installMacos(dryRun, noActivate) {
|
|
157
|
+
const plistContent = buildLaunchdPlist(SYSTEM_ENV_FILE);
|
|
158
|
+
if (!dryRun) {
|
|
159
|
+
await fs.mkdir(path.dirname(SENTINEL_PLIST), { recursive: true });
|
|
160
|
+
await fs.writeFile(SENTINEL_PLIST, plistContent, { mode: 0o644 });
|
|
161
|
+
console.error(`[sentinel] Written: ${SENTINEL_PLIST}`);
|
|
162
|
+
|
|
163
|
+
if (!noActivate && process.env.AI_SCANNER_NO_SCHEDULED_ACTIVATE !== "1") {
|
|
164
|
+
// Unload existing (ignore errors) then load
|
|
165
|
+
spawnSync("launchctl", ["unload", SENTINEL_PLIST], { stdio: "ignore" });
|
|
166
|
+
const load = spawnSync("launchctl", ["load", "-w", SENTINEL_PLIST]);
|
|
167
|
+
if (load.status !== 0) {
|
|
168
|
+
console.warn("[sentinel] launchctl load failed — daemon may need manual start");
|
|
169
|
+
} else {
|
|
170
|
+
console.error("[sentinel] Daemon loaded via launchctl");
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
} else {
|
|
174
|
+
console.error(`[sentinel] DRY-RUN: would write ${SENTINEL_PLIST}`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function _installLinux(dryRun, noActivate) {
|
|
179
|
+
const serviceContent = buildSystemdService(SYSTEM_ENV_FILE);
|
|
180
|
+
const serviceDir = path.dirname(SENTINEL_SERVICE);
|
|
181
|
+
if (!dryRun) {
|
|
182
|
+
await fs.mkdir(serviceDir, { recursive: true });
|
|
183
|
+
await fs.writeFile(SENTINEL_SERVICE, serviceContent, { mode: 0o644 });
|
|
184
|
+
console.error(`[sentinel] Written: ${SENTINEL_SERVICE}`);
|
|
185
|
+
|
|
186
|
+
if (!noActivate && process.env.AI_SCANNER_NO_SCHEDULED_ACTIVATE !== "1") {
|
|
187
|
+
spawnSync("systemctl", ["daemon-reload"], { stdio: "ignore" });
|
|
188
|
+
spawnSync("systemctl", ["enable", "--now", "coworker-sentinel"], { stdio: "ignore" });
|
|
189
|
+
console.error("[sentinel] Enabled and started via systemctl");
|
|
190
|
+
}
|
|
191
|
+
} else {
|
|
192
|
+
console.error(`[sentinel] DRY-RUN: would write ${SENTINEL_SERVICE}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Uninstall the sentinel daemon.
|
|
198
|
+
*/
|
|
199
|
+
export async function uninstallSentinel({ dryRun = false } = {}) {
|
|
200
|
+
if (!isRoot()) {
|
|
201
|
+
throw new Error("uninstallSentinel requires root privileges.");
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
console.error("[sentinel] Uninstalling coworker-sentinel…");
|
|
205
|
+
|
|
206
|
+
if (process.platform === "darwin") {
|
|
207
|
+
if (await exists(SENTINEL_PLIST)) {
|
|
208
|
+
if (!dryRun) {
|
|
209
|
+
spawnSync("launchctl", ["unload", SENTINEL_PLIST], { stdio: "ignore" });
|
|
210
|
+
await fs.unlink(SENTINEL_PLIST);
|
|
211
|
+
console.error(`[sentinel] Removed: ${SENTINEL_PLIST}`);
|
|
212
|
+
} else {
|
|
213
|
+
console.error(`[sentinel] DRY-RUN: would remove ${SENTINEL_PLIST}`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
} else {
|
|
217
|
+
if (await exists(SENTINEL_SERVICE)) {
|
|
218
|
+
if (!dryRun) {
|
|
219
|
+
spawnSync("systemctl", ["disable", "--now", "coworker-sentinel"], { stdio: "ignore" });
|
|
220
|
+
await fs.unlink(SENTINEL_SERVICE);
|
|
221
|
+
spawnSync("systemctl", ["daemon-reload"], { stdio: "ignore" });
|
|
222
|
+
console.error(`[sentinel] Removed: ${SENTINEL_SERVICE}`);
|
|
223
|
+
} else {
|
|
224
|
+
console.error(`[sentinel] DRY-RUN: would remove ${SENTINEL_SERVICE}`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (await exists(SENTINEL_BIN)) {
|
|
230
|
+
if (!dryRun) {
|
|
231
|
+
await fs.unlink(SENTINEL_BIN);
|
|
232
|
+
console.error(`[sentinel] Removed: ${SENTINEL_BIN}`);
|
|
233
|
+
} else {
|
|
234
|
+
console.error(`[sentinel] DRY-RUN: would remove ${SENTINEL_BIN}`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
console.error("[sentinel] Uninstall complete.");
|
|
239
|
+
}
|
package/src/templates.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
1
|
+
// Canonical source for the scanner hook templates installed by aidr.
|
|
2
|
+
// Each template emits a script byte-identical to the corresponding file under
|
|
3
|
+
// .{agent}/ at the root of this repo (Claude / Cursor / Windsurf / Kiro);
|
|
4
|
+
// only the access key and the verify endpoint differ.
|
|
5
5
|
|
|
6
6
|
import { buildClaudeSettings, buildGeminiSettings } from "./merge.mjs";
|
|
7
7
|
|
package/src/verify.mjs
CHANGED
|
@@ -42,6 +42,10 @@ export async function verifyKey(accessKey, env = "prod") {
|
|
|
42
42
|
status: body.status,
|
|
43
43
|
plan: body.plan || "standard",
|
|
44
44
|
expires_at: body.expires_at || null,
|
|
45
|
+
edr_enabled: body.edr_enabled || false,
|
|
46
|
+
latest_sentinel_version: body.latest_sentinel_version || null,
|
|
47
|
+
sentinel_download_urls: body.sentinel_download_urls || {},
|
|
48
|
+
sentinel_sha256: body.sentinel_sha256 || {},
|
|
45
49
|
};
|
|
46
50
|
}
|
|
47
51
|
|