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 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,7 @@
1
+ import type { ICredentialType, INodeProperties } from 'n8n-workflow';
2
+ export declare class ElearningMagicApi implements ICredentialType {
3
+ name: string;
4
+ displayName: string;
5
+ documentationUrl: string;
6
+ properties: INodeProperties[];
7
+ }
@@ -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;
@@ -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
+ }