@docspring/node-red-docspring 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.md ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 DocSpring, Inc.
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,61 @@
1
+ # @docspring/node-red-docspring
2
+
3
+ [Node-RED](https://nodered.org) nodes for [DocSpring](https://docspring.com) — turn
4
+ structured data into filled, downloadable, and **signable** PDFs from your flows.
5
+
6
+ ## Install
7
+
8
+ From the Node-RED editor: **Menu → Manage palette → Install**, search for
9
+ `@docspring/node-red-docspring`. Or from your Node-RED user directory:
10
+
11
+ ```bash
12
+ cd ~/.node-red
13
+ npm install @docspring/node-red-docspring
14
+ ```
15
+
16
+ Restart Node-RED. The **DocSpring** node appears in the palette.
17
+
18
+ ## Credentials
19
+
20
+ Add a **DocSpring** configuration (on the node, or via the config-nodes list):
21
+ choose your **Region** (US / EU / Self-hosted), and paste the **Token ID** and
22
+ **Token Secret** from the DocSpring web app (**Settings → API Tokens**). Click
23
+ **Test connection** to confirm it works. Credentials are stored encrypted by
24
+ Node-RED and never leave your instance.
25
+
26
+ ## Usage
27
+
28
+ Drop a **DocSpring** node into a flow, pick an **Operation**, and pass its parameters
29
+ in `msg.payload`. The API response is written back to `msg.payload`; errors are sent
30
+ to a **Catch** node. You can override the operation per message with `msg.operation`.
31
+
32
+ | Operation | `msg.payload` in | out |
33
+ |---|---|---|
34
+ | **Generate PDF** | `{ template_id, data: {…}, test?, password?, expires_in?, … }` | the processed submission (+ `download_url`) |
35
+ | **Combine PDFs** | `{ source_pdfs: [{ type, id\|url, template_version? }], … }` | the combined submission |
36
+ | **Create Data Request** | `{ template_id, data_requests: [{ email, name?, fields?, auth_type? }], … }` | submission + a `signing_url` per recipient |
37
+ | **Create Signing Link** | `{ data_request_id, token_type?: "email"\|"api" }` | the `signing_url` |
38
+ | **Find Template** | `{ query?, limit? }` | array of templates |
39
+ | **Find Submission** | `{ submission_id }` or `{ type?, created_after?, created_before?, limit? }` | submission(s) |
40
+
41
+ ### Example — generate a PDF
42
+
43
+ An **inject** node with this payload, wired into a **DocSpring** node (Operation:
44
+ *Generate PDF*):
45
+
46
+ ```json
47
+ { "template_id": "tpl_xxxxxxxxxxxxxxxxxxxx",
48
+ "data": { "first_name": "Jane", "last_name": "Doe" },
49
+ "test": true }
50
+ ```
51
+
52
+ `msg.payload` then contains the submission, including `download_url`.
53
+
54
+ ## Resources
55
+
56
+ - [DocSpring documentation](https://docspring.com/docs)
57
+ - [Node-RED community nodes](https://flows.nodered.org)
58
+
59
+ ## License
60
+
61
+ [MIT](LICENSE.md)
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 325 375" width="325" height="375">
2
+ <path fill="#3C8EE0" d="M82.7708709,9.04533917 C86.4749789,5.03737303 89.622499,2.53280823 92.2134313,1.53164477 C95.1943105,0.379801744 98.0002807,-0.12237578 100.631342,0.0251121959 L107,0.0251121959 L325.000002,0 L325.000002,337.5 C325.000002,358.210678 308.21068,375 287.500002,375 L0,375 L0.048844333,110 L0.048844333,105.626194 C-0.125483604,104.504595 0.16682749,102.922738 0.925777616,100.880623 C1.68472774,98.8385079 3.42554643,96.4605452 6.14823367,93.7467348 L82.7708709,9.04533917 Z"/>
3
+ </svg>
@@ -0,0 +1,61 @@
1
+ 'use strict';
2
+
3
+ const { resolveBaseUrl } = require('./regions');
4
+
5
+ // Authenticated DocSpring API request. `conn` = { region, customHost, tokenId,
6
+ // tokenSecret }. Uses the global fetch (Node 18+, which Node-RED 3.1+/4 require) so
7
+ // the package ships with zero runtime dependencies. Surfaces DocSpring's
8
+ // `{ status: 'error', errors: [...] }` payloads (and non-2xx) as a thrown Error.
9
+ async function docSpringRequest(conn, method, path, options) {
10
+ options = options || {};
11
+ const base = resolveBaseUrl(conn.region, conn.customHost, options.sync);
12
+ const url = new URL(base + '/api/v1' + path);
13
+
14
+ if (options.qs) {
15
+ for (const [k, v] of Object.entries(options.qs)) {
16
+ if (v !== undefined && v !== null && v !== '') url.searchParams.set(k, v);
17
+ }
18
+ }
19
+
20
+ const headers = {
21
+ Authorization: 'Basic ' + Buffer.from(conn.tokenId + ':' + conn.tokenSecret).toString('base64'),
22
+ Accept: 'application/json',
23
+ };
24
+ let body;
25
+ if (options.body !== undefined) {
26
+ headers['Content-Type'] = 'application/json';
27
+ body = JSON.stringify(options.body);
28
+ }
29
+
30
+ const res = await fetch(url, { method, headers, body });
31
+ const text = await res.text();
32
+ let data;
33
+ try {
34
+ data = text ? JSON.parse(text) : {};
35
+ } catch (_e) {
36
+ data = text;
37
+ }
38
+
39
+ if (!res.ok || (data && typeof data === 'object' && data.status === 'error')) {
40
+ const message =
41
+ (data && Array.isArray(data.errors) && data.errors.join(', ')) ||
42
+ (data && data.error) ||
43
+ 'DocSpring API error (HTTP ' + res.status + ')';
44
+ const err = new Error(message);
45
+ err.statusCode = res.status;
46
+ err.body = data;
47
+ throw err;
48
+ }
49
+ return data;
50
+ }
51
+
52
+ // Split a comma/newline-separated list of field names into a clean array.
53
+ function parseFields(value) {
54
+ if (value === undefined || value === null || value === '') return undefined;
55
+ const arr = (Array.isArray(value) ? value : String(value).split(/[\n,]/))
56
+ .map((s) => String(s).trim())
57
+ .filter(Boolean);
58
+ return arr.length ? arr : undefined;
59
+ }
60
+
61
+ module.exports = { docSpringRequest, parseFields };
@@ -0,0 +1,155 @@
1
+ 'use strict';
2
+
3
+ const { docSpringRequest, parseFields } = require('./docspring');
4
+
5
+ // Each handler takes (conn, params) where params come from msg.payload, and returns
6
+ // the value written to msg.payload. Mirrors the Zapier/Make/n8n implementations.
7
+
8
+ async function generatePdf(conn, p) {
9
+ if (!p.template_id) throw new Error('template_id is required');
10
+ const body = { data: p.data || {}, test: p.test || false };
11
+ if (p.metadata) body.metadata = p.metadata;
12
+ if (p.password) body.password = p.password;
13
+ if (p.editable !== undefined) body.editable = p.editable;
14
+ if (p.expires_in) body.expires_in = p.expires_in;
15
+ if (p.version) body.version = p.version;
16
+ const result = await docSpringRequest(conn, 'POST', '/templates/' + p.template_id + '/submissions', {
17
+ body,
18
+ qs: { wait: true },
19
+ sync: true,
20
+ });
21
+ return result.submission || result;
22
+ }
23
+
24
+ async function combinePdfs(conn, p) {
25
+ const sourcePdfs = (p.source_pdfs || []).map((row) => {
26
+ const entry = { type: row.type || 'submission' };
27
+ if (row.type === 'url') entry.url = row.url;
28
+ else entry.id = row.id;
29
+ if (row.template_version) entry.template_version = row.template_version;
30
+ return entry;
31
+ });
32
+ const body = { source_pdfs: sourcePdfs };
33
+ if (p.password) body.password = p.password;
34
+ if (p.expires_in) body.expires_in = p.expires_in;
35
+ if (p.metadata) body.metadata = p.metadata;
36
+ const result = await docSpringRequest(conn, 'POST', '/combined_submissions', {
37
+ body,
38
+ qs: { wait: true },
39
+ sync: true,
40
+ });
41
+ return result.combined_submission || result;
42
+ }
43
+
44
+ async function createSigningLink(conn, p) {
45
+ if (!p.data_request_id) throw new Error('data_request_id is required');
46
+ const result = await docSpringRequest(conn, 'POST', '/data_requests/' + p.data_request_id + '/tokens', {
47
+ qs: { type: p.token_type || 'email' },
48
+ });
49
+ const token = result.token || result;
50
+ return { id: token.id, signing_url: token.data_request_url, expires_at: token.expires_at };
51
+ }
52
+
53
+ async function createDataRequest(conn, p) {
54
+ if (!p.template_id) throw new Error('template_id is required');
55
+ const rows = (p.data_requests || []).filter((r) => r && r.email);
56
+ if (!rows.length) throw new Error('At least one recipient with an email address is required');
57
+ const dataRequests = rows.map((r) => {
58
+ const e = { email: r.email, auth_type: r.auth_type || 'email_link' };
59
+ if (r.name) e.name = r.name;
60
+ const fields = parseFields(r.fields);
61
+ if (fields) e.fields = fields;
62
+ return e;
63
+ });
64
+ const body = { data: p.data || {}, data_requests: dataRequests, test: p.test || false };
65
+ if (p.metadata) body.metadata = p.metadata;
66
+ if (p.expires_in) body.expires_in = p.expires_in;
67
+ if (p.version) body.version = p.version;
68
+
69
+ const result = await docSpringRequest(conn, 'POST', '/templates/' + p.template_id + '/submissions', { body });
70
+ const submission = result.submission || result;
71
+ const created = submission.data_requests || [];
72
+
73
+ // Mint a 30-day email signing link per recipient (one failure shouldn't fail all).
74
+ const enriched = [];
75
+ for (const dr of created) {
76
+ let signingUrl = null;
77
+ if (dr.id && dr.state !== 'completed') {
78
+ try {
79
+ const tok = await docSpringRequest(conn, 'POST', '/data_requests/' + dr.id + '/tokens', {
80
+ qs: { type: 'email' },
81
+ });
82
+ signingUrl = (tok.token && tok.token.data_request_url) || null;
83
+ } catch (_e) {
84
+ signingUrl = null;
85
+ }
86
+ }
87
+ enriched.push(Object.assign({}, dr, { signing_url: signingUrl }));
88
+ }
89
+ const first = enriched[0] || {};
90
+ return Object.assign({}, submission, {
91
+ data_requests: enriched,
92
+ first_data_request_id: first.id || null,
93
+ first_signing_url: first.signing_url || null,
94
+ });
95
+ }
96
+
97
+ async function findTemplate(conn, p) {
98
+ const results = [];
99
+ const limit = p.limit || 20;
100
+ let page = 1;
101
+ let hasMore = true;
102
+ while (hasMore) {
103
+ const qs = { per_page: 50, page };
104
+ if (p.query) qs.query = p.query;
105
+ const batch = await docSpringRequest(conn, 'GET', '/templates', { qs });
106
+ const list = Array.isArray(batch) ? batch : [];
107
+ results.push.apply(results, list);
108
+ hasMore = list.length >= 50 && results.length < limit;
109
+ page += 1;
110
+ }
111
+ return results.slice(0, limit);
112
+ }
113
+
114
+ async function findSubmission(conn, p) {
115
+ if (p.submission_id) {
116
+ return await docSpringRequest(conn, 'GET', '/submissions/' + p.submission_id);
117
+ }
118
+ const results = [];
119
+ const limit = p.limit || 20;
120
+ let cursor;
121
+ do {
122
+ const qs = { limit: 50, include_data: true };
123
+ if (p.type) qs.type = p.type;
124
+ if (p.created_after) qs.created_after = p.created_after;
125
+ if (p.created_before) qs.created_before = p.created_before;
126
+ if (cursor) qs.cursor = cursor;
127
+ const pageData = await docSpringRequest(conn, 'GET', '/submissions', { qs });
128
+ const subs = pageData.submissions || [];
129
+ results.push.apply(results, subs);
130
+ cursor = pageData.next_cursor;
131
+ if (!subs.length) break;
132
+ } while (cursor && results.length < limit);
133
+ return results.slice(0, limit);
134
+ }
135
+
136
+ const OPERATIONS = {
137
+ generatePdf,
138
+ combinePdfs,
139
+ createDataRequest,
140
+ createSigningLink,
141
+ findTemplate,
142
+ findSubmission,
143
+ };
144
+
145
+ // Labels for the editor dropdown (value → display).
146
+ const OPERATION_LABELS = {
147
+ generatePdf: 'Generate PDF',
148
+ combinePdfs: 'Combine PDFs',
149
+ createDataRequest: 'Create Data Request',
150
+ createSigningLink: 'Create Signing Link',
151
+ findTemplate: 'Find Template',
152
+ findSubmission: 'Find Submission',
153
+ };
154
+
155
+ module.exports = { OPERATIONS, OPERATION_LABELS };
package/lib/regions.js ADDED
@@ -0,0 +1,28 @@
1
+ 'use strict';
2
+
3
+ // Region → API host. `sync` selects the low-latency host used for synchronous PDF
4
+ // generation (Generate PDF / Combine PDFs with ?wait=true). Self-hosted installs use
5
+ // a single custom origin for both.
6
+ function resolveBaseUrl(region, customHost, sync) {
7
+ region = (region || 'us').toLowerCase();
8
+
9
+ if (region === 'self_hosted') {
10
+ const host = (customHost || '').trim();
11
+ if (!host) {
12
+ throw new Error(
13
+ 'A Self-Hosted Host is required for the Self-Hosted / Enterprise region ' +
14
+ '(e.g. https://docspring.example.com).',
15
+ );
16
+ }
17
+ return host.includes('://') ? host : 'https://' + host;
18
+ }
19
+
20
+ const hosts = {
21
+ us: { host: 'api.docspring.com', sync: 'sync.api.docspring.com' },
22
+ eu: { host: 'api-eu.docspring.com', sync: 'sync.api-eu.docspring.com' },
23
+ };
24
+ const entry = hosts[region] || hosts.us;
25
+ return 'https://' + (sync ? entry.sync : entry.host);
26
+ }
27
+
28
+ module.exports = { resolveBaseUrl };
@@ -0,0 +1,96 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType('docspring-config', {
3
+ category: 'config',
4
+ defaults: {
5
+ name: { value: '' },
6
+ region: { value: 'us', required: true },
7
+ customHost: { value: '' },
8
+ },
9
+ credentials: {
10
+ tokenId: { type: 'text' },
11
+ tokenSecret: { type: 'password' },
12
+ },
13
+ label: function () {
14
+ return this.name || 'DocSpring';
15
+ },
16
+ oneditprepare: function () {
17
+ var node = this;
18
+
19
+ var toggleHost = function () {
20
+ if ($('#node-config-input-region').val() === 'self_hosted') {
21
+ $('#docspring-host-row').show();
22
+ } else {
23
+ $('#docspring-host-row').hide();
24
+ }
25
+ };
26
+ $('#node-config-input-region').on('change', toggleHost);
27
+ toggleHost();
28
+
29
+ $('#docspring-test-btn').on('click', function () {
30
+ var payload = {
31
+ region: $('#node-config-input-region').val(),
32
+ customHost: $('#node-config-input-customHost').val(),
33
+ tokenId: $('#node-config-input-tokenId').val(),
34
+ tokenSecret: $('#node-config-input-tokenSecret').val(),
35
+ };
36
+ $('#docspring-test-result').text('Testing…').css('color', '#888');
37
+ $.ajax({
38
+ url: 'docspring-config/' + (node.id || '_new') + '/test',
39
+ type: 'POST',
40
+ contentType: 'application/json',
41
+ data: JSON.stringify(payload),
42
+ success: function (res) {
43
+ $('#docspring-test-result')
44
+ .text(res.message)
45
+ .css('color', res.ok ? '#2ea44f' : '#d1242f');
46
+ },
47
+ error: function (jqXHR) {
48
+ $('#docspring-test-result')
49
+ .text('Test failed: ' + (jqXHR.responseText || jqXHR.statusText))
50
+ .css('color', '#d1242f');
51
+ },
52
+ });
53
+ });
54
+ },
55
+ });
56
+ </script>
57
+
58
+ <script type="text/html" data-template-name="docspring-config">
59
+ <div class="form-row">
60
+ <label for="node-config-input-name"><i class="fa fa-tag"></i> Name</label>
61
+ <input type="text" id="node-config-input-name" placeholder="My DocSpring account" />
62
+ </div>
63
+ <div class="form-row">
64
+ <label for="node-config-input-region"><i class="fa fa-globe"></i> Region</label>
65
+ <select id="node-config-input-region" style="width:70%">
66
+ <option value="us">United States</option>
67
+ <option value="eu">Europe</option>
68
+ <option value="self_hosted">Self-hosted / Enterprise</option>
69
+ </select>
70
+ </div>
71
+ <div class="form-row" id="docspring-host-row">
72
+ <label for="node-config-input-customHost"><i class="fa fa-server"></i> Host</label>
73
+ <input type="text" id="node-config-input-customHost" placeholder="https://docspring.example.com" />
74
+ </div>
75
+ <div class="form-row">
76
+ <label for="node-config-input-tokenId"><i class="fa fa-user"></i> Token ID</label>
77
+ <input type="text" id="node-config-input-tokenId" placeholder="api_..." />
78
+ </div>
79
+ <div class="form-row">
80
+ <label for="node-config-input-tokenSecret"><i class="fa fa-lock"></i> Token Secret</label>
81
+ <input type="password" id="node-config-input-tokenSecret" />
82
+ </div>
83
+ <div class="form-row">
84
+ <button type="button" class="red-ui-button" id="docspring-test-btn">Test connection</button>
85
+ <span id="docspring-test-result" style="margin-left:10px;"></span>
86
+ </div>
87
+ </script>
88
+
89
+ <script type="text/html" data-help-name="docspring-config">
90
+ <p>Holds your DocSpring API connection, shared by the DocSpring action nodes.</p>
91
+ <p>
92
+ Create a token in the DocSpring web app under <b>Settings → API Tokens</b>, then enter the
93
+ <b>Token ID</b> and <b>Token Secret</b> here and pick your <b>Region</b>. Use
94
+ <b>Test connection</b> to confirm the credentials work.
95
+ </p>
96
+ </script>
@@ -0,0 +1,68 @@
1
+ module.exports = function (RED) {
2
+ 'use strict';
3
+
4
+ // Config node holding the DocSpring connection: region + custom host (non-secret)
5
+ // and the API token id/secret (stored encrypted as Node-RED credentials).
6
+ function DocSpringConfigNode(n) {
7
+ RED.nodes.createNode(this, n);
8
+ this.name = n.name;
9
+ this.region = n.region || 'us';
10
+ this.customHost = n.customHost || '';
11
+ }
12
+
13
+ RED.nodes.registerType('docspring-config', DocSpringConfigNode, {
14
+ credentials: {
15
+ tokenId: { type: 'text' },
16
+ tokenSecret: { type: 'password' },
17
+ },
18
+ });
19
+
20
+ // Admin endpoint for the "Test connection" button in the config editor.
21
+ // Hits GET /authentication with the entered (or already-saved) credentials.
22
+ RED.httpAdmin.post(
23
+ '/docspring-config/:id/test',
24
+ RED.auth.needsPermission('docspring-config.read'),
25
+ function (req, res) {
26
+ const { resolveBaseUrl } = require('../lib/regions');
27
+ const body = req.body || {};
28
+ const node = RED.nodes.getNode(req.params.id);
29
+
30
+ const tokenId = body.tokenId || (node && node.credentials && node.credentials.tokenId);
31
+ const tokenSecret =
32
+ body.tokenSecret || (node && node.credentials && node.credentials.tokenSecret);
33
+ const region = body.region || (node && node.region) || 'us';
34
+ const customHost = body.customHost || (node && node.customHost) || '';
35
+
36
+ if (!tokenId || !tokenSecret) {
37
+ res.json({ ok: false, message: 'Enter a Token ID and Token Secret first.' });
38
+ return;
39
+ }
40
+
41
+ let baseUrl;
42
+ try {
43
+ baseUrl = resolveBaseUrl(region, customHost, false);
44
+ } catch (e) {
45
+ res.json({ ok: false, message: e.message });
46
+ return;
47
+ }
48
+
49
+ const headers = {
50
+ Authorization: 'Basic ' + Buffer.from(tokenId + ':' + tokenSecret).toString('base64'),
51
+ Accept: 'application/json',
52
+ };
53
+ fetch(baseUrl + '/api/v1/authentication', { headers })
54
+ .then((r) => r.json().then((d) => ({ status: r.status, d })))
55
+ .then(({ status, d }) => {
56
+ if (status === 200 && d && d.status === 'success') {
57
+ res.json({ ok: true, message: 'Connection successful.' });
58
+ } else {
59
+ res.json({
60
+ ok: false,
61
+ message: '[' + status + '] Invalid DocSpring API token. Check your ID, secret, and region.',
62
+ });
63
+ }
64
+ })
65
+ .catch((err) => res.json({ ok: false, message: err.message }));
66
+ },
67
+ );
68
+ };
@@ -0,0 +1,77 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType('docspring', {
3
+ category: 'DocSpring',
4
+ color: '#87b7e8',
5
+ defaults: {
6
+ name: { value: '' },
7
+ operation: { value: 'generatePdf', required: true },
8
+ server: { value: '', type: 'docspring-config', required: true },
9
+ },
10
+ inputs: 1,
11
+ outputs: 1,
12
+ icon: 'docspring.svg',
13
+ paletteLabel: 'DocSpring',
14
+ label: function () {
15
+ var labels = {
16
+ generatePdf: 'Generate PDF',
17
+ combinePdfs: 'Combine PDFs',
18
+ createDataRequest: 'Create Data Request',
19
+ createSigningLink: 'Create Signing Link',
20
+ findTemplate: 'Find Template',
21
+ findSubmission: 'Find Submission',
22
+ };
23
+ return this.name || 'DocSpring: ' + (labels[this.operation] || this.operation);
24
+ },
25
+ });
26
+ </script>
27
+
28
+ <script type="text/html" data-template-name="docspring">
29
+ <div class="form-row">
30
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
31
+ <input type="text" id="node-input-name" placeholder="Name" />
32
+ </div>
33
+ <div class="form-row">
34
+ <label for="node-input-server"><i class="fa fa-cloud"></i> Connection</label>
35
+ <input type="text" id="node-input-server" />
36
+ </div>
37
+ <div class="form-row">
38
+ <label for="node-input-operation"><i class="fa fa-cogs"></i> Operation</label>
39
+ <select id="node-input-operation" style="width:70%">
40
+ <option value="generatePdf">Generate PDF</option>
41
+ <option value="combinePdfs">Combine PDFs</option>
42
+ <option value="createDataRequest">Create Data Request</option>
43
+ <option value="createSigningLink">Create Signing Link</option>
44
+ <option value="findTemplate">Find Template</option>
45
+ <option value="findSubmission">Find Submission</option>
46
+ </select>
47
+ </div>
48
+ </script>
49
+
50
+ <script type="text/html" data-help-name="docspring">
51
+ <p>
52
+ Calls the <a href="https://docspring.com/docs" target="_blank">DocSpring API</a>. Choose an
53
+ <b>Operation</b>; pass its parameters in <code>msg.payload</code>. The API response is written
54
+ back to <code>msg.payload</code>. Errors are sent to a <b>Catch</b> node.
55
+ </p>
56
+ <p>You can override the configured operation per message with <code>msg.operation</code>.</p>
57
+
58
+ <h3>Generate PDF</h3>
59
+ <p><code>msg.payload = { template_id, data: { field: value, ... }, test?, metadata?, password?, editable?, expires_in?, version? }</code></p>
60
+ <p>Returns the processed submission (with <code>download_url</code>).</p>
61
+
62
+ <h3>Combine PDFs</h3>
63
+ <p><code>msg.payload = { source_pdfs: [ { type: "submission"|"template"|"combined_submission"|"custom_file"|"url", id?, url?, template_version? } ], password?, expires_in?, metadata? }</code></p>
64
+
65
+ <h3>Create Data Request</h3>
66
+ <p><code>msg.payload = { template_id, data?, data_requests: [ { email, name?, fields?, auth_type? } ], test?, expires_in?, version? }</code></p>
67
+ <p>Returns a submission awaiting data requests, plus a 30-day <code>signing_url</code> per recipient.</p>
68
+
69
+ <h3>Create Signing Link</h3>
70
+ <p><code>msg.payload = { data_request_id, token_type?: "email"|"api" }</code> → returns the <code>signing_url</code>.</p>
71
+
72
+ <h3>Find Template</h3>
73
+ <p><code>msg.payload = { query?, limit? }</code> → an array of templates.</p>
74
+
75
+ <h3>Find Submission</h3>
76
+ <p><code>msg.payload = { submission_id }</code> (single) or <code>{ type?, created_after?, created_before?, limit? }</code> (list).</p>
77
+ </script>
@@ -0,0 +1,66 @@
1
+ module.exports = function (RED) {
2
+ 'use strict';
3
+
4
+ const { OPERATIONS, OPERATION_LABELS } = require('../lib/operations');
5
+
6
+ // One action node with an Operation selector. Request parameters come from
7
+ // msg.payload (or msg.operation overrides the configured operation); the API
8
+ // response is written back to msg.payload.
9
+ function DocSpringNode(config) {
10
+ RED.nodes.createNode(this, config);
11
+ const node = this;
12
+ node.operation = config.operation;
13
+ node.server = RED.nodes.getNode(config.server);
14
+
15
+ node.on('input', function (msg, send, done) {
16
+ send =
17
+ send ||
18
+ function () {
19
+ node.send.apply(node, arguments);
20
+ };
21
+ const fail = function (err) {
22
+ node.status({ fill: 'red', shape: 'ring', text: String(err.message || err).slice(0, 20) });
23
+ if (done) done(err);
24
+ else node.error(err, msg);
25
+ };
26
+
27
+ const operation = msg.operation || node.operation;
28
+ const handler = OPERATIONS[operation];
29
+ if (!handler) {
30
+ fail(new Error('Unknown DocSpring operation: ' + operation));
31
+ return;
32
+ }
33
+ if (!node.server || !node.server.credentials || !node.server.credentials.tokenId) {
34
+ fail(new Error('DocSpring connection is not configured (set the credential).'));
35
+ return;
36
+ }
37
+
38
+ const conn = {
39
+ region: node.server.region,
40
+ customHost: node.server.customHost,
41
+ tokenId: node.server.credentials.tokenId,
42
+ tokenSecret: node.server.credentials.tokenSecret,
43
+ };
44
+ const params =
45
+ msg.payload && typeof msg.payload === 'object' && !Array.isArray(msg.payload)
46
+ ? msg.payload
47
+ : {};
48
+
49
+ node.status({ fill: 'blue', shape: 'dot', text: OPERATION_LABELS[operation] || operation });
50
+ Promise.resolve(handler(conn, params))
51
+ .then(function (result) {
52
+ msg.payload = result;
53
+ node.status({ fill: 'green', shape: 'dot', text: 'done' });
54
+ send(msg);
55
+ if (done) done();
56
+ })
57
+ .catch(fail);
58
+ });
59
+
60
+ node.on('close', function () {
61
+ node.status({});
62
+ });
63
+ }
64
+
65
+ RED.nodes.registerType('docspring', DocSpringNode);
66
+ };
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@docspring/node-red-docspring",
3
+ "version": "0.1.0",
4
+ "description": "Node-RED nodes for DocSpring — generate, combine, and sign PDFs from your templates.",
5
+ "keywords": [
6
+ "node-red",
7
+ "docspring",
8
+ "pdf",
9
+ "documents",
10
+ "esignature"
11
+ ],
12
+ "license": "MIT",
13
+ "homepage": "https://docspring.com",
14
+ "author": {
15
+ "name": "DocSpring",
16
+ "email": "support@docspring.com"
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/DocSpring/node-red_integration.git"
21
+ },
22
+ "engines": {
23
+ "node": ">=18"
24
+ },
25
+ "node-red": {
26
+ "version": ">=3.0.0",
27
+ "nodes": {
28
+ "docspring-config": "nodes/docspring-config.js",
29
+ "docspring": "nodes/docspring.js"
30
+ }
31
+ },
32
+ "scripts": {
33
+ "test": "mocha \"test/**/*_spec.js\""
34
+ },
35
+ "files": [
36
+ "nodes",
37
+ "lib",
38
+ "icons"
39
+ ],
40
+ "devDependencies": {
41
+ "mocha": "^10.7.3",
42
+ "node-red": "^4.0.9",
43
+ "node-red-node-test-helper": "^0.3.4",
44
+ "should": "^13.2.3"
45
+ }
46
+ }