@flowsint/plugin-sdk 2.6.2 → 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 +36 -171
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,81 +1,56 @@
|
|
|
1
1
|
# @flowsint/plugin-sdk
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
`Email` and every other Flowsint entity type, plus the plugin contract, so
|
|
5
|
-
a wrong field name is a compile error instead of an empty node in the graph.
|
|
6
|
-
Types only: nothing ships at runtime.
|
|
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.
|
|
7
4
|
|
|
8
5
|
## Install
|
|
9
6
|
|
|
10
7
|
```sh
|
|
11
|
-
|
|
8
|
+
# pnpm
|
|
9
|
+
pnpm add @flowsint/plugin-sdk
|
|
10
|
+
# yarn
|
|
11
|
+
yarn add @flowsint/plugin-sdk
|
|
12
|
+
# npm
|
|
13
|
+
npm i @flowsint/plugin-sdk
|
|
12
14
|
```
|
|
13
15
|
|
|
14
16
|
## Quick start
|
|
15
17
|
|
|
16
|
-
A plugin is a folder with a `package.json`, a `README.md` and one bundled
|
|
17
|
-
CommonJS file. This one resolves a domain to its IP address.
|
|
18
|
-
|
|
19
|
-
`package.json`:
|
|
20
|
-
|
|
21
|
-
```json
|
|
22
|
-
{
|
|
23
|
-
"name": "my-domain-to-ip",
|
|
24
|
-
"version": "1.0.0",
|
|
25
|
-
"main": "dist/index.js",
|
|
26
|
-
"engines": { "flowsint": "^2.6.1" },
|
|
27
|
-
"flowsint": {
|
|
28
|
-
"category": "Domain",
|
|
29
|
-
"source": "dns",
|
|
30
|
-
"inputs": { "type": "domain", "properties": [{ "name": "domain", "type": "string" }] },
|
|
31
|
-
"outputs": { "type": "ip", "properties": [{ "name": "address", "type": "string" }] }
|
|
32
|
-
},
|
|
33
|
-
"scripts": {
|
|
34
|
-
"build": "esbuild src/index.ts --bundle --platform=node --format=cjs --target=node20 --outfile=dist/index.js"
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
`src/index.ts`:
|
|
40
|
-
|
|
41
18
|
```ts
|
|
42
|
-
import { randomUUID } from
|
|
43
|
-
import { lookup } from
|
|
44
|
-
import type {
|
|
19
|
+
import { randomUUID } from "node:crypto";
|
|
20
|
+
import { lookup } from "node:dns/promises";
|
|
21
|
+
import type {
|
|
22
|
+
Domain,
|
|
23
|
+
Ip,
|
|
24
|
+
LocalEnricherRun,
|
|
25
|
+
LocalEnricherEdge,
|
|
26
|
+
} from "@flowsint/plugin-sdk";
|
|
45
27
|
|
|
46
28
|
export const run: LocalEnricherRun = async function* (entities, logger) {
|
|
47
29
|
for (const entity of entities) {
|
|
48
|
-
if (entity.type !==
|
|
49
|
-
const domain: Domain = { domain: String(entity.properties.domain) }
|
|
50
|
-
const ip: Ip = { address: (await lookup(domain.domain)).address }
|
|
51
|
-
logger.info(`${domain.domain} resolves to ${ip.address}`)
|
|
52
|
-
const [domainId, ipId] = [randomUUID(), randomUUID()]
|
|
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
|
+
};
|
|
53
41
|
yield {
|
|
54
42
|
nodes: [
|
|
55
|
-
{ id: domainId, nodeType:
|
|
56
|
-
{ id: ipId, nodeType:
|
|
43
|
+
{ id: domainId, nodeType: "domain", nodeProperties: domain },
|
|
44
|
+
{ id: ipId, nodeType: "ip", nodeProperties: ip },
|
|
57
45
|
],
|
|
58
|
-
edges: [
|
|
59
|
-
}
|
|
46
|
+
edges: [relation],
|
|
47
|
+
};
|
|
60
48
|
}
|
|
61
|
-
}
|
|
49
|
+
};
|
|
62
50
|
```
|
|
63
51
|
|
|
64
52
|
Build it (`npm i -D @flowsint/plugin-sdk esbuild` first), then install it:
|
|
65
53
|
|
|
66
|
-
```sh
|
|
67
|
-
npm run build
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
1. Copy the folder (`package.json`, `README.md`, `dist/`) into the plugins
|
|
71
|
-
folder: `~/Library/Application Support/Flowsint/plugins/` (macOS),
|
|
72
|
-
`%APPDATA%/Flowsint/plugins/` (Windows), `~/.config/Flowsint/plugins/`
|
|
73
|
-
(Linux).
|
|
74
|
-
2. In Flowsint, open Settings -> Plugins and click "Reload plugins". Your
|
|
75
|
-
plugin shows `loaded`.
|
|
76
|
-
3. Add a domain to a sketch and run your plugin on it. Your `logger` calls
|
|
77
|
-
appear in the sketch's logs.
|
|
78
|
-
|
|
79
54
|
## Entity types
|
|
80
55
|
|
|
81
56
|
| Export | What it is |
|
|
@@ -88,119 +63,9 @@ A node is checked against its `nodeType`:
|
|
|
88
63
|
|
|
89
64
|
```ts
|
|
90
65
|
// Compile error: Ip has no `ip` field, its primary field is `address`.
|
|
91
|
-
const node: LocalEnricherNode = {
|
|
66
|
+
const node: LocalEnricherNode = {
|
|
67
|
+
id: "i",
|
|
68
|
+
nodeType: "ip",
|
|
69
|
+
nodeProperties: { ip: "8.8.8.8" },
|
|
70
|
+
};
|
|
92
71
|
```
|
|
93
|
-
|
|
94
|
-
## Plugin contract
|
|
95
|
-
|
|
96
|
-
Your `main` file exports `run` (required) and `init` (optional). Nothing
|
|
97
|
-
else is read.
|
|
98
|
-
|
|
99
|
-
| Export | Signature | Called |
|
|
100
|
-
| ------ | ----------------------------------------------------------------------------------- | --------------------------------------------------------- |
|
|
101
|
-
| `run` | `async function* (entities, logger, params, signal)`, yields `LocalEnricherFinding` | Each time a user runs the plugin. |
|
|
102
|
-
| `init` | `() => Promise<InitResult>` | At app start and on "Reload plugins". Never before a run. |
|
|
103
|
-
|
|
104
|
-
### `run` arguments
|
|
105
|
-
|
|
106
|
-
| Argument | Type | Content |
|
|
107
|
-
| ---------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
|
108
|
-
| `entities` | `EnricherEntityInput[]` | `{ type, properties }` per selected entity. Not narrowed: check `type` first, an input can be a user-defined type. |
|
|
109
|
-
| `logger` | `LocalEnricherLogger` | `info`, `warn`, `error`, `success`, shown live in the sketch's logs. |
|
|
110
|
-
| `params` | `Record<string, unknown>` | Values of your `flowsint.params`, by name. `{}` when you declare none. |
|
|
111
|
-
| `signal` | `AbortSignal` | Aborts when the user stops the run or after 6 hours. Pass it to `fetch` and `spawn`. |
|
|
112
|
-
|
|
113
|
-
### `LocalEnricherFinding` (one yielded chunk)
|
|
114
|
-
|
|
115
|
-
| Field | Type | Rule |
|
|
116
|
-
| -------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
|
|
117
|
-
| `nodes` | `LocalEnricherNode[]` | `{ id, nodeType, nodeProperties, nodeLabel?, nodeSize?, sources? }`. Use a fresh `randomUUID()` per node. |
|
|
118
|
-
| `edges` | `LocalEnricherEdge[]` | `{ id, source, target, label, sources? }`. `source` and `target` are node ids **of the same chunk**. |
|
|
119
|
-
| `events` | `LocalEnricherEvent[]` (optional) | Timeline entries: `{ timestamp, title, endTimestamp?, description?, type?, confidence?, nodeIds?, tags?, properties? }`. |
|
|
120
|
-
|
|
121
|
-
Each chunk is merged into the graph as soon as it is yielded. Yield one
|
|
122
|
-
chunk per entity. A `throw` fails the run; chunks already yielded stay.
|
|
123
|
-
|
|
124
|
-
### `InitResult`
|
|
125
|
-
|
|
126
|
-
| Value | Effect |
|
|
127
|
-
| ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
|
|
128
|
-
| `{ ok: true }` | Plugin is available. |
|
|
129
|
-
| `{ ok: false, message, fix }` | Plugin is hidden from the enricher list. Settings -> Plugins shows `message` (what is wrong) and `fix` (plain text instruction). |
|
|
130
|
-
|
|
131
|
-
```ts
|
|
132
|
-
import { spawn } from 'node:child_process'
|
|
133
|
-
import type { InitResult } from '@flowsint/plugin-sdk'
|
|
134
|
-
|
|
135
|
-
export function init(): Promise<InitResult> {
|
|
136
|
-
return new Promise((resolve) => {
|
|
137
|
-
const fail = {
|
|
138
|
-
ok: false,
|
|
139
|
-
message: 'subfinder not found on PATH',
|
|
140
|
-
fix: 'brew install subfinder'
|
|
141
|
-
} as const
|
|
142
|
-
const probe = spawn(process.platform === 'win32' ? 'where' : 'which', ['subfinder'])
|
|
143
|
-
probe.on('close', (code) => resolve(code === 0 ? { ok: true } : fail))
|
|
144
|
-
probe.on('error', () => resolve(fail))
|
|
145
|
-
})
|
|
146
|
-
}
|
|
147
|
-
```
|
|
148
|
-
|
|
149
|
-
## `package.json` manifest
|
|
150
|
-
|
|
151
|
-
Typed by `PluginPackageJson`. Flowsint reads it without running your code.
|
|
152
|
-
|
|
153
|
-
| Field | Required | Meaning |
|
|
154
|
-
| ---------------------- | -------- | ------------------------------------------------------------------------------- |
|
|
155
|
-
| `name` | yes | Plugin id, unique across installed plugins. |
|
|
156
|
-
| `main` | yes | Bundled CommonJS entry. |
|
|
157
|
-
| `engines.flowsint` | no | Flowsint versions supported, e.g. `^2.6.1`. Skipped when absent. |
|
|
158
|
-
| `flowsint.category` | yes | Group in the enricher list (`Domain`, `Ip`, ...). |
|
|
159
|
-
| `flowsint.source` | yes | Provider slug stamped on every result (`dns`, `whois`): lowercase, digits, `_`. |
|
|
160
|
-
| `flowsint.inputs` | yes | `{ type, properties }`: the entity type the plugin accepts. |
|
|
161
|
-
| `flowsint.outputs` | no | `{ type, properties }`: the main entity type it produces. |
|
|
162
|
-
| `flowsint.description` | no | One sentence shown in the app. |
|
|
163
|
-
| `flowsint.icon` | no | A [lucide](https://lucide.dev/icons/) icon name. |
|
|
164
|
-
| `flowsint.installUrl` | no | Install docs of an external dependency, linked in Settings -> Plugins. |
|
|
165
|
-
| `flowsint.params` | no | Launch parameters, see below. |
|
|
166
|
-
|
|
167
|
-
### `flowsint.params`
|
|
168
|
-
|
|
169
|
-
Each entry is `{ name, type, description, default, required, options? }`
|
|
170
|
-
(`EnricherParamSchemaItem`). The user fills them in before the run.
|
|
171
|
-
|
|
172
|
-
| `type` | Form control | Value in `params[name]` |
|
|
173
|
-
| --------------- | ----------------------------------------- | ---------------------------------------------------- |
|
|
174
|
-
| `string`, `url` | Text input | `string` |
|
|
175
|
-
| `number` | Numeric input | `number`, `undefined` when empty |
|
|
176
|
-
| `boolean` | Switch | `boolean`, `false` when untouched |
|
|
177
|
-
| `date` | Calendar | `Date` at midnight UTC, `undefined` when empty |
|
|
178
|
-
| `list` | Tags input | `string[]`, `[]` when empty |
|
|
179
|
-
| `select` | Dropdown of `options: [{ label, value }]` | The picked `value` |
|
|
180
|
-
| `vaultSecret` | Key from the local vault | The decrypted secret, `null` when optional and unset |
|
|
181
|
-
|
|
182
|
-
An invalid value fails the run before `run` is called.
|
|
183
|
-
|
|
184
|
-
## Good practice
|
|
185
|
-
|
|
186
|
-
- **Bundle everything.** Flowsint never runs `npm install`: your plugin must
|
|
187
|
-
work by `require()` alone. Ship `node_modules/` only for a native addon
|
|
188
|
-
the bundler cannot inline.
|
|
189
|
-
- **Write a `README.md`.** It is required, and shown in Settings -> Plugins.
|
|
190
|
-
Say what the plugin needs and what it returns.
|
|
191
|
-
- **Spawn with an argv array.** `spawn(cmd, args)`, never `shell: true` or a
|
|
192
|
-
command string: no shell, no injection.
|
|
193
|
-
- **Validate params before the entity loop.** A bad param should fail the
|
|
194
|
-
whole run with a clear `throw`, not one entity at a time.
|
|
195
|
-
- **Log what you run.** `logger.info` the exact argv: the logs then show
|
|
196
|
-
what happened, not just that it failed.
|
|
197
|
-
- **Develop with a symlink.** Link your folder into the plugins folder, then
|
|
198
|
-
rebuild and click "Reload plugins". No restart needed.
|
|
199
|
-
|
|
200
|
-
## Versioning
|
|
201
|
-
|
|
202
|
-
The package version is the Flowsint version. Set `engines.flowsint` to `^`
|
|
203
|
-
the version you installed. Removing or renaming an entity type or field,
|
|
204
|
-
narrowing a field's type or making a field required only happens in a
|
|
205
|
-
major release. This covers the types only, not how the app processes what
|
|
206
|
-
a plugin returns.
|
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>",
|