n8n-nodes-elearning-magic 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/LICENSE +21 -0
- package/README.md +68 -0
- package/dist/credentials/ElearningMagicApi.credentials.d.ts +7 -0
- package/dist/credentials/ElearningMagicApi.credentials.js +23 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +7 -0
- package/dist/nodes/ElearningMagic/ElearningMagicTrigger.node.d.ts +5 -0
- package/dist/nodes/ElearningMagic/ElearningMagicTrigger.node.js +192 -0
- package/package.json +62 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Discover eLearning Ltd
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# n8n-nodes-elearning-magic
|
|
2
|
+
|
|
3
|
+
Community node that receives signed payloads from the eLearning Magic SCORM wrapper. It exposes a lightweight webhook trigger that validates the `X-EM-Signature` HMAC header (SHA-256) and optionally enforces the `X-EM-Node-Id` header before emitting the request body into your workflow.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
- Webhook trigger dedicated to eLearning Magic course events
|
|
7
|
+
- HMAC validation of inbound payloads using your signing secret
|
|
8
|
+
- Optional `X-EM-Node-Id` enforcement so each connection can stay isolated
|
|
9
|
+
- Emits body, query, headers, and optional raw body for debugging
|
|
10
|
+
- Customizable HTTP response body/status for Storyline debugging
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
From your n8n root (typically `~/.n8n`):
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install n8n-nodes-elearning-magic
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Restart n8n and enable Community Nodes if prompted.
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
1. Add the **eLearning Magic Trigger** node to a workflow.
|
|
23
|
+
2. Set a **Webhook Path** (e.g., `elearning-magic` or something unique per connection).
|
|
24
|
+
3. (Recommended) Set a **Node ID** and enable **Enforce Node ID Match**. This value must match the `X-EM-Node-Id` header sent by the eLearning Magic app.
|
|
25
|
+
4. Configure the **eLearning Magic Signing Secret** credential with your chosen secret. This secret signs incoming payloads; the app will use it to produce `X-EM-Signature`.
|
|
26
|
+
5. Copy the **Webhook URL** shown in the node’s “Webhook URLs” panel and paste it into the eLearning Magic app when creating an n8n connection. Also provide the Node ID and signing secret there.
|
|
27
|
+
6. Save the workflow and activate it. Incoming Storyline triggers will appear as items with `body`, `query`, `headers`, and `meta` (signature status, nodeId match, timestamp). Enable “Include Raw Body” for debugging HMAC issues.
|
|
28
|
+
|
|
29
|
+
## Expected request format
|
|
30
|
+
- Method: `POST`
|
|
31
|
+
- Headers:
|
|
32
|
+
- `X-EM-Signature`: HMAC-SHA256 of the raw request body using the configured signing secret.
|
|
33
|
+
- `X-EM-Node-Id` (optional): Must match the configured Node ID when enforcement is enabled.
|
|
34
|
+
- Body: JSON payload forwarded by the eLearning Magic app, e.g.:
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
{
|
|
38
|
+
"connectionId": "abc-123",
|
|
39
|
+
"projectId": "storyline-project-id",
|
|
40
|
+
"wrapperVersion": "1.0.17",
|
|
41
|
+
"meta": {
|
|
42
|
+
"learnerId": "jane.doe",
|
|
43
|
+
"learnerName": "Jane Doe",
|
|
44
|
+
"launchDomain": "lms.example.com",
|
|
45
|
+
"attemptId": "attempt-001",
|
|
46
|
+
"receivedAt": "2025-01-01T12:34:56Z"
|
|
47
|
+
},
|
|
48
|
+
"fields": {
|
|
49
|
+
"score": 92,
|
|
50
|
+
"lessonStatus": "complete",
|
|
51
|
+
"learnerEmail": "jane@example.com"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The node relays the full body; you can route on any property inside your workflow.
|
|
57
|
+
|
|
58
|
+
## Response customization
|
|
59
|
+
- **Response Code**: HTTP status returned to the caller (default `200`).
|
|
60
|
+
- **Response Body**: JSON string or plain text returned to the caller (default `{"received":true}`).
|
|
61
|
+
|
|
62
|
+
## Development
|
|
63
|
+
```bash
|
|
64
|
+
npm install
|
|
65
|
+
npm run build
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Publish to npm with `npm publish` once you are happy with the build output in `dist/`.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ElearningMagicApi = void 0;
|
|
4
|
+
class ElearningMagicApi {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.name = 'elearningMagicApi';
|
|
7
|
+
this.displayName = 'eLearning Magic Signing Secret';
|
|
8
|
+
this.documentationUrl = 'https://github.com/discoverelearning/n8n-nodes-elearning-magic';
|
|
9
|
+
this.properties = [
|
|
10
|
+
{
|
|
11
|
+
displayName: 'Signing Secret',
|
|
12
|
+
name: 'signingSecret',
|
|
13
|
+
type: 'string',
|
|
14
|
+
typeOptions: {
|
|
15
|
+
password: true,
|
|
16
|
+
},
|
|
17
|
+
default: '',
|
|
18
|
+
description: 'HMAC SHA-256 secret to verify the X-EM-Signature header from eLearning Magic',
|
|
19
|
+
},
|
|
20
|
+
];
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
exports.ElearningMagicApi = ElearningMagicApi;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { ElearningMagicTrigger } from './nodes/ElearningMagic/ElearningMagicTrigger.node';
|
|
2
|
+
import { ElearningMagicApi } from './credentials/ElearningMagicApi.credentials';
|
|
3
|
+
export declare const nodes: (typeof ElearningMagicTrigger)[];
|
|
4
|
+
export declare const credentials: (typeof ElearningMagicApi)[];
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.credentials = exports.nodes = void 0;
|
|
4
|
+
const ElearningMagicTrigger_node_1 = require("./nodes/ElearningMagic/ElearningMagicTrigger.node");
|
|
5
|
+
const ElearningMagicApi_credentials_1 = require("./credentials/ElearningMagicApi.credentials");
|
|
6
|
+
exports.nodes = [ElearningMagicTrigger_node_1.ElearningMagicTrigger];
|
|
7
|
+
exports.credentials = [ElearningMagicApi_credentials_1.ElearningMagicApi];
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { INodeType, INodeTypeDescription, IWebhookFunctions, IWebhookResponseData } from 'n8n-workflow';
|
|
2
|
+
export declare class ElearningMagicTrigger implements INodeType {
|
|
3
|
+
description: INodeTypeDescription;
|
|
4
|
+
webhook(this: IWebhookFunctions): Promise<IWebhookResponseData>;
|
|
5
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ElearningMagicTrigger = void 0;
|
|
4
|
+
const crypto_1 = require("crypto");
|
|
5
|
+
const computeSignature = (secret, rawBody) => {
|
|
6
|
+
return (0, crypto_1.createHmac)('sha256', secret).update(rawBody).digest('hex');
|
|
7
|
+
};
|
|
8
|
+
const safeCompare = (a, b) => {
|
|
9
|
+
if (!a || !b || a.length !== b.length) {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
try {
|
|
13
|
+
return (0, crypto_1.timingSafeEqual)(Buffer.from(a), Buffer.from(b));
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
class ElearningMagicTrigger {
|
|
20
|
+
constructor() {
|
|
21
|
+
this.description = {
|
|
22
|
+
displayName: 'eLearning Magic Trigger',
|
|
23
|
+
name: 'elearningMagicTrigger',
|
|
24
|
+
icon: 'fa:magic',
|
|
25
|
+
group: ['trigger'],
|
|
26
|
+
version: 1,
|
|
27
|
+
description: 'Receive SCORM wrapper payloads from eLearning Magic via a signed webhook',
|
|
28
|
+
defaults: {
|
|
29
|
+
name: 'eLearning Magic Trigger',
|
|
30
|
+
},
|
|
31
|
+
subtitle: '={{$parameter["path"]}}',
|
|
32
|
+
inputs: [],
|
|
33
|
+
outputs: ['main'],
|
|
34
|
+
credentials: [
|
|
35
|
+
{
|
|
36
|
+
name: 'elearningMagicApi',
|
|
37
|
+
required: false,
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
webhooks: [
|
|
41
|
+
{
|
|
42
|
+
name: 'default',
|
|
43
|
+
httpMethod: 'POST',
|
|
44
|
+
responseMode: 'onReceived',
|
|
45
|
+
path: '={{$parameter["path"]}}',
|
|
46
|
+
restartWebhook: true,
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
properties: [
|
|
50
|
+
{
|
|
51
|
+
displayName: 'Webhook Path',
|
|
52
|
+
name: 'path',
|
|
53
|
+
type: 'string',
|
|
54
|
+
default: 'elearning-magic',
|
|
55
|
+
description: 'Unique path segment to receive data on (visible in the Webhook URLs panel)',
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
displayName: 'Node ID (optional)',
|
|
59
|
+
name: 'nodeId',
|
|
60
|
+
type: 'string',
|
|
61
|
+
default: '',
|
|
62
|
+
description: 'If set, the trigger expects the X-EM-Node-Id header to match this value',
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
displayName: 'Enforce Node ID Match',
|
|
66
|
+
name: 'enforceNodeId',
|
|
67
|
+
type: 'boolean',
|
|
68
|
+
default: false,
|
|
69
|
+
description: 'Reject requests whose X-EM-Node-Id header does not match the configured Node ID',
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
displayName: 'Require Signature',
|
|
73
|
+
name: 'requireSignature',
|
|
74
|
+
type: 'boolean',
|
|
75
|
+
default: true,
|
|
76
|
+
description: 'Reject requests without a valid X-EM-Signature HMAC (SHA-256) header',
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
displayName: 'Include Raw Body',
|
|
80
|
+
name: 'includeRawBody',
|
|
81
|
+
type: 'boolean',
|
|
82
|
+
default: false,
|
|
83
|
+
description: 'Attach the raw request body to the emitted item for debugging',
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
displayName: 'Response Code',
|
|
87
|
+
name: 'responseCode',
|
|
88
|
+
type: 'number',
|
|
89
|
+
default: 200,
|
|
90
|
+
typeOptions: {
|
|
91
|
+
minValue: 100,
|
|
92
|
+
maxValue: 599,
|
|
93
|
+
},
|
|
94
|
+
description: 'HTTP status returned to the caller after processing',
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
displayName: 'Response Body',
|
|
98
|
+
name: 'responseBody',
|
|
99
|
+
type: 'string',
|
|
100
|
+
typeOptions: {
|
|
101
|
+
rows: 3,
|
|
102
|
+
},
|
|
103
|
+
default: '{"received":true}',
|
|
104
|
+
description: 'JSON string or plain text to return to the caller',
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
async webhook() {
|
|
110
|
+
const req = this.getRequestObject();
|
|
111
|
+
const res = this.getResponseObject();
|
|
112
|
+
const nodeId = (this.getNodeParameter('nodeId') || '').trim();
|
|
113
|
+
const enforceNodeId = this.getNodeParameter('enforceNodeId');
|
|
114
|
+
const requireSignature = this.getNodeParameter('requireSignature');
|
|
115
|
+
const includeRawBody = this.getNodeParameter('includeRawBody');
|
|
116
|
+
const responseCode = this.getNodeParameter('responseCode');
|
|
117
|
+
const responseBodyParam = this.getNodeParameter('responseBody');
|
|
118
|
+
const credentials = await this.getCredentials('elearningMagicApi');
|
|
119
|
+
const signingSecret = (credentials === null || credentials === void 0 ? void 0 : credentials.signingSecret) || '';
|
|
120
|
+
const rawBody = req.rawBody !== undefined
|
|
121
|
+
? req.rawBody.toString('utf8')
|
|
122
|
+
: JSON.stringify(req.body || {});
|
|
123
|
+
const headerSignature = req.headers['x-em-signature'] || '';
|
|
124
|
+
const headerNodeId = req.headers['x-em-node-id'] || '';
|
|
125
|
+
let signatureValid = false;
|
|
126
|
+
if (headerSignature && signingSecret) {
|
|
127
|
+
const computed = computeSignature(signingSecret, rawBody);
|
|
128
|
+
signatureValid = safeCompare(computed, headerSignature);
|
|
129
|
+
}
|
|
130
|
+
if (requireSignature) {
|
|
131
|
+
if (!signingSecret) {
|
|
132
|
+
res.status(401);
|
|
133
|
+
return {
|
|
134
|
+
webhookResponse: { error: 'Signing secret is not configured on this node' },
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
if (!headerSignature || !signatureValid) {
|
|
138
|
+
res.status(403);
|
|
139
|
+
return {
|
|
140
|
+
webhookResponse: { error: 'Invalid or missing X-EM-Signature header' },
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
let nodeIdMatched = true;
|
|
145
|
+
if (enforceNodeId) {
|
|
146
|
+
nodeIdMatched = !!nodeId && !!headerNodeId && nodeId === headerNodeId;
|
|
147
|
+
if (!nodeIdMatched) {
|
|
148
|
+
res.status(403);
|
|
149
|
+
return {
|
|
150
|
+
webhookResponse: { error: 'X-EM-Node-Id header did not match the configured Node ID' },
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
else if (nodeId) {
|
|
155
|
+
nodeIdMatched = headerNodeId ? nodeId === headerNodeId : true;
|
|
156
|
+
}
|
|
157
|
+
const payload = {
|
|
158
|
+
json: {
|
|
159
|
+
body: req.body,
|
|
160
|
+
query: req.query,
|
|
161
|
+
headers: req.headers,
|
|
162
|
+
meta: {
|
|
163
|
+
receivedAt: new Date().toISOString(),
|
|
164
|
+
signatureValid: requireSignature ? signatureValid : null,
|
|
165
|
+
nodeIdMatched,
|
|
166
|
+
webhookPath: req.url,
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
if (includeRawBody) {
|
|
171
|
+
payload.json.rawBody = rawBody;
|
|
172
|
+
}
|
|
173
|
+
let responseBody = responseBodyParam;
|
|
174
|
+
if (typeof responseBodyParam === 'string') {
|
|
175
|
+
const trimmed = responseBodyParam.trim();
|
|
176
|
+
if ((trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']'))) {
|
|
177
|
+
try {
|
|
178
|
+
responseBody = JSON.parse(trimmed);
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
responseBody = responseBodyParam;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
res.status(responseCode || 200);
|
|
186
|
+
return {
|
|
187
|
+
webhookResponse: responseBody,
|
|
188
|
+
workflowData: [this.helpers.returnJsonArray([payload.json])],
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
exports.ElearningMagicTrigger = ElearningMagicTrigger;
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "n8n-nodes-elearning-magic",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "n8n community node for receiving signed payloads from the eLearning Magic SCORM wrapper",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"n8n-community-node",
|
|
7
|
+
"elearning",
|
|
8
|
+
"storyline",
|
|
9
|
+
"scorm",
|
|
10
|
+
"webhook"
|
|
11
|
+
],
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"author": {
|
|
14
|
+
"name": "Discover eLearning Ltd",
|
|
15
|
+
"email": "support@discoverelearning.com"
|
|
16
|
+
},
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "https://github.com/discoverelearning/n8n-nodes-elearning-magic.git"
|
|
20
|
+
},
|
|
21
|
+
"main": "dist/index.js",
|
|
22
|
+
"types": "dist/index.d.ts",
|
|
23
|
+
"files": [
|
|
24
|
+
"dist",
|
|
25
|
+
"README.md",
|
|
26
|
+
"LICENSE"
|
|
27
|
+
],
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "tsc",
|
|
30
|
+
"prepare": "npm run build"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/express": "^4.17.21",
|
|
34
|
+
"@types/node": "^22.10.2",
|
|
35
|
+
"@types/ssh2": "^1.11.15",
|
|
36
|
+
"nock": "^13.4.0",
|
|
37
|
+
"typescript": "^5.6.3"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"n8n-core": "^1.0.0",
|
|
41
|
+
"n8n-workflow": "^1.0.0"
|
|
42
|
+
},
|
|
43
|
+
"overrides": {
|
|
44
|
+
"form-data": "^4.0.4"
|
|
45
|
+
},
|
|
46
|
+
"engines": {
|
|
47
|
+
"node": ">=16.0.0"
|
|
48
|
+
},
|
|
49
|
+
"n8n": {
|
|
50
|
+
"n8nNodesApiVersion": 1,
|
|
51
|
+
"nodes": [
|
|
52
|
+
{
|
|
53
|
+
"type": "dist/nodes/ElearningMagic/ElearningMagicTrigger.node.js"
|
|
54
|
+
}
|
|
55
|
+
],
|
|
56
|
+
"credentials": [
|
|
57
|
+
{
|
|
58
|
+
"type": "dist/credentials/ElearningMagicApi.credentials.js"
|
|
59
|
+
}
|
|
60
|
+
]
|
|
61
|
+
}
|
|
62
|
+
}
|