@relazio/plugin-sdk 0.1.0 → 0.2.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.
@@ -0,0 +1,515 @@
1
+ # External Plugin Response Format
2
+
3
+ This document defines the required response format for external plugins when returning entities and edges.
4
+
5
+ ## Response Modes
6
+
7
+ External plugins can return results in two modes:
8
+
9
+ 1. **Synchronous (sync)**: Immediate results
10
+ 2. **Asynchronous (async)**: Background job with webhook completion
11
+
12
+ ## Response Structure
13
+
14
+ ### Synchronous Response
15
+
16
+ ```typescript
17
+ {
18
+ "async": false,
19
+ "result": {
20
+ "success": true,
21
+ "entities": [...], // Array of entities (format below)
22
+ "edges": [...], // Array of edges (format below)
23
+ "message": "Optional success message",
24
+ "metadata": {} // Optional additional metadata
25
+ }
26
+ }
27
+ ```
28
+
29
+ ### Asynchronous Response
30
+
31
+ ```typescript
32
+ {
33
+ "async": true,
34
+ "jobId": "your-plugin-job-id-123",
35
+ "estimatedTime": 30 // Estimated time in seconds (optional)
36
+ }
37
+ ```
38
+
39
+ ## Entity Format
40
+
41
+ ### Structure
42
+
43
+ ```typescript
44
+ {
45
+ "id": string, // REQUIRED - Unique ID generated by plugin
46
+ "type": string, // REQUIRED - Entity type (see list below)
47
+ "value": string, // REQUIRED - Short entity value
48
+ "label": string, // OPTIONAL - Display text (default: value)
49
+ "metadata": object // OPTIONAL - Additional metadata
50
+ }
51
+ ```
52
+
53
+ ### Required Rules
54
+
55
+ 1. **`id` must be unique** across the entire response
56
+ 2. **`type` must be valid** (see list below)
57
+ 3. **`value` must be a non-empty string**
58
+ 4. **Use `metadata` only** - not `properties`
59
+ 5. **IDs should be stable** and deterministic when possible
60
+
61
+ ### Supported Entity Types
62
+
63
+ ```typescript
64
+ type EntityType =
65
+ | 'email' // Email address
66
+ | 'domain' // Domain name
67
+ | 'ip' // IP address
68
+ | 'person' // Person name
69
+ | 'username' // Username/handle
70
+ | 'phone' // Phone number
71
+ | 'organization' // Organization/company
72
+ | 'hash' // Hash/checksum
73
+ | 'credential' // Credential pair
74
+ | 'social' // Social media profile
75
+ | 'document' // Document
76
+ | 'note' // Text note
77
+ | 'image' // Image
78
+ | 'video' // Video
79
+ | 'location' // Geographic location
80
+ | 'wallet' // Crypto wallet
81
+ | 'transaction' // Transaction
82
+ | 'exchange' // Exchange
83
+ | 'url' // URL
84
+ | 'maps' // Map view
85
+ | 'custom'; // Custom entity
86
+ ```
87
+
88
+ ### Entity Examples
89
+
90
+ **IP Address**:
91
+ ```json
92
+ {
93
+ "id": "ip-8.8.8.8",
94
+ "type": "ip",
95
+ "value": "8.8.8.8",
96
+ "label": "Google DNS",
97
+ "metadata": {
98
+ "country": "US",
99
+ "isp": "Google LLC"
100
+ }
101
+ }
102
+ ```
103
+
104
+ **Location**:
105
+ ```json
106
+ {
107
+ "id": "loc-mountain-view-ca",
108
+ "type": "location",
109
+ "value": "Mountain View, CA",
110
+ "metadata": {
111
+ "latitude": 37.386,
112
+ "longitude": -122.084,
113
+ "country": "United States"
114
+ }
115
+ }
116
+ ```
117
+
118
+ **Organization**:
119
+ ```json
120
+ {
121
+ "id": "org-google-llc",
122
+ "type": "organization",
123
+ "value": "Google LLC",
124
+ "metadata": {
125
+ "industry": "Technology",
126
+ "website": "google.com"
127
+ }
128
+ }
129
+ ```
130
+
131
+ **Note with Markdown**:
132
+ ```json
133
+ {
134
+ "id": "note-ip-analysis-1",
135
+ "type": "note",
136
+ "value": "IP Analysis",
137
+ "label": "## IP: 8.8.8.8\n\n**Location**: Mountain View, CA\n**ISP**: Google LLC",
138
+ "metadata": {
139
+ "tags": ["ip-lookup", "analysis"],
140
+ "format": "markdown"
141
+ }
142
+ }
143
+ ```
144
+
145
+ ## Edge Format
146
+
147
+ ### Structure
148
+
149
+ ```typescript
150
+ {
151
+ "id": string, // REQUIRED - Unique edge ID
152
+ "sourceId": string, // REQUIRED - Source entity ID
153
+ "targetId": string, // REQUIRED - Target entity ID
154
+ "label": string, // REQUIRED - Visible label
155
+ "relationship": string, // OPTIONAL - Relationship type (default: label)
156
+ "metadata": object // OPTIONAL - Additional metadata
157
+ }
158
+ ```
159
+
160
+ ### Required Rules
161
+
162
+ 1. **`id` must be unique** across the entire response
163
+ 2. **`sourceId` must match an entity ID** (or input entity ID)
164
+ 3. **`targetId` must match an entity ID** returned
165
+ 4. **Use `sourceId/targetId` only** - not `from/to`
166
+ 5. **`label` is required** and visible in the graph
167
+
168
+ ### Edge Examples
169
+
170
+ **IP to Location**:
171
+ ```json
172
+ {
173
+ "id": "edge-ip-to-location-1",
174
+ "sourceId": "input-node-id",
175
+ "targetId": "loc-mountain-view-ca",
176
+ "label": "located in",
177
+ "relationship": "geolocation"
178
+ }
179
+ ```
180
+
181
+ **IP to Organization**:
182
+ ```json
183
+ {
184
+ "id": "edge-ip-to-org-1",
185
+ "sourceId": "input-node-id",
186
+ "targetId": "org-google-llc",
187
+ "label": "assigned by",
188
+ "relationship": "isp_assignment"
189
+ }
190
+ ```
191
+
192
+ ## Complete Example
193
+
194
+ ### Input Transform
195
+
196
+ ```json
197
+ {
198
+ "transformId": "lookup-ip",
199
+ "input": {
200
+ "entity": {
201
+ "id": "node-abc123",
202
+ "type": "ip",
203
+ "value": "8.8.8.8"
204
+ }
205
+ },
206
+ "callbackUrl": "https://app.example.com/api/webhooks/transforms/job-xyz?token=..."
207
+ }
208
+ ```
209
+
210
+ ### Synchronous Output
211
+
212
+ ```json
213
+ {
214
+ "async": false,
215
+ "result": {
216
+ "success": true,
217
+ "entities": [
218
+ {
219
+ "id": "loc-mountain-view-ca",
220
+ "type": "location",
221
+ "value": "Mountain View, CA",
222
+ "metadata": {
223
+ "latitude": 37.386,
224
+ "longitude": -122.084,
225
+ "country": "United States"
226
+ }
227
+ },
228
+ {
229
+ "id": "org-google-llc",
230
+ "type": "organization",
231
+ "value": "Google LLC",
232
+ "metadata": {
233
+ "type": "isp",
234
+ "asn": "AS15169"
235
+ }
236
+ }
237
+ ],
238
+ "edges": [
239
+ {
240
+ "id": "edge-ip-to-loc",
241
+ "sourceId": "node-abc123",
242
+ "targetId": "loc-mountain-view-ca",
243
+ "label": "located in",
244
+ "relationship": "geolocation"
245
+ },
246
+ {
247
+ "id": "edge-ip-to-org",
248
+ "sourceId": "node-abc123",
249
+ "targetId": "org-google-llc",
250
+ "label": "assigned by",
251
+ "relationship": "isp_assignment"
252
+ }
253
+ ],
254
+ "message": "Successfully analyzed IP 8.8.8.8"
255
+ }
256
+ }
257
+ ```
258
+
259
+ ### Asynchronous Output
260
+
261
+ ```json
262
+ {
263
+ "async": true,
264
+ "jobId": "scan-job-12345",
265
+ "estimatedTime": 60
266
+ }
267
+ ```
268
+
269
+ ## Webhook for Async Jobs
270
+
271
+ When an async job completes, the plugin must send a webhook to the provided callback URL.
272
+
273
+ ### Required Headers
274
+
275
+ ```
276
+ Content-Type: application/json
277
+ X-Plugin-Signature: sha256={hmac_signature}
278
+ ```
279
+
280
+ The HMAC signature is calculated on the JSON body using the plugin's webhook secret.
281
+
282
+ ### Webhook Body - Completion
283
+
284
+ ```json
285
+ {
286
+ "jobId": "scan-job-12345",
287
+ "status": "completed",
288
+ "progress": 100,
289
+ "progressMessage": "Scan complete",
290
+ "result": {
291
+ "success": true,
292
+ "entities": [
293
+ {
294
+ "id": "...",
295
+ "type": "...",
296
+ "value": "..."
297
+ }
298
+ ],
299
+ "edges": [
300
+ {
301
+ "id": "...",
302
+ "sourceId": "...",
303
+ "targetId": "...",
304
+ "label": "..."
305
+ }
306
+ ]
307
+ }
308
+ }
309
+ ```
310
+
311
+ ### Webhook Body - Progress Update
312
+
313
+ ```json
314
+ {
315
+ "jobId": "scan-job-12345",
316
+ "status": "processing",
317
+ "progress": 45,
318
+ "progressMessage": "Scanning ports..."
319
+ }
320
+ ```
321
+
322
+ ### Webhook Body - Failure
323
+
324
+ ```json
325
+ {
326
+ "jobId": "scan-job-12345",
327
+ "status": "failed",
328
+ "error": "Connection timeout"
329
+ }
330
+ ```
331
+
332
+ ## ID Generation Best Practices
333
+
334
+ ### Deterministic IDs
335
+
336
+ Use predictable, deterministic formats:
337
+
338
+ ```javascript
339
+ const entityId = `${type}-${hashValue(value)}`;
340
+ // Example: "location-a3f42bc1"
341
+ ```
342
+
343
+ ### Unique IDs
344
+
345
+ Ensure every entity has a unique ID:
346
+
347
+ ```javascript
348
+ const entityId = `${type}-${normalizedValue}-${Date.now()}`;
349
+ const edgeId = `edge-${sourceId}-${targetId}-${Date.now()}`;
350
+ ```
351
+
352
+ ### Clear Prefixes
353
+
354
+ Use prefixes to identify the type:
355
+
356
+ ```javascript
357
+ "ip-8.8.8.8"
358
+ "loc-nyc-us"
359
+ "org-google"
360
+ "note-analysis-1"
361
+ "edge-ip-to-loc-1"
362
+ ```
363
+
364
+ ### Input Entity ID
365
+
366
+ Always use the input entity ID in edges as `sourceId`:
367
+
368
+ ```javascript
369
+ // Input contains:
370
+ {
371
+ "entity": {
372
+ "id": "node-abc123", // Use this as sourceId
373
+ "type": "ip",
374
+ "value": "8.8.8.8"
375
+ }
376
+ }
377
+
378
+ // Edges must use this ID:
379
+ {
380
+ "id": "edge-1",
381
+ "sourceId": "node-abc123", // Input entity ID
382
+ "targetId": "loc-nyc",
383
+ "label": "located in"
384
+ }
385
+ ```
386
+
387
+ ## SDK Example
388
+
389
+ ```typescript
390
+ import crypto from 'crypto';
391
+
392
+ function generateEntityId(type: string, value: string): string {
393
+ const hash = crypto.createHash('md5')
394
+ .update(value.toLowerCase().trim())
395
+ .digest('hex')
396
+ .substring(0, 8);
397
+ return `${type}-${hash}`;
398
+ }
399
+
400
+ function generateEdgeId(sourceId: string, targetId: string): string {
401
+ const hash = crypto.createHash('md5')
402
+ .update(`${sourceId}-${targetId}`)
403
+ .digest('hex')
404
+ .substring(0, 8);
405
+ return `edge-${hash}`;
406
+ }
407
+
408
+ async function executeTransform(input) {
409
+ const { entity } = input;
410
+
411
+ const lookupResult = await lookupIP(entity.value);
412
+
413
+ const entities = [
414
+ {
415
+ id: generateEntityId('location', lookupResult.city),
416
+ type: 'location',
417
+ value: lookupResult.city,
418
+ metadata: {
419
+ latitude: lookupResult.lat,
420
+ longitude: lookupResult.lon
421
+ }
422
+ },
423
+ {
424
+ id: generateEntityId('organization', lookupResult.isp),
425
+ type: 'organization',
426
+ value: lookupResult.isp,
427
+ metadata: {
428
+ asn: lookupResult.asn
429
+ }
430
+ }
431
+ ];
432
+
433
+ const edges = [
434
+ {
435
+ id: generateEdgeId(entity.id, entities[0].id),
436
+ sourceId: entity.id,
437
+ targetId: entities[0].id,
438
+ label: 'located in',
439
+ relationship: 'geolocation'
440
+ },
441
+ {
442
+ id: generateEdgeId(entity.id, entities[1].id),
443
+ sourceId: entity.id,
444
+ targetId: entities[1].id,
445
+ label: 'assigned by',
446
+ relationship: 'isp_assignment'
447
+ }
448
+ ];
449
+
450
+ return {
451
+ async: false,
452
+ result: {
453
+ success: true,
454
+ entities,
455
+ edges,
456
+ message: `Analyzed IP ${entity.value}`
457
+ }
458
+ };
459
+ }
460
+ ```
461
+
462
+ ## Validation
463
+
464
+ The Relazio system validates responses and rejects non-compliant ones.
465
+
466
+ ### Applied Validations
467
+
468
+ - Each entity must have `id`, `type`, `value`
469
+ - Each edge must have `id`, `sourceId`, `targetId`, `label`
470
+ - `type` must be a valid type
471
+ - All IDs must be unique
472
+ - Edge `targetId` must correspond to a returned entity
473
+ - Edge `sourceId` must be the input entity ID or a returned entity
474
+
475
+ ### Common Errors
476
+
477
+ ```
478
+ "Entity missing required field: id"
479
+ "Edge missing required field: sourceId"
480
+ "Invalid entity type: unknown"
481
+ "Edge references non-existent entity: xyz"
482
+ "Duplicate entity ID: loc-1"
483
+ ```
484
+
485
+ ## HMAC Security
486
+
487
+ For webhooks, calculate the HMAC signature:
488
+
489
+ ```typescript
490
+ import crypto from 'crypto';
491
+
492
+ function generateHMACSignature(payload: string, secret: string): string {
493
+ return crypto
494
+ .createHmac('sha256', secret)
495
+ .update(payload)
496
+ .digest('hex');
497
+ }
498
+
499
+ // Usage:
500
+ const body = JSON.stringify(webhookPayload);
501
+ const signature = generateHMACSignature(body, webhookSecret);
502
+
503
+ // Send with header:
504
+ headers: {
505
+ 'Content-Type': 'application/json',
506
+ 'X-Plugin-Signature': `sha256=${signature}`
507
+ }
508
+ ```
509
+
510
+ ## References
511
+
512
+ - [Quick Start Guide](./quick-start.md)
513
+ - [Builders Guide](./builders-guide.md)
514
+ - [Examples Documentation](./examples.md)
515
+ - [Main README](../README.md)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@relazio/plugin-sdk",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Official SDK for building external plugins for Relazio",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -22,11 +22,13 @@
22
22
  "license": "MIT",
23
23
  "repository": {
24
24
  "type": "git",
25
- "url": "https://github.com/relazio/plugin-sdk.git"
25
+ "url": "git+https://github.com/relazio/plugin-sdk.git"
26
26
  },
27
27
  "files": [
28
28
  "dist",
29
+ "docs",
29
30
  "README.md",
31
+ "CHANGELOG.md",
30
32
  "LICENSE"
31
33
  ],
32
34
  "publishConfig": {
@@ -43,10 +45,9 @@
43
45
  "@typescript-eslint/parser": "^6.17.0",
44
46
  "eslint": "^8.56.0",
45
47
  "typescript": "^5.3.3",
46
- "vitest": "^1.1.0"
48
+ "vitest": "^4.0.16"
47
49
  },
48
50
  "engines": {
49
51
  "node": ">=18.0.0"
50
52
  }
51
53
  }
52
-