@better-webhook/cli 3.8.0 → 3.10.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.
Files changed (59) hide show
  1. package/LICENSE +0 -1
  2. package/dist/_binary_entry.js +29 -0
  3. package/dist/commands/capture.d.ts +2 -0
  4. package/dist/commands/capture.js +33 -0
  5. package/dist/commands/captures.d.ts +2 -0
  6. package/dist/commands/captures.js +316 -0
  7. package/dist/commands/dashboard.d.ts +2 -0
  8. package/dist/commands/dashboard.js +70 -0
  9. package/dist/commands/index.d.ts +6 -0
  10. package/dist/commands/index.js +6 -0
  11. package/dist/commands/replay.d.ts +2 -0
  12. package/dist/commands/replay.js +140 -0
  13. package/dist/commands/run.d.ts +2 -0
  14. package/dist/commands/run.js +182 -0
  15. package/dist/commands/templates.d.ts +2 -0
  16. package/dist/commands/templates.js +285 -0
  17. package/dist/core/capture-server.d.ts +37 -0
  18. package/dist/core/capture-server.js +400 -0
  19. package/dist/core/capture-server.test.d.ts +1 -0
  20. package/dist/core/capture-server.test.js +86 -0
  21. package/dist/core/cli-version.d.ts +1 -0
  22. package/dist/core/cli-version.js +30 -0
  23. package/dist/core/cli-version.test.d.ts +1 -0
  24. package/dist/core/cli-version.test.js +42 -0
  25. package/dist/core/dashboard-api.d.ts +8 -0
  26. package/dist/core/dashboard-api.js +333 -0
  27. package/dist/core/dashboard-server.d.ts +24 -0
  28. package/dist/core/dashboard-server.js +224 -0
  29. package/dist/core/debug-output.d.ts +3 -0
  30. package/dist/core/debug-output.js +69 -0
  31. package/dist/core/debug-verify.d.ts +25 -0
  32. package/dist/core/debug-verify.js +253 -0
  33. package/dist/core/executor.d.ts +11 -0
  34. package/dist/core/executor.js +152 -0
  35. package/dist/core/index.d.ts +5 -0
  36. package/dist/core/index.js +5 -0
  37. package/dist/core/replay-engine.d.ts +20 -0
  38. package/dist/core/replay-engine.js +293 -0
  39. package/dist/core/replay-engine.test.d.ts +1 -0
  40. package/dist/core/replay-engine.test.js +482 -0
  41. package/dist/core/runtime-paths.d.ts +2 -0
  42. package/dist/core/runtime-paths.js +65 -0
  43. package/dist/core/runtime-paths.test.d.ts +1 -0
  44. package/dist/core/runtime-paths.test.js +50 -0
  45. package/dist/core/signature.d.ts +25 -0
  46. package/dist/core/signature.js +224 -0
  47. package/dist/core/signature.test.d.ts +1 -0
  48. package/dist/core/signature.test.js +38 -0
  49. package/dist/core/template-manager.d.ts +33 -0
  50. package/dist/core/template-manager.js +313 -0
  51. package/dist/core/template-manager.test.d.ts +1 -0
  52. package/dist/core/template-manager.test.js +236 -0
  53. package/dist/index.cjs +3472 -262
  54. package/dist/index.d.cts +2 -1
  55. package/dist/index.d.ts +2 -1
  56. package/dist/index.js +3509 -276
  57. package/dist/types/index.d.ts +312 -0
  58. package/dist/types/index.js +87 -0
  59. package/package.json +1 -1
@@ -0,0 +1,224 @@
1
+ import { Buffer } from "node:buffer";
2
+ import { createHmac } from "crypto";
3
+ export function generateStripeSignature(payload, secret, timestamp) {
4
+ const ts = timestamp || Math.floor(Date.now() / 1000);
5
+ const signedPayload = `${ts}.${payload}`;
6
+ const signature = createHmac("sha256", secret)
7
+ .update(signedPayload)
8
+ .digest("hex");
9
+ return {
10
+ header: "Stripe-Signature",
11
+ value: `t=${ts},v1=${signature}`,
12
+ };
13
+ }
14
+ export function generateGitHubSignature(payload, secret) {
15
+ const signature = createHmac("sha256", secret).update(payload).digest("hex");
16
+ return {
17
+ header: "X-Hub-Signature-256",
18
+ value: `sha256=${signature}`,
19
+ };
20
+ }
21
+ export function generateShopifySignature(payload, secret) {
22
+ const signature = createHmac("sha256", secret)
23
+ .update(payload)
24
+ .digest("base64");
25
+ return {
26
+ header: "X-Shopify-Hmac-SHA256",
27
+ value: signature,
28
+ };
29
+ }
30
+ export function generateTwilioSignature(payload, secret, url) {
31
+ const signatureInput = url + payload;
32
+ const signature = createHmac("sha1", secret)
33
+ .update(signatureInput)
34
+ .digest("base64");
35
+ return {
36
+ header: "X-Twilio-Signature",
37
+ value: signature,
38
+ };
39
+ }
40
+ export function generateSlackSignature(payload, secret, timestamp) {
41
+ const ts = timestamp || Math.floor(Date.now() / 1000);
42
+ const signatureBaseString = `v0:${ts}:${payload}`;
43
+ const signature = createHmac("sha256", secret)
44
+ .update(signatureBaseString)
45
+ .digest("hex");
46
+ return {
47
+ header: "X-Slack-Signature",
48
+ value: `v0=${signature}`,
49
+ };
50
+ }
51
+ export function generateLinearSignature(payload, secret) {
52
+ const signature = createHmac("sha256", secret).update(payload).digest("hex");
53
+ return {
54
+ header: "Linear-Signature",
55
+ value: signature,
56
+ };
57
+ }
58
+ export function generateClerkSignature(payload, secret, timestamp, webhookId) {
59
+ const ts = timestamp || Math.floor(Date.now() / 1000);
60
+ const msgId = webhookId || `msg_${Date.now()}`;
61
+ const signedPayload = `${msgId}.${ts}.${payload}`;
62
+ const signature = createHmac("sha256", secret)
63
+ .update(signedPayload)
64
+ .digest("base64");
65
+ return {
66
+ header: "Svix-Signature",
67
+ value: `v1,${signature}`,
68
+ };
69
+ }
70
+ export function generateSendGridSignature(payload, secret, timestamp) {
71
+ const ts = timestamp || Math.floor(Date.now() / 1000);
72
+ const signedPayload = `${ts}${payload}`;
73
+ const signature = createHmac("sha256", secret)
74
+ .update(signedPayload)
75
+ .digest("base64");
76
+ return {
77
+ header: "X-Twilio-Email-Event-Webhook-Signature",
78
+ value: signature,
79
+ };
80
+ }
81
+ export function generateRagieSignature(payload, secret) {
82
+ const signature = createHmac("sha256", secret).update(payload).digest("hex");
83
+ return {
84
+ header: "X-Signature",
85
+ value: signature,
86
+ };
87
+ }
88
+ export function generateRecallSignature(payload, secret, timestamp, webhookId) {
89
+ if (!secret.startsWith("whsec_")) {
90
+ throw new Error("Recall signature generation requires a secret with the whsec_ prefix");
91
+ }
92
+ const ts = timestamp ?? Math.floor(Date.now() / 1000);
93
+ const msgId = webhookId ?? `msg_${Date.now()}`;
94
+ const key = Buffer.from(secret.slice("whsec_".length), "base64");
95
+ if (key.length === 0) {
96
+ throw new Error("Recall signing secret is invalid");
97
+ }
98
+ const signedPayload = `${msgId}.${ts}.${payload}`;
99
+ const signature = createHmac("sha256", key)
100
+ .update(signedPayload)
101
+ .digest("base64");
102
+ return {
103
+ header: "Webhook-Signature",
104
+ value: `v1,${signature}`,
105
+ };
106
+ }
107
+ export function generateSignature(provider, payload, secret, options) {
108
+ const timestamp = options?.timestamp;
109
+ switch (provider) {
110
+ case "stripe":
111
+ return generateStripeSignature(payload, secret, timestamp);
112
+ case "github":
113
+ return generateGitHubSignature(payload, secret);
114
+ case "shopify":
115
+ return generateShopifySignature(payload, secret);
116
+ case "twilio":
117
+ if (!options?.url) {
118
+ throw new Error("Twilio signature requires URL");
119
+ }
120
+ return generateTwilioSignature(payload, secret, options.url);
121
+ case "slack":
122
+ return generateSlackSignature(payload, secret, timestamp);
123
+ case "linear":
124
+ return generateLinearSignature(payload, secret);
125
+ case "clerk":
126
+ return generateClerkSignature(payload, secret, timestamp, options?.webhookId);
127
+ case "sendgrid":
128
+ return generateSendGridSignature(payload, secret, timestamp);
129
+ case "ragie":
130
+ return generateRagieSignature(payload, secret);
131
+ case "recall":
132
+ return generateRecallSignature(payload, secret, timestamp, options?.webhookId);
133
+ case "discord":
134
+ case "custom":
135
+ default:
136
+ return null;
137
+ }
138
+ }
139
+ export function getProviderHeaders(provider, options) {
140
+ const headers = [];
141
+ const timestamp = options?.timestamp || Math.floor(Date.now() / 1000);
142
+ switch (provider) {
143
+ case "stripe":
144
+ headers.push({ key: "Content-Type", value: "application/json" }, {
145
+ key: "User-Agent",
146
+ value: "Stripe/1.0 (+https://stripe.com/docs/webhooks)",
147
+ });
148
+ break;
149
+ case "github":
150
+ headers.push({ key: "Content-Type", value: "application/json" }, { key: "User-Agent", value: "GitHub-Hookshot/better-webhook" }, { key: "X-GitHub-Event", value: options?.event || "push" }, {
151
+ key: "X-GitHub-Delivery",
152
+ value: options?.webhookId || generateDeliveryId(),
153
+ });
154
+ break;
155
+ case "shopify":
156
+ 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" });
157
+ break;
158
+ case "slack":
159
+ headers.push({ key: "Content-Type", value: "application/json" }, { key: "X-Slack-Request-Timestamp", value: String(timestamp) });
160
+ break;
161
+ case "clerk":
162
+ headers.push({ key: "Content-Type", value: "application/json" }, { key: "Svix-Id", value: options?.webhookId || `msg_${Date.now()}` }, { key: "Svix-Timestamp", value: String(timestamp) });
163
+ break;
164
+ case "sendgrid":
165
+ headers.push({ key: "Content-Type", value: "application/json" }, {
166
+ key: "X-Twilio-Email-Event-Webhook-Timestamp",
167
+ value: String(timestamp),
168
+ });
169
+ break;
170
+ case "twilio":
171
+ headers.push({
172
+ key: "Content-Type",
173
+ value: "application/x-www-form-urlencoded",
174
+ });
175
+ break;
176
+ case "linear":
177
+ headers.push({ key: "Content-Type", value: "application/json" }, {
178
+ key: "Linear-Delivery",
179
+ value: options?.webhookId || generateDeliveryId(),
180
+ });
181
+ break;
182
+ case "discord":
183
+ headers.push({ key: "Content-Type", value: "application/json" }, { key: "User-Agent", value: "Discord-Webhook/1.0" });
184
+ break;
185
+ case "ragie":
186
+ headers.push({ key: "Content-Type", value: "application/json" });
187
+ break;
188
+ case "recall":
189
+ headers.push({ key: "Content-Type", value: "application/json" }, { key: "Webhook-Id", value: options?.webhookId || `msg_${Date.now()}` }, { key: "Webhook-Timestamp", value: String(timestamp) });
190
+ break;
191
+ default:
192
+ headers.push({ key: "Content-Type", value: "application/json" });
193
+ }
194
+ return headers;
195
+ }
196
+ function generateDeliveryId() {
197
+ const chars = "0123456789abcdef";
198
+ let id = "";
199
+ for (let i = 0; i < 36; i++) {
200
+ if (i === 8 || i === 13 || i === 18 || i === 23) {
201
+ id += "-";
202
+ }
203
+ else {
204
+ id += chars[Math.floor(Math.random() * chars.length)];
205
+ }
206
+ }
207
+ return id;
208
+ }
209
+ export function verifySignature(provider, payload, signature, secret, options) {
210
+ const generated = generateSignature(provider, payload, secret, options);
211
+ if (!generated) {
212
+ return false;
213
+ }
214
+ const a = generated.value;
215
+ const b = signature;
216
+ if (a.length !== b.length) {
217
+ return false;
218
+ }
219
+ let result = 0;
220
+ for (let i = 0; i < a.length; i++) {
221
+ result |= a.charCodeAt(i) ^ b.charCodeAt(i);
222
+ }
223
+ return result === 0;
224
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,38 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { generateRecallSignature, generateSignature, getProviderHeaders, } from "./signature.js";
3
+ describe("signature helpers", () => {
4
+ describe("generateRecallSignature", () => {
5
+ it("generates a v1 signature for Recall payloads", () => {
6
+ const payload = '{"event":"transcript.data"}';
7
+ const timestamp = 1731705121;
8
+ const webhookId = "msg_test_123";
9
+ const secret = "whsec_dGVzdC1yZWNhbGwtc2VjcmV0";
10
+ const signature = generateRecallSignature(payload, secret, timestamp, webhookId);
11
+ expect(signature.header).toBe("Webhook-Signature");
12
+ expect(signature.value).toBe("v1,/Tpb01gdtwsQOKZ92HQ+9qzHEHG5ZxDXmPhaxPG4yFs=");
13
+ });
14
+ it("throws when secret does not use whsec_ prefix", () => {
15
+ expect(() => generateRecallSignature('{"event":"participant_events.join"}', "invalid-secret")).toThrow("Recall signature generation requires a secret with the whsec_ prefix");
16
+ });
17
+ });
18
+ describe("generateSignature", () => {
19
+ it("generates Recall signature with provider dispatch", () => {
20
+ const generated = generateSignature("recall", '{"event":"bot.done"}', "whsec_dGVzdC1yZWNhbGwtc2VjcmV0", { timestamp: 1731705121, webhookId: "msg_test_456" });
21
+ expect(generated?.header).toBe("Webhook-Signature");
22
+ expect(generated?.value).toBe("v1,1S4uDLnwC3qN9n7rIcbvZeQpikXxcznCf/DzNCSEzXA=");
23
+ });
24
+ });
25
+ describe("getProviderHeaders", () => {
26
+ it("returns Recall webhook id and timestamp headers", () => {
27
+ const headers = getProviderHeaders("recall", {
28
+ timestamp: 1731705121,
29
+ webhookId: "msg_test_789",
30
+ });
31
+ expect(headers).toEqual(expect.arrayContaining([
32
+ { key: "Content-Type", value: "application/json" },
33
+ { key: "Webhook-Id", value: "msg_test_789" },
34
+ { key: "Webhook-Timestamp", value: "1731705121" },
35
+ ]));
36
+ });
37
+ });
38
+ });
@@ -0,0 +1,33 @@
1
+ import { type TemplatesIndex, type WebhookTemplate, type LocalTemplate, type RemoteTemplate, type SaveAsTemplateResult } 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
+ templateExists(templateId: string): boolean;
24
+ private generateTemplateId;
25
+ saveUserTemplate(template: WebhookTemplate, options?: {
26
+ id?: string;
27
+ name?: string;
28
+ event?: string;
29
+ description?: string;
30
+ overwrite?: boolean;
31
+ }): SaveAsTemplateResult;
32
+ }
33
+ export declare function getTemplateManager(baseDir?: string): TemplateManager;
@@ -0,0 +1,313 @@
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
+ const TEMPLATE_ID_PATTERN = /^[a-z0-9][a-z0-9._-]*$/i;
9
+ function isValidTemplateId(id) {
10
+ if (!id || id.length > 128)
11
+ return false;
12
+ if (id.includes("/") || id.includes("\\") || id.includes(".."))
13
+ return false;
14
+ return TEMPLATE_ID_PATTERN.test(id);
15
+ }
16
+ export class TemplateManager {
17
+ baseDir;
18
+ templatesDir;
19
+ cacheFile;
20
+ indexCache = null;
21
+ constructor(baseDir) {
22
+ this.baseDir = baseDir || join(homedir(), ".better-webhook");
23
+ this.templatesDir = join(this.baseDir, "templates");
24
+ this.cacheFile = join(this.baseDir, "templates-cache.json");
25
+ if (!existsSync(this.baseDir)) {
26
+ mkdirSync(this.baseDir, { recursive: true });
27
+ }
28
+ if (!existsSync(this.templatesDir)) {
29
+ mkdirSync(this.templatesDir, { recursive: true });
30
+ }
31
+ }
32
+ getTemplatesDir() {
33
+ return this.templatesDir;
34
+ }
35
+ async fetchRemoteIndex(forceRefresh = false) {
36
+ if (!forceRefresh && this.indexCache) {
37
+ return this.indexCache;
38
+ }
39
+ if (!forceRefresh && existsSync(this.cacheFile)) {
40
+ try {
41
+ const cached = JSON.parse(readFileSync(this.cacheFile, "utf-8"));
42
+ const cacheAge = Date.now() - (cached.cachedAt || 0);
43
+ if (cacheAge < 3600000) {
44
+ this.indexCache = cached.index;
45
+ return cached.index;
46
+ }
47
+ }
48
+ catch {
49
+ }
50
+ }
51
+ try {
52
+ const { statusCode, body } = await request(TEMPLATES_INDEX_URL);
53
+ if (statusCode !== 200) {
54
+ throw new Error(`HTTP ${statusCode}`);
55
+ }
56
+ const text = await body.text();
57
+ const json = JSON.parse(text);
58
+ const index = TemplatesIndexSchema.parse(json);
59
+ this.indexCache = index;
60
+ writeFileSync(this.cacheFile, JSON.stringify({ index, cachedAt: Date.now() }, null, 2));
61
+ return index;
62
+ }
63
+ catch (error) {
64
+ if (existsSync(this.cacheFile)) {
65
+ try {
66
+ const cached = JSON.parse(readFileSync(this.cacheFile, "utf-8"));
67
+ if (cached.index) {
68
+ this.indexCache = cached.index;
69
+ return cached.index;
70
+ }
71
+ }
72
+ catch {
73
+ }
74
+ }
75
+ throw new Error(`Failed to fetch templates index: ${error.message}`);
76
+ }
77
+ }
78
+ async listRemoteTemplates(options) {
79
+ const index = await this.fetchRemoteIndex(!!options?.forceRefresh);
80
+ const localIds = new Set(this.listLocalTemplates().map((t) => t.id));
81
+ return index.templates.map((metadata) => ({
82
+ metadata,
83
+ isDownloaded: localIds.has(metadata.id),
84
+ }));
85
+ }
86
+ async downloadTemplate(templateId) {
87
+ const index = await this.fetchRemoteIndex();
88
+ const templateMeta = index.templates.find((t) => t.id === templateId);
89
+ if (!templateMeta) {
90
+ throw new Error(`Template not found: ${templateId}`);
91
+ }
92
+ const templateUrl = `${GITHUB_RAW_BASE}/templates/${templateMeta.file}`;
93
+ try {
94
+ const { statusCode, body } = await request(templateUrl);
95
+ if (statusCode !== 200) {
96
+ throw new Error(`HTTP ${statusCode}`);
97
+ }
98
+ const text = await body.text();
99
+ const json = JSON.parse(text);
100
+ const template = WebhookTemplateSchema.parse(json);
101
+ const providerDir = join(this.templatesDir, templateMeta.provider);
102
+ if (!existsSync(providerDir)) {
103
+ mkdirSync(providerDir, { recursive: true });
104
+ }
105
+ const fileName = `${templateId}.json`;
106
+ const filePath = join(providerDir, fileName);
107
+ const localTemplate = {
108
+ id: templateId,
109
+ metadata: templateMeta,
110
+ template,
111
+ downloadedAt: new Date().toISOString(),
112
+ filePath,
113
+ };
114
+ const saveData = {
115
+ ...template,
116
+ _metadata: {
117
+ ...templateMeta,
118
+ downloadedAt: localTemplate.downloadedAt,
119
+ },
120
+ };
121
+ writeFileSync(filePath, JSON.stringify(saveData, null, 2));
122
+ return localTemplate;
123
+ }
124
+ catch (error) {
125
+ throw new Error(`Failed to download template ${templateId}: ${error.message}`);
126
+ }
127
+ }
128
+ listLocalTemplates() {
129
+ const templates = [];
130
+ if (!existsSync(this.templatesDir)) {
131
+ return templates;
132
+ }
133
+ const scanDir = (dir) => {
134
+ const entries = readdirSync(dir, { withFileTypes: true });
135
+ for (const entry of entries) {
136
+ const fullPath = join(dir, entry.name);
137
+ if (entry.isDirectory()) {
138
+ scanDir(fullPath);
139
+ }
140
+ else if (entry.isFile() && entry.name.endsWith(".json")) {
141
+ try {
142
+ const content = JSON.parse(readFileSync(fullPath, "utf-8"));
143
+ const metadata = content._metadata;
144
+ if (metadata) {
145
+ const { _metadata, ...templateData } = content;
146
+ templates.push({
147
+ id: metadata.id,
148
+ metadata,
149
+ template: templateData,
150
+ downloadedAt: metadata.downloadedAt || new Date().toISOString(),
151
+ filePath: fullPath,
152
+ });
153
+ }
154
+ else {
155
+ const id = basename(entry.name, ".json");
156
+ templates.push({
157
+ id,
158
+ metadata: {
159
+ id,
160
+ name: id,
161
+ provider: "custom",
162
+ event: "unknown",
163
+ file: entry.name,
164
+ },
165
+ template: content,
166
+ downloadedAt: new Date().toISOString(),
167
+ filePath: fullPath,
168
+ });
169
+ }
170
+ }
171
+ catch {
172
+ }
173
+ }
174
+ }
175
+ };
176
+ scanDir(this.templatesDir);
177
+ return templates;
178
+ }
179
+ getLocalTemplate(templateId) {
180
+ const templates = this.listLocalTemplates();
181
+ return templates.find((t) => t.id === templateId) || null;
182
+ }
183
+ deleteLocalTemplate(templateId) {
184
+ const template = this.getLocalTemplate(templateId);
185
+ if (!template) {
186
+ return false;
187
+ }
188
+ try {
189
+ unlinkSync(template.filePath);
190
+ return true;
191
+ }
192
+ catch {
193
+ return false;
194
+ }
195
+ }
196
+ async searchTemplates(query) {
197
+ const queryLower = query.toLowerCase();
198
+ const remote = await this.listRemoteTemplates();
199
+ const local = this.listLocalTemplates();
200
+ const matchesMeta = (meta) => {
201
+ return (meta.id.toLowerCase().includes(queryLower) ||
202
+ meta.name.toLowerCase().includes(queryLower) ||
203
+ meta.provider.toLowerCase().includes(queryLower) ||
204
+ meta.event.toLowerCase().includes(queryLower) ||
205
+ (meta.description?.toLowerCase().includes(queryLower) ?? false));
206
+ };
207
+ return {
208
+ remote: remote.filter((t) => matchesMeta(t.metadata)),
209
+ local: local.filter((t) => matchesMeta(t.metadata)),
210
+ };
211
+ }
212
+ clearCache() {
213
+ this.indexCache = null;
214
+ if (existsSync(this.cacheFile)) {
215
+ unlinkSync(this.cacheFile);
216
+ }
217
+ }
218
+ deleteAllLocalTemplates() {
219
+ const templates = this.listLocalTemplates();
220
+ let deleted = 0;
221
+ for (const template of templates) {
222
+ try {
223
+ unlinkSync(template.filePath);
224
+ deleted++;
225
+ }
226
+ catch {
227
+ }
228
+ }
229
+ if (existsSync(this.templatesDir)) {
230
+ const entries = readdirSync(this.templatesDir, { withFileTypes: true });
231
+ for (const entry of entries) {
232
+ if (entry.isDirectory()) {
233
+ const dirPath = join(this.templatesDir, entry.name);
234
+ try {
235
+ const contents = readdirSync(dirPath);
236
+ if (contents.length === 0) {
237
+ rmdirSync(dirPath);
238
+ }
239
+ }
240
+ catch {
241
+ }
242
+ }
243
+ }
244
+ }
245
+ return deleted;
246
+ }
247
+ templateExists(templateId) {
248
+ return this.getLocalTemplate(templateId) !== null;
249
+ }
250
+ generateTemplateId(provider, event) {
251
+ const providerPart = provider || "custom";
252
+ const eventPart = event || "webhook";
253
+ const baseId = `${providerPart}-${eventPart}`
254
+ .toLowerCase()
255
+ .replace(/\s+/g, "-");
256
+ if (!this.templateExists(baseId)) {
257
+ return baseId;
258
+ }
259
+ let counter = 1;
260
+ while (this.templateExists(`${baseId}-${counter}`)) {
261
+ counter++;
262
+ }
263
+ return `${baseId}-${counter}`;
264
+ }
265
+ saveUserTemplate(template, options = {}) {
266
+ const provider = template.provider || "custom";
267
+ const event = options.event || template.event || "webhook";
268
+ const templateId = options.id || this.generateTemplateId(provider, event);
269
+ const name = options.name || templateId;
270
+ const description = options.description || template.description;
271
+ if (!isValidTemplateId(templateId)) {
272
+ throw new Error(`Invalid template ID "${templateId}". IDs must start with alphanumeric, contain only letters, numbers, dots, underscores, and hyphens.`);
273
+ }
274
+ if (!options.overwrite && this.templateExists(templateId)) {
275
+ throw new Error(`Template with ID "${templateId}" already exists. Use --overwrite to replace it.`);
276
+ }
277
+ const providerDir = join(this.templatesDir, provider);
278
+ if (!existsSync(providerDir)) {
279
+ mkdirSync(providerDir, { recursive: true });
280
+ }
281
+ const metadata = {
282
+ id: templateId,
283
+ name,
284
+ provider,
285
+ event,
286
+ file: `${provider}/${templateId}.json`,
287
+ description,
288
+ source: "capture",
289
+ createdAt: new Date().toISOString(),
290
+ };
291
+ const saveData = {
292
+ ...template,
293
+ provider,
294
+ event,
295
+ description,
296
+ _metadata: metadata,
297
+ };
298
+ const filePath = join(providerDir, `${templateId}.json`);
299
+ writeFileSync(filePath, JSON.stringify(saveData, null, 2));
300
+ return {
301
+ id: templateId,
302
+ filePath,
303
+ template: saveData,
304
+ };
305
+ }
306
+ }
307
+ let instance = null;
308
+ export function getTemplateManager(baseDir) {
309
+ if (!instance) {
310
+ instance = new TemplateManager(baseDir);
311
+ }
312
+ return instance;
313
+ }
@@ -0,0 +1 @@
1
+ export {};