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 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
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ import { DecryptedSubmission, DecryptWebhookOptions } from './types';
2
+ export declare function decryptWebhookData(options: DecryptWebhookOptions): Promise<DecryptedSubmission>;
@@ -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
+ }
@@ -0,0 +1,3 @@
1
+ export { decryptWebhookData } from './decrypt';
2
+ export { verifyWebhookSignature } from './verify';
3
+ export type { WebhookPayload, DecryptedSubmission, DecryptWebhookOptions, VerifySignatureOptions, } from './types';
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; } });
@@ -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
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,2 @@
1
+ import { VerifySignatureOptions } from './types';
2
+ export declare function verifyWebhookSignature(options: VerifySignatureOptions): Promise<boolean>;
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
+ }