@secondlayer/sentinel 0.4.0 → 0.6.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 +2 -1
- package/dist/cli.js +842 -28
- package/dist/index.js +96 -2
- package/dist/mcp.js +614 -20
- package/dist/sentinel-client.d.ts +483 -2
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -85,6 +85,13 @@ class SentinelError extends Error {
|
|
|
85
85
|
this.name = "SentinelError";
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
|
+
var runInProgress = (status) => status === "queued" || status === "running";
|
|
89
|
+
function runPhase(run) {
|
|
90
|
+
if (run.status === "queued")
|
|
91
|
+
return "queued";
|
|
92
|
+
const last = run.events.at(-1);
|
|
93
|
+
return last ? [last.name ?? last.kind, last.state].filter(Boolean).join(" ") : "starting";
|
|
94
|
+
}
|
|
88
95
|
var rowsOf = (intents) => intents.flatMap((i) => i.rows);
|
|
89
96
|
var subjectKey = (s) => s.kind === "fn" ? `fn:${s.fn}` : s.kind === "group" ? `group:${s.group}` : `asset:${s.asset}`;
|
|
90
97
|
var CONTROL_NAME = "Control changes";
|
|
@@ -191,10 +198,30 @@ function mutedUntilOf(w, now = Date.now()) {
|
|
|
191
198
|
const at = w.delivery.mutedUntil;
|
|
192
199
|
return at && Date.parse(at) > now ? at : null;
|
|
193
200
|
}
|
|
201
|
+
function changeText(c) {
|
|
202
|
+
const name = c.subject.kind === "asset" ? subjectName(c.subject, c.units) : c.watch.title;
|
|
203
|
+
const rule = (r) => ruleSummary(r, c.units);
|
|
204
|
+
switch (c.action) {
|
|
205
|
+
case "on":
|
|
206
|
+
return `${c.watch.title} turned on`;
|
|
207
|
+
case "off":
|
|
208
|
+
return `${c.watch.title} turned off`;
|
|
209
|
+
case "muted":
|
|
210
|
+
case "cooldown":
|
|
211
|
+
return `${c.watch.title}: ${c.action === "muted" ? "mute" : "cooldown"} ${c.note ?? ""}`.trim();
|
|
212
|
+
case "applied":
|
|
213
|
+
if (c.prevRule && c.rule)
|
|
214
|
+
return `${name} rule changed: ${rule(c.prevRule)} → ${rule(c.rule)}`;
|
|
215
|
+
return c.rule ? `${name} rule set: ${rule(c.rule)}` : `${name} rule set`;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
194
218
|
function createClient(opts) {
|
|
195
219
|
const baseUrl = (opts.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
196
220
|
const doFetch = opts.fetch ?? globalThis.fetch;
|
|
197
|
-
const authHeader = () => ({
|
|
221
|
+
const authHeader = () => ({
|
|
222
|
+
authorization: `Bearer ${opts.token}`,
|
|
223
|
+
...opts.client ? { "x-sentinel-client": opts.client } : {}
|
|
224
|
+
});
|
|
198
225
|
async function call(method, path, body) {
|
|
199
226
|
const res = await doFetch(`${baseUrl}${path}`, {
|
|
200
227
|
method,
|
|
@@ -211,6 +238,7 @@ function createClient(opts) {
|
|
|
211
238
|
}
|
|
212
239
|
const plan = (id) => `/v1/plans/${encodeURIComponent(id)}`;
|
|
213
240
|
const appPlan = (id) => `/plan/${encodeURIComponent(id)}`;
|
|
241
|
+
const appProject = (id) => `/projects/${encodeURIComponent(id)}`;
|
|
214
242
|
return {
|
|
215
243
|
me: () => call("GET", "/v1/me"),
|
|
216
244
|
plans: async () => (await call("GET", "/v1/plans")).plans,
|
|
@@ -226,6 +254,8 @@ function createClient(opts) {
|
|
|
226
254
|
testsFromRepo: (id) => call("POST", `${appPlan(id)}/tests`, { fromRepo: true }),
|
|
227
255
|
previewPlan: (contractId) => call("POST", "/plan/preview", { contractId }),
|
|
228
256
|
createPlan: (contractId) => call("POST", "/plan", { contractId }),
|
|
257
|
+
previewImport: (url) => call("POST", "/projects/import/preview", { url }),
|
|
258
|
+
importRepo: (input) => call("POST", "/projects/import", input),
|
|
229
259
|
billing: () => call("GET", "/billing"),
|
|
230
260
|
watches: (planId) => call("GET", `${appPlan(planId)}/watches`),
|
|
231
261
|
setRule: (planId, watchId, rule) => call("POST", `${appPlan(planId)}/watches/${encodeURIComponent(watchId)}/rule`, { rule }),
|
|
@@ -242,9 +272,70 @@ function createClient(opts) {
|
|
|
242
272
|
watchContract: (contractId) => call("POST", "/contracts/watch", {
|
|
243
273
|
contractId
|
|
244
274
|
}),
|
|
245
|
-
stopMonitoring: (planId) => call("DELETE", `${appPlan(planId)}/monitor`)
|
|
275
|
+
stopMonitoring: (planId) => call("DELETE", `${appPlan(planId)}/monitor`),
|
|
276
|
+
watchActivity: (planId, intentId, opts = {}) => call("GET", `${appPlan(planId)}/watches/${encodeURIComponent(intentId)}/activity${query(opts)}`),
|
|
277
|
+
functions: async (planId) => (await call("GET", `${appPlan(planId)}/functions`)).functions,
|
|
278
|
+
functionActivity: (planId, fn) => call("GET", `${appPlan(planId)}/functions/${encodeURIComponent(fn)}/activity`),
|
|
279
|
+
alertPreview: (planId, example) => call("GET", `${appPlan(planId)}/alert-preview${query({ example })}`),
|
|
280
|
+
threshold: (planId, asset) => call("GET", `${appPlan(planId)}/watches/asset/${encodeURIComponent(asset)}/threshold`),
|
|
281
|
+
setThreshold: (planId, asset, amount) => call("POST", `${appPlan(planId)}/watches/asset/${encodeURIComponent(asset)}/threshold`, { amount }),
|
|
282
|
+
retryHistory: (planId) => call("POST", `${appPlan(planId)}/history/retry`, {}),
|
|
283
|
+
readAlert: (alertId) => call("POST", `/alerts/${encodeURIComponent(alertId)}/read`),
|
|
284
|
+
readAllAlerts: () => call("POST", "/alerts/read-all"),
|
|
285
|
+
ask: (q) => call("POST", "/ask", { q }),
|
|
286
|
+
acknowledge: (planId, finding, title, reason, note = null) => call("POST", `${appPlan(planId)}/acknowledge`, {
|
|
287
|
+
finding,
|
|
288
|
+
title,
|
|
289
|
+
reason,
|
|
290
|
+
note
|
|
291
|
+
}),
|
|
292
|
+
reopen: (planId, finding, title) => call("POST", `${appPlan(planId)}/reopen`, { finding, title }),
|
|
293
|
+
ackHistory: async (planId) => (await call("GET", `${appPlan(planId)}/acks`)).events,
|
|
294
|
+
notes: async (planId, finding) => (await call("GET", `${appPlan(planId)}/findings/${finding}/notes`)).notes,
|
|
295
|
+
addNote: (planId, finding, title, body) => call("POST", `${appPlan(planId)}/findings/${finding}/notes`, {
|
|
296
|
+
title,
|
|
297
|
+
body
|
|
298
|
+
}),
|
|
299
|
+
tests: (planId, files) => call("POST", `${appPlan(planId)}/tests`, { files }),
|
|
300
|
+
fuzzCheck: (planId, harness) => call("POST", `${appPlan(planId)}/fuzzcheck`, { harness }),
|
|
301
|
+
fuzz: (planId, invariants) => call("POST", `${appPlan(planId)}/fuzz`, { invariants }),
|
|
302
|
+
latestCommit: (planId) => call("GET", `${appPlan(planId)}/latest`),
|
|
303
|
+
updatePlan: (planId, commit) => call("POST", `${appPlan(planId)}/update`, commit ? { commit } : {}),
|
|
304
|
+
setAutoPlan: (planId, auto) => call("POST", `${appPlan(planId)}/wait`, { auto }),
|
|
305
|
+
setLink: (planId, dependency, url) => call("POST", `${appPlan(planId)}/link`, { id: dependency, url }),
|
|
306
|
+
planLinked: (planId) => call("POST", `${appPlan(planId)}/plan-linked`, {}),
|
|
307
|
+
renameContract: (planId, name) => call("POST", `${appPlan(planId)}/settings`, { name }),
|
|
308
|
+
removeContract: (planId, confirm) => call("POST", `${appPlan(planId)}/remove`, {
|
|
309
|
+
confirm
|
|
310
|
+
}),
|
|
311
|
+
projects: () => call("GET", "/projects"),
|
|
312
|
+
project: (projectId) => call("GET", appProject(projectId)),
|
|
313
|
+
projectOverview: (projectId, opts = {}) => call("GET", `${appProject(projectId)}/overview${opts.watches ? "?watches=1" : ""}`),
|
|
314
|
+
renameProject: (projectId, name) => call("POST", `${appProject(projectId)}/rename`, { name }),
|
|
315
|
+
removeProject: (projectId, confirm) => call("POST", `${appProject(projectId)}/remove`, {
|
|
316
|
+
confirm
|
|
317
|
+
}),
|
|
318
|
+
answerMergeSuggestion: (from, into, accept) => call("POST", "/projects/merge", { from, into, accept }),
|
|
319
|
+
mergeProject: (projectId, into) => call("POST", `${appProject(projectId)}/merge-into`, { into }),
|
|
320
|
+
moveProjectContract: (projectId, contractId, into) => call("POST", `${appProject(projectId)}/contracts/${encodeURIComponent(contractId)}/move`, { into }),
|
|
321
|
+
removeProjectContract: (projectId, contractId, confirm) => call("POST", `${appProject(projectId)}/contracts/${encodeURIComponent(contractId)}/remove`, { confirm }),
|
|
322
|
+
previewProjectContract: (projectId, contractId) => call("POST", `${appProject(projectId)}/contracts/preview`, { contractId }),
|
|
323
|
+
addProjectContract: (projectId, contractId, opts = {}) => call("POST", `${appProject(projectId)}/contracts`, { contractId, ...opts }),
|
|
324
|
+
setProjectDestinations: (projectId, dest) => call("POST", `${appProject(projectId)}/destinations`, dest),
|
|
325
|
+
setProjectMonitoring: (projectId, on) => call("POST", `${appProject(projectId)}/monitoring`, { on }),
|
|
326
|
+
projectWatches: (projectId) => call("GET", `${appProject(projectId)}/watches`),
|
|
327
|
+
projectActivity: (projectId, intentId, opts = {}) => call("GET", `${appProject(projectId)}/watches/${encodeURIComponent(intentId)}/activity${query(opts)}`),
|
|
328
|
+
projectChanges: (projectId, opts = {}) => call("GET", `${appProject(projectId)}/changes${query(opts)}`)
|
|
246
329
|
};
|
|
247
330
|
}
|
|
331
|
+
function query(params) {
|
|
332
|
+
const q = new URLSearchParams;
|
|
333
|
+
for (const [k, v] of Object.entries(params))
|
|
334
|
+
if (v !== undefined && v !== null)
|
|
335
|
+
q.set(k, String(v));
|
|
336
|
+
const s = q.toString();
|
|
337
|
+
return s ? `?${s}` : "";
|
|
338
|
+
}
|
|
248
339
|
var HOUR_MS = 3600000;
|
|
249
340
|
var muteEnd = (hours) => new Date(Date.now() + Math.min(hours * HOUR_MS, 7 * 24 * HOUR_MS - 60000)).toISOString();
|
|
250
341
|
async function watchContext(client, planId) {
|
|
@@ -418,6 +509,9 @@ async function whoami(client, io) {
|
|
|
418
509
|
return 0;
|
|
419
510
|
}
|
|
420
511
|
|
|
512
|
+
// cli/sentinel.ts
|
|
513
|
+
import { readFileSync } from "node:fs";
|
|
514
|
+
|
|
421
515
|
// client/kind.ts
|
|
422
516
|
var isGreen = (f) => f.poc === "green";
|
|
423
517
|
function findingKind(f) {
|
|
@@ -432,7 +526,7 @@ function findingKind(f) {
|
|
|
432
526
|
}
|
|
433
527
|
var URGENT_CONSEQUENCES = new Set(["authority", "code", "value"]);
|
|
434
528
|
var KIND_LABEL = {
|
|
435
|
-
exploitable: "
|
|
529
|
+
exploitable: "Reproducible",
|
|
436
530
|
trust: "Trust assumption",
|
|
437
531
|
worth_a_look: "Worth a look",
|
|
438
532
|
refuted: "refuted"
|
|
@@ -494,9 +588,9 @@ function assertionsOf(output) {
|
|
|
494
588
|
var FENCE = "```";
|
|
495
589
|
var FOOTER = "Sentinel's findings are a starting point for your own review, not an audit sign-off.";
|
|
496
590
|
var KIND_LINE = {
|
|
497
|
-
exploitable: "
|
|
591
|
+
exploitable: "Reproducible (verified, re-verified, reproduced in the sandbox)",
|
|
498
592
|
trust: "Trust assumption (watched: Sentinel alerts when it's used)",
|
|
499
|
-
worth_a_look: "Worth a look (not
|
|
593
|
+
worth_a_look: "Worth a look (not reproduced)",
|
|
500
594
|
refuted: "Refuted"
|
|
501
595
|
};
|
|
502
596
|
var tick = (s) => `\`${s}\``;
|
|
@@ -587,16 +681,16 @@ function issueMd(f, withCondition) {
|
|
|
587
681
|
"## The issue",
|
|
588
682
|
f.headline && f.headline !== f.title ? `${f.headline} (${f.title})` : f.title,
|
|
589
683
|
...f.blastRadius ? ["", `**Impact:** ${f.blastRadius}`] : [],
|
|
590
|
-
...withCondition && f.precondition ? ["", `**
|
|
684
|
+
...withCondition && f.precondition ? ["", `**Reproduces when:** ${f.precondition}`] : [],
|
|
591
685
|
""
|
|
592
686
|
];
|
|
593
687
|
}
|
|
594
688
|
function prelaunchBrief(f, ctx, kind) {
|
|
595
|
-
const kindLine = kind === "exploitable" ? "
|
|
689
|
+
const kindLine = kind === "exploitable" ? "Reproducible · look before launch" : kind === "trust" ? "Trust assumption" : "Worth a look before launch";
|
|
596
690
|
return [
|
|
597
691
|
"# Task: review a Sentinel finding on pre-launch code",
|
|
598
692
|
"",
|
|
599
|
-
`Sentinel found an issue${f.targetFn ? ` in ${tick(f.targetFn)}` : ""}: ${f.title}. PoCs and
|
|
693
|
+
`Sentinel found an issue${f.targetFn ? ` in ${tick(f.targetFn)}` : ""}: ${f.title}. PoCs and reproduction detail open once Sentinel confirms this code isn't already live on mainnet; possible approaches are shown now, so this brief carries the finding and a direction, not the PoC. Look at the code before launch: the watch arms at launch, but it isn't a substitute for the review.`,
|
|
600
694
|
"",
|
|
601
695
|
"## Start here",
|
|
602
696
|
`1. ${where(f, ctx)}`,
|
|
@@ -628,7 +722,7 @@ function trustBrief(f, ctx) {
|
|
|
628
722
|
`2. Find every place that reads what ${f.targetFn ? tick(f.targetFn) : "this power"} changes, and show each one, so the reach of this power is clear.`,
|
|
629
723
|
"3. Only then discuss options that fit this codebase's design.",
|
|
630
724
|
"",
|
|
631
|
-
...reportBackMd(ctx, "- There is no PoC for a trust assumption. Don't write
|
|
725
|
+
...reportBackMd(ctx, "- There is no PoC for a trust assumption. Don't write a PoC for it."),
|
|
632
726
|
...contextMd(f, ctx, "trust"),
|
|
633
727
|
"- Class: trust assumption (a power held by design)",
|
|
634
728
|
"- Evidence: checked by Sentinel's adversarial verifier. No PoC; nothing to run.",
|
|
@@ -750,6 +844,9 @@ var USAGE = `sentinel <command>
|
|
|
750
844
|
logout forget the saved login (also signs the secondlayer CLI out)
|
|
751
845
|
whoami who you are and where the credential came from
|
|
752
846
|
new <contract> [--yes] plan a contract: see what it would do and cost; --yes starts it
|
|
847
|
+
import <repo-url> [--only a,b] [--all] [--project <id>|new] [--yes]
|
|
848
|
+
plan a public Clarinet repo's contracts into one project: see each one and the
|
|
849
|
+
cost (the suggested ones, unless --only or --all); --yes imports them
|
|
753
850
|
balance your balance and what a plan, a deep run and a monitored event cost
|
|
754
851
|
alerts [--contract <id>] your alerts, newest first
|
|
755
852
|
|
|
@@ -758,7 +855,7 @@ var USAGE = `sentinel <command>
|
|
|
758
855
|
finding <id> <n> [--poc] [--run] [--brief] [--json] one finding; --poc adds the PoC source, --run the command
|
|
759
856
|
to re-run it, --brief the agent brief (the finding page's "Copy agent brief")
|
|
760
857
|
runs <id> follow-up runs on a plan
|
|
761
|
-
watch <id> follow running runs until none are left
|
|
858
|
+
watch <id> follow running (and queued) runs until none are left
|
|
762
859
|
reverify <id> [--yes] check a plan's leads again (stronger second pass, $3); only starts with --yes
|
|
763
860
|
|
|
764
861
|
paid runs on a plan you own (each starts only with --yes; without it you see what it would do):
|
|
@@ -766,10 +863,44 @@ paid runs on a plan you own (each starts only with --yes; without it you see wha
|
|
|
766
863
|
challenge <plan> <n> "<reason>" [--yes] re-verify finding n against your argument
|
|
767
864
|
replay <plan> <n> [--yes] re-run finding n's stored PoC
|
|
768
865
|
directed <plan> "<what to watch>" [--yes] re-plan around what you want watched
|
|
769
|
-
tests <plan> --from-repo [--yes]
|
|
866
|
+
tests <plan> --from-repo|<file>... [--yes] run the Clarinet suite from a pre-launch plan's repo, or your test files
|
|
867
|
+
fuzz <plan> <invariants.clar> [--yes] compile-check the invariants (free); --yes fuzzes them in the sandbox
|
|
868
|
+
|
|
869
|
+
your plans and findings (owners; a write only happens with --yes):
|
|
870
|
+
latest <plan> whether a pre-launch plan's branch moved on, and what an update reviews
|
|
871
|
+
update <plan> [--commit <sha>] [--yes] plan the latest commit (or --commit): free while monitoring is on
|
|
872
|
+
auto-plan <plan> on|off [--yes] plan a waiting plan on its own once the contracts it waits on deploy
|
|
873
|
+
link <plan> <contract> <url>|--clear [--yes] link code for a contract the plan waits on
|
|
874
|
+
plan-linked <plan> [--yes] plan now with the linked code (a paid plan)
|
|
875
|
+
name <plan> "<name>" [--yes] your own name for the contract ("" clears it)
|
|
876
|
+
remove <plan> --confirm <name> [--yes] take the contract off your account: monitoring, alerts and claim
|
|
877
|
+
ack <plan> <n> accepted|fixed|disagree [--note "<text>"] [--yes] label finding n for your team
|
|
878
|
+
reopen <plan> <n> [--yes] put an acknowledged finding back on the list
|
|
879
|
+
acks <plan> every acknowledgement change on the contract
|
|
880
|
+
notes <plan> <n> your notes on finding n
|
|
881
|
+
note <plan> <n> "<text>" [--yes] add a note to finding n
|
|
882
|
+
ask "<question>" an answer from Sentinel's docs
|
|
883
|
+
|
|
884
|
+
projects (the contracts of one protocol; a write only happens with --yes):
|
|
885
|
+
projects [--json] your projects and the merges Sentinel suggests
|
|
886
|
+
projects show <project> [--json] its contracts, each one's plan and monitoring
|
|
887
|
+
projects overview <project> [--watches] the project, its plans and (--watches) watches, as JSON
|
|
888
|
+
projects add <project> <contract> [--into <project>|new] [--yes] where it would go; --yes plans it (paid)
|
|
889
|
+
projects rename <project> "<name>" [--yes]
|
|
890
|
+
projects merge <project> <into> [--yes] move every contract into another of your projects
|
|
891
|
+
projects suggestion <from> <into> accept|dismiss [--yes] answer a merge Sentinel suggested
|
|
892
|
+
projects move <project> <contract> <into>|new [--yes]
|
|
893
|
+
projects remove-contract <project> <contract> [--confirm <name>] [--yes] a contract you added needs --confirm
|
|
894
|
+
projects remove <project> --confirm "<name>" [--yes] every contract you added in it, and the project
|
|
895
|
+
projects watches <project> [--json] the project's watches as one list
|
|
896
|
+
projects activity <project> <watch> [--contract <id>] [--window 7|90] [--json]
|
|
897
|
+
projects changes <project> [--watch <watch>] [--mutes] [--limit N] [--cursor C] [--json] who turned watches on/off or changed rules
|
|
898
|
+
projects destinations <project> [--email on|off] [--webhook URL] [--slack URL] [--yes] a URL left out is cleared
|
|
899
|
+
projects monitoring <project> on|off [--yes]
|
|
770
900
|
|
|
771
|
-
A key needs Plans & findings (read for the above, write for new
|
|
772
|
-
(read to look, write to
|
|
901
|
+
A key needs Plans & findings (read for the above, write for new, the runs and changes above, and project changes);
|
|
902
|
+
monitoring commands and a project's watches, destinations and monitoring need Monitoring (read to look, write to
|
|
903
|
+
change); mute-alert and mark-read need Alerts: write.
|
|
773
904
|
|
|
774
905
|
monitoring (a write only happens with --yes; without it you see what would change):
|
|
775
906
|
watches <plan> [--json] subjects, rules, status, mutes
|
|
@@ -783,23 +914,42 @@ monitoring (a write only happens with --yes; without it you see what would chang
|
|
|
783
914
|
test-alert <plan> [--yes] a sample alert to every saved destination
|
|
784
915
|
follow <contract> [--yes] start monitoring a deployed contract
|
|
785
916
|
unfollow <contract> [--yes]
|
|
917
|
+
activity <plan> <watch> [--window 7|90] [--json] what a watch saw (<watch>: its title or id) and what alerted
|
|
918
|
+
functions <plan> [<fn>] [--json] every public function and its watch; with <fn>, what a watch
|
|
919
|
+
on it would have seen
|
|
920
|
+
alert-preview <plan> [--example money_out|finding] a sample alert as email and Slack get it
|
|
921
|
+
threshold <plan> <asset> [<amount>] [--yes] an asset's Money out limit; with <amount> (base units), set it
|
|
922
|
+
retry-history <plan> [--yes] read the watches' history again after a failed read
|
|
923
|
+
mark-read <alert>|--all [--yes] mark one alert, or every alert, read
|
|
786
924
|
|
|
787
925
|
<subject> is a function name or a token symbol, as the watches command shows it.
|
|
788
926
|
|
|
789
927
|
credential: SENTINEL_API_KEY, then --token <key>, then sentinel login. env: SENTINEL_API_URL
|
|
790
928
|
`;
|
|
791
929
|
var POLL_MS = 5000;
|
|
792
|
-
var VALUE_FLAGS = new Set([
|
|
930
|
+
var VALUE_FLAGS = new Set([
|
|
931
|
+
"--email",
|
|
932
|
+
"--webhook",
|
|
933
|
+
"--slack",
|
|
934
|
+
"--contract",
|
|
935
|
+
"--only",
|
|
936
|
+
"--project",
|
|
937
|
+
"--confirm",
|
|
938
|
+
"--note",
|
|
939
|
+
"--commit",
|
|
940
|
+
"--into",
|
|
941
|
+
"--window",
|
|
942
|
+
"--example",
|
|
943
|
+
"--watch",
|
|
944
|
+
"--limit",
|
|
945
|
+
"--cursor"
|
|
946
|
+
]);
|
|
793
947
|
var cell2 = (v, empty = "—") => v === null || v === undefined || v === "" ? empty : String(v);
|
|
794
948
|
var table = (rows) => {
|
|
795
949
|
const text = rows.map((r) => r.map((c) => cell2(c)));
|
|
796
950
|
const w = text[0]?.map((_, i) => Math.max(...text.map((r) => r[i].length))) ?? [];
|
|
797
951
|
return text.map((r) => r.map((c, i) => c.padEnd(w[i])).join(" ").trimEnd());
|
|
798
952
|
};
|
|
799
|
-
var phaseOf = (r) => {
|
|
800
|
-
const last = r.events.at(-1);
|
|
801
|
-
return last ? [last.name ?? last.kind, last.state].filter(Boolean).join(" ") : "starting";
|
|
802
|
-
};
|
|
803
953
|
var verdictLine = (p) => {
|
|
804
954
|
const r = p.report;
|
|
805
955
|
const head = `${p.contractId} ${p.status}`;
|
|
@@ -837,7 +987,10 @@ ${USAGE}` : USAGE);
|
|
|
837
987
|
"--from-repo",
|
|
838
988
|
"--poc",
|
|
839
989
|
"--run",
|
|
840
|
-
"--brief"
|
|
990
|
+
"--brief",
|
|
991
|
+
"--all",
|
|
992
|
+
"--clear",
|
|
993
|
+
"--watches"
|
|
841
994
|
]);
|
|
842
995
|
if (flags.some((f) => !known.has(f)))
|
|
843
996
|
return usage(`Unknown flag ${flags.find((f) => !known.has(f))}`);
|
|
@@ -949,7 +1102,7 @@ No run command: no PoC is stored for this finding.`);
|
|
|
949
1102
|
r.id,
|
|
950
1103
|
r.kind,
|
|
951
1104
|
r.status,
|
|
952
|
-
r.status
|
|
1105
|
+
runInProgress(r.status) ? runPhase(r) : r.finishedAt ?? ""
|
|
953
1106
|
])))
|
|
954
1107
|
io.out(l);
|
|
955
1108
|
return 0;
|
|
@@ -964,9 +1117,9 @@ No run command: no PoC is stored for this finding.`);
|
|
|
964
1117
|
const sleep = io.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
965
1118
|
const seen = new Map;
|
|
966
1119
|
for (;; ) {
|
|
967
|
-
const running = (await io.client.runs(id)).filter((r) => r.status
|
|
1120
|
+
const running = (await io.client.runs(id)).filter((r) => runInProgress(r.status));
|
|
968
1121
|
for (const r of running) {
|
|
969
|
-
const phase =
|
|
1122
|
+
const phase = runPhase(r);
|
|
970
1123
|
if (seen.get(r.id) !== phase) {
|
|
971
1124
|
seen.set(r.id, phase);
|
|
972
1125
|
io.out(`${r.kind} ${r.id}: ${phase}`);
|
|
@@ -1019,6 +1172,8 @@ No run command: no PoC is stored for this finding.`);
|
|
|
1019
1172
|
io.out(`Follow it with: sentinel runs ${r.planId}`);
|
|
1020
1173
|
return 0;
|
|
1021
1174
|
}
|
|
1175
|
+
case "import":
|
|
1176
|
+
return await importRepo(args.slice(1), flags, values, io, usage);
|
|
1022
1177
|
case "balance": {
|
|
1023
1178
|
const b = await io.client.billing();
|
|
1024
1179
|
if (json)
|
|
@@ -1051,6 +1206,39 @@ No run command: no PoC is stored for this finding.`);
|
|
|
1051
1206
|
case "directed":
|
|
1052
1207
|
case "tests":
|
|
1053
1208
|
return await ownerRun(cmd, args.slice(1), flags, io, usage);
|
|
1209
|
+
case "fuzz":
|
|
1210
|
+
return await fuzz(args.slice(1), flags, io, usage);
|
|
1211
|
+
case "latest":
|
|
1212
|
+
case "update":
|
|
1213
|
+
case "auto-plan":
|
|
1214
|
+
case "link":
|
|
1215
|
+
case "plan-linked":
|
|
1216
|
+
case "name":
|
|
1217
|
+
case "remove":
|
|
1218
|
+
case "ack":
|
|
1219
|
+
case "reopen":
|
|
1220
|
+
case "acks":
|
|
1221
|
+
case "notes":
|
|
1222
|
+
case "note":
|
|
1223
|
+
return await upkeep(cmd, args.slice(1), flags, values, io, usage);
|
|
1224
|
+
case "ask": {
|
|
1225
|
+
if (!id)
|
|
1226
|
+
return usage("sentinel ask needs a question, in quotes");
|
|
1227
|
+
const a = await io.client.ask(args.slice(1).join(" "));
|
|
1228
|
+
io.out(a.answer);
|
|
1229
|
+
if (a.sources.length)
|
|
1230
|
+
io.out(`Sources: ${a.sources.join(", ")}`);
|
|
1231
|
+
return 0;
|
|
1232
|
+
}
|
|
1233
|
+
case "projects":
|
|
1234
|
+
return await projects(args.slice(1), flags, values, io, usage);
|
|
1235
|
+
case "activity":
|
|
1236
|
+
case "functions":
|
|
1237
|
+
case "alert-preview":
|
|
1238
|
+
case "threshold":
|
|
1239
|
+
case "retry-history":
|
|
1240
|
+
case "mark-read":
|
|
1241
|
+
return await monitoringMore(cmd, args.slice(1), flags, values, io, usage);
|
|
1054
1242
|
case "watches":
|
|
1055
1243
|
case "rule":
|
|
1056
1244
|
case "mute":
|
|
@@ -1071,6 +1259,64 @@ No run command: no PoC is stored for this finding.`);
|
|
|
1071
1259
|
}
|
|
1072
1260
|
var usd = (n) => `$${n.toFixed(2)}`;
|
|
1073
1261
|
var RUN_NOTE = "A paid run from your balance, charged only if it finishes.";
|
|
1262
|
+
async function importRepo(pos, flags, values, io, usage) {
|
|
1263
|
+
const [url] = pos;
|
|
1264
|
+
if (!url)
|
|
1265
|
+
return usage("sentinel import needs a GitHub repo link");
|
|
1266
|
+
const only = values["--only"]?.split(",").map((n) => n.trim()).filter(Boolean);
|
|
1267
|
+
if (only && flags.includes("--all"))
|
|
1268
|
+
return usage("Use --only or --all, not both");
|
|
1269
|
+
const p = await io.client.previewImport(url);
|
|
1270
|
+
const picked = only ? only : p.contracts.filter((c) => flags.includes("--all") || c.suggested).map((c) => c.name);
|
|
1271
|
+
const missing = picked.filter((n) => !p.contracts.some((c) => c.name === n));
|
|
1272
|
+
if (missing.length)
|
|
1273
|
+
return usage(`Not in ${p.repo} at ${p.commit.slice(0, 7)}: ${missing.join(", ")}`);
|
|
1274
|
+
io.out(`${p.owner}/${p.repo}${p.root ? `/${p.root}` : ""} @ ${p.commit.slice(0, 7)} (${p.ref})`);
|
|
1275
|
+
for (const l of table([
|
|
1276
|
+
[" ", "NAME", "DEPS", "STATE", "SUGGESTED"],
|
|
1277
|
+
...p.contracts.map((c) => [
|
|
1278
|
+
picked.includes(c.name) ? "+" : " ",
|
|
1279
|
+
c.name,
|
|
1280
|
+
c.deps.join(",") || "—",
|
|
1281
|
+
c.state,
|
|
1282
|
+
c.suggested ? "yes" : `no (${c.why})`
|
|
1283
|
+
])
|
|
1284
|
+
]))
|
|
1285
|
+
io.out(l);
|
|
1286
|
+
if (p.cycle)
|
|
1287
|
+
io.out("These contracts call each other in a loop: listed in manifest order.");
|
|
1288
|
+
for (const s of p.skipped)
|
|
1289
|
+
io.out(`Skipped ${s.path}: ${s.reason}`);
|
|
1290
|
+
for (const d of p.mainnetMatch.deployers)
|
|
1291
|
+
io.out(`Live on mainnet under ${d.deployer}: ${d.files.map((f) => `${f.name} (${f.kind} match of ${f.contractId})`).join(", ")}`);
|
|
1292
|
+
if (picked.length === 0) {
|
|
1293
|
+
io.out("Nothing to import. Pick contracts with --only a,b or --all.");
|
|
1294
|
+
return 0;
|
|
1295
|
+
}
|
|
1296
|
+
const fresh = p.contracts.filter((c) => picked.includes(c.name) && c.state === "new").length;
|
|
1297
|
+
io.out(fresh === 0 ? `Importing ${picked.length}: no new plan, nothing to pay.` : p.cost.runUsdMicros === null ? `Importing ${picked.length}: ${fresh} new plan${fresh === 1 ? "" : "s"}. Billing isn't linked: the free daily plan limit applies.` : `Importing ${picked.length}: ${fresh} new plan${fresh === 1 ? "" : "s"}, ${usd(p.cost.runUsdMicros * fresh / 1e6)}, each charged only if it finishes.`);
|
|
1298
|
+
if (fresh > p.runsAtOnce)
|
|
1299
|
+
io.out(`Plans run ${p.runsAtOnce} at a time; the rest wait their turn (queued).`);
|
|
1300
|
+
const project = values["--project"];
|
|
1301
|
+
if (!flags.includes("--yes")) {
|
|
1302
|
+
const tail = [
|
|
1303
|
+
only ? `--only ${only.join(",")}` : flags.includes("--all") ? "--all" : "",
|
|
1304
|
+
project ? `--project ${project}` : ""
|
|
1305
|
+
].filter(Boolean).join(" ");
|
|
1306
|
+
io.out(`Run again with --yes to import: sentinel import ${url}${tail ? ` ${tail}` : ""} --yes`);
|
|
1307
|
+
return 0;
|
|
1308
|
+
}
|
|
1309
|
+
const r = await io.client.importRepo({ url, commit: p.commit, contracts: picked, project });
|
|
1310
|
+
io.out(`Project: ${r.projectId ?? "—"}`);
|
|
1311
|
+
for (const x of r.plans)
|
|
1312
|
+
io.out(`${x.name} ${x.status === "kept" ? "already in the project" : x.cached ? "already planned" : x.status === "queued" ? "queued" : "started"} ${x.planId ?? "—"}`);
|
|
1313
|
+
for (const x of r.notStarted ?? [])
|
|
1314
|
+
io.out(`${x.name} not started: ${x.error}`);
|
|
1315
|
+
const first = r.plans.find((x) => x.planId)?.planId;
|
|
1316
|
+
if (first)
|
|
1317
|
+
io.out(`Follow a plan with: sentinel runs ${first}`);
|
|
1318
|
+
return 0;
|
|
1319
|
+
}
|
|
1074
1320
|
async function ownerRun(cmd, pos, flags, io, usage) {
|
|
1075
1321
|
const [planId, second, third] = pos;
|
|
1076
1322
|
if (!planId)
|
|
@@ -1085,16 +1331,19 @@ async function ownerRun(cmd, pos, flags, io, usage) {
|
|
|
1085
1331
|
return usage("sentinel challenge needs a reason, in quotes");
|
|
1086
1332
|
if (cmd === "directed" && !second)
|
|
1087
1333
|
return usage("sentinel directed needs what to watch, in quotes");
|
|
1088
|
-
|
|
1089
|
-
|
|
1334
|
+
const fromRepo = flags.includes("--from-repo");
|
|
1335
|
+
const testPaths = cmd === "tests" ? pos.slice(1) : [];
|
|
1336
|
+
if (cmd === "tests" && fromRepo === testPaths.length > 0)
|
|
1337
|
+
return usage("sentinel tests needs --from-repo (a pre-launch plan's repo tests) or your test files");
|
|
1338
|
+
const files = testPaths.map((path) => ({ path, content: readLocal(io, path) }));
|
|
1090
1339
|
const willDo = {
|
|
1091
1340
|
reproduce: `Write and run a PoC for finding ${finding} in the airgapped sandbox (never touches mainnet).`,
|
|
1092
1341
|
replay: `Re-run finding ${finding}'s stored PoC in the airgapped sandbox.`,
|
|
1093
1342
|
challenge: `Re-verify finding ${finding} against your argument.`,
|
|
1094
1343
|
directed: "Re-plan this contract around what you asked to have watched.",
|
|
1095
|
-
tests: "Run the Clarinet suite from this pre-launch plan's repo in the airgapped sandbox."
|
|
1344
|
+
tests: fromRepo ? "Run the Clarinet suite from this pre-launch plan's repo in the airgapped sandbox." : `Run ${files.length} test file(s) against this plan's contract in the airgapped sandbox.`
|
|
1096
1345
|
}[cmd];
|
|
1097
|
-
const flagTail = cmd === "tests" ? " --from-repo" : "";
|
|
1346
|
+
const flagTail = cmd === "tests" ? fromRepo ? " --from-repo" : ` ${testPaths.join(" ")}` : "";
|
|
1098
1347
|
const quoted = (v) => v ? ` ${JSON.stringify(v)}` : "";
|
|
1099
1348
|
const argTail = {
|
|
1100
1349
|
reproduce: ` ${finding}`,
|
|
@@ -1114,7 +1363,7 @@ async function ownerRun(cmd, pos, flags, io, usage) {
|
|
|
1114
1363
|
replay: () => io.client.replay(planId, finding),
|
|
1115
1364
|
challenge: () => io.client.challenge(planId, finding, third),
|
|
1116
1365
|
directed: () => io.client.directed(planId, second),
|
|
1117
|
-
tests: () => io.client.testsFromRepo(planId)
|
|
1366
|
+
tests: () => fromRepo ? io.client.testsFromRepo(planId) : io.client.tests(planId, files)
|
|
1118
1367
|
}[cmd]();
|
|
1119
1368
|
io.out(`${r.existing ? "Already running" : "Started"}: ${r.planId}`);
|
|
1120
1369
|
return 0;
|
|
@@ -1276,6 +1525,571 @@ async function monitoring(cmd, pos, on, flags, values, io, usage) {
|
|
|
1276
1525
|
io.out(mute ? `Muted ${g.name} until ${mutedUntil}` : `Unmuted ${g.name}.`);
|
|
1277
1526
|
return 0;
|
|
1278
1527
|
}
|
|
1528
|
+
function readLocal(io, path) {
|
|
1529
|
+
try {
|
|
1530
|
+
return (io.readFile ?? ((p) => readFileSync(p, "utf8")))(path);
|
|
1531
|
+
} catch (e) {
|
|
1532
|
+
throw new SentinelError(0, `Can't read ${path}: ${e.message}`);
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
var SHA = /^[0-9a-f]{40}$/;
|
|
1536
|
+
var short = (sha) => sha.slice(0, 7);
|
|
1537
|
+
var onOff = (v) => v === "on" ? true : v === "off" ? false : null;
|
|
1538
|
+
async function fuzz(pos, flags, io, usage) {
|
|
1539
|
+
const [planId, file] = pos;
|
|
1540
|
+
if (!planId || !file)
|
|
1541
|
+
return usage("sentinel fuzz needs a plan id and an invariants file");
|
|
1542
|
+
const harness = readLocal(io, file);
|
|
1543
|
+
const check = await io.client.fuzzCheck(planId, harness);
|
|
1544
|
+
if (!check.ok) {
|
|
1545
|
+
io.err(`The invariants don't compile: ${check.message ?? "no detail"}`);
|
|
1546
|
+
return 1;
|
|
1547
|
+
}
|
|
1548
|
+
io.out("The invariants compile.");
|
|
1549
|
+
if (!flags.includes("--yes")) {
|
|
1550
|
+
io.out("Fuzz the contract against them in the airgapped sandbox (never touches mainnet).");
|
|
1551
|
+
io.out(RUN_NOTE);
|
|
1552
|
+
io.out(`Run again with --yes to start: sentinel fuzz ${planId} ${file} --yes`);
|
|
1553
|
+
return 0;
|
|
1554
|
+
}
|
|
1555
|
+
const r = await io.client.fuzz(planId, harness);
|
|
1556
|
+
io.out(`Started: ${r.planId}`);
|
|
1557
|
+
return 0;
|
|
1558
|
+
}
|
|
1559
|
+
async function findingTitle(io, planId, n, usage) {
|
|
1560
|
+
if (n === undefined || !/^\d+$/.test(n))
|
|
1561
|
+
return usage("It needs a plan id and a finding number");
|
|
1562
|
+
return (await io.client.finding(planId, Number(n))).title;
|
|
1563
|
+
}
|
|
1564
|
+
async function upkeep(cmd, pos, flags, values, io, usage) {
|
|
1565
|
+
const [planId, second, third] = pos;
|
|
1566
|
+
if (!planId)
|
|
1567
|
+
return usage(`sentinel ${cmd} needs a plan id`);
|
|
1568
|
+
const yes = flags.includes("--yes");
|
|
1569
|
+
switch (cmd) {
|
|
1570
|
+
case "latest": {
|
|
1571
|
+
const l = await io.client.latestCommit(planId);
|
|
1572
|
+
if (l.status === "unsupported")
|
|
1573
|
+
io.out("Only a plan made from a GitHub link follows newer commits.");
|
|
1574
|
+
else if (l.status === "current")
|
|
1575
|
+
io.out(`On the latest commit of ${l.ref}: ${short(l.commit)}.`);
|
|
1576
|
+
else if (l.status === "gone")
|
|
1577
|
+
io.out(`The file isn't on ${l.ref} anymore (head ${short(l.head)}).`);
|
|
1578
|
+
else {
|
|
1579
|
+
io.out(`${l.ahead} commit(s) behind ${l.ref}: ${short(l.commit)} → ${short(l.head)}. An update would be ${l.mode}${l.affected.length ? ` (${l.affected.join(", ")})` : ""}.`);
|
|
1580
|
+
io.out(l.free ? "Free: monitoring is on." : "Priced as a new plan: monitoring is off.");
|
|
1581
|
+
io.out(`Plan it with: sentinel update ${planId} --yes`);
|
|
1582
|
+
}
|
|
1583
|
+
return 0;
|
|
1584
|
+
}
|
|
1585
|
+
case "update": {
|
|
1586
|
+
const commit = values["--commit"];
|
|
1587
|
+
if (commit !== undefined && !SHA.test(commit))
|
|
1588
|
+
return usage("--commit takes a full 40-character sha");
|
|
1589
|
+
const willDo = `Plan ${commit ? `commit ${short(commit)}` : "the branch's latest commit"}; the new plan replaces this one when it finishes.`;
|
|
1590
|
+
if (previewOnly(io, flags, willDo, "Free while monitoring is on, else a paid plan."))
|
|
1591
|
+
return 0;
|
|
1592
|
+
const r = await io.client.updatePlan(planId, commit);
|
|
1593
|
+
io.out(`${r.existing ? "Already running" : "Started"}: ${r.planId}${r.mode ? ` (${r.mode})` : ""}`);
|
|
1594
|
+
return 0;
|
|
1595
|
+
}
|
|
1596
|
+
case "auto-plan": {
|
|
1597
|
+
const on = onOff(second);
|
|
1598
|
+
if (on === null)
|
|
1599
|
+
return usage("sentinel auto-plan takes on or off");
|
|
1600
|
+
const willDo = on ? "Plan this on its own once every contract it waits on is deployed (a paid plan)." : "Don't plan this on its own when its contracts deploy.";
|
|
1601
|
+
if (previewOnly(io, flags, willDo, "Takes effect now."))
|
|
1602
|
+
return 0;
|
|
1603
|
+
await io.client.setAutoPlan(planId, on);
|
|
1604
|
+
io.out(`Auto-plan is ${on ? "on" : "off"}.`);
|
|
1605
|
+
return 0;
|
|
1606
|
+
}
|
|
1607
|
+
case "link": {
|
|
1608
|
+
const clear = flags.includes("--clear");
|
|
1609
|
+
if (!second || clear === Boolean(third))
|
|
1610
|
+
return usage("sentinel link needs a plan id, the contract it waits on, and a link or --clear");
|
|
1611
|
+
const willDo = clear ? `Clear the code linked for ${second}.` : `Link ${third} as the code of ${second} (a repo link also fills the others it holds).`;
|
|
1612
|
+
if (previewOnly(io, flags, willDo, "Takes effect now."))
|
|
1613
|
+
return 0;
|
|
1614
|
+
const r = await io.client.setLink(planId, second, clear ? null : third);
|
|
1615
|
+
io.out(`Linked: ${r.links.map((l) => l.id).join(", ") || "nothing"}`);
|
|
1616
|
+
return 0;
|
|
1617
|
+
}
|
|
1618
|
+
case "plan-linked": {
|
|
1619
|
+
if (!yes) {
|
|
1620
|
+
io.out("Plan this contract now, with the linked code loaded at its real principals.");
|
|
1621
|
+
io.out(RUN_NOTE);
|
|
1622
|
+
io.out(`Run again with --yes to start: sentinel plan-linked ${planId} --yes`);
|
|
1623
|
+
return 0;
|
|
1624
|
+
}
|
|
1625
|
+
const r = await io.client.planLinked(planId);
|
|
1626
|
+
io.out(`Started: ${r.planId}`);
|
|
1627
|
+
return 0;
|
|
1628
|
+
}
|
|
1629
|
+
case "name": {
|
|
1630
|
+
if (second === undefined)
|
|
1631
|
+
return usage('sentinel name needs a plan id and a name ("" clears it)');
|
|
1632
|
+
const willDo = second.trim() ? `Call this contract "${second.trim()}".` : "Clear your name for this contract.";
|
|
1633
|
+
if (previewOnly(io, flags, willDo, "Takes effect now."))
|
|
1634
|
+
return 0;
|
|
1635
|
+
const r = await io.client.renameContract(planId, second);
|
|
1636
|
+
io.out(r.name ? `Named: ${r.name}` : "Name cleared.");
|
|
1637
|
+
return 0;
|
|
1638
|
+
}
|
|
1639
|
+
case "remove": {
|
|
1640
|
+
const p = await io.client.plan(planId);
|
|
1641
|
+
const name = contractName(p.contractId);
|
|
1642
|
+
const confirm = values["--confirm"];
|
|
1643
|
+
if (confirm === undefined)
|
|
1644
|
+
return usage(`sentinel remove needs --confirm ${name} (the contract's name, typed)`);
|
|
1645
|
+
const willDo = `Take ${p.contractId} off your account: its monitoring, alerts and claim go.`;
|
|
1646
|
+
if (previewOnly(io, flags, willDo, "Can't be undone."))
|
|
1647
|
+
return 0;
|
|
1648
|
+
const r = await io.client.removeContract(planId, confirm);
|
|
1649
|
+
io.out(`Removed ${p.contractId}${r.monitoringOff ? "; monitoring is off" : ""}.`);
|
|
1650
|
+
return 0;
|
|
1651
|
+
}
|
|
1652
|
+
case "ack":
|
|
1653
|
+
case "reopen": {
|
|
1654
|
+
const title = await findingTitle(io, planId, second, usage);
|
|
1655
|
+
if (typeof title === "number")
|
|
1656
|
+
return title;
|
|
1657
|
+
if (cmd === "reopen") {
|
|
1658
|
+
if (previewOnly(io, flags, `Put "${title}" back on the list.`, "Takes effect now."))
|
|
1659
|
+
return 0;
|
|
1660
|
+
await io.client.reopen(planId, Number(second), title);
|
|
1661
|
+
io.out(`Reopened: ${title}`);
|
|
1662
|
+
return 0;
|
|
1663
|
+
}
|
|
1664
|
+
if (third !== "accepted" && third !== "fixed" && third !== "disagree")
|
|
1665
|
+
return usage("sentinel ack takes a reason: accepted, fixed or disagree");
|
|
1666
|
+
const note = values["--note"] ?? null;
|
|
1667
|
+
const willDo = `Label "${title}" ${third}${note ? `, noting: ${note}` : ""}. It changes no verdict.`;
|
|
1668
|
+
if (previewOnly(io, flags, willDo, "Your team sees it now."))
|
|
1669
|
+
return 0;
|
|
1670
|
+
const r = await io.client.acknowledge(planId, Number(second), title, third, note);
|
|
1671
|
+
io.out(`Labelled ${third}: ${title}`);
|
|
1672
|
+
if (r.watchId)
|
|
1673
|
+
io.out(`A watch on it is suggested (${r.watchId}): turn it on to keep watching.`);
|
|
1674
|
+
return 0;
|
|
1675
|
+
}
|
|
1676
|
+
case "acks": {
|
|
1677
|
+
const events = await io.client.ackHistory(planId);
|
|
1678
|
+
if (events.length === 0)
|
|
1679
|
+
io.out("No acknowledgements yet.");
|
|
1680
|
+
for (const l of table(events.map((e) => [e.at, e.action, e.reason, e.by ?? "Sentinel", e.title, e.note])))
|
|
1681
|
+
io.out(l);
|
|
1682
|
+
return 0;
|
|
1683
|
+
}
|
|
1684
|
+
case "notes": {
|
|
1685
|
+
if (second === undefined || !/^\d+$/.test(second))
|
|
1686
|
+
return usage("sentinel notes needs a plan id and a finding number");
|
|
1687
|
+
const notes = await io.client.notes(planId, Number(second));
|
|
1688
|
+
if (notes.length === 0)
|
|
1689
|
+
io.out("No notes yet.");
|
|
1690
|
+
for (const n of notes)
|
|
1691
|
+
io.out(`${n.at} ${n.system ? "Sentinel" : n.by ?? ""}
|
|
1692
|
+
${n.body}`);
|
|
1693
|
+
return 0;
|
|
1694
|
+
}
|
|
1695
|
+
default: {
|
|
1696
|
+
const title = await findingTitle(io, planId, second, usage);
|
|
1697
|
+
if (typeof title === "number")
|
|
1698
|
+
return title;
|
|
1699
|
+
if (!third?.trim())
|
|
1700
|
+
return usage("sentinel note needs the note, in quotes");
|
|
1701
|
+
if (previewOnly(io, flags, `Add a note to "${title}": ${third.trim()}`, "Your team sees it now."))
|
|
1702
|
+
return 0;
|
|
1703
|
+
await io.client.addNote(planId, Number(second), title, third);
|
|
1704
|
+
io.out("Noted.");
|
|
1705
|
+
return 0;
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
function pickIntent(intents, name) {
|
|
1710
|
+
const q = name.trim().toLowerCase();
|
|
1711
|
+
const hit = intents.find((i) => i.id === name) ?? intents.find((i) => i.title.toLowerCase() === q);
|
|
1712
|
+
if (hit)
|
|
1713
|
+
return hit;
|
|
1714
|
+
throw new SubjectError(`No watch called "${name}". Watches: ${intents.map((i) => i.title).join(", ") || "none yet"}`, intents.map((i) => i.title));
|
|
1715
|
+
}
|
|
1716
|
+
function windowOf(values) {
|
|
1717
|
+
const w = values["--window"];
|
|
1718
|
+
if (w === undefined)
|
|
1719
|
+
return;
|
|
1720
|
+
return w === "7" ? 7 : w === "90" ? 90 : null;
|
|
1721
|
+
}
|
|
1722
|
+
function printActivity(io, title, a) {
|
|
1723
|
+
io.out(`${title}: ${a.counts.total} event(s), ${a.counts.over} over a rule's line, last ${a.window} days${a.history === "ready" ? "" : ` (history ${a.history})`}`);
|
|
1724
|
+
for (const l of table(a.events.map((e) => [
|
|
1725
|
+
e.t,
|
|
1726
|
+
e.fn,
|
|
1727
|
+
e.value !== null && a.unit ? `${toUnits(e.value, a.unit.decimals)} ${a.unit.symbol}` : e.value,
|
|
1728
|
+
e.caller,
|
|
1729
|
+
e.result
|
|
1730
|
+
])))
|
|
1731
|
+
io.out(l);
|
|
1732
|
+
}
|
|
1733
|
+
async function monitoringMore(cmd, pos, flags, values, io, usage) {
|
|
1734
|
+
const [first, second, third] = pos;
|
|
1735
|
+
const json = flags.includes("--json");
|
|
1736
|
+
if (cmd === "mark-read") {
|
|
1737
|
+
const all = flags.includes("--all");
|
|
1738
|
+
if (all === Boolean(first))
|
|
1739
|
+
return usage("sentinel mark-read needs an alert id or --all");
|
|
1740
|
+
if (previewOnly(io, flags, all ? "Mark every alert read." : `Mark alert ${first} read.`, ""))
|
|
1741
|
+
return 0;
|
|
1742
|
+
if (all)
|
|
1743
|
+
io.out(`Marked ${(await io.client.readAllAlerts()).count} read.`);
|
|
1744
|
+
else {
|
|
1745
|
+
await io.client.readAlert(first);
|
|
1746
|
+
io.out("Marked read.");
|
|
1747
|
+
}
|
|
1748
|
+
return 0;
|
|
1749
|
+
}
|
|
1750
|
+
if (!first)
|
|
1751
|
+
return usage(`sentinel ${cmd} needs a plan id`);
|
|
1752
|
+
const planId = first;
|
|
1753
|
+
switch (cmd) {
|
|
1754
|
+
case "activity": {
|
|
1755
|
+
if (!second)
|
|
1756
|
+
return usage("sentinel activity needs a plan id and a watch (its title or id)");
|
|
1757
|
+
const window = windowOf(values);
|
|
1758
|
+
if (window === null)
|
|
1759
|
+
return usage("--window takes 7 or 90");
|
|
1760
|
+
const intent = pickIntent((await io.client.watches(planId)).intents, second);
|
|
1761
|
+
const a = await io.client.watchActivity(planId, intent.id, { window });
|
|
1762
|
+
if (json)
|
|
1763
|
+
io.out(JSON.stringify(a, null, 2));
|
|
1764
|
+
else
|
|
1765
|
+
printActivity(io, intent.title, a);
|
|
1766
|
+
return 0;
|
|
1767
|
+
}
|
|
1768
|
+
case "functions": {
|
|
1769
|
+
if (second) {
|
|
1770
|
+
const r = await io.client.functionActivity(planId, second);
|
|
1771
|
+
if (json)
|
|
1772
|
+
io.out(JSON.stringify(r, null, 2));
|
|
1773
|
+
else if (!r.collected)
|
|
1774
|
+
io.out(`Sentinel is still reading ${second}'s history. Try again in a few minutes.`);
|
|
1775
|
+
else
|
|
1776
|
+
printActivity(io, `${second} (if watched)`, r.activity);
|
|
1777
|
+
return 0;
|
|
1778
|
+
}
|
|
1779
|
+
const fns = await io.client.functions(planId);
|
|
1780
|
+
if (json)
|
|
1781
|
+
io.out(JSON.stringify(fns, null, 2));
|
|
1782
|
+
else
|
|
1783
|
+
for (const l of table([
|
|
1784
|
+
["FUNCTION", "DOES", "WATCH", "DESCRIPTION"],
|
|
1785
|
+
...fns.map((f) => [f.fn, f.consequence, f.intentId ?? "not watched", f.description])
|
|
1786
|
+
]))
|
|
1787
|
+
io.out(l);
|
|
1788
|
+
return 0;
|
|
1789
|
+
}
|
|
1790
|
+
case "alert-preview": {
|
|
1791
|
+
const example = values["--example"];
|
|
1792
|
+
if (example !== undefined && example !== "money_out" && example !== "finding")
|
|
1793
|
+
return usage("--example takes money_out or finding");
|
|
1794
|
+
const p = await io.client.alertPreview(planId, example);
|
|
1795
|
+
if (json)
|
|
1796
|
+
io.out(JSON.stringify(p, null, 2));
|
|
1797
|
+
else
|
|
1798
|
+
io.out(`${p.message.subject}
|
|
1799
|
+
|
|
1800
|
+
${p.message.text}`);
|
|
1801
|
+
return 0;
|
|
1802
|
+
}
|
|
1803
|
+
case "threshold": {
|
|
1804
|
+
if (!second)
|
|
1805
|
+
return usage("sentinel threshold needs a plan id and an asset (its symbol)");
|
|
1806
|
+
const ctx = await watchContext(io.client, planId);
|
|
1807
|
+
const g = resolveSubject(ctx.watches.filter((w) => w.subject.kind === "asset"), second);
|
|
1808
|
+
const subject = g.watches[0].subject;
|
|
1809
|
+
if (subject.kind !== "asset")
|
|
1810
|
+
return usage(`${second} isn't an asset`);
|
|
1811
|
+
const units = g.watches[0].units;
|
|
1812
|
+
const inUnits = (base) => units ? `${toUnits(base, units.decimals)} ${units.symbol}` : `${base} base units`;
|
|
1813
|
+
const { threshold } = await io.client.threshold(planId, subject.asset);
|
|
1814
|
+
io.out(`${g.name}: ${threshold.amount === null ? "every outflow alerts" : `alerts over ${inUnits(threshold.amount)}`}${threshold.confirmed ? "" : " (not confirmed yet)"}`);
|
|
1815
|
+
if (third === undefined)
|
|
1816
|
+
return 0;
|
|
1817
|
+
if (!/^\d{1,30}$/.test(third))
|
|
1818
|
+
return usage("The amount is in base units, digits only");
|
|
1819
|
+
if (previewOnly(io, flags, `Alert on ${g.name} outflows over ${inUnits(third)}.`, "Live now."))
|
|
1820
|
+
return 0;
|
|
1821
|
+
await io.client.setThreshold(planId, subject.asset, third);
|
|
1822
|
+
io.out(`${g.name} alerts over ${inUnits(third)}.`);
|
|
1823
|
+
return 0;
|
|
1824
|
+
}
|
|
1825
|
+
default: {
|
|
1826
|
+
if (previewOnly(io, flags, "Read the history of this plan's watches again.", "Starts now."))
|
|
1827
|
+
return 0;
|
|
1828
|
+
await io.client.retryHistory(planId);
|
|
1829
|
+
io.out("Reading again; activity fills in over the next few minutes.");
|
|
1830
|
+
return 0;
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
async function projects(pos, flags, values, io, usage) {
|
|
1835
|
+
const [sub, id, second, third] = pos;
|
|
1836
|
+
const json = flags.includes("--json");
|
|
1837
|
+
if (!sub) {
|
|
1838
|
+
const r = await io.client.projects();
|
|
1839
|
+
if (json) {
|
|
1840
|
+
io.out(JSON.stringify(r, null, 2));
|
|
1841
|
+
return 0;
|
|
1842
|
+
}
|
|
1843
|
+
for (const l of table([
|
|
1844
|
+
["PROJECT", "CONTRACTS", "MONITORING", "FINDINGS", "ALERTS 7D", "ID"],
|
|
1845
|
+
...r.projects.map((p) => [
|
|
1846
|
+
p.name,
|
|
1847
|
+
p.contracts.length,
|
|
1848
|
+
p.status,
|
|
1849
|
+
p.findings,
|
|
1850
|
+
p.alerts7d,
|
|
1851
|
+
p.id
|
|
1852
|
+
])
|
|
1853
|
+
]))
|
|
1854
|
+
io.out(l);
|
|
1855
|
+
const nameOf = (pid) => r.projects.find((p) => p.id === pid)?.name ?? pid;
|
|
1856
|
+
for (const s of r.suggestions)
|
|
1857
|
+
io.out(`Suggested: merge ${nameOf(s.from)} into ${nameOf(s.into)} (${s.evidence.kind}). Answer with: sentinel projects suggestion ${s.from} ${s.into} accept|dismiss`);
|
|
1858
|
+
return 0;
|
|
1859
|
+
}
|
|
1860
|
+
const subs = [
|
|
1861
|
+
"show",
|
|
1862
|
+
"overview",
|
|
1863
|
+
"add",
|
|
1864
|
+
"rename",
|
|
1865
|
+
"merge",
|
|
1866
|
+
"suggestion",
|
|
1867
|
+
"move",
|
|
1868
|
+
"remove-contract",
|
|
1869
|
+
"remove",
|
|
1870
|
+
"watches",
|
|
1871
|
+
"activity",
|
|
1872
|
+
"changes",
|
|
1873
|
+
"destinations",
|
|
1874
|
+
"monitoring"
|
|
1875
|
+
];
|
|
1876
|
+
if (!subs.includes(sub))
|
|
1877
|
+
return usage(`Unknown command projects ${sub}`);
|
|
1878
|
+
if (!id)
|
|
1879
|
+
return usage(`sentinel projects ${sub} needs a project id (sentinel projects lists them)`);
|
|
1880
|
+
const projectNamed = async (pid) => {
|
|
1881
|
+
const { projects: all } = await io.client.projects();
|
|
1882
|
+
const p = all.find((x) => x.id === pid);
|
|
1883
|
+
if (!p)
|
|
1884
|
+
throw new SentinelError(404, `No project ${pid}. Yours: ${all.map((x) => `${x.name} (${x.id})`).join(", ") || "none"}`);
|
|
1885
|
+
return p;
|
|
1886
|
+
};
|
|
1887
|
+
switch (sub) {
|
|
1888
|
+
case "show": {
|
|
1889
|
+
const p = await io.client.project(id);
|
|
1890
|
+
if (json) {
|
|
1891
|
+
io.out(JSON.stringify(p, null, 2));
|
|
1892
|
+
return 0;
|
|
1893
|
+
}
|
|
1894
|
+
io.out(`${p.name} ${p.status} findings ${p.findings} alerts 7d ${p.alerts7d}`);
|
|
1895
|
+
for (const l of table([
|
|
1896
|
+
["CONTRACT", "HOW", "ROLE", "PLAN"],
|
|
1897
|
+
...p.contracts.map((c) => [c.contractId, c.origin, c.role, c.planId])
|
|
1898
|
+
]))
|
|
1899
|
+
io.out(l);
|
|
1900
|
+
return 0;
|
|
1901
|
+
}
|
|
1902
|
+
case "overview":
|
|
1903
|
+
io.out(JSON.stringify(await io.client.projectOverview(id, { watches: flags.includes("--watches") }), null, 2));
|
|
1904
|
+
return 0;
|
|
1905
|
+
case "add": {
|
|
1906
|
+
if (!second)
|
|
1907
|
+
return usage("sentinel projects add needs a project id and a contract (SP….name or a GitHub link)");
|
|
1908
|
+
const into = values["--into"] ?? id;
|
|
1909
|
+
const p = await io.client.previewProjectContract(id, second);
|
|
1910
|
+
const s = p.suggestion;
|
|
1911
|
+
const target = into === "new" ? "a new project" : into;
|
|
1912
|
+
io.out(`${p.contractId} goes into ${target}.`);
|
|
1913
|
+
if (s.project !== into && s.kind !== "none")
|
|
1914
|
+
io.out(`Sentinel suggests ${s.project === "new" ? "a new project" : s.project} (${s.kind}${s.with ? ` via ${s.with}` : ""}): add --into ${s.project} to put it there.`);
|
|
1915
|
+
const b = await io.client.billing();
|
|
1916
|
+
io.out(b.prices ? `A new plan costs ${usd(b.prices.run)}, charged only if it finishes.` : "Billing isn't linked: the free daily plan limit applies.");
|
|
1917
|
+
if (!flags.includes("--yes")) {
|
|
1918
|
+
io.out(`Run again with --yes to add it: sentinel projects add ${id} ${second}${values["--into"] ? ` --into ${into}` : ""} --yes`);
|
|
1919
|
+
return 0;
|
|
1920
|
+
}
|
|
1921
|
+
const r = await io.client.addProjectContract(id, second, into === id ? {} : { project: into });
|
|
1922
|
+
io.out(`${r.cached ? "Already planned" : "Started"}: ${r.planId} in project ${r.projectId}`);
|
|
1923
|
+
return 0;
|
|
1924
|
+
}
|
|
1925
|
+
case "rename": {
|
|
1926
|
+
if (!second?.trim())
|
|
1927
|
+
return usage("sentinel projects rename needs a project id and a name");
|
|
1928
|
+
if (previewOnly(io, flags, `Rename ${id} to "${second.trim()}".`, "Takes effect now."))
|
|
1929
|
+
return 0;
|
|
1930
|
+
io.out(`Renamed: ${(await io.client.renameProject(id, second)).name}`);
|
|
1931
|
+
return 0;
|
|
1932
|
+
}
|
|
1933
|
+
case "merge": {
|
|
1934
|
+
if (!second)
|
|
1935
|
+
return usage("sentinel projects merge needs a project id and the project to merge it into");
|
|
1936
|
+
const [from, to] = [await projectNamed(id), await projectNamed(second)];
|
|
1937
|
+
const willDo = `Move every contract of ${from.name} (${from.contracts.length}) into ${to.name}; ${from.name} goes. Monitoring, watches and verification stay as they are.`;
|
|
1938
|
+
if (previewOnly(io, flags, willDo, "Takes effect now."))
|
|
1939
|
+
return 0;
|
|
1940
|
+
const r = await io.client.mergeProject(id, second);
|
|
1941
|
+
io.out(`Merged ${r.contracts} contract(s) into ${to.name}.`);
|
|
1942
|
+
return 0;
|
|
1943
|
+
}
|
|
1944
|
+
case "suggestion": {
|
|
1945
|
+
const accept = third === "accept" ? true : third === "dismiss" ? false : null;
|
|
1946
|
+
if (!second || accept === null)
|
|
1947
|
+
return usage("sentinel projects suggestion needs <from> <into> accept|dismiss");
|
|
1948
|
+
const r = await io.client.projects();
|
|
1949
|
+
if (!r.suggestions.some((s) => s.from === id && s.into === second))
|
|
1950
|
+
throw new SentinelError(404, `No open suggestion to merge ${id} into ${second}.`);
|
|
1951
|
+
const nameOf = (pid) => r.projects.find((p) => p.id === pid)?.name ?? pid;
|
|
1952
|
+
const willDo = accept ? `Merge ${nameOf(id)} into ${nameOf(second)}: every contract moves.` : `Dismiss the suggestion to merge ${nameOf(id)} into ${nameOf(second)}.`;
|
|
1953
|
+
if (previewOnly(io, flags, willDo, "Takes effect now."))
|
|
1954
|
+
return 0;
|
|
1955
|
+
await io.client.answerMergeSuggestion(id, second, accept);
|
|
1956
|
+
io.out(accept ? "Merged." : "Dismissed.");
|
|
1957
|
+
return 0;
|
|
1958
|
+
}
|
|
1959
|
+
case "move": {
|
|
1960
|
+
if (!second || !third)
|
|
1961
|
+
return usage("sentinel projects move needs a project id, a contract and where to (a project id or new)");
|
|
1962
|
+
const to = third === "new" ? "a new project named after it" : (await projectNamed(third)).name;
|
|
1963
|
+
if (previewOnly(io, flags, `Move ${second} to ${to}.`, "Takes effect now."))
|
|
1964
|
+
return 0;
|
|
1965
|
+
const r = await io.client.moveProjectContract(id, second, third);
|
|
1966
|
+
io.out(`Moved to ${r.projectId}${r.projectRemoved ? `; ${id} had nothing left and is gone` : ""}.`);
|
|
1967
|
+
return 0;
|
|
1968
|
+
}
|
|
1969
|
+
case "remove-contract": {
|
|
1970
|
+
if (!second)
|
|
1971
|
+
return usage("sentinel projects remove-contract needs a project id and a contract");
|
|
1972
|
+
const member = (await io.client.project(id)).contracts.find((c) => c.contractId === second);
|
|
1973
|
+
if (!member)
|
|
1974
|
+
throw new SentinelError(404, `${second} isn't in project ${id}.`);
|
|
1975
|
+
const added = member.origin === "added";
|
|
1976
|
+
const confirm = values["--confirm"];
|
|
1977
|
+
if (added && confirm === undefined)
|
|
1978
|
+
return usage(`${second} is one you added: removing it takes its monitoring, alerts and claim. Type its name: --confirm ${contractName(second)}`);
|
|
1979
|
+
const willDo = added ? `Remove ${second}: its monitoring, alerts and claim go.` : `Take ${second} out of the project (Sentinel found it; you never claimed it).`;
|
|
1980
|
+
if (previewOnly(io, flags, willDo, added ? "Can't be undone." : ""))
|
|
1981
|
+
return 0;
|
|
1982
|
+
const r = await io.client.removeProjectContract(id, second, added ? confirm : undefined);
|
|
1983
|
+
io.out(`Removed ${second}${r.projectRemoved ? `; ${id} had nothing left and is gone` : ""}.`);
|
|
1984
|
+
return 0;
|
|
1985
|
+
}
|
|
1986
|
+
case "remove": {
|
|
1987
|
+
const p = await io.client.project(id);
|
|
1988
|
+
const confirm = values["--confirm"];
|
|
1989
|
+
if (confirm === undefined)
|
|
1990
|
+
return usage(`sentinel projects remove needs --confirm "${p.name}" (the project's name, typed)`);
|
|
1991
|
+
const added = p.contracts.filter((c) => c.origin === "added").length;
|
|
1992
|
+
const willDo = `Remove ${p.name} and the ${added} contract(s) you added in it: their monitoring, alerts and claims go.`;
|
|
1993
|
+
if (previewOnly(io, flags, willDo, "Can't be undone."))
|
|
1994
|
+
return 0;
|
|
1995
|
+
const r = await io.client.removeProject(id, confirm);
|
|
1996
|
+
io.out(`Removed ${p.name} (${r.contracts} contract(s)).`);
|
|
1997
|
+
return 0;
|
|
1998
|
+
}
|
|
1999
|
+
case "watches": {
|
|
2000
|
+
const w = await io.client.projectWatches(id);
|
|
2001
|
+
if (json) {
|
|
2002
|
+
io.out(JSON.stringify(w, null, 2));
|
|
2003
|
+
return 0;
|
|
2004
|
+
}
|
|
2005
|
+
io.out(`${w.counts.on} of ${w.counts.total} on`);
|
|
2006
|
+
for (const i of w.intents)
|
|
2007
|
+
io.out([i.title, i.on ? "on" : "off", i.summary].join(" "));
|
|
2008
|
+
return 0;
|
|
2009
|
+
}
|
|
2010
|
+
case "activity": {
|
|
2011
|
+
if (!second)
|
|
2012
|
+
return usage("sentinel projects activity needs a project id and a watch (its title or id)");
|
|
2013
|
+
const window = windowOf(values);
|
|
2014
|
+
if (window === null)
|
|
2015
|
+
return usage("--window takes 7 or 90");
|
|
2016
|
+
const intent = pickIntent((await io.client.projectWatches(id)).intents, second);
|
|
2017
|
+
const a = await io.client.projectActivity(id, intent.id, {
|
|
2018
|
+
window,
|
|
2019
|
+
contract: values["--contract"]
|
|
2020
|
+
});
|
|
2021
|
+
if (json)
|
|
2022
|
+
io.out(JSON.stringify(a, null, 2));
|
|
2023
|
+
else
|
|
2024
|
+
printActivity(io, intent.title, a);
|
|
2025
|
+
return 0;
|
|
2026
|
+
}
|
|
2027
|
+
case "changes": {
|
|
2028
|
+
const limit = values["--limit"] === undefined ? undefined : Number(values["--limit"]);
|
|
2029
|
+
if (limit !== undefined && !(Number.isInteger(limit) && limit > 0))
|
|
2030
|
+
return usage("--limit takes a whole number above 0");
|
|
2031
|
+
const watchName = values["--watch"];
|
|
2032
|
+
const watch = watchName ? pickIntent((await io.client.projectWatches(id)).intents, watchName).id : undefined;
|
|
2033
|
+
const r = await io.client.projectChanges(id, {
|
|
2034
|
+
limit,
|
|
2035
|
+
cursor: values["--cursor"],
|
|
2036
|
+
watch,
|
|
2037
|
+
...flags.includes("--mutes") ? { filter: "mutes" } : {}
|
|
2038
|
+
});
|
|
2039
|
+
if (json) {
|
|
2040
|
+
io.out(JSON.stringify(r, null, 2));
|
|
2041
|
+
return 0;
|
|
2042
|
+
}
|
|
2043
|
+
if (!r.events.length)
|
|
2044
|
+
io.out("No changes yet.");
|
|
2045
|
+
for (const c of r.events)
|
|
2046
|
+
io.out([c.at, changeText(c), c.source ?? ""].join(" ").trimEnd());
|
|
2047
|
+
if (r.next)
|
|
2048
|
+
io.out(`More: --cursor ${r.next}`);
|
|
2049
|
+
return 0;
|
|
2050
|
+
}
|
|
2051
|
+
case "destinations": {
|
|
2052
|
+
const email = values["--email"];
|
|
2053
|
+
if (email !== undefined && onOff(email) === null)
|
|
2054
|
+
return usage("--email takes on or off");
|
|
2055
|
+
if (email === undefined && !("--webhook" in values) && !("--slack" in values))
|
|
2056
|
+
return usage("sentinel projects destinations needs --email, --webhook or --slack");
|
|
2057
|
+
const dest = {
|
|
2058
|
+
...email === undefined ? {} : { email: email === "on" },
|
|
2059
|
+
...values["--webhook"] ? { webhookUrl: values["--webhook"] } : {},
|
|
2060
|
+
...values["--slack"] ? { slackUrl: values["--slack"] } : {}
|
|
2061
|
+
};
|
|
2062
|
+
const places = [
|
|
2063
|
+
dest.email === true ? "email" : null,
|
|
2064
|
+
dest.webhookUrl ? "webhook" : null,
|
|
2065
|
+
dest.slackUrl ? "Slack" : null
|
|
2066
|
+
].filter(Boolean).join(", ") || "nowhere new";
|
|
2067
|
+
const willDo = `Alerts for every contract you added in ${id} go to: ${places}. A webhook or Slack URL left out is cleared.`;
|
|
2068
|
+
if (previewOnly(io, flags, willDo, "Takes effect now."))
|
|
2069
|
+
return 0;
|
|
2070
|
+
const r = await io.client.setProjectDestinations(id, dest);
|
|
2071
|
+
for (const c of r.contracts) {
|
|
2072
|
+
io.out(`${c.contractId}: ${c.result}${c.error ? ` (${c.error})` : ""}`);
|
|
2073
|
+
if (c.signingSecret)
|
|
2074
|
+
io.out(` webhook signing secret (shown once): ${c.signingSecret}`);
|
|
2075
|
+
}
|
|
2076
|
+
return 0;
|
|
2077
|
+
}
|
|
2078
|
+
default: {
|
|
2079
|
+
const on = onOff(second);
|
|
2080
|
+
if (on === null)
|
|
2081
|
+
return usage("sentinel projects monitoring takes on or off");
|
|
2082
|
+
const willDo = `Turn monitoring ${on ? "on" : "off"} for every contract you added in ${id}.`;
|
|
2083
|
+
if (previewOnly(io, flags, willDo, on ? "Live now; events are billed." : "Off now."))
|
|
2084
|
+
return 0;
|
|
2085
|
+
const r = await io.client.setProjectMonitoring(id, on);
|
|
2086
|
+
for (const c of r.contracts)
|
|
2087
|
+
io.out(`${c.contractId}: ${c.result}${c.error ? ` (${c.error})` : ""}`);
|
|
2088
|
+
io.out(`Project monitoring: ${r.status}`);
|
|
2089
|
+
return 0;
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
}
|
|
1279
2093
|
|
|
1280
2094
|
// cli/main.ts
|
|
1281
2095
|
function takeToken(argv) {
|
|
@@ -1311,7 +2125,7 @@ async function main() {
|
|
|
1311
2125
|
return 2;
|
|
1312
2126
|
}
|
|
1313
2127
|
const baseUrl = process.env.SENTINEL_API_URL;
|
|
1314
|
-
const clientFor = (token) => createClient({ token, baseUrl });
|
|
2128
|
+
const clientFor = (token) => createClient({ token, baseUrl, client: "cli" });
|
|
1315
2129
|
return run(argv, {
|
|
1316
2130
|
client: clientFor(credential?.token ?? ""),
|
|
1317
2131
|
out: console.log,
|