@flowsint/plugin-sdk 2.6.1 → 2.6.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/README.md +55 -358
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,374 +1,71 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @flowsint/plugin-sdk
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
TypeScript interface per Flowsint entity type. Types only.
|
|
3
|
+
⚠️ This is a package used for later versions of Flowsint (>v2.0). This exposes Flowsint types through a typescript interface. This will later be useful for creating local (desktop version) enrichers, and extend the application.
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
npm i -D @flowsint/plugin-sdk@<Flowsint version you target>
|
|
8
|
-
```
|
|
9
|
-
|
|
10
|
-
The package version is the Flowsint version. A plugin built against
|
|
11
|
-
`@flowsint/plugin-sdk@X.Y.Z` declares `"engines": { "flowsint": "^X.Y.Z" }`:
|
|
12
|
-
removing or renaming a spec field only ships in a major release. This
|
|
13
|
-
guarantee covers the published types only, not the app's runtime
|
|
14
|
-
handling of what a plugin returns.
|
|
15
|
-
|
|
16
|
-
A plugin is a directory dropped into Flowsint's plugins folder
|
|
17
|
-
(`~/Library/Application Support/Flowsint/plugins/` on macOS,
|
|
18
|
-
`%APPDATA%/Flowsint/plugins/` on Windows,
|
|
19
|
-
`~/.config/Flowsint/plugins/` on Linux), containing:
|
|
20
|
-
|
|
21
|
-
- `package.json` - see shape below.
|
|
22
|
-
- `README.md` - **required**. Shown in Settings -> Plugins ("README"
|
|
23
|
-
button, rendered as markdown). Cover: what the dependency is, how to
|
|
24
|
-
install it, and your inputs/outputs contract. The section below
|
|
25
|
-
shows the layout Flowsint's own bundled plugins use.
|
|
26
|
-
- Whatever `package.json`'s `"main"` points to. Your bundled entry
|
|
27
|
-
file, exporting `run` per the signature below.
|
|
28
|
-
- `node_modules/`. Only if your bundler couldn't inline a dependency
|
|
29
|
-
(e.g. a native addon).
|
|
30
|
-
|
|
31
|
-
Flowsint never runs `npm install` for you. Bundle your dependencies
|
|
32
|
-
(esbuild, tsup, whatever) before distributing - your plugin must work
|
|
33
|
-
by `require()` alone, on a machine with no package manager.
|
|
34
|
-
|
|
35
|
-
## package.json
|
|
36
|
-
|
|
37
|
-
```json
|
|
38
|
-
{
|
|
39
|
-
"name": "my-plugin",
|
|
40
|
-
"version": "1.0.0",
|
|
41
|
-
"main": "dist/index.js",
|
|
42
|
-
"author": "you",
|
|
43
|
-
"engines": { "flowsint": "^2.6.1" },
|
|
44
|
-
"flowsint": {
|
|
45
|
-
"category": "Domain",
|
|
46
|
-
"description": "What this does, one sentence.",
|
|
47
|
-
"inputs": { "type": "domain", "properties": [{ "name": "domain", "type": "string" }] },
|
|
48
|
-
"outputs": { "type": "ip", "properties": [{ "name": "address", "type": "string" }] },
|
|
49
|
-
"icon": "Globe",
|
|
50
|
-
"installUrl": "https://example.com/install-docs",
|
|
51
|
-
"params": [
|
|
52
|
-
{
|
|
53
|
-
"name": "LIMIT",
|
|
54
|
-
"type": "string",
|
|
55
|
-
"description": "Max results",
|
|
56
|
-
"default": "50",
|
|
57
|
-
"required": false
|
|
58
|
-
},
|
|
59
|
-
{
|
|
60
|
-
"name": "API_KEY",
|
|
61
|
-
"type": "vaultSecret",
|
|
62
|
-
"description": "Your API key",
|
|
63
|
-
"default": "",
|
|
64
|
-
"required": true
|
|
65
|
-
}
|
|
66
|
-
]
|
|
67
|
-
},
|
|
68
|
-
"dependencies": {}
|
|
69
|
-
}
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
- `name` is your plugin's id - must be unique across every installed
|
|
73
|
-
plugin. Two plugins with the same `name`: the one Flowsint finds
|
|
74
|
-
first (alphabetical by directory name) loads, the other is skipped
|
|
75
|
-
with an error visible in Settings → Plugins.
|
|
76
|
-
- `engines.flowsint` is a normal semver range, checked against the
|
|
77
|
-
running app version. Omit it to skip the check (not recommended).
|
|
78
|
-
- `flowsint.category` and `flowsint.inputs` are required. Everything
|
|
79
|
-
else in the `flowsint` key is optional.
|
|
80
|
-
- `flowsint.icon` is a [lucide-react](https://lucide.dev/icons/) icon
|
|
81
|
-
name.
|
|
82
|
-
- `flowsint.installUrl` - link to your dependency's install docs, shown
|
|
83
|
-
as a clickable link in Settings → Plugins. Omit it if your plugin has
|
|
84
|
-
no external dependency (pure Node/OS APIs only). This is static
|
|
85
|
-
metadata, unrelated to `init()`'s `fix` message below - see "Checking
|
|
86
|
-
a dependency."
|
|
87
|
-
- `flowsint.params` - optional. An array of
|
|
88
|
-
`{ name, type, description, default, required }` entries, shown
|
|
89
|
-
in the launch settings drawer before your plugin runs. A
|
|
90
|
-
`type: "vaultSecret"` entry is resolved against Flowsint's local
|
|
91
|
-
vault (the same store backing the chat feature's provider keys)
|
|
92
|
-
before your `run()` is called - you receive the decrypted secret
|
|
93
|
-
as a plain string in `params[name]` (or `null` if it's optional and
|
|
94
|
-
the user never set one), never a key id or reference.
|
|
95
|
-
`type: "select"` renders a dropdown of its
|
|
96
|
-
`options: [{ "label": "...", "value": "..." }]` (malformed entries
|
|
97
|
-
are dropped) and passes the picked `value`. `type: "date"` renders a
|
|
98
|
-
calendar and passes a `Date` at midnight UTC of the picked day
|
|
99
|
-
(`undefined` if left empty). `type: "number"` renders a numeric
|
|
100
|
-
input and passes a `number` (`undefined` if left empty).
|
|
101
|
-
`type: "boolean"` renders a switch and passes a `boolean` (`false`
|
|
102
|
-
if never touched). `type: "list"` renders a tags input and passes a
|
|
103
|
-
`string[]` (`[]` if empty). An invalid value fails the run before
|
|
104
|
-
`run()` is called. Any other `type` is passed through as the raw string the
|
|
105
|
-
user typed.
|
|
106
|
-
|
|
107
|
-
## `main`'s export
|
|
108
|
-
|
|
109
|
-
Two things your entry file can export - `run` is required, `init` is
|
|
110
|
-
optional, nothing else is read. The contract lives in
|
|
111
|
-
this package's `plugin.d.ts`, the one definition the app's plugin
|
|
112
|
-
runner also imports. Shown here without
|
|
113
|
-
its doc comments:
|
|
114
|
-
|
|
115
|
-
```ts
|
|
116
|
-
interface EnricherEntityInput {
|
|
117
|
-
type: string
|
|
118
|
-
properties: Record<string, unknown>
|
|
119
|
-
}
|
|
5
|
+
## Install
|
|
120
6
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
interface LocalEnricherEdge {
|
|
131
|
-
id: string
|
|
132
|
-
source: string
|
|
133
|
-
target: string
|
|
134
|
-
label: string
|
|
135
|
-
sources?: string[]
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
interface LocalEnricherEvent {
|
|
139
|
-
timestamp: number
|
|
140
|
-
endTimestamp?: number | null
|
|
141
|
-
title: string
|
|
142
|
-
description?: string | null
|
|
143
|
-
type?: string
|
|
144
|
-
confidence?: number | null
|
|
145
|
-
nodeIds?: string[] // ids of nodes in this same chunk
|
|
146
|
-
tags?: string[]
|
|
147
|
-
properties?: Record<string, unknown>
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
interface LocalEnricherFinding {
|
|
151
|
-
nodes: LocalEnricherNode[]
|
|
152
|
-
edges: LocalEnricherEdge[]
|
|
153
|
-
events?: LocalEnricherEvent[]
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
interface LocalEnricherLogger {
|
|
157
|
-
info(message: string): void
|
|
158
|
-
warn(message: string): void
|
|
159
|
-
error(message: string): void
|
|
160
|
-
success(message: string): void
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// An async generator (`async function* run`): each yielded finding is a
|
|
164
|
-
// chunk, merged into the graph as soon as it is yielded.
|
|
165
|
-
function run(
|
|
166
|
-
entities: EnricherEntityInput[],
|
|
167
|
-
logger: LocalEnricherLogger,
|
|
168
|
-
params: Record<string, unknown>,
|
|
169
|
-
signal: AbortSignal
|
|
170
|
-
): AsyncIterable<LocalEnricherFinding>
|
|
171
|
-
```
|
|
172
|
-
|
|
173
|
-
Yield one chunk per entity: the entity's node, what you found for it
|
|
174
|
-
and the edges between them. **An edge's `source` and `target` (and an
|
|
175
|
-
event's `nodeIds`) must be nodes of the same chunk.** When results are
|
|
176
|
-
shared across entities, yield everything once at the end instead.
|
|
177
|
-
Merging a chunk twice is a no-op.
|
|
178
|
-
|
|
179
|
-
`signal` aborts after one hour: Flowsint stops reading your chunks
|
|
180
|
-
and fails the run, keeping the chunks already merged. Pass it to
|
|
181
|
-
`spawn(cmd, args, { signal })` and `fetch(url, { signal })`, and call
|
|
182
|
-
`signal.throwIfAborted()` in a per-entity `catch` so an abort ends the
|
|
183
|
-
run instead of being logged as one failed entity.
|
|
184
|
-
|
|
185
|
-
`entities` are not narrowed per type: an input can be a user-defined
|
|
186
|
-
custom type. Check `entity.type` before reading `entity.properties`.
|
|
187
|
-
|
|
188
|
-
`params` mirrors your `flowsint.params` declaration by name - a
|
|
189
|
-
plugin with no `params` field in `package.json` still receives an
|
|
190
|
-
(empty) object, never `undefined`. See the `flowsint.params` bullet
|
|
191
|
-
above for what a `vaultSecret` entry resolves to.
|
|
192
|
-
|
|
193
|
-
```ts
|
|
194
|
-
type InitResult = { ok: true } | { ok: false; message: string; fix: string }
|
|
195
|
-
|
|
196
|
-
// Optional - omit entirely if your plugin has no runtime precondition
|
|
197
|
-
// to check. Called once at app boot and again whenever the user clicks
|
|
198
|
-
// "Reload plugins" in Settings → Plugins - never on a timer, never
|
|
199
|
-
// before a run. A missing/failing dependency keeps your plugin out of
|
|
200
|
-
// the enricher picker until init() resolves { ok: true }.
|
|
201
|
-
function init(): Promise<InitResult>
|
|
7
|
+
```sh
|
|
8
|
+
# pnpm
|
|
9
|
+
pnpm add @flowsint/plugin-sdk
|
|
10
|
+
# yarn
|
|
11
|
+
yarn add @flowsint/plugin-sdk
|
|
12
|
+
# npm
|
|
13
|
+
npm i @flowsint/plugin-sdk
|
|
202
14
|
```
|
|
203
15
|
|
|
204
|
-
|
|
205
|
-
`throw` surfaces as a failed run there too (chunks already yielded stay).
|
|
206
|
-
|
|
207
|
-
## Typing your plugin with `@flowsint/plugin-sdk`
|
|
208
|
-
|
|
209
|
-
Import the contract instead of copying it. Types only: use
|
|
210
|
-
`import type`, your bundler erases it and nothing ships at runtime.
|
|
16
|
+
## Quick start
|
|
211
17
|
|
|
212
18
|
```ts
|
|
19
|
+
import { randomUUID } from "node:crypto";
|
|
20
|
+
import { lookup } from "node:dns/promises";
|
|
213
21
|
import type {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
} from
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
(
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
(`;`, `|`, `` ` ``, `$()`) by construction: no shell ever parses the
|
|
243
|
-
strings you pass, the OS execs your binary directly with each array
|
|
244
|
-
entry as one literal argv token. Do not build commands by
|
|
245
|
-
concatenating strings into `exec()`; use `spawn()` with an argv array
|
|
246
|
-
every time.
|
|
247
|
-
|
|
248
|
-
That guarantee alone is not a reason to pass a param straight through
|
|
249
|
-
unchecked, though. A malformed value can still confuse your CLI's own
|
|
250
|
-
argument parser (a stray value swallowed as the wrong flag's argument,
|
|
251
|
-
a numeric flag rejecting a non-numeric string with a cryptic error) or
|
|
252
|
-
trigger something your plugin didn't intend (a flag with a very
|
|
253
|
-
different meaning than the one the param name suggests). Validate each
|
|
254
|
-
param before using it, and fail loud - `throw` before calling `spawn`
|
|
255
|
-
at all, not after - so the whole run aborts with a clear error instead
|
|
256
|
-
of silently doing the wrong thing:
|
|
257
|
-
|
|
258
|
-
```ts
|
|
259
|
-
const INT_RE = /^\d+$/
|
|
260
|
-
const NAME_LIST_RE = /^[A-Za-z0-9_-]+(,[A-Za-z0-9_-]+)*$/
|
|
261
|
-
|
|
262
|
-
function requireInt(name: string, v: unknown): string | undefined {
|
|
263
|
-
if (typeof v !== 'string' || v.trim() === '') return undefined
|
|
264
|
-
const s = v.trim()
|
|
265
|
-
if (!INT_RE.test(s))
|
|
266
|
-
throw new Error(`Invalid ${name}: expected a non-negative integer, got "${s}"`)
|
|
267
|
-
return s
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
function requireList(name: string, v: unknown, pattern: RegExp): string | undefined {
|
|
271
|
-
if (typeof v !== 'string' || v.trim() === '') return undefined
|
|
272
|
-
const s = v.trim()
|
|
273
|
-
if (!pattern.test(s))
|
|
274
|
-
throw new Error(`Invalid ${name}: "${s}" contains characters outside the allowed set`)
|
|
275
|
-
return s
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// usage: requireList('SOURCES', params.SOURCES, NAME_LIST_RE)
|
|
22
|
+
Domain,
|
|
23
|
+
Ip,
|
|
24
|
+
LocalEnricherRun,
|
|
25
|
+
LocalEnricherEdge,
|
|
26
|
+
} from "@flowsint/plugin-sdk";
|
|
27
|
+
|
|
28
|
+
export const run: LocalEnricherRun = async function* (entities, logger) {
|
|
29
|
+
for (const entity of entities) {
|
|
30
|
+
if (entity.type !== "domain") continue;
|
|
31
|
+
const domain: Domain = { domain: String(entity.properties.domain) };
|
|
32
|
+
const ip: Ip = { address: (await lookup(domain.domain)).address };
|
|
33
|
+
logger.info(`${domain.domain} resolves to ${ip.address}`);
|
|
34
|
+
const [domainId, ipId] = [randomUUID(), randomUUID()];
|
|
35
|
+
const relation: LocalEnricherEdge = {
|
|
36
|
+
id: randomUUID(),
|
|
37
|
+
source: domainId,
|
|
38
|
+
target: ipId,
|
|
39
|
+
label: "RESOLVES_TO",
|
|
40
|
+
};
|
|
41
|
+
yield {
|
|
42
|
+
nodes: [
|
|
43
|
+
{ id: domainId, nodeType: "domain", nodeProperties: domain },
|
|
44
|
+
{ id: ipId, nodeType: "ip", nodeProperties: ip },
|
|
45
|
+
],
|
|
46
|
+
edges: [relation],
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
};
|
|
279
50
|
```
|
|
280
51
|
|
|
281
|
-
|
|
282
|
-
`new URL(value)` - it throws on anything that isn't a real URL, which
|
|
283
|
-
is exactly the check you want, and it's already in Node's stdlib.
|
|
284
|
-
|
|
285
|
-
Build the whole argv array **once, before your entity loop**, not
|
|
286
|
-
inside the per-entity `try`/`catch` most plugins use around their
|
|
287
|
-
`spawn()` call. If a param is invalid, that's a configuration
|
|
288
|
-
mistake, not a search failure for one entity - it should abort the
|
|
289
|
-
whole run with a thrown error, not get silently caught and logged as
|
|
290
|
-
a per-entity warning while the rest of the batch runs on stale/wrong
|
|
291
|
-
settings.
|
|
52
|
+
Build it (`npm i -D @flowsint/plugin-sdk esbuild` first), then install it:
|
|
292
53
|
|
|
293
|
-
|
|
294
|
-
built from the same array you pass to `spawn`, never a re-derived
|
|
295
|
-
string) costs one line and pays for itself the first time a param
|
|
296
|
-
doesn't do what someone expected - the sketch's Logs panel then shows
|
|
297
|
-
precisely what ran, not just "started" or "failed."
|
|
54
|
+
## Entity types
|
|
298
55
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
`
|
|
303
|
-
|
|
304
|
-
comma-separated allowlisted lists, an exact-match set, a boolean
|
|
305
|
-
flag) in one file.
|
|
56
|
+
| Export | What it is |
|
|
57
|
+
| ------------------------------------------ | ---------------------------------------------------------------------- |
|
|
58
|
+
| `Domain`, `Ip`, `Email`, `Individual`, ... | One interface per Flowsint entity type: the shape of `nodeProperties`. |
|
|
59
|
+
| `FlowsintTypeMap` | Type key (`"domain"`, `"ip"`, ...) to its interface. |
|
|
60
|
+
| `FlowsintTypeKey` | Union of every type key. |
|
|
306
61
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
If your plugin shells out to an external tool - a CLI, a binary that
|
|
310
|
-
must already be on the user's `PATH` - check for it in `init()` instead
|
|
311
|
-
of letting `run()` fail with a bare "spawn ENOENT". There's no shared
|
|
312
|
-
helper to import (see "nothing else is read" above) - copy this
|
|
313
|
-
pattern into your own `src/index.ts`:
|
|
62
|
+
A node is checked against its `nodeType`:
|
|
314
63
|
|
|
315
64
|
```ts
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
resolve(code === 0 ? { ok: true } : { ok: false, message: `${bin} not found on PATH`, fix })
|
|
323
|
-
)
|
|
324
|
-
probe.on('error', () => resolve({ ok: false, message: `${bin} not found on PATH`, fix }))
|
|
325
|
-
})
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
export async function init(): Promise<InitResult> {
|
|
329
|
-
return checkBinaryOnPath(
|
|
330
|
-
'subfinder',
|
|
331
|
-
"install with `brew install subfinder` or see the plugin's README"
|
|
332
|
-
)
|
|
333
|
-
}
|
|
65
|
+
// Compile error: Ip has no `ip` field, its primary field is `address`.
|
|
66
|
+
const node: LocalEnricherNode = {
|
|
67
|
+
id: "i",
|
|
68
|
+
nodeType: "ip",
|
|
69
|
+
nodeProperties: { ip: "8.8.8.8" },
|
|
70
|
+
};
|
|
334
71
|
```
|
|
335
|
-
|
|
336
|
-
`message` is what's wrong, shown next to your plugin in Settings →
|
|
337
|
-
Plugins. `fix` is free text rendered as-is, no link-parsing - a short
|
|
338
|
-
human instruction, not necessarily a URL. The clickable link in
|
|
339
|
-
Settings → Plugins comes from `flowsint.installUrl` in `package.json`
|
|
340
|
-
instead (see above) - a separate, static piece of metadata that has
|
|
341
|
-
nothing to do with `init()`.
|
|
342
|
-
|
|
343
|
-
## Install / uninstall
|
|
344
|
-
|
|
345
|
-
Develop wherever you like. The plugins directory above is an install
|
|
346
|
-
target, not a workspace. Copy your plugin's folder into it, then
|
|
347
|
-
either restart Flowsint or click "Reload plugins" in Settings ->
|
|
348
|
-
Plugins. Both re-scan the plugins directory and re-run every plugin's
|
|
349
|
-
`init()` check. To remove a plugin, delete its folder and do the same.
|
|
350
|
-
There is no enable/disable toggle in v1. Settings -> Plugins is
|
|
351
|
-
read-only, showing what's loaded, what's degraded and why, and what's
|
|
352
|
-
failed to load entirely.
|
|
353
|
-
|
|
354
|
-
Symlinking your dev folder into the plugins directory (instead of
|
|
355
|
-
copying on every change) saves the copy step - rebuild, then just
|
|
356
|
-
click "Reload plugins".
|
|
357
|
-
|
|
358
|
-
## Testing your plugin
|
|
359
|
-
|
|
360
|
-
No test harness ships in the app. "does it work" is checked by
|
|
361
|
-
running it for real:
|
|
362
|
-
|
|
363
|
-
1. Settings → Plugins - your plugin should show `loaded`. `degraded`
|
|
364
|
-
or `failed` shows the `init()` message/fix or load error right
|
|
365
|
-
there; fix that before anything else.
|
|
366
|
-
2. Open a sketch, add an entity matching your `flowsint.inputs.type`.
|
|
367
|
-
3. Pick your plugin from that entity's enricher list and run it.
|
|
368
|
-
4. Bottom panel → "terminal" tab (`Cmd+Shift+T` to toggle) shows your
|
|
369
|
-
`logger.info/warn/error/success` calls live, plus anything your
|
|
370
|
-
`run`/`init` throws.
|
|
371
|
-
5. Check the sketch's graph for the `nodes`/`edges` you returned.
|
|
372
|
-
|
|
373
|
-
Changed the code? Rebuild, click "Reload plugins" in Settings →
|
|
374
|
-
Plugins, re-run step 3 - no restart needed.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flowsint/plugin-sdk",
|
|
3
|
-
"version": "2.6.
|
|
3
|
+
"version": "2.6.4",
|
|
4
4
|
"description": "SDK for Flowsint local enricher plugins: the plugin contract and the entity property types. Types only: consume with `import type`.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "dextmorgn <contact@flowsint.io>",
|