lockform 1.0.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 +235 -0
- package/dist/crypto.d.ts +6 -0
- package/dist/crypto.js +64 -0
- package/dist/decrypt.d.ts +2 -0
- package/dist/decrypt.js +31 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +7 -0
- package/dist/types.d.ts +33 -0
- package/dist/types.js +2 -0
- package/dist/verify.d.ts +2 -0
- package/dist/verify.js +9 -0
- package/package.json +32 -0
package/README.md
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
# Lockform
|
|
2
|
+
|
|
3
|
+
Official SDK for processing Lockform webhook submissions with end-to-end encryption.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install lockform
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- **Decrypt webhook data**: Easily decrypt encrypted form submissions received via webhooks
|
|
14
|
+
- **Signature verification**: Verify webhook authenticity using HMAC-SHA256 signatures
|
|
15
|
+
- **Field mapping**: Automatically map field IDs to human-readable CSV names
|
|
16
|
+
- **TypeScript support**: Full type definitions included
|
|
17
|
+
|
|
18
|
+
## Quick Start
|
|
19
|
+
|
|
20
|
+
### Decrypting Webhook Data
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
import { decryptWebhookData } from 'lockform'
|
|
24
|
+
|
|
25
|
+
const privateKey = `-----BEGIN PRIVATE KEY-----
|
|
26
|
+
YOUR_PRIVATE_KEY_HERE
|
|
27
|
+
-----END PRIVATE KEY-----`
|
|
28
|
+
|
|
29
|
+
app.post('/webhook', async (req, res) => {
|
|
30
|
+
const payload = req.body
|
|
31
|
+
|
|
32
|
+
const result = await decryptWebhookData({
|
|
33
|
+
payload,
|
|
34
|
+
privateKey,
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
console.log('Mapped data:', result.mappedData)
|
|
38
|
+
|
|
39
|
+
res.json({ success: true })
|
|
40
|
+
})
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Verifying Webhook Signatures
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
import { verifyWebhookSignature, decryptWebhookData } from 'lockform'
|
|
47
|
+
|
|
48
|
+
app.post('/webhook', async (req, res) => {
|
|
49
|
+
const signature = req.headers['x-signature-sha256']
|
|
50
|
+
const webhookSecret = process.env.WEBHOOK_SECRET
|
|
51
|
+
|
|
52
|
+
const isValid = await verifyWebhookSignature({
|
|
53
|
+
payload: JSON.stringify(req.body),
|
|
54
|
+
signature,
|
|
55
|
+
secret: webhookSecret,
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
if (!isValid) {
|
|
59
|
+
return res.status(401).json({ error: 'Invalid signature' })
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const result = await decryptWebhookData({
|
|
63
|
+
payload: req.body,
|
|
64
|
+
privateKey: process.env.PRIVATE_KEY,
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
console.log('Decrypted data:', result.mappedData)
|
|
68
|
+
|
|
69
|
+
res.json({ success: true })
|
|
70
|
+
})
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## API Reference
|
|
74
|
+
|
|
75
|
+
### `decryptWebhookData(options)`
|
|
76
|
+
|
|
77
|
+
Decrypts an encrypted webhook payload from Lockform.
|
|
78
|
+
|
|
79
|
+
**Parameters:**
|
|
80
|
+
- `options.payload` (WebhookPayload): The webhook payload received from Lockform
|
|
81
|
+
- `options.privateKey` (string): Your RSA private key in PEM format
|
|
82
|
+
|
|
83
|
+
**Returns:** `Promise<DecryptedSubmission>`
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
{
|
|
87
|
+
rawData: Record<string, unknown>, // Decrypted data with field IDs as keys
|
|
88
|
+
mappedData: Record<string, unknown>, // Decrypted data with CSV names as keys
|
|
89
|
+
metadata: {
|
|
90
|
+
event_type: string,
|
|
91
|
+
submission_id: string,
|
|
92
|
+
form_id: string,
|
|
93
|
+
timestamp: string,
|
|
94
|
+
nonce: string,
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**Example:**
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
const result = await decryptWebhookData({
|
|
103
|
+
payload: webhookPayload,
|
|
104
|
+
privateKey: myPrivateKey,
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
console.log(result.mappedData)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### `verifyWebhookSignature(options)`
|
|
111
|
+
|
|
112
|
+
Verifies the HMAC-SHA256 signature of a webhook payload.
|
|
113
|
+
|
|
114
|
+
**Parameters:**
|
|
115
|
+
- `options.payload` (string): The raw JSON string of the webhook payload
|
|
116
|
+
- `options.signature` (string): The signature from the `X-Signature-SHA256` header
|
|
117
|
+
- `options.secret` (string): Your webhook secret
|
|
118
|
+
|
|
119
|
+
**Returns:** `Promise<boolean>` - `true` if the signature is valid, `false` otherwise
|
|
120
|
+
|
|
121
|
+
**Example:**
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
const isValid = await verifyWebhookSignature({
|
|
125
|
+
payload: JSON.stringify(req.body),
|
|
126
|
+
signature: req.headers['x-signature-sha256'],
|
|
127
|
+
secret: process.env.WEBHOOK_SECRET,
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
if (!isValid) {
|
|
131
|
+
throw new Error('Invalid webhook signature')
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Types
|
|
136
|
+
|
|
137
|
+
### WebhookPayload
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
interface WebhookPayload {
|
|
141
|
+
event_type: string
|
|
142
|
+
submission_id: string
|
|
143
|
+
form_id: string
|
|
144
|
+
ciphertext: string
|
|
145
|
+
iv: string
|
|
146
|
+
wrapped_key: string
|
|
147
|
+
auth_tag: string
|
|
148
|
+
algorithm: string
|
|
149
|
+
nonce: string
|
|
150
|
+
timestamp: string
|
|
151
|
+
field_mapping: Record<string, string>
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### DecryptedSubmission
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
interface DecryptedSubmission {
|
|
159
|
+
rawData: Record<string, unknown>
|
|
160
|
+
mappedData: Record<string, unknown>
|
|
161
|
+
metadata: {
|
|
162
|
+
event_type: string
|
|
163
|
+
submission_id: string
|
|
164
|
+
form_id: string
|
|
165
|
+
timestamp: string
|
|
166
|
+
nonce: string
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Complete Example
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
import express from 'express'
|
|
175
|
+
import { decryptWebhookData, verifyWebhookSignature } from 'lockform'
|
|
176
|
+
|
|
177
|
+
const app = express()
|
|
178
|
+
app.use(express.json())
|
|
179
|
+
|
|
180
|
+
const PRIVATE_KEY = process.env.LOCKFORM_PRIVATE_KEY
|
|
181
|
+
const WEBHOOK_SECRET = process.env.LOCKFORM_WEBHOOK_SECRET
|
|
182
|
+
|
|
183
|
+
app.post('/lockform-webhook', async (req, res) => {
|
|
184
|
+
try {
|
|
185
|
+
const signature = req.headers['x-signature-sha256'] as string
|
|
186
|
+
const payload = req.body
|
|
187
|
+
|
|
188
|
+
if (WEBHOOK_SECRET && signature) {
|
|
189
|
+
const isValid = await verifyWebhookSignature({
|
|
190
|
+
payload: JSON.stringify(payload),
|
|
191
|
+
signature,
|
|
192
|
+
secret: WEBHOOK_SECRET,
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
if (!isValid) {
|
|
196
|
+
return res.status(401).json({ error: 'Invalid signature' })
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const result = await decryptWebhookData({
|
|
201
|
+
payload,
|
|
202
|
+
privateKey: PRIVATE_KEY,
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
console.log('Form ID:', result.metadata.form_id)
|
|
206
|
+
console.log('Submission ID:', result.metadata.submission_id)
|
|
207
|
+
console.log('Data:', result.mappedData)
|
|
208
|
+
|
|
209
|
+
res.json({ success: true })
|
|
210
|
+
} catch (error) {
|
|
211
|
+
console.error('Error processing webhook:', error)
|
|
212
|
+
res.status(500).json({ error: 'Failed to process webhook' })
|
|
213
|
+
}
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
app.listen(3000, () => {
|
|
217
|
+
console.log('Webhook server listening on port 3000')
|
|
218
|
+
})
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## Security Best Practices
|
|
222
|
+
|
|
223
|
+
1. **Always verify signatures**: Use `verifyWebhookSignature` to ensure webhooks are genuinely from Lockform
|
|
224
|
+
2. **Keep private keys secure**: Store your private key in environment variables, never commit it to version control
|
|
225
|
+
3. **Use HTTPS**: Always use HTTPS endpoints for webhooks in production
|
|
226
|
+
4. **Validate data**: Always validate the decrypted data before processing it
|
|
227
|
+
|
|
228
|
+
## Requirements
|
|
229
|
+
|
|
230
|
+
- Node.js 18.0.0 or higher
|
|
231
|
+
- TypeScript 5.0.0 or higher (for TypeScript projects)
|
|
232
|
+
|
|
233
|
+
## License
|
|
234
|
+
|
|
235
|
+
MIT
|
package/dist/crypto.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { webcrypto } from 'node:crypto';
|
|
2
|
+
export declare function base64ToArrayBuffer(base64: string): Promise<ArrayBuffer>;
|
|
3
|
+
export declare function arrayBufferToString(buffer: ArrayBuffer): string;
|
|
4
|
+
export declare function importPrivateKey(pemKey: string): Promise<webcrypto.CryptoKey>;
|
|
5
|
+
export declare function decryptSubmission(ciphertext: string, iv: string, wrappedKey: string, authTag: string, privateKey: webcrypto.CryptoKey): Promise<string>;
|
|
6
|
+
export declare function createHmacSignature(payload: string, secret: string): Promise<string>;
|
package/dist/crypto.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.base64ToArrayBuffer = base64ToArrayBuffer;
|
|
4
|
+
exports.arrayBufferToString = arrayBufferToString;
|
|
5
|
+
exports.importPrivateKey = importPrivateKey;
|
|
6
|
+
exports.decryptSubmission = decryptSubmission;
|
|
7
|
+
exports.createHmacSignature = createHmacSignature;
|
|
8
|
+
const node_crypto_1 = require("node:crypto");
|
|
9
|
+
const crypto = node_crypto_1.webcrypto;
|
|
10
|
+
async function base64ToArrayBuffer(base64) {
|
|
11
|
+
const binaryString = Buffer.from(base64, 'base64').toString('binary');
|
|
12
|
+
const bytes = new Uint8Array(binaryString.length);
|
|
13
|
+
for (let i = 0; i < binaryString.length; i++) {
|
|
14
|
+
bytes[i] = binaryString.charCodeAt(i);
|
|
15
|
+
}
|
|
16
|
+
return bytes.buffer;
|
|
17
|
+
}
|
|
18
|
+
function arrayBufferToString(buffer) {
|
|
19
|
+
return new TextDecoder().decode(buffer);
|
|
20
|
+
}
|
|
21
|
+
async function importPrivateKey(pemKey) {
|
|
22
|
+
const pemHeader = '-----BEGIN PRIVATE KEY-----';
|
|
23
|
+
const pemFooter = '-----END PRIVATE KEY-----';
|
|
24
|
+
const pemContents = pemKey
|
|
25
|
+
.replace(pemHeader, '')
|
|
26
|
+
.replace(pemFooter, '')
|
|
27
|
+
.replace(/\s/g, '');
|
|
28
|
+
const binaryDer = await base64ToArrayBuffer(pemContents);
|
|
29
|
+
return await crypto.subtle.importKey('pkcs8', binaryDer, {
|
|
30
|
+
name: 'RSA-OAEP',
|
|
31
|
+
hash: 'SHA-256',
|
|
32
|
+
}, false, ['unwrapKey']);
|
|
33
|
+
}
|
|
34
|
+
async function decryptSubmission(ciphertext, iv, wrappedKey, authTag, privateKey) {
|
|
35
|
+
const ciphertextBuffer = await base64ToArrayBuffer(ciphertext);
|
|
36
|
+
const ivBuffer = await base64ToArrayBuffer(iv);
|
|
37
|
+
const wrappedKeyBuffer = await base64ToArrayBuffer(wrappedKey);
|
|
38
|
+
const authTagBuffer = await base64ToArrayBuffer(authTag);
|
|
39
|
+
const combinedCiphertext = new Uint8Array(ciphertextBuffer.byteLength + authTagBuffer.byteLength);
|
|
40
|
+
combinedCiphertext.set(new Uint8Array(ciphertextBuffer), 0);
|
|
41
|
+
combinedCiphertext.set(new Uint8Array(authTagBuffer), ciphertextBuffer.byteLength);
|
|
42
|
+
const unwrappedKey = await crypto.subtle.unwrapKey('raw', wrappedKeyBuffer, privateKey, {
|
|
43
|
+
name: 'RSA-OAEP',
|
|
44
|
+
hash: { name: 'SHA-256' },
|
|
45
|
+
}, {
|
|
46
|
+
name: 'AES-GCM',
|
|
47
|
+
length: 256,
|
|
48
|
+
}, false, ['decrypt']);
|
|
49
|
+
const decryptedBuffer = await crypto.subtle.decrypt({
|
|
50
|
+
name: 'AES-GCM',
|
|
51
|
+
iv: ivBuffer,
|
|
52
|
+
}, unwrappedKey, combinedCiphertext);
|
|
53
|
+
return arrayBufferToString(decryptedBuffer);
|
|
54
|
+
}
|
|
55
|
+
async function createHmacSignature(payload, secret) {
|
|
56
|
+
const encoder = new TextEncoder();
|
|
57
|
+
const keyData = encoder.encode(secret);
|
|
58
|
+
const messageData = encoder.encode(payload);
|
|
59
|
+
const cryptoKey = await crypto.subtle.importKey('raw', keyData, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
|
|
60
|
+
const signature = await crypto.subtle.sign('HMAC', cryptoKey, messageData);
|
|
61
|
+
return Array.from(new Uint8Array(signature))
|
|
62
|
+
.map(b => b.toString(16).padStart(2, '0'))
|
|
63
|
+
.join('');
|
|
64
|
+
}
|
package/dist/decrypt.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.decryptWebhookData = decryptWebhookData;
|
|
4
|
+
const crypto_1 = require("./crypto");
|
|
5
|
+
async function decryptWebhookData(options) {
|
|
6
|
+
const { payload, privateKey } = options;
|
|
7
|
+
const cryptoKey = await (0, crypto_1.importPrivateKey)(privateKey);
|
|
8
|
+
const decryptedData = await (0, crypto_1.decryptSubmission)(payload.ciphertext, payload.iv, payload.wrapped_key, payload.auth_tag, cryptoKey);
|
|
9
|
+
const rawData = JSON.parse(decryptedData);
|
|
10
|
+
const mappedData = {};
|
|
11
|
+
for (const [fieldId, value] of Object.entries(rawData)) {
|
|
12
|
+
const csvName = payload.field_mapping[fieldId];
|
|
13
|
+
if (csvName) {
|
|
14
|
+
mappedData[csvName] = value;
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
mappedData[fieldId] = value;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return {
|
|
21
|
+
rawData,
|
|
22
|
+
mappedData,
|
|
23
|
+
metadata: {
|
|
24
|
+
event_type: payload.event_type,
|
|
25
|
+
submission_id: payload.submission_id,
|
|
26
|
+
form_id: payload.form_id,
|
|
27
|
+
timestamp: payload.timestamp,
|
|
28
|
+
nonce: payload.nonce,
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.verifyWebhookSignature = exports.decryptWebhookData = void 0;
|
|
4
|
+
var decrypt_1 = require("./decrypt");
|
|
5
|
+
Object.defineProperty(exports, "decryptWebhookData", { enumerable: true, get: function () { return decrypt_1.decryptWebhookData; } });
|
|
6
|
+
var verify_1 = require("./verify");
|
|
7
|
+
Object.defineProperty(exports, "verifyWebhookSignature", { enumerable: true, get: function () { return verify_1.verifyWebhookSignature; } });
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export interface WebhookPayload {
|
|
2
|
+
event_type: string;
|
|
3
|
+
submission_id: string;
|
|
4
|
+
form_id: string;
|
|
5
|
+
ciphertext: string;
|
|
6
|
+
iv: string;
|
|
7
|
+
wrapped_key: string;
|
|
8
|
+
auth_tag: string;
|
|
9
|
+
algorithm: string;
|
|
10
|
+
nonce: string;
|
|
11
|
+
timestamp: string;
|
|
12
|
+
field_mapping: Record<string, string>;
|
|
13
|
+
}
|
|
14
|
+
export interface DecryptedSubmission {
|
|
15
|
+
rawData: Record<string, unknown>;
|
|
16
|
+
mappedData: Record<string, unknown>;
|
|
17
|
+
metadata: {
|
|
18
|
+
event_type: string;
|
|
19
|
+
submission_id: string;
|
|
20
|
+
form_id: string;
|
|
21
|
+
timestamp: string;
|
|
22
|
+
nonce: string;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export interface DecryptWebhookOptions {
|
|
26
|
+
payload: WebhookPayload;
|
|
27
|
+
privateKey: string;
|
|
28
|
+
}
|
|
29
|
+
export interface VerifySignatureOptions {
|
|
30
|
+
payload: string;
|
|
31
|
+
signature: string;
|
|
32
|
+
secret: string;
|
|
33
|
+
}
|
package/dist/types.js
ADDED
package/dist/verify.d.ts
ADDED
package/dist/verify.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.verifyWebhookSignature = verifyWebhookSignature;
|
|
4
|
+
const crypto_1 = require("./crypto");
|
|
5
|
+
async function verifyWebhookSignature(options) {
|
|
6
|
+
const { payload, signature, secret } = options;
|
|
7
|
+
const expectedSignature = await (0, crypto_1.createHmacSignature)(payload, secret);
|
|
8
|
+
return expectedSignature === signature;
|
|
9
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "lockform",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Official SDK for processing Lockform webhook submissions with end-to-end encryption",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist"
|
|
9
|
+
],
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"prepublishOnly": "npm run build"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"lockform",
|
|
16
|
+
"encryption",
|
|
17
|
+
"webhook",
|
|
18
|
+
"e2e",
|
|
19
|
+
"forms",
|
|
20
|
+
"rsa",
|
|
21
|
+
"aes"
|
|
22
|
+
],
|
|
23
|
+
"author": "",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/node": "^20.0.0",
|
|
27
|
+
"typescript": "^5.0.0"
|
|
28
|
+
},
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=18.0.0"
|
|
31
|
+
}
|
|
32
|
+
}
|