@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.
Files changed (2) hide show
  1. package/README.md +55 -358
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,374 +1,71 @@
1
- # Flowsint enricher plugins (`@flowsint/plugin-sdk`)
1
+ # @flowsint/plugin-sdk
2
2
 
3
- The SDK for Flowsint local enricher plugins: the plugin contract and one
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
- ```sh
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
- interface LocalEnricherNode {
122
- id: string
123
- nodeType: string
124
- nodeProperties: Record<string, unknown> // typed per nodeType, see below
125
- nodeLabel?: string // falls back to `nodeType`
126
- nodeSize?: number // falls back to the app's default size
127
- sources?: string[] // overrides the finding's `source`
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
- `logger` calls show up live in the sketch's Logs panel; anything you
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
- EnricherEntityInput,
215
- LocalEnricherFinding,
216
- LocalEnricherLogger,
217
- LocalEnricherNode
218
- } from '@flowsint/plugin-sdk'
219
-
220
- // Compile error: Ip has no `ip` field, its primary field is `address`.
221
- const node: LocalEnricherNode = { id, nodeType: 'ip', nodeProperties: { ip: '8.8.8.8' } }
222
- ```
223
-
224
- `LocalEnricherNode` is discriminated by `nodeType`: `nodeProperties`
225
- is checked against that type's spec (the interfaces are generated from
226
- Flowsint's own type specs, so they never drift from them). Narrow on
227
- `nodeType` before reading properties
228
- (`nodes.filter((n) => n.nodeType === 'dnsrecord')`).
229
-
230
- `LocalEnricherRun` types `run` as a whole (`export const run:
231
- LocalEnricherRun = async function* (entities, logger, params, signal) {...}`).
232
- `PluginPackageJson` is the shape of your `package.json` and
233
- `LocalEnricherPlugin` the module the host loads from `main`.
234
-
235
- ## Validating params before you spawn anything
236
-
237
- If your plugin shells out to a CLI (most do), the params you declared
238
- in `flowsint.params` will usually become argv values for that
239
- `spawn()` call. Calling `spawn(cmd, args)` with an argv array, never
240
- with `shell: true` and never by building a single command string
241
- yourself, already rules out classic shell-metacharacter injection
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
- A URL-shaped param (a proxy, a webhook) is simplest to validate with
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
- Logging the exact command you're about to run (`logger.info(...)`,
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
- Flowsint's own bundled plugins (seeded into your plugins folder by
300
- the app) all
301
- follow this pattern - `domain-to-dns-records`, `ip-to-ports`, and
302
- `username-to-social-accounts` in particular are worth reading if
303
- you want a fuller example with several param types (integers,
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
- ## Checking a dependency (optional `init`)
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
- import { spawn } from 'node:child_process'
317
-
318
- async function checkBinaryOnPath(bin: string, fix: string): Promise<InitResult> {
319
- return new Promise((resolve) => {
320
- const probe = spawn(process.platform === 'win32' ? 'where' : 'which', [bin])
321
- probe.on('close', (code) =>
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.1",
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>",