@better-webhook/cli 3.3.0 → 3.4.1

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.
@@ -0,0 +1,205 @@
1
+ import { createHmac } from "crypto";
2
+ export function generateStripeSignature(payload, secret, timestamp) {
3
+ const ts = timestamp || Math.floor(Date.now() / 1000);
4
+ const signedPayload = `${ts}.${payload}`;
5
+ const signature = createHmac("sha256", secret)
6
+ .update(signedPayload)
7
+ .digest("hex");
8
+ return {
9
+ header: "Stripe-Signature",
10
+ value: `t=${ts},v1=${signature}`,
11
+ };
12
+ }
13
+ export function generateGitHubSignature(payload, secret) {
14
+ const signature = createHmac("sha256", secret).update(payload).digest("hex");
15
+ return {
16
+ header: "X-Hub-Signature-256",
17
+ value: `sha256=${signature}`,
18
+ };
19
+ }
20
+ export function generateShopifySignature(payload, secret) {
21
+ const signature = createHmac("sha256", secret)
22
+ .update(payload)
23
+ .digest("base64");
24
+ return {
25
+ header: "X-Shopify-Hmac-SHA256",
26
+ value: signature,
27
+ };
28
+ }
29
+ export function generateTwilioSignature(payload, secret, url) {
30
+ const signatureInput = url + payload;
31
+ const signature = createHmac("sha1", secret)
32
+ .update(signatureInput)
33
+ .digest("base64");
34
+ return {
35
+ header: "X-Twilio-Signature",
36
+ value: signature,
37
+ };
38
+ }
39
+ export function generateSlackSignature(payload, secret, timestamp) {
40
+ const ts = timestamp || Math.floor(Date.now() / 1000);
41
+ const signatureBaseString = `v0:${ts}:${payload}`;
42
+ const signature = createHmac("sha256", secret)
43
+ .update(signatureBaseString)
44
+ .digest("hex");
45
+ return {
46
+ header: "X-Slack-Signature",
47
+ value: `v0=${signature}`,
48
+ };
49
+ }
50
+ export function generateLinearSignature(payload, secret) {
51
+ const signature = createHmac("sha256", secret).update(payload).digest("hex");
52
+ return {
53
+ header: "Linear-Signature",
54
+ value: signature,
55
+ };
56
+ }
57
+ export function generateClerkSignature(payload, secret, timestamp, webhookId) {
58
+ const ts = timestamp || Math.floor(Date.now() / 1000);
59
+ const msgId = webhookId || `msg_${Date.now()}`;
60
+ const signedPayload = `${msgId}.${ts}.${payload}`;
61
+ const signature = createHmac("sha256", secret)
62
+ .update(signedPayload)
63
+ .digest("base64");
64
+ return {
65
+ header: "Svix-Signature",
66
+ value: `v1,${signature}`,
67
+ };
68
+ }
69
+ export function generateSendGridSignature(payload, secret, timestamp) {
70
+ const ts = timestamp || Math.floor(Date.now() / 1000);
71
+ const signedPayload = `${ts}${payload}`;
72
+ const signature = createHmac("sha256", secret)
73
+ .update(signedPayload)
74
+ .digest("base64");
75
+ return {
76
+ header: "X-Twilio-Email-Event-Webhook-Signature",
77
+ value: signature,
78
+ };
79
+ }
80
+ export function generateRagieSignature(payload, secret) {
81
+ const signature = createHmac("sha256", secret).update(payload).digest("hex");
82
+ return {
83
+ header: "X-Signature",
84
+ value: signature,
85
+ };
86
+ }
87
+ export function generateSignature(provider, payload, secret, options) {
88
+ const timestamp = options?.timestamp;
89
+ switch (provider) {
90
+ case "stripe":
91
+ return generateStripeSignature(payload, secret, timestamp);
92
+ case "github":
93
+ return generateGitHubSignature(payload, secret);
94
+ case "shopify":
95
+ return generateShopifySignature(payload, secret);
96
+ case "twilio":
97
+ if (!options?.url) {
98
+ throw new Error("Twilio signature requires URL");
99
+ }
100
+ return generateTwilioSignature(payload, secret, options.url);
101
+ case "slack":
102
+ return generateSlackSignature(payload, secret, timestamp);
103
+ case "linear":
104
+ return generateLinearSignature(payload, secret);
105
+ case "clerk":
106
+ return generateClerkSignature(payload, secret, timestamp, options?.webhookId);
107
+ case "sendgrid":
108
+ return generateSendGridSignature(payload, secret, timestamp);
109
+ case "ragie":
110
+ return generateRagieSignature(payload, secret);
111
+ case "discord":
112
+ case "custom":
113
+ default:
114
+ return null;
115
+ }
116
+ }
117
+ export function getProviderHeaders(provider, options) {
118
+ const headers = [];
119
+ const timestamp = options?.timestamp || Math.floor(Date.now() / 1000);
120
+ switch (provider) {
121
+ case "stripe":
122
+ headers.push({ key: "Content-Type", value: "application/json" }, {
123
+ key: "User-Agent",
124
+ value: "Stripe/1.0 (+https://stripe.com/docs/webhooks)",
125
+ });
126
+ break;
127
+ case "github":
128
+ headers.push({ key: "Content-Type", value: "application/json" }, { key: "User-Agent", value: "GitHub-Hookshot/better-webhook" }, { key: "X-GitHub-Event", value: options?.event || "push" }, {
129
+ key: "X-GitHub-Delivery",
130
+ value: options?.webhookId || generateDeliveryId(),
131
+ });
132
+ break;
133
+ case "shopify":
134
+ headers.push({ key: "Content-Type", value: "application/json" }, { key: "X-Shopify-Topic", value: options?.event || "orders/create" }, { key: "X-Shopify-Shop-Domain", value: "example.myshopify.com" }, { key: "X-Shopify-API-Version", value: "2024-01" });
135
+ break;
136
+ case "slack":
137
+ headers.push({ key: "Content-Type", value: "application/json" }, { key: "X-Slack-Request-Timestamp", value: String(timestamp) });
138
+ break;
139
+ case "clerk":
140
+ headers.push({ key: "Content-Type", value: "application/json" }, { key: "Svix-Id", value: options?.webhookId || `msg_${Date.now()}` }, { key: "Svix-Timestamp", value: String(timestamp) });
141
+ break;
142
+ case "sendgrid":
143
+ headers.push({ key: "Content-Type", value: "application/json" }, {
144
+ key: "X-Twilio-Email-Event-Webhook-Timestamp",
145
+ value: String(timestamp),
146
+ });
147
+ break;
148
+ case "twilio":
149
+ headers.push({
150
+ key: "Content-Type",
151
+ value: "application/x-www-form-urlencoded",
152
+ });
153
+ break;
154
+ case "linear":
155
+ headers.push({ key: "Content-Type", value: "application/json" }, {
156
+ key: "Linear-Delivery",
157
+ value: options?.webhookId || generateDeliveryId(),
158
+ });
159
+ break;
160
+ case "discord":
161
+ headers.push({ key: "Content-Type", value: "application/json" }, { key: "User-Agent", value: "Discord-Webhook/1.0" });
162
+ break;
163
+ case "ragie":
164
+ headers.push({ key: "Content-Type", value: "application/json" }, {
165
+ key: "X-Ragie-Event",
166
+ value: options?.event || "document_status_updated",
167
+ }, {
168
+ key: "X-Ragie-Delivery",
169
+ value: options?.webhookId || generateDeliveryId(),
170
+ });
171
+ break;
172
+ default:
173
+ headers.push({ key: "Content-Type", value: "application/json" });
174
+ }
175
+ return headers;
176
+ }
177
+ function generateDeliveryId() {
178
+ const chars = "0123456789abcdef";
179
+ let id = "";
180
+ for (let i = 0; i < 36; i++) {
181
+ if (i === 8 || i === 13 || i === 18 || i === 23) {
182
+ id += "-";
183
+ }
184
+ else {
185
+ id += chars[Math.floor(Math.random() * chars.length)];
186
+ }
187
+ }
188
+ return id;
189
+ }
190
+ export function verifySignature(provider, payload, signature, secret, options) {
191
+ const generated = generateSignature(provider, payload, secret, options);
192
+ if (!generated) {
193
+ return false;
194
+ }
195
+ const a = generated.value;
196
+ const b = signature;
197
+ if (a.length !== b.length) {
198
+ return false;
199
+ }
200
+ let result = 0;
201
+ for (let i = 0; i < a.length; i++) {
202
+ result |= a.charCodeAt(i) ^ b.charCodeAt(i);
203
+ }
204
+ return result === 0;
205
+ }
@@ -0,0 +1,24 @@
1
+ import { type TemplatesIndex, type LocalTemplate, type RemoteTemplate } from "../types/index.js";
2
+ export declare class TemplateManager {
3
+ private baseDir;
4
+ private templatesDir;
5
+ private cacheFile;
6
+ private indexCache;
7
+ constructor(baseDir?: string);
8
+ getTemplatesDir(): string;
9
+ fetchRemoteIndex(forceRefresh?: boolean): Promise<TemplatesIndex>;
10
+ listRemoteTemplates(options?: {
11
+ forceRefresh?: boolean;
12
+ }): Promise<RemoteTemplate[]>;
13
+ downloadTemplate(templateId: string): Promise<LocalTemplate>;
14
+ listLocalTemplates(): LocalTemplate[];
15
+ getLocalTemplate(templateId: string): LocalTemplate | null;
16
+ deleteLocalTemplate(templateId: string): boolean;
17
+ searchTemplates(query: string): Promise<{
18
+ remote: RemoteTemplate[];
19
+ local: LocalTemplate[];
20
+ }>;
21
+ clearCache(): void;
22
+ deleteAllLocalTemplates(): number;
23
+ }
24
+ export declare function getTemplateManager(baseDir?: string): TemplateManager;
@@ -0,0 +1,246 @@
1
+ import { request } from "undici";
2
+ import { existsSync, mkdirSync, readFileSync, readdirSync, rmdirSync, unlinkSync, writeFileSync, } from "fs";
3
+ import { join, basename } from "path";
4
+ import { homedir } from "os";
5
+ import { TemplatesIndexSchema, WebhookTemplateSchema, } from "../types/index.js";
6
+ const GITHUB_RAW_BASE = "https://raw.githubusercontent.com/endalk200/better-webhook/main";
7
+ const TEMPLATES_INDEX_URL = `${GITHUB_RAW_BASE}/templates/templates.json`;
8
+ export class TemplateManager {
9
+ baseDir;
10
+ templatesDir;
11
+ cacheFile;
12
+ indexCache = null;
13
+ constructor(baseDir) {
14
+ this.baseDir = baseDir || join(homedir(), ".better-webhook");
15
+ this.templatesDir = join(this.baseDir, "templates");
16
+ this.cacheFile = join(this.baseDir, "templates-cache.json");
17
+ if (!existsSync(this.baseDir)) {
18
+ mkdirSync(this.baseDir, { recursive: true });
19
+ }
20
+ if (!existsSync(this.templatesDir)) {
21
+ mkdirSync(this.templatesDir, { recursive: true });
22
+ }
23
+ }
24
+ getTemplatesDir() {
25
+ return this.templatesDir;
26
+ }
27
+ async fetchRemoteIndex(forceRefresh = false) {
28
+ if (!forceRefresh && this.indexCache) {
29
+ return this.indexCache;
30
+ }
31
+ if (!forceRefresh && existsSync(this.cacheFile)) {
32
+ try {
33
+ const cached = JSON.parse(readFileSync(this.cacheFile, "utf-8"));
34
+ const cacheAge = Date.now() - (cached.cachedAt || 0);
35
+ if (cacheAge < 3600000) {
36
+ this.indexCache = cached.index;
37
+ return cached.index;
38
+ }
39
+ }
40
+ catch {
41
+ }
42
+ }
43
+ try {
44
+ const { statusCode, body } = await request(TEMPLATES_INDEX_URL);
45
+ if (statusCode !== 200) {
46
+ throw new Error(`HTTP ${statusCode}`);
47
+ }
48
+ const text = await body.text();
49
+ const json = JSON.parse(text);
50
+ const index = TemplatesIndexSchema.parse(json);
51
+ this.indexCache = index;
52
+ writeFileSync(this.cacheFile, JSON.stringify({ index, cachedAt: Date.now() }, null, 2));
53
+ return index;
54
+ }
55
+ catch (error) {
56
+ if (existsSync(this.cacheFile)) {
57
+ try {
58
+ const cached = JSON.parse(readFileSync(this.cacheFile, "utf-8"));
59
+ if (cached.index) {
60
+ this.indexCache = cached.index;
61
+ return cached.index;
62
+ }
63
+ }
64
+ catch {
65
+ }
66
+ }
67
+ throw new Error(`Failed to fetch templates index: ${error.message}`);
68
+ }
69
+ }
70
+ async listRemoteTemplates(options) {
71
+ const index = await this.fetchRemoteIndex(!!options?.forceRefresh);
72
+ const localIds = new Set(this.listLocalTemplates().map((t) => t.id));
73
+ return index.templates.map((metadata) => ({
74
+ metadata,
75
+ isDownloaded: localIds.has(metadata.id),
76
+ }));
77
+ }
78
+ async downloadTemplate(templateId) {
79
+ const index = await this.fetchRemoteIndex();
80
+ const templateMeta = index.templates.find((t) => t.id === templateId);
81
+ if (!templateMeta) {
82
+ throw new Error(`Template not found: ${templateId}`);
83
+ }
84
+ const templateUrl = `${GITHUB_RAW_BASE}/templates/${templateMeta.file}`;
85
+ try {
86
+ const { statusCode, body } = await request(templateUrl);
87
+ if (statusCode !== 200) {
88
+ throw new Error(`HTTP ${statusCode}`);
89
+ }
90
+ const text = await body.text();
91
+ const json = JSON.parse(text);
92
+ const template = WebhookTemplateSchema.parse(json);
93
+ const providerDir = join(this.templatesDir, templateMeta.provider);
94
+ if (!existsSync(providerDir)) {
95
+ mkdirSync(providerDir, { recursive: true });
96
+ }
97
+ const fileName = `${templateId}.json`;
98
+ const filePath = join(providerDir, fileName);
99
+ const localTemplate = {
100
+ id: templateId,
101
+ metadata: templateMeta,
102
+ template,
103
+ downloadedAt: new Date().toISOString(),
104
+ filePath,
105
+ };
106
+ const saveData = {
107
+ ...template,
108
+ _metadata: {
109
+ ...templateMeta,
110
+ downloadedAt: localTemplate.downloadedAt,
111
+ },
112
+ };
113
+ writeFileSync(filePath, JSON.stringify(saveData, null, 2));
114
+ return localTemplate;
115
+ }
116
+ catch (error) {
117
+ throw new Error(`Failed to download template ${templateId}: ${error.message}`);
118
+ }
119
+ }
120
+ listLocalTemplates() {
121
+ const templates = [];
122
+ if (!existsSync(this.templatesDir)) {
123
+ return templates;
124
+ }
125
+ const scanDir = (dir) => {
126
+ const entries = readdirSync(dir, { withFileTypes: true });
127
+ for (const entry of entries) {
128
+ const fullPath = join(dir, entry.name);
129
+ if (entry.isDirectory()) {
130
+ scanDir(fullPath);
131
+ }
132
+ else if (entry.isFile() && entry.name.endsWith(".json")) {
133
+ try {
134
+ const content = JSON.parse(readFileSync(fullPath, "utf-8"));
135
+ const metadata = content._metadata;
136
+ if (metadata) {
137
+ const { _metadata, ...templateData } = content;
138
+ templates.push({
139
+ id: metadata.id,
140
+ metadata,
141
+ template: templateData,
142
+ downloadedAt: metadata.downloadedAt || new Date().toISOString(),
143
+ filePath: fullPath,
144
+ });
145
+ }
146
+ else {
147
+ const id = basename(entry.name, ".json");
148
+ templates.push({
149
+ id,
150
+ metadata: {
151
+ id,
152
+ name: id,
153
+ provider: "custom",
154
+ event: "unknown",
155
+ file: entry.name,
156
+ },
157
+ template: content,
158
+ downloadedAt: new Date().toISOString(),
159
+ filePath: fullPath,
160
+ });
161
+ }
162
+ }
163
+ catch {
164
+ }
165
+ }
166
+ }
167
+ };
168
+ scanDir(this.templatesDir);
169
+ return templates;
170
+ }
171
+ getLocalTemplate(templateId) {
172
+ const templates = this.listLocalTemplates();
173
+ return templates.find((t) => t.id === templateId) || null;
174
+ }
175
+ deleteLocalTemplate(templateId) {
176
+ const template = this.getLocalTemplate(templateId);
177
+ if (!template) {
178
+ return false;
179
+ }
180
+ try {
181
+ unlinkSync(template.filePath);
182
+ return true;
183
+ }
184
+ catch {
185
+ return false;
186
+ }
187
+ }
188
+ async searchTemplates(query) {
189
+ const queryLower = query.toLowerCase();
190
+ const remote = await this.listRemoteTemplates();
191
+ const local = this.listLocalTemplates();
192
+ const matchesMeta = (meta) => {
193
+ return (meta.id.toLowerCase().includes(queryLower) ||
194
+ meta.name.toLowerCase().includes(queryLower) ||
195
+ meta.provider.toLowerCase().includes(queryLower) ||
196
+ meta.event.toLowerCase().includes(queryLower) ||
197
+ (meta.description?.toLowerCase().includes(queryLower) ?? false));
198
+ };
199
+ return {
200
+ remote: remote.filter((t) => matchesMeta(t.metadata)),
201
+ local: local.filter((t) => matchesMeta(t.metadata)),
202
+ };
203
+ }
204
+ clearCache() {
205
+ this.indexCache = null;
206
+ if (existsSync(this.cacheFile)) {
207
+ unlinkSync(this.cacheFile);
208
+ }
209
+ }
210
+ deleteAllLocalTemplates() {
211
+ const templates = this.listLocalTemplates();
212
+ let deleted = 0;
213
+ for (const template of templates) {
214
+ try {
215
+ unlinkSync(template.filePath);
216
+ deleted++;
217
+ }
218
+ catch {
219
+ }
220
+ }
221
+ if (existsSync(this.templatesDir)) {
222
+ const entries = readdirSync(this.templatesDir, { withFileTypes: true });
223
+ for (const entry of entries) {
224
+ if (entry.isDirectory()) {
225
+ const dirPath = join(this.templatesDir, entry.name);
226
+ try {
227
+ const contents = readdirSync(dirPath);
228
+ if (contents.length === 0) {
229
+ rmdirSync(dirPath);
230
+ }
231
+ }
232
+ catch {
233
+ }
234
+ }
235
+ }
236
+ }
237
+ return deleted;
238
+ }
239
+ }
240
+ let instance = null;
241
+ export function getTemplateManager(baseDir) {
242
+ if (!instance) {
243
+ instance = new TemplateManager(baseDir);
244
+ }
245
+ return instance;
246
+ }
package/dist/index.cjs CHANGED
@@ -25,6 +25,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
25
25
 
26
26
  // src/index.ts
27
27
  var import_commander7 = require("commander");
28
+ var import_node_module = require("module");
28
29
 
29
30
  // src/commands/templates.ts
30
31
  var import_commander = require("commander");
@@ -745,6 +746,13 @@ function generateSendGridSignature(payload, secret, timestamp) {
745
746
  value: signature
746
747
  };
747
748
  }
749
+ function generateRagieSignature(payload, secret) {
750
+ const signature = (0, import_crypto.createHmac)("sha256", secret).update(payload).digest("hex");
751
+ return {
752
+ header: "X-Signature",
753
+ value: signature
754
+ };
755
+ }
748
756
  function generateSignature(provider, payload, secret, options) {
749
757
  const timestamp = options?.timestamp;
750
758
  switch (provider) {
@@ -772,6 +780,8 @@ function generateSignature(provider, payload, secret, options) {
772
780
  );
773
781
  case "sendgrid":
774
782
  return generateSendGridSignature(payload, secret, timestamp);
783
+ case "ragie":
784
+ return generateRagieSignature(payload, secret);
775
785
  case "discord":
776
786
  case "custom":
777
787
  default:
@@ -853,6 +863,19 @@ function getProviderHeaders(provider, options) {
853
863
  { key: "User-Agent", value: "Discord-Webhook/1.0" }
854
864
  );
855
865
  break;
866
+ case "ragie":
867
+ headers.push(
868
+ { key: "Content-Type", value: "application/json" },
869
+ {
870
+ key: "X-Ragie-Event",
871
+ value: options?.event || "document_status_updated"
872
+ },
873
+ {
874
+ key: "X-Ragie-Delivery",
875
+ value: options?.webhookId || generateDeliveryId()
876
+ }
877
+ );
878
+ break;
856
879
  default:
857
880
  headers.push({ key: "Content-Type", value: "application/json" });
858
881
  }
@@ -2586,8 +2609,11 @@ var dashboard = new import_commander6.Command().name("dashboard").description("S
2586
2609
  });
2587
2610
 
2588
2611
  // src/index.ts
2612
+ var import_meta2 = {};
2613
+ var require2 = (0, import_node_module.createRequire)(import_meta2.url);
2614
+ var packageJson = require2("../package.json");
2589
2615
  var program = new import_commander7.Command().name("better-webhook").description(
2590
2616
  "Modern CLI for developing, capturing, and replaying webhooks locally"
2591
- ).version("2.0.0");
2617
+ ).version(packageJson.version);
2592
2618
  program.addCommand(templates).addCommand(run).addCommand(capture).addCommand(captures).addCommand(replay).addCommand(dashboard);
2593
2619
  program.parseAsync(process.argv);
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  // src/index.ts
4
4
  import { Command as Command7 } from "commander";
5
+ import { createRequire } from "module";
5
6
 
6
7
  // src/commands/templates.ts
7
8
  import { Command } from "commander";
@@ -730,6 +731,13 @@ function generateSendGridSignature(payload, secret, timestamp) {
730
731
  value: signature
731
732
  };
732
733
  }
734
+ function generateRagieSignature(payload, secret) {
735
+ const signature = createHmac("sha256", secret).update(payload).digest("hex");
736
+ return {
737
+ header: "X-Signature",
738
+ value: signature
739
+ };
740
+ }
733
741
  function generateSignature(provider, payload, secret, options) {
734
742
  const timestamp = options?.timestamp;
735
743
  switch (provider) {
@@ -757,6 +765,8 @@ function generateSignature(provider, payload, secret, options) {
757
765
  );
758
766
  case "sendgrid":
759
767
  return generateSendGridSignature(payload, secret, timestamp);
768
+ case "ragie":
769
+ return generateRagieSignature(payload, secret);
760
770
  case "discord":
761
771
  case "custom":
762
772
  default:
@@ -838,6 +848,19 @@ function getProviderHeaders(provider, options) {
838
848
  { key: "User-Agent", value: "Discord-Webhook/1.0" }
839
849
  );
840
850
  break;
851
+ case "ragie":
852
+ headers.push(
853
+ { key: "Content-Type", value: "application/json" },
854
+ {
855
+ key: "X-Ragie-Event",
856
+ value: options?.event || "document_status_updated"
857
+ },
858
+ {
859
+ key: "X-Ragie-Delivery",
860
+ value: options?.webhookId || generateDeliveryId()
861
+ }
862
+ );
863
+ break;
841
864
  default:
842
865
  headers.push({ key: "Content-Type", value: "application/json" });
843
866
  }
@@ -2579,8 +2602,10 @@ var dashboard = new Command6().name("dashboard").description("Start the local da
2579
2602
  });
2580
2603
 
2581
2604
  // src/index.ts
2605
+ var require2 = createRequire(import.meta.url);
2606
+ var packageJson = require2("../package.json");
2582
2607
  var program = new Command7().name("better-webhook").description(
2583
2608
  "Modern CLI for developing, capturing, and replaying webhooks locally"
2584
- ).version("2.0.0");
2609
+ ).version(packageJson.version);
2585
2610
  program.addCommand(templates).addCommand(run).addCommand(capture).addCommand(captures).addCommand(replay).addCommand(dashboard);
2586
2611
  program.parseAsync(process.argv);