@secondlayer/sentinel 0.1.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/LICENSE +21 -0
- package/README.md +46 -0
- package/dist/cli.js +919 -0
- package/dist/credentials.d.ts +29 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +306 -0
- package/dist/mcp.js +20733 -0
- package/dist/sentinel-client.d.ts +384 -0
- package/package.json +26 -0
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client for the Sentinel v1 API (docs/api.md). Shared by the MCP server and the CLI. No dependencies:
|
|
3
|
+
* global fetch. Auth is one function, `authHeader`, so another credential can be swapped in later.
|
|
4
|
+
*/
|
|
5
|
+
export declare const DEFAULT_BASE_URL = "https://api.runsentinel.app";
|
|
6
|
+
export declare class SentinelError extends Error {
|
|
7
|
+
readonly status: number;
|
|
8
|
+
constructor(status: number, message: string);
|
|
9
|
+
}
|
|
10
|
+
export type RunEvent = {
|
|
11
|
+
at: number;
|
|
12
|
+
kind: string;
|
|
13
|
+
state?: string;
|
|
14
|
+
name?: string;
|
|
15
|
+
detail?: string;
|
|
16
|
+
};
|
|
17
|
+
export type Claim = {
|
|
18
|
+
contractId: string;
|
|
19
|
+
planId: string;
|
|
20
|
+
createdAt: string;
|
|
21
|
+
access: "verified" | "claimed";
|
|
22
|
+
status: string;
|
|
23
|
+
watches: number;
|
|
24
|
+
monitoring: string;
|
|
25
|
+
finishedAt: string | null;
|
|
26
|
+
evidence: {
|
|
27
|
+
tiles: string[];
|
|
28
|
+
worst: {
|
|
29
|
+
severity: string;
|
|
30
|
+
evidence: string;
|
|
31
|
+
} | null;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
export type RunSummary = {
|
|
35
|
+
id: string;
|
|
36
|
+
kind: string;
|
|
37
|
+
status: string;
|
|
38
|
+
startedAt: string;
|
|
39
|
+
finishedAt: string | null;
|
|
40
|
+
events: RunEvent[];
|
|
41
|
+
};
|
|
42
|
+
export type Finding = {
|
|
43
|
+
title: string;
|
|
44
|
+
headline?: string;
|
|
45
|
+
severity: string;
|
|
46
|
+
class: string;
|
|
47
|
+
verdict: string;
|
|
48
|
+
disposition: string;
|
|
49
|
+
poc?: string;
|
|
50
|
+
[field: string]: unknown;
|
|
51
|
+
};
|
|
52
|
+
/** A plan at the caller's access level: `access` is absent on the teaser. */
|
|
53
|
+
export type PlanView = {
|
|
54
|
+
view: string;
|
|
55
|
+
access?: "verified" | "claimed";
|
|
56
|
+
id: string;
|
|
57
|
+
contractId: string;
|
|
58
|
+
status: string;
|
|
59
|
+
report?: {
|
|
60
|
+
findings?: Finding[];
|
|
61
|
+
plan?: {
|
|
62
|
+
watches?: unknown[];
|
|
63
|
+
};
|
|
64
|
+
[field: string]: unknown;
|
|
65
|
+
} | null;
|
|
66
|
+
runs?: RunSummary[];
|
|
67
|
+
[field: string]: unknown;
|
|
68
|
+
};
|
|
69
|
+
export type Alert = {
|
|
70
|
+
id: string;
|
|
71
|
+
contractId: string;
|
|
72
|
+
level: string;
|
|
73
|
+
severity: string;
|
|
74
|
+
summary: string;
|
|
75
|
+
createdAt: string;
|
|
76
|
+
read: boolean;
|
|
77
|
+
[field: string]: unknown;
|
|
78
|
+
};
|
|
79
|
+
/** A watch's rule. Amounts are token base units, as digit strings. */
|
|
80
|
+
export type WatchRule = {
|
|
81
|
+
kind: "signature";
|
|
82
|
+
findingTitle: string;
|
|
83
|
+
severity: string;
|
|
84
|
+
asset?: string;
|
|
85
|
+
precondition?: string;
|
|
86
|
+
} | {
|
|
87
|
+
kind: "callerAllowlist";
|
|
88
|
+
allow: string[];
|
|
89
|
+
} | {
|
|
90
|
+
kind: "outflowSingle";
|
|
91
|
+
amount: string | null;
|
|
92
|
+
} | {
|
|
93
|
+
kind: "audit";
|
|
94
|
+
triggerClass: string;
|
|
95
|
+
} | {
|
|
96
|
+
kind: "outflowWindow";
|
|
97
|
+
amount: string;
|
|
98
|
+
blocks: number;
|
|
99
|
+
} | {
|
|
100
|
+
kind: "actor";
|
|
101
|
+
signals: ("newCaller" | "firstCall" | "roleChange")[];
|
|
102
|
+
allow: string[];
|
|
103
|
+
} | {
|
|
104
|
+
kind: "valueNewActor";
|
|
105
|
+
amount: string;
|
|
106
|
+
};
|
|
107
|
+
/** The rule kinds a caller can add or edit (the others come from a verified finding or governance). */
|
|
108
|
+
export type EditableRule = Exclude<WatchRule, {
|
|
109
|
+
kind: "signature" | "audit";
|
|
110
|
+
}>;
|
|
111
|
+
export type WatchSubject = {
|
|
112
|
+
kind: "fn";
|
|
113
|
+
fn: string;
|
|
114
|
+
} | {
|
|
115
|
+
kind: "asset";
|
|
116
|
+
asset: string;
|
|
117
|
+
};
|
|
118
|
+
export type WatchStatus = "suggested" | "live" | "paused";
|
|
119
|
+
export type Units = {
|
|
120
|
+
decimals: number;
|
|
121
|
+
symbol: string;
|
|
122
|
+
};
|
|
123
|
+
export type WatchDelivery = {
|
|
124
|
+
severity: string;
|
|
125
|
+
routes: string[];
|
|
126
|
+
cooldownBlocks: number;
|
|
127
|
+
mutedUntil: string | null;
|
|
128
|
+
};
|
|
129
|
+
/** One of the caller's own watches on a plan's contract, as `GET /plan/:id/watches` returns it. */
|
|
130
|
+
export type StoredWatch = {
|
|
131
|
+
id: string;
|
|
132
|
+
contractId: string;
|
|
133
|
+
subject: WatchSubject;
|
|
134
|
+
rule: WatchRule;
|
|
135
|
+
suggestedRule: WatchRule | null;
|
|
136
|
+
delivery: WatchDelivery;
|
|
137
|
+
provenance: {
|
|
138
|
+
kind: string;
|
|
139
|
+
ref: string;
|
|
140
|
+
note?: string;
|
|
141
|
+
}[];
|
|
142
|
+
status: WatchStatus;
|
|
143
|
+
title: string;
|
|
144
|
+
reason: string;
|
|
145
|
+
createdAt: string;
|
|
146
|
+
updatedAt: string;
|
|
147
|
+
units: Units | null;
|
|
148
|
+
};
|
|
149
|
+
export type WatchesView = {
|
|
150
|
+
watches: StoredWatch[];
|
|
151
|
+
events: {
|
|
152
|
+
id: string;
|
|
153
|
+
watchId: string;
|
|
154
|
+
action: "proposed" | "applied" | "rejected" | "muted" | "cooldown";
|
|
155
|
+
rule: WatchRule | null;
|
|
156
|
+
note: string | null;
|
|
157
|
+
createdAt: string;
|
|
158
|
+
}[];
|
|
159
|
+
seed: Record<string, {
|
|
160
|
+
historyCalls: number;
|
|
161
|
+
throughBlock: number | null;
|
|
162
|
+
seededAt: string;
|
|
163
|
+
}>;
|
|
164
|
+
};
|
|
165
|
+
/** What starting to monitor a contract (`POST /contracts/watch`) answers: the account's own set. */
|
|
166
|
+
export type StartedMonitoring = {
|
|
167
|
+
contractId: string;
|
|
168
|
+
planId: string;
|
|
169
|
+
status: "draft" | "requested" | "live";
|
|
170
|
+
watches: StoredWatchView[];
|
|
171
|
+
webhookUrl: string | null;
|
|
172
|
+
slackUrl: string | null;
|
|
173
|
+
emailAlerts: boolean;
|
|
174
|
+
hasSigningSecret: boolean;
|
|
175
|
+
};
|
|
176
|
+
type StoredWatchView = Pick<StoredWatch, "id" | "subject" | "rule" | "status" | "title">;
|
|
177
|
+
export type WatchChoice = {
|
|
178
|
+
watchId?: string;
|
|
179
|
+
fn: string;
|
|
180
|
+
on: boolean;
|
|
181
|
+
threshold?: string;
|
|
182
|
+
};
|
|
183
|
+
export type MonitorSettingsInput = {
|
|
184
|
+
deployer?: string;
|
|
185
|
+
watches?: WatchChoice[];
|
|
186
|
+
webhookUrl?: string;
|
|
187
|
+
slackUrl?: string;
|
|
188
|
+
emailAlerts?: boolean;
|
|
189
|
+
};
|
|
190
|
+
export type ParsedRules = {
|
|
191
|
+
rules: EditableRule[];
|
|
192
|
+
elsewhere: {
|
|
193
|
+
phrase: string;
|
|
194
|
+
subjectKey: string;
|
|
195
|
+
}[];
|
|
196
|
+
};
|
|
197
|
+
export type DeliveryResult = {
|
|
198
|
+
to: "email" | "webhook" | "slack";
|
|
199
|
+
ok: boolean;
|
|
200
|
+
detail: string;
|
|
201
|
+
};
|
|
202
|
+
export declare const subjectKey: (s: WatchSubject) => string;
|
|
203
|
+
/** The subject's display name: the fn name, or the token's symbol (never the raw token id). */
|
|
204
|
+
export declare function subjectName(s: WatchSubject, units: Units | null): string;
|
|
205
|
+
type HasSubject = {
|
|
206
|
+
subject: WatchSubject;
|
|
207
|
+
units: Units | null;
|
|
208
|
+
};
|
|
209
|
+
export type SubjectMatch<W extends HasSubject> = {
|
|
210
|
+
key: string;
|
|
211
|
+
name: string;
|
|
212
|
+
watches: W[];
|
|
213
|
+
};
|
|
214
|
+
/** Thrown when a name matches no subject, or more than one. `candidates` are the display names to choose from. */
|
|
215
|
+
export declare class SubjectError extends Error {
|
|
216
|
+
readonly candidates: string[];
|
|
217
|
+
constructor(message: string, candidates: string[]);
|
|
218
|
+
}
|
|
219
|
+
/** The watches grouped by subject, in first-seen order. */
|
|
220
|
+
export declare function groupSubjects<W extends HasSubject>(watches: W[]): SubjectMatch<W>[];
|
|
221
|
+
/**
|
|
222
|
+
* The watches on the subject `name` points at: its display name (fn name, token symbol, or the part
|
|
223
|
+
* after `::`), its subjectKey (`fn:…` / `asset:…`) or a full asset id, case-insensitive. A name that
|
|
224
|
+
* matches nothing, or more than one subject, throws a SubjectError listing the candidates.
|
|
225
|
+
*/
|
|
226
|
+
export declare function resolveSubject<W extends HasSubject>(watches: W[], name: string): SubjectMatch<W>;
|
|
227
|
+
/** `("1200000000000", 6)` → `"1,200,000"`. Anything that isn't a digit string comes back as it was. */
|
|
228
|
+
export declare function toUnits(base: string, decimals: number): string;
|
|
229
|
+
/** One line for a rule, in token units where the decimals are known. */
|
|
230
|
+
export declare function ruleSummary(rule: WatchRule, units: Units | null): string;
|
|
231
|
+
export type WatchKindTag = "finding" | "outflow" | "who-acts" | "governance" | "upgrade";
|
|
232
|
+
/** The plain tag a rule is listed under. */
|
|
233
|
+
export declare function kindTag(rule: WatchRule): WatchKindTag;
|
|
234
|
+
/** A subject's status from its rules: live if any is, else paused if all are, else suggested. */
|
|
235
|
+
export declare function subjectStatus(ws: {
|
|
236
|
+
status: WatchStatus;
|
|
237
|
+
}[]): WatchStatus;
|
|
238
|
+
/** Whether a rule's watch can be muted: a verified finding's and a governance review's can't. */
|
|
239
|
+
export declare const isMutableRule: (rule: WatchRule) => boolean;
|
|
240
|
+
/** The watch's mute end while it is in the future, else null. */
|
|
241
|
+
export declare function mutedUntilOf(w: {
|
|
242
|
+
delivery: WatchDelivery;
|
|
243
|
+
}, now?: number): string | null;
|
|
244
|
+
/** `POST /plan/preview`: what a paste would plan. Nothing starts and nothing is charged. */
|
|
245
|
+
export type PlanPreview = {
|
|
246
|
+
kind: "deployed" | "prelaunch";
|
|
247
|
+
contractId: string;
|
|
248
|
+
planId: string | null;
|
|
249
|
+
owned: boolean;
|
|
250
|
+
[field: string]: unknown;
|
|
251
|
+
};
|
|
252
|
+
/** `GET /billing`: balance and the prices the API charges (USD). Null until the account is linked. */
|
|
253
|
+
export type BillingView = {
|
|
254
|
+
enabled: boolean;
|
|
255
|
+
linked: boolean;
|
|
256
|
+
balanceUsd: number | null;
|
|
257
|
+
prices: {
|
|
258
|
+
run: number;
|
|
259
|
+
deepAudit: number;
|
|
260
|
+
monitoredEvent: number;
|
|
261
|
+
} | null;
|
|
262
|
+
unavailable: boolean;
|
|
263
|
+
[field: string]: unknown;
|
|
264
|
+
};
|
|
265
|
+
export type StartedRun = {
|
|
266
|
+
planId: string;
|
|
267
|
+
existing?: boolean;
|
|
268
|
+
};
|
|
269
|
+
export type ClientOptions = {
|
|
270
|
+
baseUrl?: string;
|
|
271
|
+
token: string;
|
|
272
|
+
fetch?: typeof globalThis.fetch;
|
|
273
|
+
};
|
|
274
|
+
export declare function createClient(opts: ClientOptions): {
|
|
275
|
+
me: () => Promise<Record<string, unknown>>;
|
|
276
|
+
plans: () => Promise<Claim[]>;
|
|
277
|
+
plan: (id: string) => Promise<PlanView>;
|
|
278
|
+
finding: (id: string, n: number) => Promise<Finding>;
|
|
279
|
+
runs: (id: string) => Promise<RunSummary[]>;
|
|
280
|
+
alerts: (contract?: string) => Promise<Alert[]>;
|
|
281
|
+
reverify: (id: string) => Promise<{
|
|
282
|
+
planId: string;
|
|
283
|
+
existing?: boolean;
|
|
284
|
+
}>;
|
|
285
|
+
reproduce: (id: string, finding: number) => Promise<{
|
|
286
|
+
planId: string;
|
|
287
|
+
existing?: boolean;
|
|
288
|
+
}>;
|
|
289
|
+
challenge: (id: string, finding: number, reason: string) => Promise<StartedRun>;
|
|
290
|
+
replay: (id: string, finding: number) => Promise<StartedRun>;
|
|
291
|
+
directed: (id: string, prompt: string) => Promise<StartedRun>;
|
|
292
|
+
/** Run the Clarinet suite from a pre-launch plan's repo in the sandbox. */
|
|
293
|
+
testsFromRepo: (id: string) => Promise<StartedRun>;
|
|
294
|
+
/** What a paste would plan. Writes and charges nothing. */
|
|
295
|
+
previewPlan: (contractId: string) => Promise<PlanPreview>;
|
|
296
|
+
/** Start a plan for a contract. Spends balance (charged only if it finishes), so preview first. */
|
|
297
|
+
createPlan: (contractId: string) => Promise<{
|
|
298
|
+
planId: string;
|
|
299
|
+
status: string;
|
|
300
|
+
cached: boolean;
|
|
301
|
+
}>;
|
|
302
|
+
billing: () => Promise<BillingView>;
|
|
303
|
+
watches: (planId: string) => Promise<WatchesView>;
|
|
304
|
+
/** Set a new rule for a watch. Applies now. */
|
|
305
|
+
setRule: (planId: string, watchId: string, rule: EditableRule) => Promise<{
|
|
306
|
+
watch: StoredWatch;
|
|
307
|
+
}>;
|
|
308
|
+
/** Add a rule kind to a subject Sentinel already watches. Applies now. */
|
|
309
|
+
addRule: (planId: string, subject: WatchSubject, rule: EditableRule) => Promise<{
|
|
310
|
+
watch: StoredWatch;
|
|
311
|
+
}>;
|
|
312
|
+
/** Turn watches on or off. Applies now. Every other choice is kept. */
|
|
313
|
+
setWatches: (planId: string, watchIds: string[], on: boolean) => Promise<{
|
|
314
|
+
monitoring: string | null;
|
|
315
|
+
}>;
|
|
316
|
+
/** Mute a watch until a time (at most 7 days out), or clear its mute with null. Takes effect at once. */
|
|
317
|
+
mute: (planId: string, watchId: string, mutedUntil: string | null) => Promise<{
|
|
318
|
+
watch: StoredWatch;
|
|
319
|
+
}>;
|
|
320
|
+
/** Mute the watch behind an alert for 1, 24 or 168 hours; 0 clears. */
|
|
321
|
+
muteAlert: (alertId: string, hours: 0 | 1 | 24 | 168) => Promise<{
|
|
322
|
+
mutedUntil: string | null;
|
|
323
|
+
watchTitle: string;
|
|
324
|
+
}>;
|
|
325
|
+
/** Read a sentence as rule proposals for one subject. Writes nothing. */
|
|
326
|
+
parseRule: (planId: string, subject: WatchSubject, text: string) => Promise<ParsedRules>;
|
|
327
|
+
monitorSettings: (planId: string, settings: MonitorSettingsInput) => Promise<{
|
|
328
|
+
monitoring: string | null;
|
|
329
|
+
signingSecret?: string;
|
|
330
|
+
}>;
|
|
331
|
+
/** Send a sample alert to email plus the saved (or given) webhook and Slack. */
|
|
332
|
+
testAlert: (planId: string, dest?: {
|
|
333
|
+
webhookUrl?: string;
|
|
334
|
+
slackUrl?: string;
|
|
335
|
+
emailAlerts?: boolean;
|
|
336
|
+
}) => Promise<{
|
|
337
|
+
results: DeliveryResult[];
|
|
338
|
+
}>;
|
|
339
|
+
/** Start monitoring a deployed contract (idempotent): its plan id is where to manage it. */
|
|
340
|
+
watchContract: (contractId: string) => Promise<StartedMonitoring | {
|
|
341
|
+
planId: string;
|
|
342
|
+
status: string;
|
|
343
|
+
}>;
|
|
344
|
+
/** Stop monitoring a plan's contract: your watches, alerts and settings for it are removed. */
|
|
345
|
+
stopMonitoring: (planId: string) => Promise<{
|
|
346
|
+
ok: boolean;
|
|
347
|
+
}>;
|
|
348
|
+
};
|
|
349
|
+
export type SentinelClient = ReturnType<typeof createClient>;
|
|
350
|
+
/** A mute end `hours` out, a minute inside the 7-day limit so clock skew can't tip it over. */
|
|
351
|
+
export declare const muteEnd: (hours: number) => string;
|
|
352
|
+
/** What a plan's watches are to this caller: the account's own set on that contract. */
|
|
353
|
+
export type WatchContext = {
|
|
354
|
+
planId: string;
|
|
355
|
+
contractId: string;
|
|
356
|
+
/** The plan's access level for this caller; a claimed one hides exploit detail. */
|
|
357
|
+
access: PlanView["access"];
|
|
358
|
+
watches: StoredWatch[];
|
|
359
|
+
};
|
|
360
|
+
/** The caller's watches for a plan. */
|
|
361
|
+
export declare function watchContext(client: SentinelClient, planId: string): Promise<WatchContext>;
|
|
362
|
+
export type SavedMonitor = {
|
|
363
|
+
status: string | null;
|
|
364
|
+
watches: {
|
|
365
|
+
watchId?: string;
|
|
366
|
+
fn: string;
|
|
367
|
+
on: boolean;
|
|
368
|
+
threshold?: string;
|
|
369
|
+
}[] | null;
|
|
370
|
+
webhookUrl: string | null;
|
|
371
|
+
slackUrl: string | null;
|
|
372
|
+
emailAlerts: boolean;
|
|
373
|
+
};
|
|
374
|
+
/** The account's saved monitoring settings (on the plan view). Refuses before monitoring is turned on. */
|
|
375
|
+
export declare function savedMonitor(client: SentinelClient, planId: string): Promise<SavedMonitor>;
|
|
376
|
+
export declare const savedChoices: (client: SentinelClient, planId: string) => Promise<{
|
|
377
|
+
watchId?: string;
|
|
378
|
+
fn: string;
|
|
379
|
+
on: boolean;
|
|
380
|
+
threshold?: string;
|
|
381
|
+
}[] | null>;
|
|
382
|
+
/** The plan id of a contract the account claims or monitors, from its own list. */
|
|
383
|
+
export declare function planOfContract(client: SentinelClient, contractId: string): Promise<string>;
|
|
384
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@secondlayer/sentinel",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Sentinel from a terminal or an agent: the CLI, an MCP server and a client for your Stacks contract monitoring.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": "https://runsentinel.app/docs",
|
|
7
|
+
"homepage": "https://runsentinel.app/docs",
|
|
8
|
+
"type": "module",
|
|
9
|
+
"engines": {
|
|
10
|
+
"node": ">=22"
|
|
11
|
+
},
|
|
12
|
+
"bin": {
|
|
13
|
+
"sentinel": "dist/cli.js",
|
|
14
|
+
"sentinel-mcp": "dist/mcp.js"
|
|
15
|
+
},
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"import": "./dist/index.js"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"files": ["dist", "README.md", "LICENSE"],
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public"
|
|
25
|
+
}
|
|
26
|
+
}
|