n8n-nodes-ilovevideoeditor 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/README.md +132 -0
- package/dist/credentials/IloveVideoEditorApi.credentials.js +50 -0
- package/dist/nodes/IloveVideoEditor/IloveVideoEditor.node.js +294 -0
- package/dist/nodes/IloveVideoEditor/IloveVideoEditorTrigger.node.js +197 -0
- package/dist/nodes/IloveVideoEditor/ilovevideoeditor.svg +6 -0
- package/package.json +53 -0
package/README.md
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# n8n-nodes-ilovevideoeditor
|
|
2
|
+
|
|
3
|
+
n8n community nodes for the [iLoveVideoEditor](https://ilovevideoeditor.com) API — generate videos from templates or raw VideoJSON, and start workflows when renders finish.
|
|
4
|
+
|
|
5
|
+
This package contains:
|
|
6
|
+
|
|
7
|
+
- **iLoveVideoEditor** — action node (declarative) for renders and templates
|
|
8
|
+
- **iLoveVideoEditor Trigger** — webhook trigger that fires on `render.completed` / `render.failed`, with optional HMAC signature verification
|
|
9
|
+
- **iLoveVideoEditor API** — credentials (API key + base URL + optional webhook secret)
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
### Community nodes UI (recommended)
|
|
14
|
+
|
|
15
|
+
1. In your self-hosted n8n instance, go to **Settings → Community nodes**.
|
|
16
|
+
2. Choose **Install**, enter `n8n-nodes-ilovevideoeditor` and confirm.
|
|
17
|
+
3. Reload the editor — the nodes appear in the node palette.
|
|
18
|
+
|
|
19
|
+
> Community nodes require a self-hosted n8n instance with community packages enabled (`N8N_COMMUNITY_PACKAGES_ENABLED=true`, which is the default for self-hosted). They are not available on n8n Cloud.
|
|
20
|
+
|
|
21
|
+
### npm
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
# inside your n8n custom extensions directory (e.g. ~/.n8n/custom)
|
|
25
|
+
npm install n8n-nodes-ilovevideoeditor
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Restart n8n afterwards.
|
|
29
|
+
|
|
30
|
+
## Credentials
|
|
31
|
+
|
|
32
|
+
Create an **iLoveVideoEditor API** credential in n8n:
|
|
33
|
+
|
|
34
|
+
| Field | Description |
|
|
35
|
+
| --- | --- |
|
|
36
|
+
| API Key | Create one in the iLoveVideoEditor dashboard under **Settings → API keys**. Live keys start with `vf_live_`. Sent as the `x-api-key` header. |
|
|
37
|
+
| Base URL | Defaults to `https://api.ilovevideoeditor.com`. Change only for staging/self-hosted deployments. |
|
|
38
|
+
| Webhook Secret | Optional. The `secret` returned when a webhook subscription is created (`POST /v1/webhooks`). When set, the Trigger node verifies the `X-ILVE-Signature` header of every delivery and silently drops requests with a missing/invalid signature or a timestamp older than 5 minutes. |
|
|
39
|
+
|
|
40
|
+
The credential test calls `GET /v1/render/tier`, which returns your tier and credit balance.
|
|
41
|
+
|
|
42
|
+
## Operations
|
|
43
|
+
|
|
44
|
+
### iLoveVideoEditor (action node)
|
|
45
|
+
|
|
46
|
+
| Resource | Operation | Endpoint | Notes |
|
|
47
|
+
| --- | --- | --- | --- |
|
|
48
|
+
| Render | Create From Template | `POST /v1/templates/{templateId}/render` | Pick a template from the dropdown (loaded live from your account) and pass `variables` as JSON. Optional `webhookUrl`. Returns `{ jobId, status, stage }`. |
|
|
49
|
+
| Render | Create | `POST /v1/render` | Render a full **Video JSON** document. Optional `webhookUrl`. Returns `{ jobId, status, stage }`. |
|
|
50
|
+
| Render | Get | `GET /v1/render/{renderId}` | Poll status: `{ id, status, stage, progress, url, outputs, error, createdAt, completedAt }`. |
|
|
51
|
+
| Render | Get Many | `GET /v1/render?limit=N` | Recent renders, newest first (limit 1–100, default 20). |
|
|
52
|
+
| Template | Get Many | `GET /v1/templates` | All templates with their `variablesSchema`. |
|
|
53
|
+
|
|
54
|
+
### iLoveVideoEditor Trigger
|
|
55
|
+
|
|
56
|
+
When the workflow is activated, the trigger registers a webhook subscription on your iLoveVideoEditor account (`POST /v1/webhooks` with the node's webhook URL and the selected events). On deactivation it deletes the subscription again. Each delivery becomes one workflow item containing the full payload:
|
|
57
|
+
|
|
58
|
+
```json
|
|
59
|
+
{
|
|
60
|
+
"event": "render.completed",
|
|
61
|
+
"id": "job_...",
|
|
62
|
+
"status": "completed",
|
|
63
|
+
"stage": "done",
|
|
64
|
+
"progress": 100,
|
|
65
|
+
"url": "https://...",
|
|
66
|
+
"outputs": [],
|
|
67
|
+
"duration": 12.5,
|
|
68
|
+
"width": 1920,
|
|
69
|
+
"height": 1080,
|
|
70
|
+
"templateId": "tpl_...",
|
|
71
|
+
"cost": 3,
|
|
72
|
+
"error": null,
|
|
73
|
+
"createdAt": "...",
|
|
74
|
+
"completedAt": "..."
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Deliveries carry `X-ILVE-Event` (e.g. `render.completed`) and `X-ILVE-Signature: t=<unix>,v1=<hex>` headers. Events not selected in the node's **Events** parameter are acknowledged but do not start the workflow.
|
|
79
|
+
|
|
80
|
+
## Example flows
|
|
81
|
+
|
|
82
|
+
**Render on new Airtable row, continue when the video is ready**
|
|
83
|
+
|
|
84
|
+
Two workflows (the render call returns immediately with a `jobId`; the webhook pattern avoids polling):
|
|
85
|
+
|
|
86
|
+
1. *Workflow A*: **Airtable Trigger** (new row) → **iLoveVideoEditor → Create From Template** (`templateId` = your promo template, `variables` = mapped Airtable fields). Leave `webhookUrl` empty — the Trigger node manages its own subscription.
|
|
87
|
+
2. *Workflow B*: **iLoveVideoEditor Trigger** (`render.completed`) → **HTTP Request** to download `{{ $json.url }}` → upload to storage / send notification.
|
|
88
|
+
|
|
89
|
+
**Simple polling alternative (single workflow)**
|
|
90
|
+
|
|
91
|
+
Airtable Trigger → **Create From Template** → **Wait** (60 s) → **iLoveVideoEditor → Get** (`renderId` = `{{ $json.jobId }}`) → **IF** `status == completed` → download; else loop back to Wait.
|
|
92
|
+
|
|
93
|
+
**Custom compositions**
|
|
94
|
+
|
|
95
|
+
Use **Render → Create** and build the `videoJSON` document in a preceding Code/Set node — e.g. generated by an AI agent from a prompt.
|
|
96
|
+
|
|
97
|
+
## Signature verification
|
|
98
|
+
|
|
99
|
+
Webhook payloads are signed with the subscription's secret:
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
X-ILVE-Signature: t=1719936000,v1=<hex hmac-sha256("1719936000." + rawBody)>
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
If you fill in **Webhook Secret** on the credential, the Trigger node recomputes the HMAC-SHA256 over `t` + `.` + raw request body, compares it in constant time, and rejects deliveries whose signature is missing/invalid or whose timestamp differs by more than 5 minutes (replay protection). Without a secret, all deliveries for the selected events are accepted.
|
|
106
|
+
|
|
107
|
+
> Note: the secret shown by the API belongs to one webhook subscription. If you run multiple trigger nodes, use one credential per subscription, or leave the secret empty and filter inside the workflow.
|
|
108
|
+
|
|
109
|
+
## Development
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
npm install
|
|
113
|
+
npm run build # tsc + copies SVG icons into dist/
|
|
114
|
+
npm run lint # tsc --noEmit
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
To test locally, link the built package into your n8n custom directory:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
mkdir -p ~/.n8n/custom && cd ~/.n8n/custom
|
|
121
|
+
npm init -y && npm install /path/to/integrations/n8n-nodes-ilovevideoeditor
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Compatibility
|
|
125
|
+
|
|
126
|
+
- n8n Nodes API version 1 (`n8nNodesApiVersion: 1`)
|
|
127
|
+
- Requires n8n ≥ 1.x and Node.js ≥ 18.10
|
|
128
|
+
- Zero runtime dependencies (`n8n-workflow` is a peer dependency provided by n8n)
|
|
129
|
+
|
|
130
|
+
## License
|
|
131
|
+
|
|
132
|
+
MIT
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.IloveVideoEditorApi = void 0;
|
|
4
|
+
class IloveVideoEditorApi {
|
|
5
|
+
name = 'iloveVideoEditorApi';
|
|
6
|
+
displayName = 'iLoveVideoEditor API';
|
|
7
|
+
documentationUrl = 'https://docs.ilovevideoeditor.com/api';
|
|
8
|
+
properties = [
|
|
9
|
+
{
|
|
10
|
+
displayName: 'API Key',
|
|
11
|
+
name: 'apiKey',
|
|
12
|
+
type: 'string',
|
|
13
|
+
typeOptions: { password: true },
|
|
14
|
+
default: '',
|
|
15
|
+
required: true,
|
|
16
|
+
description: 'API key from the iLoveVideoEditor dashboard (Settings → API keys). Live keys start with "vf_live_".',
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
displayName: 'Base URL',
|
|
20
|
+
name: 'baseUrl',
|
|
21
|
+
type: 'string',
|
|
22
|
+
default: 'https://api.ilovevideoeditor.com',
|
|
23
|
+
description: 'Base URL of the iLoveVideoEditor API. Change only for self-hosted or staging deployments.',
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
displayName: 'Webhook Secret',
|
|
27
|
+
name: 'webhookSecret',
|
|
28
|
+
type: 'string',
|
|
29
|
+
typeOptions: { password: true },
|
|
30
|
+
default: '',
|
|
31
|
+
description: 'Optional. Secret of a webhook subscription (returned by POST /v1/webhooks). When set, the iLoveVideoEditor Trigger node verifies the X-ILVE-Signature header of every incoming webhook.',
|
|
32
|
+
},
|
|
33
|
+
];
|
|
34
|
+
authenticate = {
|
|
35
|
+
type: 'generic',
|
|
36
|
+
properties: {
|
|
37
|
+
headers: {
|
|
38
|
+
'x-api-key': '={{$credentials.apiKey}}',
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
test = {
|
|
43
|
+
request: {
|
|
44
|
+
baseURL: '={{$credentials.baseUrl}}',
|
|
45
|
+
url: '/v1/render/tier',
|
|
46
|
+
method: 'GET',
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
exports.IloveVideoEditorApi = IloveVideoEditorApi;
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.IloveVideoEditor = void 0;
|
|
4
|
+
class IloveVideoEditor {
|
|
5
|
+
description = {
|
|
6
|
+
displayName: 'iLoveVideoEditor',
|
|
7
|
+
name: 'iloveVideoEditor',
|
|
8
|
+
icon: 'file:ilovevideoeditor.svg',
|
|
9
|
+
group: ['transform'],
|
|
10
|
+
version: 1,
|
|
11
|
+
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
|
12
|
+
description: 'Generate videos with the iLoveVideoEditor API',
|
|
13
|
+
defaults: {
|
|
14
|
+
name: 'iLoveVideoEditor',
|
|
15
|
+
},
|
|
16
|
+
inputs: ['main'],
|
|
17
|
+
outputs: ['main'],
|
|
18
|
+
credentials: [
|
|
19
|
+
{
|
|
20
|
+
name: 'iloveVideoEditorApi',
|
|
21
|
+
required: true,
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
requestDefaults: {
|
|
25
|
+
baseURL: '={{$credentials.baseUrl}}',
|
|
26
|
+
headers: {
|
|
27
|
+
Accept: 'application/json',
|
|
28
|
+
'Content-Type': 'application/json',
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
properties: [
|
|
32
|
+
{
|
|
33
|
+
displayName: 'Resource',
|
|
34
|
+
name: 'resource',
|
|
35
|
+
type: 'options',
|
|
36
|
+
noDataExpression: true,
|
|
37
|
+
options: [
|
|
38
|
+
{
|
|
39
|
+
name: 'Render',
|
|
40
|
+
value: 'render',
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: 'Template',
|
|
44
|
+
value: 'template',
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
default: 'render',
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
displayName: 'Operation',
|
|
51
|
+
name: 'operation',
|
|
52
|
+
type: 'options',
|
|
53
|
+
noDataExpression: true,
|
|
54
|
+
displayOptions: {
|
|
55
|
+
show: {
|
|
56
|
+
resource: ['render'],
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
options: [
|
|
60
|
+
{
|
|
61
|
+
name: 'Create',
|
|
62
|
+
value: 'create',
|
|
63
|
+
description: 'Render a video from a VideoJSON document',
|
|
64
|
+
action: 'Create a render',
|
|
65
|
+
routing: {
|
|
66
|
+
request: {
|
|
67
|
+
method: 'POST',
|
|
68
|
+
url: '/v1/render',
|
|
69
|
+
},
|
|
70
|
+
send: {
|
|
71
|
+
preSend: [
|
|
72
|
+
async function (requestOptions) {
|
|
73
|
+
const videoJSON = this.getNodeParameter('videoJSON');
|
|
74
|
+
const webhookUrl = this.getNodeParameter('webhookUrl', '');
|
|
75
|
+
const body = { videoJSON };
|
|
76
|
+
if (webhookUrl) {
|
|
77
|
+
body.webhookUrl = webhookUrl;
|
|
78
|
+
}
|
|
79
|
+
requestOptions.body = body;
|
|
80
|
+
return requestOptions;
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: 'Create From Template',
|
|
88
|
+
value: 'createFromTemplate',
|
|
89
|
+
description: 'Render a video from a template and a set of variables',
|
|
90
|
+
action: 'Create a render from a template',
|
|
91
|
+
routing: {
|
|
92
|
+
request: {
|
|
93
|
+
method: 'POST',
|
|
94
|
+
url: '=/v1/templates/{{$parameter.templateId}}/render',
|
|
95
|
+
},
|
|
96
|
+
send: {
|
|
97
|
+
preSend: [
|
|
98
|
+
async function (requestOptions) {
|
|
99
|
+
const variables = this.getNodeParameter('variables', {});
|
|
100
|
+
const webhookUrl = this.getNodeParameter('webhookUrl', '');
|
|
101
|
+
const body = { variables };
|
|
102
|
+
if (webhookUrl) {
|
|
103
|
+
body.webhookUrl = webhookUrl;
|
|
104
|
+
}
|
|
105
|
+
requestOptions.body = body;
|
|
106
|
+
return requestOptions;
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
name: 'Get',
|
|
114
|
+
value: 'get',
|
|
115
|
+
description: 'Get the status and result of a render',
|
|
116
|
+
action: 'Get a render',
|
|
117
|
+
routing: {
|
|
118
|
+
request: {
|
|
119
|
+
method: 'GET',
|
|
120
|
+
url: '=/v1/render/{{$parameter.renderId}}',
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
name: 'Get Many',
|
|
126
|
+
value: 'getMany',
|
|
127
|
+
description: 'List recent renders, newest first',
|
|
128
|
+
action: 'Get many renders',
|
|
129
|
+
routing: {
|
|
130
|
+
request: {
|
|
131
|
+
method: 'GET',
|
|
132
|
+
url: '/v1/render',
|
|
133
|
+
qs: {
|
|
134
|
+
limit: '={{$parameter.limit}}',
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
default: 'createFromTemplate',
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
displayName: 'Operation',
|
|
144
|
+
name: 'operation',
|
|
145
|
+
type: 'options',
|
|
146
|
+
noDataExpression: true,
|
|
147
|
+
displayOptions: {
|
|
148
|
+
show: {
|
|
149
|
+
resource: ['template'],
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
options: [
|
|
153
|
+
{
|
|
154
|
+
name: 'Get Many',
|
|
155
|
+
value: 'getMany',
|
|
156
|
+
description: 'List all available templates',
|
|
157
|
+
action: 'Get many templates',
|
|
158
|
+
routing: {
|
|
159
|
+
request: {
|
|
160
|
+
method: 'GET',
|
|
161
|
+
url: '/v1/templates',
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
],
|
|
166
|
+
default: 'getMany',
|
|
167
|
+
},
|
|
168
|
+
// ----------------------------------
|
|
169
|
+
// render:createFromTemplate
|
|
170
|
+
// ----------------------------------
|
|
171
|
+
{
|
|
172
|
+
displayName: 'Template Name or ID',
|
|
173
|
+
name: 'templateId',
|
|
174
|
+
type: 'options',
|
|
175
|
+
typeOptions: {
|
|
176
|
+
loadOptionsMethod: 'getTemplates',
|
|
177
|
+
},
|
|
178
|
+
default: '',
|
|
179
|
+
required: true,
|
|
180
|
+
displayOptions: {
|
|
181
|
+
show: {
|
|
182
|
+
resource: ['render'],
|
|
183
|
+
operation: ['createFromTemplate'],
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
description: 'Template to render. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
displayName: 'Variables',
|
|
190
|
+
name: 'variables',
|
|
191
|
+
type: 'json',
|
|
192
|
+
default: '{}',
|
|
193
|
+
required: true,
|
|
194
|
+
displayOptions: {
|
|
195
|
+
show: {
|
|
196
|
+
resource: ['render'],
|
|
197
|
+
operation: ['createFromTemplate'],
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
description: 'Template variables as a JSON object, e.g. {"headline": "Hello", "cta": "Buy now"}',
|
|
201
|
+
},
|
|
202
|
+
// ----------------------------------
|
|
203
|
+
// render:create
|
|
204
|
+
// ----------------------------------
|
|
205
|
+
{
|
|
206
|
+
displayName: 'Video JSON',
|
|
207
|
+
name: 'videoJSON',
|
|
208
|
+
type: 'json',
|
|
209
|
+
default: '{\n "width": 1920,\n "height": 1080,\n "duration": 5,\n "scenes": []\n}',
|
|
210
|
+
required: true,
|
|
211
|
+
displayOptions: {
|
|
212
|
+
show: {
|
|
213
|
+
resource: ['render'],
|
|
214
|
+
operation: ['create'],
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
description: 'Full VideoJSON document to render',
|
|
218
|
+
},
|
|
219
|
+
// ----------------------------------
|
|
220
|
+
// render:get
|
|
221
|
+
// ----------------------------------
|
|
222
|
+
{
|
|
223
|
+
displayName: 'Render ID',
|
|
224
|
+
name: 'renderId',
|
|
225
|
+
type: 'string',
|
|
226
|
+
default: '',
|
|
227
|
+
required: true,
|
|
228
|
+
displayOptions: {
|
|
229
|
+
show: {
|
|
230
|
+
resource: ['render'],
|
|
231
|
+
operation: ['get'],
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
description: 'ID of the render job (jobId returned by a create operation)',
|
|
235
|
+
},
|
|
236
|
+
// ----------------------------------
|
|
237
|
+
// render:getMany
|
|
238
|
+
// ----------------------------------
|
|
239
|
+
{
|
|
240
|
+
displayName: 'Limit',
|
|
241
|
+
name: 'limit',
|
|
242
|
+
type: 'number',
|
|
243
|
+
typeOptions: {
|
|
244
|
+
minValue: 1,
|
|
245
|
+
maxValue: 100,
|
|
246
|
+
},
|
|
247
|
+
default: 20,
|
|
248
|
+
displayOptions: {
|
|
249
|
+
show: {
|
|
250
|
+
resource: ['render'],
|
|
251
|
+
operation: ['getMany'],
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
description: 'Max number of recent renders to return',
|
|
255
|
+
},
|
|
256
|
+
// ----------------------------------
|
|
257
|
+
// shared
|
|
258
|
+
// ----------------------------------
|
|
259
|
+
{
|
|
260
|
+
displayName: 'Webhook URL',
|
|
261
|
+
name: 'webhookUrl',
|
|
262
|
+
type: 'string',
|
|
263
|
+
default: '',
|
|
264
|
+
displayOptions: {
|
|
265
|
+
show: {
|
|
266
|
+
resource: ['render'],
|
|
267
|
+
operation: ['create', 'createFromTemplate'],
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
placeholder: 'https://your-n8n.example.com/webhook/...',
|
|
271
|
+
description: 'Optional. URL to notify with render.completed / render.failed when the render reaches a terminal state. Use the iLoveVideoEditor Trigger node to receive it.',
|
|
272
|
+
},
|
|
273
|
+
],
|
|
274
|
+
};
|
|
275
|
+
methods = {
|
|
276
|
+
loadOptions: {
|
|
277
|
+
async getTemplates() {
|
|
278
|
+
const credentials = await this.getCredentials('iloveVideoEditorApi');
|
|
279
|
+
const baseUrl = String(credentials.baseUrl || 'https://api.ilovevideoeditor.com').replace(/\/+$/, '');
|
|
280
|
+
const templates = (await this.helpers.httpRequestWithAuthentication.call(this, 'iloveVideoEditorApi', {
|
|
281
|
+
method: 'GET',
|
|
282
|
+
baseURL: baseUrl,
|
|
283
|
+
url: '/v1/templates',
|
|
284
|
+
json: true,
|
|
285
|
+
}));
|
|
286
|
+
return templates.map((template) => ({
|
|
287
|
+
name: template.name,
|
|
288
|
+
value: template.id,
|
|
289
|
+
}));
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
exports.IloveVideoEditor = IloveVideoEditor;
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.IloveVideoEditorTrigger = void 0;
|
|
4
|
+
const node_crypto_1 = require("node:crypto");
|
|
5
|
+
const SIGNATURE_TOLERANCE_SECONDS = 300;
|
|
6
|
+
function parseSignatureHeader(header) {
|
|
7
|
+
let timestamp;
|
|
8
|
+
let signature;
|
|
9
|
+
for (const part of header.split(',')) {
|
|
10
|
+
const separatorIndex = part.indexOf('=');
|
|
11
|
+
if (separatorIndex === -1) {
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
const key = part.slice(0, separatorIndex).trim();
|
|
15
|
+
const value = part.slice(separatorIndex + 1).trim();
|
|
16
|
+
if (key === 't') {
|
|
17
|
+
timestamp = value;
|
|
18
|
+
}
|
|
19
|
+
else if (key === 'v1') {
|
|
20
|
+
signature = value;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
if (!timestamp || !signature) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
return { timestamp, signature };
|
|
27
|
+
}
|
|
28
|
+
function verifySignature(signatureHeader, secret, rawBody) {
|
|
29
|
+
const parsed = parseSignatureHeader(signatureHeader);
|
|
30
|
+
if (!parsed) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
const { timestamp, signature } = parsed;
|
|
34
|
+
if (!/^\d+$/.test(timestamp) || !/^[0-9a-fA-F]{64}$/.test(signature)) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
const ageSeconds = Math.abs(Date.now() / 1000 - Number(timestamp));
|
|
38
|
+
if (ageSeconds > SIGNATURE_TOLERANCE_SECONDS) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
const expected = (0, node_crypto_1.createHmac)('sha256', secret)
|
|
42
|
+
.update(`${timestamp}.${rawBody}`)
|
|
43
|
+
.digest('hex');
|
|
44
|
+
const expectedBuffer = Buffer.from(expected, 'hex');
|
|
45
|
+
const receivedBuffer = Buffer.from(signature, 'hex');
|
|
46
|
+
if (expectedBuffer.length !== receivedBuffer.length) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
return (0, node_crypto_1.timingSafeEqual)(expectedBuffer, receivedBuffer);
|
|
50
|
+
}
|
|
51
|
+
function headerValue(headers, name) {
|
|
52
|
+
const value = headers[name];
|
|
53
|
+
if (Array.isArray(value)) {
|
|
54
|
+
return typeof value[0] === 'string' ? value[0] : undefined;
|
|
55
|
+
}
|
|
56
|
+
return typeof value === 'string' ? value : undefined;
|
|
57
|
+
}
|
|
58
|
+
class IloveVideoEditorTrigger {
|
|
59
|
+
description = {
|
|
60
|
+
displayName: 'iLoveVideoEditor Trigger',
|
|
61
|
+
name: 'iloveVideoEditorTrigger',
|
|
62
|
+
icon: 'file:ilovevideoeditor.svg',
|
|
63
|
+
group: ['trigger'],
|
|
64
|
+
version: 1,
|
|
65
|
+
description: 'Starts the workflow when an iLoveVideoEditor render completes or fails',
|
|
66
|
+
defaults: {
|
|
67
|
+
name: 'iLoveVideoEditor Trigger',
|
|
68
|
+
},
|
|
69
|
+
inputs: [],
|
|
70
|
+
outputs: ['main'],
|
|
71
|
+
credentials: [
|
|
72
|
+
{
|
|
73
|
+
name: 'iloveVideoEditorApi',
|
|
74
|
+
required: true,
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
webhooks: [
|
|
78
|
+
{
|
|
79
|
+
name: 'default',
|
|
80
|
+
httpMethod: 'POST',
|
|
81
|
+
responseMode: 'onReceived',
|
|
82
|
+
path: 'webhook',
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
properties: [
|
|
86
|
+
{
|
|
87
|
+
displayName: 'Events',
|
|
88
|
+
name: 'events',
|
|
89
|
+
type: 'multiOptions',
|
|
90
|
+
options: [
|
|
91
|
+
{
|
|
92
|
+
name: 'Render Completed',
|
|
93
|
+
value: 'render.completed',
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
name: 'Render Failed',
|
|
97
|
+
value: 'render.failed',
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
default: ['render.completed', 'render.failed'],
|
|
101
|
+
required: true,
|
|
102
|
+
description: 'Render events that start this workflow. Other events are acknowledged but ignored.',
|
|
103
|
+
},
|
|
104
|
+
],
|
|
105
|
+
};
|
|
106
|
+
webhookMethods = {
|
|
107
|
+
default: {
|
|
108
|
+
async checkExists() {
|
|
109
|
+
const webhookUrl = this.getNodeWebhookUrl('default');
|
|
110
|
+
if (!webhookUrl) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
const credentials = await this.getCredentials('iloveVideoEditorApi');
|
|
114
|
+
const baseUrl = String(credentials.baseUrl || 'https://api.ilovevideoeditor.com').replace(/\/+$/, '');
|
|
115
|
+
const subscriptions = (await this.helpers.httpRequestWithAuthentication.call(this, 'iloveVideoEditorApi', {
|
|
116
|
+
method: 'GET',
|
|
117
|
+
baseURL: baseUrl,
|
|
118
|
+
url: '/v1/webhooks',
|
|
119
|
+
json: true,
|
|
120
|
+
}));
|
|
121
|
+
return subscriptions.some((subscription) => subscription.url === webhookUrl);
|
|
122
|
+
},
|
|
123
|
+
async create() {
|
|
124
|
+
const webhookUrl = this.getNodeWebhookUrl('default');
|
|
125
|
+
if (!webhookUrl) {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
const events = this.getNodeParameter('events');
|
|
129
|
+
const credentials = await this.getCredentials('iloveVideoEditorApi');
|
|
130
|
+
const baseUrl = String(credentials.baseUrl || 'https://api.ilovevideoeditor.com').replace(/\/+$/, '');
|
|
131
|
+
const subscription = (await this.helpers.httpRequestWithAuthentication.call(this, 'iloveVideoEditorApi', {
|
|
132
|
+
method: 'POST',
|
|
133
|
+
baseURL: baseUrl,
|
|
134
|
+
url: '/v1/webhooks',
|
|
135
|
+
body: {
|
|
136
|
+
url: webhookUrl,
|
|
137
|
+
events,
|
|
138
|
+
},
|
|
139
|
+
json: true,
|
|
140
|
+
}));
|
|
141
|
+
const webhookData = this.getWorkflowStaticData('node');
|
|
142
|
+
webhookData.webhookId = subscription.id;
|
|
143
|
+
return true;
|
|
144
|
+
},
|
|
145
|
+
async delete() {
|
|
146
|
+
const webhookData = this.getWorkflowStaticData('node');
|
|
147
|
+
if (!webhookData.webhookId) {
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
const credentials = await this.getCredentials('iloveVideoEditorApi');
|
|
151
|
+
const baseUrl = String(credentials.baseUrl || 'https://api.ilovevideoeditor.com').replace(/\/+$/, '');
|
|
152
|
+
try {
|
|
153
|
+
await this.helpers.httpRequestWithAuthentication.call(this, 'iloveVideoEditorApi', {
|
|
154
|
+
method: 'DELETE',
|
|
155
|
+
baseURL: baseUrl,
|
|
156
|
+
url: `/v1/webhooks/${webhookData.webhookId}`,
|
|
157
|
+
json: true,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
delete webhookData.webhookId;
|
|
164
|
+
return true;
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
async webhook() {
|
|
169
|
+
const bodyData = this.getBodyData();
|
|
170
|
+
const headers = this.getHeaderData();
|
|
171
|
+
// Ignore events the user did not subscribe to in this node.
|
|
172
|
+
const selectedEvents = this.getNodeParameter('events');
|
|
173
|
+
const event = headerValue(headers, 'x-ilve-event') ??
|
|
174
|
+
bodyData.event;
|
|
175
|
+
if (event && !selectedEvents.includes(event)) {
|
|
176
|
+
return { noWebhookResponse: true };
|
|
177
|
+
}
|
|
178
|
+
// Verify the HMAC signature when a webhook secret is configured on the credential.
|
|
179
|
+
const credentials = await this.getCredentials('iloveVideoEditorApi');
|
|
180
|
+
const webhookSecret = credentials.webhookSecret;
|
|
181
|
+
if (webhookSecret) {
|
|
182
|
+
const signatureHeader = headerValue(headers, 'x-ilve-signature');
|
|
183
|
+
const request = this.getRequestObject();
|
|
184
|
+
const rawBody = typeof request.rawBody === 'string'
|
|
185
|
+
? request.rawBody
|
|
186
|
+
: (request.rawBody?.toString('utf8') ?? JSON.stringify(bodyData));
|
|
187
|
+
if (!signatureHeader ||
|
|
188
|
+
!verifySignature(signatureHeader, webhookSecret, rawBody)) {
|
|
189
|
+
return { noWebhookResponse: true };
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
workflowData: [[{ json: bodyData }]],
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
exports.IloveVideoEditorTrigger = IloveVideoEditorTrigger;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60" fill="none">
|
|
2
|
+
<title>iLoveVideoEditor</title>
|
|
3
|
+
<rect width="60" height="60" rx="14" fill="#6D28D9"/>
|
|
4
|
+
<path d="M24 20.6v18.8c0 1.2 1.3 1.9 2.3 1.3l14.9-9.4c1-.6 1-2.1 0-2.7l-14.9-9.4c-1-.6-2.3.1-2.3 1.4Z" fill="#FFFFFF"/>
|
|
5
|
+
<path d="M43.5 41.5c-1.2-3-4.5-4.6-7.3-3.5-1.3.5-2.3 1.5-2.9 2.7-.6-1.2-1.6-2.2-2.9-2.7-2.8-1.1-6.1.5-7.3 3.5-.6 1.5-.4 3.2.6 4.6 2.1 3 6 6.1 9 7.9.4.2.8.2 1.2 0 3-1.8 6.9-4.9 9-7.9 1-1.4 1.2-3.1.6-4.6Z" fill="#F472B6" transform="translate(28.5 25) scale(0.5)"/>
|
|
6
|
+
</svg>
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "n8n-nodes-ilovevideoeditor",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "n8n community nodes for the iLoveVideoEditor API — generate videos from templates or VideoJSON and receive render webhooks",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"homepage": "https://ilovevideoeditor.com",
|
|
7
|
+
"author": {
|
|
8
|
+
"name": "iLoveVideoEditor"
|
|
9
|
+
},
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "https://github.com/AndreiMMM/video-frames",
|
|
13
|
+
"directory": "integrations/n8n-nodes-ilovevideoeditor"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"n8n-community-node-package",
|
|
17
|
+
"n8n",
|
|
18
|
+
"video",
|
|
19
|
+
"rendering",
|
|
20
|
+
"ai"
|
|
21
|
+
],
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=18.10.0"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist"
|
|
27
|
+
],
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "tsc && npm run copy-icons",
|
|
30
|
+
"copy-icons": "node scripts/copy-icons.mjs",
|
|
31
|
+
"dev": "tsc --watch",
|
|
32
|
+
"lint": "tsc --noEmit",
|
|
33
|
+
"prepublishOnly": "npm run lint && npm run build"
|
|
34
|
+
},
|
|
35
|
+
"n8n": {
|
|
36
|
+
"n8nNodesApiVersion": 1,
|
|
37
|
+
"credentials": [
|
|
38
|
+
"dist/credentials/IloveVideoEditorApi.credentials.js"
|
|
39
|
+
],
|
|
40
|
+
"nodes": [
|
|
41
|
+
"dist/nodes/IloveVideoEditor/IloveVideoEditor.node.js",
|
|
42
|
+
"dist/nodes/IloveVideoEditor/IloveVideoEditorTrigger.node.js"
|
|
43
|
+
]
|
|
44
|
+
},
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"n8n-workflow": "*"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@types/node": "^22.0.0",
|
|
50
|
+
"n8n-workflow": "^2.16.0",
|
|
51
|
+
"typescript": "^5.6.0"
|
|
52
|
+
}
|
|
53
|
+
}
|