@zyphr-dev/mcp-server 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/README.md +117 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2315 -0
- package/dist/index.js.map +1 -0
- package/package.json +62 -0
- package/src/client.ts +25 -0
- package/src/config.test.ts +64 -0
- package/src/config.ts +33 -0
- package/src/index.ts +24 -0
- package/src/integration/quickstart/email.ts +646 -0
- package/src/integration/quickstart/inbox.ts +222 -0
- package/src/integration/quickstart/index.ts +45 -0
- package/src/integration/quickstart/push.ts +216 -0
- package/src/integration/quickstart/quickstart.test.ts +108 -0
- package/src/integration/quickstart/sms.ts +205 -0
- package/src/integration/quickstart/webhook.ts +664 -0
- package/src/integration/quickstart-types.ts +31 -0
- package/src/integration/sdk-snippets.test.ts +63 -0
- package/src/integration/sdk-snippets.ts +248 -0
- package/src/result.test.ts +107 -0
- package/src/result.ts +65 -0
- package/src/schemas.ts +231 -0
- package/src/server.ts +26 -0
- package/src/tools/index.ts +7 -0
- package/src/tools/integration.ts +54 -0
- package/src/tools/send.ts +153 -0
- package/src/tools/subscribers.ts +126 -0
- package/src/tools/templates.ts +87 -0
- package/src/tools/webhooks.ts +82 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2315 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
|
|
6
|
+
// src/server.ts
|
|
7
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
8
|
+
|
|
9
|
+
// src/config.ts
|
|
10
|
+
function loadToolGuards() {
|
|
11
|
+
const readOnly = process.env.ZYPHR_READ_ONLY === "true" || process.env.ZYPHR_READ_ONLY === "1";
|
|
12
|
+
const raw = process.env.ZYPHR_ALLOWED_TOOLS;
|
|
13
|
+
const allowedTools = raw ? new Set(
|
|
14
|
+
raw.split(",").map((s) => s.trim()).filter(Boolean)
|
|
15
|
+
) : null;
|
|
16
|
+
return { readOnly, allowedTools };
|
|
17
|
+
}
|
|
18
|
+
function isToolEnabled(tool, guards) {
|
|
19
|
+
if (guards.allowedTools) {
|
|
20
|
+
return guards.allowedTools.has(tool.name);
|
|
21
|
+
}
|
|
22
|
+
if (guards.readOnly && tool.mutates) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// src/client.ts
|
|
29
|
+
import { Zyphr } from "@zyphr-dev/node-sdk";
|
|
30
|
+
var DEFAULT_BASE_URL = "https://api.zyphr.dev/v1";
|
|
31
|
+
var cached;
|
|
32
|
+
function getZyphrClient() {
|
|
33
|
+
if (cached) return cached;
|
|
34
|
+
const apiKey = process.env.ZYPHR_API_KEY;
|
|
35
|
+
if (!apiKey) {
|
|
36
|
+
process.stderr.write(
|
|
37
|
+
"[zyphr-mcp] ZYPHR_API_KEY is not set. Provide a zy_live_* or zy_test_* key via the `env` block of your MCP client config.\n"
|
|
38
|
+
);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
const baseUrl = process.env.ZYPHR_BASE_URL || DEFAULT_BASE_URL;
|
|
42
|
+
cached = new Zyphr({ apiKey, baseUrl });
|
|
43
|
+
return cached;
|
|
44
|
+
}
|
|
45
|
+
function getBaseUrl() {
|
|
46
|
+
return process.env.ZYPHR_BASE_URL || DEFAULT_BASE_URL;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// src/result.ts
|
|
50
|
+
import { ZyphrError, ZyphrRateLimitError } from "@zyphr-dev/node-sdk";
|
|
51
|
+
function toolResult(data) {
|
|
52
|
+
return {
|
|
53
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
async function runTool(fn) {
|
|
57
|
+
try {
|
|
58
|
+
const data = await fn();
|
|
59
|
+
return toolResult(data);
|
|
60
|
+
} catch (err) {
|
|
61
|
+
return await renderApiError(err);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function errorPayload(content) {
|
|
65
|
+
return {
|
|
66
|
+
isError: true,
|
|
67
|
+
content: [{ type: "text", text: JSON.stringify(content, null, 2) }]
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
async function renderApiError(err) {
|
|
71
|
+
if (err instanceof ZyphrError) {
|
|
72
|
+
const payload = {
|
|
73
|
+
name: err.name,
|
|
74
|
+
message: err.message,
|
|
75
|
+
status: err.status
|
|
76
|
+
};
|
|
77
|
+
if (err.code) payload.code = err.code;
|
|
78
|
+
if (err.requestId) payload.requestId = err.requestId;
|
|
79
|
+
if (err.details) payload.details = err.details;
|
|
80
|
+
if (err instanceof ZyphrRateLimitError && err.retryAfter !== void 0) {
|
|
81
|
+
payload.retryAfter = err.retryAfter;
|
|
82
|
+
}
|
|
83
|
+
return errorPayload(payload);
|
|
84
|
+
}
|
|
85
|
+
if (err && typeof err === "object" && "response" in err) {
|
|
86
|
+
const response = err.response;
|
|
87
|
+
if (response && typeof response.text === "function") {
|
|
88
|
+
try {
|
|
89
|
+
const text = await response.text();
|
|
90
|
+
let parsed = text;
|
|
91
|
+
try {
|
|
92
|
+
parsed = JSON.parse(text);
|
|
93
|
+
} catch {
|
|
94
|
+
}
|
|
95
|
+
return errorPayload({ status: response.status, body: parsed });
|
|
96
|
+
} catch {
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
101
|
+
const name = err instanceof Error ? err.name : "Error";
|
|
102
|
+
return errorPayload({ name, message });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// src/schemas.ts
|
|
106
|
+
import { z } from "zod";
|
|
107
|
+
var emailAddress = z.object({
|
|
108
|
+
email: z.string().email(),
|
|
109
|
+
name: z.string().optional()
|
|
110
|
+
});
|
|
111
|
+
var recipient = z.union([z.string().email(), emailAddress]);
|
|
112
|
+
var sendEmailShape = {
|
|
113
|
+
to: z.union([recipient, z.array(recipient).min(1)]).describe("Recipient email address (string or {email,name}) or an array of them"),
|
|
114
|
+
from: z.union([z.string().email(), emailAddress]).optional().describe('Sender address. Defaults to the account-level "from" address.'),
|
|
115
|
+
replyTo: z.union([z.string().email(), emailAddress]).optional(),
|
|
116
|
+
cc: z.array(z.string().email()).optional(),
|
|
117
|
+
bcc: z.array(z.string().email()).optional(),
|
|
118
|
+
subject: z.string().min(1),
|
|
119
|
+
html: z.string().optional().describe("Rendered HTML body. Mutually exclusive with templateId."),
|
|
120
|
+
text: z.string().optional().describe("Plain-text body. Mutually exclusive with templateId."),
|
|
121
|
+
templateId: z.string().optional().describe("Template ID. When set, html/text are ignored."),
|
|
122
|
+
templateData: z.record(z.unknown()).optional().describe("Variables to interpolate into the template"),
|
|
123
|
+
tags: z.array(z.string()).optional(),
|
|
124
|
+
metadata: z.record(z.unknown()).optional(),
|
|
125
|
+
subscriberId: z.string().optional(),
|
|
126
|
+
category: z.string().optional(),
|
|
127
|
+
scheduledAt: z.string().datetime().optional().describe("ISO 8601 timestamp for scheduled delivery")
|
|
128
|
+
};
|
|
129
|
+
var sendPushShape = {
|
|
130
|
+
userId: z.string().optional().describe("Send to all devices for this user/subscriber"),
|
|
131
|
+
deviceId: z.string().optional().describe("Send to a specific device only"),
|
|
132
|
+
title: z.string().optional(),
|
|
133
|
+
body: z.string().optional(),
|
|
134
|
+
data: z.record(z.unknown()).optional().describe("Custom data payload delivered to the device"),
|
|
135
|
+
badge: z.number().int().nonnegative().optional(),
|
|
136
|
+
sound: z.string().optional(),
|
|
137
|
+
imageUrl: z.string().url().optional(),
|
|
138
|
+
contentAvailable: z.boolean().optional().describe("Silent/background push"),
|
|
139
|
+
tags: z.array(z.string()).optional(),
|
|
140
|
+
metadata: z.record(z.unknown()).optional(),
|
|
141
|
+
collapseKey: z.string().optional(),
|
|
142
|
+
subscriberId: z.string().optional(),
|
|
143
|
+
subscriberExternalId: z.string().optional(),
|
|
144
|
+
category: z.string().optional(),
|
|
145
|
+
force: z.boolean().optional().describe("Skip subscriber preference checks"),
|
|
146
|
+
sendAt: z.string().datetime().optional(),
|
|
147
|
+
delay: z.number().int().nonnegative().optional()
|
|
148
|
+
};
|
|
149
|
+
var sendSmsShape = {
|
|
150
|
+
to: z.string().min(1).describe("Recipient phone number in E.164 format (e.g. +14155551234)"),
|
|
151
|
+
from: z.string().optional().describe("Sender phone number or sender ID"),
|
|
152
|
+
body: z.string().min(1),
|
|
153
|
+
subscriberId: z.string().optional(),
|
|
154
|
+
scheduledAt: z.string().datetime().optional(),
|
|
155
|
+
metadata: z.record(z.unknown()).optional()
|
|
156
|
+
};
|
|
157
|
+
var sdkLanguages = ["node", "python", "ruby", "go", "php", "csharp"];
|
|
158
|
+
var quickstartChannels = ["email", "push", "sms", "inbox", "webhook"];
|
|
159
|
+
var getQuickstartShape = {
|
|
160
|
+
channel: z.enum(quickstartChannels).describe("Which Zyphr channel to wire up"),
|
|
161
|
+
language: z.enum(["node", "python", "ruby", "go", "php", "csharp"]).describe("Target language for the integration"),
|
|
162
|
+
framework: z.string().optional().describe(
|
|
163
|
+
'Optional framework hint (e.g. "express", "nextjs", "flask", "fastapi", "rails", "gin", "laravel", "aspnetcore"). Falls back to plain SDK code when unrecognized.'
|
|
164
|
+
)
|
|
165
|
+
};
|
|
166
|
+
var getSdkInstallShape = {
|
|
167
|
+
language: z.enum(sdkLanguages).describe("Target language for the integration"),
|
|
168
|
+
packageManager: z.string().optional().describe(
|
|
169
|
+
'Optional package manager override (e.g. "yarn" instead of "npm"). When recognized, only that manager is returned; otherwise the full list is returned.'
|
|
170
|
+
)
|
|
171
|
+
};
|
|
172
|
+
var listTemplatesShape = {
|
|
173
|
+
limit: z.number().int().positive().max(200).optional(),
|
|
174
|
+
offset: z.number().int().nonnegative().optional()
|
|
175
|
+
};
|
|
176
|
+
var getTemplateShape = {
|
|
177
|
+
id: z.string().min(1).describe("Template ID")
|
|
178
|
+
};
|
|
179
|
+
var renderTemplateShape = {
|
|
180
|
+
id: z.string().min(1).describe("Template ID"),
|
|
181
|
+
variables: z.record(z.unknown()).describe("Key/value variables to interpolate into the template")
|
|
182
|
+
};
|
|
183
|
+
var createTemplateShape = {
|
|
184
|
+
name: z.string().min(1),
|
|
185
|
+
description: z.string().optional(),
|
|
186
|
+
subject: z.string().optional().describe("Default subject (email templates)"),
|
|
187
|
+
html: z.string().optional(),
|
|
188
|
+
text: z.string().optional()
|
|
189
|
+
};
|
|
190
|
+
var findSubscriberShape = {
|
|
191
|
+
externalId: z.string().min(1).describe("Subscriber external ID (your application user/customer ID)")
|
|
192
|
+
};
|
|
193
|
+
var listSubscribersShape = {
|
|
194
|
+
status: z.enum(["active", "inactive"]).optional(),
|
|
195
|
+
email: z.string().email().optional(),
|
|
196
|
+
limit: z.number().int().positive().max(200).optional(),
|
|
197
|
+
offset: z.number().int().nonnegative().optional()
|
|
198
|
+
};
|
|
199
|
+
var createSubscriberShape = {
|
|
200
|
+
externalId: z.string().min(1).describe("Your application user/customer ID"),
|
|
201
|
+
email: z.string().email().optional(),
|
|
202
|
+
phone: z.string().optional(),
|
|
203
|
+
name: z.string().optional(),
|
|
204
|
+
avatarUrl: z.string().url().optional(),
|
|
205
|
+
timezone: z.string().optional(),
|
|
206
|
+
locale: z.string().optional(),
|
|
207
|
+
metadata: z.record(z.unknown()).optional()
|
|
208
|
+
};
|
|
209
|
+
var updateSubscriberShape = {
|
|
210
|
+
id: z.string().min(1).describe("Zyphr subscriber ID"),
|
|
211
|
+
email: z.string().email().nullable().optional(),
|
|
212
|
+
phone: z.string().nullable().optional(),
|
|
213
|
+
name: z.string().nullable().optional(),
|
|
214
|
+
avatarUrl: z.string().url().nullable().optional(),
|
|
215
|
+
timezone: z.string().optional(),
|
|
216
|
+
locale: z.string().optional(),
|
|
217
|
+
metadata: z.record(z.unknown()).optional(),
|
|
218
|
+
status: z.enum(["active", "inactive"]).optional()
|
|
219
|
+
};
|
|
220
|
+
var setSubscriberPreferencesShape = {
|
|
221
|
+
id: z.string().min(1).describe("Zyphr subscriber ID"),
|
|
222
|
+
preferences: z.array(
|
|
223
|
+
z.object({
|
|
224
|
+
categoryId: z.string().optional(),
|
|
225
|
+
channel: z.string().optional().describe("email | push | sms | in_app"),
|
|
226
|
+
enabled: z.boolean().optional()
|
|
227
|
+
})
|
|
228
|
+
).min(1)
|
|
229
|
+
};
|
|
230
|
+
var listWebhooksShape = {
|
|
231
|
+
limit: z.number().int().positive().max(200).optional(),
|
|
232
|
+
offset: z.number().int().nonnegative().optional()
|
|
233
|
+
};
|
|
234
|
+
var createWebhookShape = {
|
|
235
|
+
url: z.string().url().describe("Receiver URL that Zyphr will POST events to"),
|
|
236
|
+
events: z.array(z.string().min(1)).min(1).describe('Event types to subscribe to (e.g. ["email.*", "subscriber.created"])'),
|
|
237
|
+
description: z.string().optional(),
|
|
238
|
+
secret: z.string().optional().describe("Optional secret used to sign payloads. If omitted, Zyphr generates one."),
|
|
239
|
+
metadata: z.record(z.unknown()).optional(),
|
|
240
|
+
headers: z.record(z.string()).optional().describe("Custom headers to send with every delivery"),
|
|
241
|
+
version: z.string().optional(),
|
|
242
|
+
rateLimit: z.number().int().positive().optional()
|
|
243
|
+
};
|
|
244
|
+
var getWebhookDeliveriesShape = {
|
|
245
|
+
webhookId: z.string().min(1).describe("Webhook endpoint ID"),
|
|
246
|
+
status: z.enum(["pending", "delivering", "delivered", "failed", "exhausted"]).optional(),
|
|
247
|
+
eventType: z.string().optional(),
|
|
248
|
+
search: z.string().optional(),
|
|
249
|
+
startDate: z.string().datetime().optional(),
|
|
250
|
+
endDate: z.string().datetime().optional(),
|
|
251
|
+
limit: z.number().int().positive().max(200).optional(),
|
|
252
|
+
offset: z.number().int().nonnegative().optional()
|
|
253
|
+
};
|
|
254
|
+
var sendInboxMessageShape = {
|
|
255
|
+
subscriberId: z.string().min(1).describe("Subscriber to deliver the in-app message to"),
|
|
256
|
+
title: z.string().min(1),
|
|
257
|
+
body: z.string().optional(),
|
|
258
|
+
actionUrl: z.string().url().optional(),
|
|
259
|
+
actionLabel: z.string().optional(),
|
|
260
|
+
imageUrl: z.string().url().optional(),
|
|
261
|
+
icon: z.string().optional(),
|
|
262
|
+
category: z.string().optional(),
|
|
263
|
+
priority: z.enum(["low", "normal", "high", "urgent"]).optional(),
|
|
264
|
+
data: z.record(z.unknown()).optional(),
|
|
265
|
+
tags: z.array(z.string()).optional(),
|
|
266
|
+
expiresAt: z.string().datetime().optional()
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
// src/tools/send.ts
|
|
270
|
+
function normalizeEmailAddress(value) {
|
|
271
|
+
if (typeof value === "string") return { email: value };
|
|
272
|
+
if (value && typeof value === "object" && "email" in value) {
|
|
273
|
+
return value;
|
|
274
|
+
}
|
|
275
|
+
return void 0;
|
|
276
|
+
}
|
|
277
|
+
function normalizeRecipients(to) {
|
|
278
|
+
if (Array.isArray(to)) {
|
|
279
|
+
return to.map(normalizeEmailAddress).filter((v) => Boolean(v));
|
|
280
|
+
}
|
|
281
|
+
const one = normalizeEmailAddress(to);
|
|
282
|
+
return one ? [one] : [];
|
|
283
|
+
}
|
|
284
|
+
function registerSendTools(server, guards) {
|
|
285
|
+
if (isToolEnabled({ name: "send_email", mutates: true }, guards)) {
|
|
286
|
+
server.registerTool(
|
|
287
|
+
"send_email",
|
|
288
|
+
{
|
|
289
|
+
title: "Send email",
|
|
290
|
+
description: "Send a transactional email via Zyphr. Use either html/text OR templateId+templateData, not both.",
|
|
291
|
+
inputSchema: sendEmailShape
|
|
292
|
+
},
|
|
293
|
+
async (args) => {
|
|
294
|
+
return runTool(async () => {
|
|
295
|
+
const zyphr = getZyphrClient();
|
|
296
|
+
return await zyphr.emails.sendEmail({
|
|
297
|
+
to: normalizeRecipients(args.to),
|
|
298
|
+
from: normalizeEmailAddress(args.from),
|
|
299
|
+
replyTo: normalizeEmailAddress(args.replyTo),
|
|
300
|
+
cc: args.cc,
|
|
301
|
+
bcc: args.bcc,
|
|
302
|
+
subject: args.subject,
|
|
303
|
+
html: args.html,
|
|
304
|
+
text: args.text,
|
|
305
|
+
templateId: args.templateId,
|
|
306
|
+
templateData: args.templateData,
|
|
307
|
+
tags: args.tags,
|
|
308
|
+
metadata: args.metadata,
|
|
309
|
+
subscriberId: args.subscriberId,
|
|
310
|
+
category: args.category,
|
|
311
|
+
scheduledAt: args.scheduledAt ? new Date(args.scheduledAt) : void 0
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
if (isToolEnabled({ name: "send_push", mutates: true }, guards)) {
|
|
318
|
+
server.registerTool(
|
|
319
|
+
"send_push",
|
|
320
|
+
{
|
|
321
|
+
title: "Send push notification",
|
|
322
|
+
description: "Send a push notification. Target one of: userId (all devices for a user), deviceId (specific device), subscriberId, or subscriberExternalId.",
|
|
323
|
+
inputSchema: sendPushShape
|
|
324
|
+
},
|
|
325
|
+
async (args) => {
|
|
326
|
+
return runTool(async () => {
|
|
327
|
+
const zyphr = getZyphrClient();
|
|
328
|
+
return await zyphr.push.sendPush({
|
|
329
|
+
userId: args.userId,
|
|
330
|
+
deviceId: args.deviceId,
|
|
331
|
+
title: args.title,
|
|
332
|
+
body: args.body,
|
|
333
|
+
data: args.data,
|
|
334
|
+
badge: args.badge,
|
|
335
|
+
sound: args.sound,
|
|
336
|
+
imageUrl: args.imageUrl,
|
|
337
|
+
contentAvailable: args.contentAvailable,
|
|
338
|
+
tags: args.tags,
|
|
339
|
+
metadata: args.metadata,
|
|
340
|
+
collapseKey: args.collapseKey,
|
|
341
|
+
subscriberId: args.subscriberId,
|
|
342
|
+
subscriberExternalId: args.subscriberExternalId,
|
|
343
|
+
category: args.category,
|
|
344
|
+
force: args.force,
|
|
345
|
+
sendAt: args.sendAt ? new Date(args.sendAt) : void 0,
|
|
346
|
+
delay: args.delay
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
if (isToolEnabled({ name: "send_sms", mutates: true }, guards)) {
|
|
353
|
+
server.registerTool(
|
|
354
|
+
"send_sms",
|
|
355
|
+
{
|
|
356
|
+
title: "Send SMS",
|
|
357
|
+
description: "Send an SMS message via Zyphr. The recipient must be in E.164 format.",
|
|
358
|
+
inputSchema: sendSmsShape
|
|
359
|
+
},
|
|
360
|
+
async (args) => {
|
|
361
|
+
return runTool(async () => {
|
|
362
|
+
const zyphr = getZyphrClient();
|
|
363
|
+
return await zyphr.sms.sendSms({
|
|
364
|
+
to: args.to,
|
|
365
|
+
from: args.from,
|
|
366
|
+
body: args.body,
|
|
367
|
+
subscriberId: args.subscriberId,
|
|
368
|
+
scheduledAt: args.scheduledAt ? new Date(args.scheduledAt) : void 0,
|
|
369
|
+
metadata: args.metadata
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
if (isToolEnabled({ name: "send_inbox_message", mutates: true }, guards)) {
|
|
376
|
+
server.registerTool(
|
|
377
|
+
"send_inbox_message",
|
|
378
|
+
{
|
|
379
|
+
title: "Send in-app inbox message",
|
|
380
|
+
description: "Deliver an in-app inbox notification to a Zyphr subscriber.",
|
|
381
|
+
inputSchema: sendInboxMessageShape
|
|
382
|
+
},
|
|
383
|
+
async (args) => {
|
|
384
|
+
return runTool(async () => {
|
|
385
|
+
const zyphr = getZyphrClient();
|
|
386
|
+
return await zyphr.inbox.sendInApp({
|
|
387
|
+
subscriberId: args.subscriberId,
|
|
388
|
+
title: args.title,
|
|
389
|
+
body: args.body,
|
|
390
|
+
actionUrl: args.actionUrl,
|
|
391
|
+
actionLabel: args.actionLabel,
|
|
392
|
+
imageUrl: args.imageUrl,
|
|
393
|
+
icon: args.icon,
|
|
394
|
+
category: args.category,
|
|
395
|
+
priority: args.priority,
|
|
396
|
+
data: args.data,
|
|
397
|
+
tags: args.tags,
|
|
398
|
+
expiresAt: args.expiresAt ? new Date(args.expiresAt) : void 0
|
|
399
|
+
});
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// src/tools/subscribers.ts
|
|
407
|
+
function registerSubscriberTools(server, guards) {
|
|
408
|
+
if (isToolEnabled({ name: "find_subscriber", mutates: false }, guards)) {
|
|
409
|
+
server.registerTool(
|
|
410
|
+
"find_subscriber",
|
|
411
|
+
{
|
|
412
|
+
title: "Find subscriber by external ID",
|
|
413
|
+
description: "Look up a subscriber by the external ID you assigned (typically your application user/customer ID).",
|
|
414
|
+
inputSchema: findSubscriberShape
|
|
415
|
+
},
|
|
416
|
+
async (args) => {
|
|
417
|
+
return runTool(async () => {
|
|
418
|
+
const zyphr = getZyphrClient();
|
|
419
|
+
return await zyphr.subscribers.getSubscriberByExternalId(args.externalId);
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
if (isToolEnabled({ name: "list_subscribers", mutates: false }, guards)) {
|
|
425
|
+
server.registerTool(
|
|
426
|
+
"list_subscribers",
|
|
427
|
+
{
|
|
428
|
+
title: "List subscribers",
|
|
429
|
+
description: "List subscribers, optionally filtered by status or email.",
|
|
430
|
+
inputSchema: listSubscribersShape
|
|
431
|
+
},
|
|
432
|
+
async (args) => {
|
|
433
|
+
return runTool(async () => {
|
|
434
|
+
const zyphr = getZyphrClient();
|
|
435
|
+
return await zyphr.subscribers.listSubscribers(
|
|
436
|
+
args.status,
|
|
437
|
+
args.email,
|
|
438
|
+
args.limit,
|
|
439
|
+
args.offset
|
|
440
|
+
);
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
if (isToolEnabled({ name: "create_subscriber", mutates: true }, guards)) {
|
|
446
|
+
server.registerTool(
|
|
447
|
+
"create_subscriber",
|
|
448
|
+
{
|
|
449
|
+
title: "Create subscriber",
|
|
450
|
+
description: "Create a new subscriber. `externalId` must be unique within your account.",
|
|
451
|
+
inputSchema: createSubscriberShape
|
|
452
|
+
},
|
|
453
|
+
async (args) => {
|
|
454
|
+
return runTool(async () => {
|
|
455
|
+
const zyphr = getZyphrClient();
|
|
456
|
+
return await zyphr.subscribers.createSubscriber({
|
|
457
|
+
externalId: args.externalId,
|
|
458
|
+
email: args.email,
|
|
459
|
+
phone: args.phone,
|
|
460
|
+
name: args.name,
|
|
461
|
+
avatarUrl: args.avatarUrl,
|
|
462
|
+
timezone: args.timezone,
|
|
463
|
+
locale: args.locale,
|
|
464
|
+
metadata: args.metadata
|
|
465
|
+
});
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
if (isToolEnabled({ name: "update_subscriber", mutates: true }, guards)) {
|
|
471
|
+
server.registerTool(
|
|
472
|
+
"update_subscriber",
|
|
473
|
+
{
|
|
474
|
+
title: "Update subscriber",
|
|
475
|
+
description: "Update a subscriber by Zyphr ID. Pass null for email/phone/name/avatarUrl to clear those fields.",
|
|
476
|
+
inputSchema: updateSubscriberShape
|
|
477
|
+
},
|
|
478
|
+
async (args) => {
|
|
479
|
+
return runTool(async () => {
|
|
480
|
+
const zyphr = getZyphrClient();
|
|
481
|
+
return await zyphr.subscribers.updateSubscriber(args.id, {
|
|
482
|
+
email: args.email ?? void 0,
|
|
483
|
+
phone: args.phone ?? void 0,
|
|
484
|
+
name: args.name ?? void 0,
|
|
485
|
+
avatarUrl: args.avatarUrl ?? void 0,
|
|
486
|
+
timezone: args.timezone,
|
|
487
|
+
locale: args.locale,
|
|
488
|
+
metadata: args.metadata,
|
|
489
|
+
status: args.status
|
|
490
|
+
});
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
if (isToolEnabled({ name: "set_subscriber_preferences", mutates: true }, guards)) {
|
|
496
|
+
server.registerTool(
|
|
497
|
+
"set_subscriber_preferences",
|
|
498
|
+
{
|
|
499
|
+
title: "Set subscriber preferences",
|
|
500
|
+
description: "Set notification preferences for a subscriber. Each preference targets a category and/or channel and toggles `enabled`.",
|
|
501
|
+
inputSchema: setSubscriberPreferencesShape
|
|
502
|
+
},
|
|
503
|
+
async (args) => {
|
|
504
|
+
return runTool(async () => {
|
|
505
|
+
const zyphr = getZyphrClient();
|
|
506
|
+
return await zyphr.subscribers.setSubscriberPreferences(args.id, {
|
|
507
|
+
preferences: args.preferences
|
|
508
|
+
});
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// src/tools/templates.ts
|
|
516
|
+
function registerTemplateTools(server, guards) {
|
|
517
|
+
if (isToolEnabled({ name: "list_templates", mutates: false }, guards)) {
|
|
518
|
+
server.registerTool(
|
|
519
|
+
"list_templates",
|
|
520
|
+
{
|
|
521
|
+
title: "List templates",
|
|
522
|
+
description: "List notification templates in the account.",
|
|
523
|
+
inputSchema: listTemplatesShape
|
|
524
|
+
},
|
|
525
|
+
async (args) => {
|
|
526
|
+
return runTool(async () => {
|
|
527
|
+
const zyphr = getZyphrClient();
|
|
528
|
+
return await zyphr.templates.listTemplates(args.limit, args.offset);
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
if (isToolEnabled({ name: "get_template", mutates: false }, guards)) {
|
|
534
|
+
server.registerTool(
|
|
535
|
+
"get_template",
|
|
536
|
+
{
|
|
537
|
+
title: "Get template",
|
|
538
|
+
description: "Fetch a single template by ID.",
|
|
539
|
+
inputSchema: getTemplateShape
|
|
540
|
+
},
|
|
541
|
+
async (args) => {
|
|
542
|
+
return runTool(async () => {
|
|
543
|
+
const zyphr = getZyphrClient();
|
|
544
|
+
return await zyphr.templates.getTemplate(args.id);
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
if (isToolEnabled({ name: "render_template", mutates: false }, guards)) {
|
|
550
|
+
server.registerTool(
|
|
551
|
+
"render_template",
|
|
552
|
+
{
|
|
553
|
+
title: "Render template",
|
|
554
|
+
description: "Preview a template with the given variables WITHOUT sending. Returns the rendered subject/html/text so the AI can show the user what would be sent.",
|
|
555
|
+
inputSchema: renderTemplateShape
|
|
556
|
+
},
|
|
557
|
+
async (args) => {
|
|
558
|
+
return runTool(async () => {
|
|
559
|
+
const zyphr = getZyphrClient();
|
|
560
|
+
return await zyphr.templates.renderTemplate(args.id, { variables: args.variables });
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
if (isToolEnabled({ name: "create_template", mutates: true }, guards)) {
|
|
566
|
+
server.registerTool(
|
|
567
|
+
"create_template",
|
|
568
|
+
{
|
|
569
|
+
title: "Create template",
|
|
570
|
+
description: "Create a new notification template.",
|
|
571
|
+
inputSchema: createTemplateShape
|
|
572
|
+
},
|
|
573
|
+
async (args) => {
|
|
574
|
+
return runTool(async () => {
|
|
575
|
+
const zyphr = getZyphrClient();
|
|
576
|
+
return await zyphr.templates.createTemplate({
|
|
577
|
+
name: args.name,
|
|
578
|
+
description: args.description,
|
|
579
|
+
subject: args.subject,
|
|
580
|
+
html: args.html,
|
|
581
|
+
text: args.text
|
|
582
|
+
});
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// src/integration/quickstart/email.ts
|
|
590
|
+
var DOCS = "https://docs.zyphr.dev/channels/email";
|
|
591
|
+
var ENV = ["ZYPHR_API_KEY"];
|
|
592
|
+
var NEXT_STEPS = [
|
|
593
|
+
"Add ZYPHR_API_KEY to your .env file (run get_sdk_install_for_language to confirm the install).",
|
|
594
|
+
"Wire the new service into your app entrypoint.",
|
|
595
|
+
"Verify your sender domain in the Zyphr dashboard before sending to real recipients."
|
|
596
|
+
];
|
|
597
|
+
var emailChannel = {
|
|
598
|
+
node: {
|
|
599
|
+
sdk: {
|
|
600
|
+
channel: "email",
|
|
601
|
+
language: "node",
|
|
602
|
+
framework: null,
|
|
603
|
+
variant: "sdk",
|
|
604
|
+
files: [
|
|
605
|
+
{
|
|
606
|
+
path: "src/lib/zyphr.ts",
|
|
607
|
+
purpose: "Zyphr SDK client singleton",
|
|
608
|
+
contents: "import { Zyphr } from '@zyphr-dev/node-sdk';\n\nexport const zyphr = new Zyphr({ apiKey: process.env.ZYPHR_API_KEY! });\n",
|
|
609
|
+
overwrite: false
|
|
610
|
+
},
|
|
611
|
+
{
|
|
612
|
+
path: "src/services/notify.ts",
|
|
613
|
+
purpose: "Sends a transactional email through Zyphr",
|
|
614
|
+
contents: "import { zyphr } from '../lib/zyphr.js';\n\nexport async function sendWelcomeEmail(to: string, name: string) {\n return await zyphr.emails.sendEmail({\n to: [{ email: to, name }],\n subject: `Welcome, ${name}!`,\n html: `<h1>Welcome aboard, ${name}!</h1><p>Glad you're here.</p>`,\n });\n}\n",
|
|
615
|
+
overwrite: false
|
|
616
|
+
}
|
|
617
|
+
],
|
|
618
|
+
envVarsNeeded: ENV,
|
|
619
|
+
nextSteps: NEXT_STEPS,
|
|
620
|
+
docsUrl: DOCS
|
|
621
|
+
},
|
|
622
|
+
frameworks: {
|
|
623
|
+
express: {
|
|
624
|
+
channel: "email",
|
|
625
|
+
language: "node",
|
|
626
|
+
framework: "express",
|
|
627
|
+
variant: "sdk",
|
|
628
|
+
files: [
|
|
629
|
+
{
|
|
630
|
+
path: "src/lib/zyphr.ts",
|
|
631
|
+
purpose: "Zyphr SDK client singleton",
|
|
632
|
+
contents: "import { Zyphr } from '@zyphr-dev/node-sdk';\n\nexport const zyphr = new Zyphr({ apiKey: process.env.ZYPHR_API_KEY! });\n",
|
|
633
|
+
overwrite: false
|
|
634
|
+
},
|
|
635
|
+
{
|
|
636
|
+
path: "src/routes/notify.ts",
|
|
637
|
+
purpose: "Express route that sends a welcome email",
|
|
638
|
+
contents: "import { Router } from 'express';\nimport { zyphr } from '../lib/zyphr.js';\n\nexport const notifyRouter = Router();\n\nnotifyRouter.post('/notify', async (req, res, next) => {\n try {\n const { to, name } = req.body as { to: string; name: string };\n const result = await zyphr.emails.sendEmail({\n to: [{ email: to, name }],\n subject: `Welcome, ${name}!`,\n html: `<h1>Welcome, ${name}!</h1>`,\n });\n res.json(result);\n } catch (err) {\n next(err);\n }\n});\n",
|
|
639
|
+
overwrite: false
|
|
640
|
+
}
|
|
641
|
+
],
|
|
642
|
+
envVarsNeeded: ENV,
|
|
643
|
+
nextSteps: [
|
|
644
|
+
...NEXT_STEPS,
|
|
645
|
+
"Mount the router in app.ts: app.use('/api', notifyRouter)",
|
|
646
|
+
`Test: curl -X POST http://localhost:3000/api/notify -d '{"to":"you@example.com","name":"You"}' -H "Content-Type: application/json"`
|
|
647
|
+
],
|
|
648
|
+
docsUrl: DOCS
|
|
649
|
+
},
|
|
650
|
+
nextjs: {
|
|
651
|
+
channel: "email",
|
|
652
|
+
language: "node",
|
|
653
|
+
framework: "nextjs",
|
|
654
|
+
variant: "sdk",
|
|
655
|
+
files: [
|
|
656
|
+
{
|
|
657
|
+
path: "src/lib/zyphr.ts",
|
|
658
|
+
purpose: "Zyphr SDK client singleton (server-only)",
|
|
659
|
+
contents: "import 'server-only';\nimport { Zyphr } from '@zyphr-dev/node-sdk';\n\nexport const zyphr = new Zyphr({ apiKey: process.env.ZYPHR_API_KEY! });\n",
|
|
660
|
+
overwrite: false
|
|
661
|
+
},
|
|
662
|
+
{
|
|
663
|
+
path: "src/app/api/notify/route.ts",
|
|
664
|
+
purpose: "Next.js App Router route handler that sends a welcome email",
|
|
665
|
+
contents: "import { NextResponse } from 'next/server';\nimport { zyphr } from '@/lib/zyphr';\n\nexport async function POST(req: Request) {\n const { to, name } = (await req.json()) as { to: string; name: string };\n const result = await zyphr.emails.sendEmail({\n to: [{ email: to, name }],\n subject: `Welcome, ${name}!`,\n html: `<h1>Welcome, ${name}!</h1>`,\n });\n return NextResponse.json(result);\n}\n",
|
|
666
|
+
overwrite: false
|
|
667
|
+
}
|
|
668
|
+
],
|
|
669
|
+
envVarsNeeded: ENV,
|
|
670
|
+
nextSteps: [
|
|
671
|
+
...NEXT_STEPS,
|
|
672
|
+
`Test: curl -X POST http://localhost:3000/api/notify -d '{"to":"you@example.com","name":"You"}' -H "Content-Type: application/json"`
|
|
673
|
+
],
|
|
674
|
+
docsUrl: DOCS
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
},
|
|
678
|
+
python: {
|
|
679
|
+
sdk: {
|
|
680
|
+
channel: "email",
|
|
681
|
+
language: "python",
|
|
682
|
+
framework: null,
|
|
683
|
+
variant: "sdk",
|
|
684
|
+
files: [
|
|
685
|
+
{
|
|
686
|
+
path: "app/zyphr_client.py",
|
|
687
|
+
purpose: "REST helper for the Zyphr API",
|
|
688
|
+
contents: 'import os\nimport requests\n\nZYPHR_API_KEY = os.environ["ZYPHR_API_KEY"]\nBASE_URL = "https://api.zyphr.dev/v1"\n\nheaders = {\n "X-API-Key": ZYPHR_API_KEY,\n "Content-Type": "application/json",\n}\n\ndef zyphr_request(method, path, json=None, params=None):\n response = requests.request(\n method, f"{BASE_URL}{path}", headers=headers, json=json, params=params,\n )\n response.raise_for_status()\n return response.json()\n',
|
|
689
|
+
overwrite: false
|
|
690
|
+
},
|
|
691
|
+
{
|
|
692
|
+
path: "app/notify.py",
|
|
693
|
+
purpose: "Send a welcome email through Zyphr",
|
|
694
|
+
contents: 'from .zyphr_client import zyphr_request\n\ndef send_welcome_email(to: str, name: str) -> dict:\n return zyphr_request("POST", "/emails", json={\n "to": [{"email": to, "name": name}],\n "subject": f"Welcome, {name}!",\n "html": f"<h1>Welcome, {name}!</h1>",\n })\n',
|
|
695
|
+
overwrite: false
|
|
696
|
+
}
|
|
697
|
+
],
|
|
698
|
+
envVarsNeeded: ENV,
|
|
699
|
+
nextSteps: NEXT_STEPS,
|
|
700
|
+
docsUrl: DOCS
|
|
701
|
+
},
|
|
702
|
+
frameworks: {
|
|
703
|
+
flask: {
|
|
704
|
+
channel: "email",
|
|
705
|
+
language: "python",
|
|
706
|
+
framework: "flask",
|
|
707
|
+
variant: "sdk",
|
|
708
|
+
files: [
|
|
709
|
+
{
|
|
710
|
+
path: "app/zyphr_client.py",
|
|
711
|
+
purpose: "REST helper for the Zyphr API",
|
|
712
|
+
contents: 'import os\nimport requests\n\nBASE_URL = "https://api.zyphr.dev/v1"\n\ndef zyphr_request(method, path, json=None, params=None):\n response = requests.request(\n method, f"{BASE_URL}{path}",\n headers={"X-API-Key": os.environ["ZYPHR_API_KEY"], "Content-Type": "application/json"},\n json=json, params=params,\n )\n response.raise_for_status()\n return response.json()\n',
|
|
713
|
+
overwrite: false
|
|
714
|
+
},
|
|
715
|
+
{
|
|
716
|
+
path: "app/routes/notify.py",
|
|
717
|
+
purpose: "Flask blueprint that sends a welcome email",
|
|
718
|
+
contents: `from flask import Blueprint, request, jsonify
|
|
719
|
+
from ..zyphr_client import zyphr_request
|
|
720
|
+
|
|
721
|
+
notify_bp = Blueprint("notify", __name__)
|
|
722
|
+
|
|
723
|
+
@notify_bp.route("/notify", methods=["POST"])
|
|
724
|
+
def notify():
|
|
725
|
+
body = request.get_json() or {}
|
|
726
|
+
result = zyphr_request("POST", "/emails", json={
|
|
727
|
+
"to": [{"email": body["to"], "name": body.get("name", "")}],
|
|
728
|
+
"subject": f"Welcome, {body.get('name', 'friend')}!",
|
|
729
|
+
"html": f"<h1>Welcome!</h1>",
|
|
730
|
+
})
|
|
731
|
+
return jsonify(result)
|
|
732
|
+
`,
|
|
733
|
+
overwrite: false
|
|
734
|
+
}
|
|
735
|
+
],
|
|
736
|
+
envVarsNeeded: ENV,
|
|
737
|
+
nextSteps: [
|
|
738
|
+
...NEXT_STEPS,
|
|
739
|
+
'Register the blueprint: app.register_blueprint(notify_bp, url_prefix="/api")'
|
|
740
|
+
],
|
|
741
|
+
docsUrl: DOCS
|
|
742
|
+
},
|
|
743
|
+
fastapi: {
|
|
744
|
+
channel: "email",
|
|
745
|
+
language: "python",
|
|
746
|
+
framework: "fastapi",
|
|
747
|
+
variant: "sdk",
|
|
748
|
+
files: [
|
|
749
|
+
{
|
|
750
|
+
path: "app/zyphr_client.py",
|
|
751
|
+
purpose: "REST helper for the Zyphr API",
|
|
752
|
+
contents: 'import os\nimport httpx\n\nBASE_URL = "https://api.zyphr.dev/v1"\n\nasync def zyphr_request(method: str, path: str, json: dict | None = None) -> dict:\n async with httpx.AsyncClient() as client:\n resp = await client.request(\n method, f"{BASE_URL}{path}",\n headers={"X-API-Key": os.environ["ZYPHR_API_KEY"], "Content-Type": "application/json"},\n json=json,\n )\n resp.raise_for_status()\n return resp.json()\n',
|
|
753
|
+
overwrite: false
|
|
754
|
+
},
|
|
755
|
+
{
|
|
756
|
+
path: "app/routers/notify.py",
|
|
757
|
+
purpose: "FastAPI router that sends a welcome email",
|
|
758
|
+
contents: 'from fastapi import APIRouter\nfrom pydantic import BaseModel, EmailStr\nfrom ..zyphr_client import zyphr_request\n\nrouter = APIRouter()\n\nclass NotifyIn(BaseModel):\n to: EmailStr\n name: str\n\n@router.post("/notify")\nasync def notify(body: NotifyIn):\n return await zyphr_request("POST", "/emails", json={\n "to": [{"email": body.to, "name": body.name}],\n "subject": f"Welcome, {body.name}!",\n "html": f"<h1>Welcome, {body.name}!</h1>",\n })\n',
|
|
759
|
+
overwrite: false
|
|
760
|
+
}
|
|
761
|
+
],
|
|
762
|
+
envVarsNeeded: ENV,
|
|
763
|
+
nextSteps: [
|
|
764
|
+
...NEXT_STEPS,
|
|
765
|
+
"Install httpx: `pip install httpx` (or `poetry add httpx`)",
|
|
766
|
+
'Mount the router: app.include_router(router, prefix="/api")'
|
|
767
|
+
],
|
|
768
|
+
docsUrl: DOCS
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
},
|
|
772
|
+
ruby: {
|
|
773
|
+
sdk: {
|
|
774
|
+
channel: "email",
|
|
775
|
+
language: "ruby",
|
|
776
|
+
framework: null,
|
|
777
|
+
variant: "sdk",
|
|
778
|
+
files: [
|
|
779
|
+
{
|
|
780
|
+
path: "config/initializers/zyphr.rb",
|
|
781
|
+
purpose: "Zyphr SDK configuration",
|
|
782
|
+
contents: "require 'zyphr'\n\nZyphr.configure do |config|\n config.api_key['X-API-Key'] = ENV.fetch('ZYPHR_API_KEY')\nend\n",
|
|
783
|
+
overwrite: false
|
|
784
|
+
},
|
|
785
|
+
{
|
|
786
|
+
path: "app/services/notify_service.rb",
|
|
787
|
+
purpose: "Service object that sends a welcome email",
|
|
788
|
+
contents: 'class NotifyService\n def self.send_welcome_email(to:, name:)\n Zyphr::EmailsApi.new.send_email(\n Zyphr::SendEmailRequest.new(\n to: [{ email: to, name: name }],\n subject: "Welcome, #{name}!",\n html: "<h1>Welcome, #{name}!</h1>"\n )\n )\n end\nend\n',
|
|
789
|
+
overwrite: false
|
|
790
|
+
}
|
|
791
|
+
],
|
|
792
|
+
envVarsNeeded: ENV,
|
|
793
|
+
nextSteps: NEXT_STEPS,
|
|
794
|
+
docsUrl: DOCS
|
|
795
|
+
},
|
|
796
|
+
frameworks: {
|
|
797
|
+
rails: {
|
|
798
|
+
channel: "email",
|
|
799
|
+
language: "ruby",
|
|
800
|
+
framework: "rails",
|
|
801
|
+
variant: "sdk",
|
|
802
|
+
files: [
|
|
803
|
+
{
|
|
804
|
+
path: "config/initializers/zyphr.rb",
|
|
805
|
+
purpose: "Zyphr SDK configuration",
|
|
806
|
+
contents: "require 'zyphr'\n\nZyphr.configure do |config|\n config.api_key['X-API-Key'] = ENV.fetch('ZYPHR_API_KEY')\nend\n",
|
|
807
|
+
overwrite: false
|
|
808
|
+
},
|
|
809
|
+
{
|
|
810
|
+
path: "app/controllers/notify_controller.rb",
|
|
811
|
+
purpose: "Rails controller that sends a welcome email",
|
|
812
|
+
contents: 'class NotifyController < ApplicationController\n def create\n result = Zyphr::EmailsApi.new.send_email(\n Zyphr::SendEmailRequest.new(\n to: [{ email: params.require(:to), name: params[:name] }],\n subject: "Welcome, #{params[:name]}!",\n html: "<h1>Welcome, #{params[:name]}!</h1>"\n )\n )\n render json: result\n end\nend\n',
|
|
813
|
+
overwrite: false
|
|
814
|
+
}
|
|
815
|
+
],
|
|
816
|
+
envVarsNeeded: ENV,
|
|
817
|
+
nextSteps: [
|
|
818
|
+
...NEXT_STEPS,
|
|
819
|
+
"Add a route in config/routes.rb: post '/notify', to: 'notify#create'"
|
|
820
|
+
],
|
|
821
|
+
docsUrl: DOCS
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
},
|
|
825
|
+
go: {
|
|
826
|
+
sdk: {
|
|
827
|
+
channel: "email",
|
|
828
|
+
language: "go",
|
|
829
|
+
framework: null,
|
|
830
|
+
variant: "sdk",
|
|
831
|
+
files: [
|
|
832
|
+
{
|
|
833
|
+
path: "internal/zyphr/client.go",
|
|
834
|
+
purpose: "Thin REST client for the Zyphr API",
|
|
835
|
+
contents: 'package zyphr\n\nimport (\n "bytes"\n "encoding/json"\n "fmt"\n "io"\n "net/http"\n "os"\n)\n\nconst baseURL = "https://api.zyphr.dev/v1"\n\ntype Client struct {\n APIKey string\n HTTPClient *http.Client\n}\n\nfunc NewClient() *Client {\n return &Client{APIKey: os.Getenv("ZYPHR_API_KEY"), HTTPClient: &http.Client{}}\n}\n\nfunc (c *Client) Do(method, path string, body any) ([]byte, error) {\n var buf io.Reader\n if body != nil {\n b, err := json.Marshal(body)\n if err != nil { return nil, fmt.Errorf("marshal: %w", err) }\n buf = bytes.NewReader(b)\n }\n req, _ := http.NewRequest(method, baseURL+path, buf)\n req.Header.Set("X-API-Key", c.APIKey)\n req.Header.Set("Content-Type", "application/json")\n resp, err := c.HTTPClient.Do(req)\n if err != nil { return nil, err }\n defer resp.Body.Close()\n data, _ := io.ReadAll(resp.Body)\n if resp.StatusCode >= 400 { return nil, fmt.Errorf("zyphr %d: %s", resp.StatusCode, data) }\n return data, nil\n}\n',
|
|
836
|
+
overwrite: false
|
|
837
|
+
},
|
|
838
|
+
{
|
|
839
|
+
path: "internal/notify/notify.go",
|
|
840
|
+
purpose: "Send a welcome email through Zyphr",
|
|
841
|
+
contents: 'package notify\n\nimport "yourapp/internal/zyphr"\n\nfunc SendWelcomeEmail(client *zyphr.Client, to, name string) ([]byte, error) {\n return client.Do("POST", "/emails", map[string]any{\n "to": []map[string]string{{"email": to, "name": name}},\n "subject": "Welcome, " + name + "!",\n "html": "<h1>Welcome, " + name + "!</h1>",\n })\n}\n',
|
|
842
|
+
overwrite: false
|
|
843
|
+
}
|
|
844
|
+
],
|
|
845
|
+
envVarsNeeded: ENV,
|
|
846
|
+
nextSteps: NEXT_STEPS,
|
|
847
|
+
docsUrl: DOCS
|
|
848
|
+
}
|
|
849
|
+
},
|
|
850
|
+
php: {
|
|
851
|
+
sdk: {
|
|
852
|
+
channel: "email",
|
|
853
|
+
language: "php",
|
|
854
|
+
framework: null,
|
|
855
|
+
variant: "sdk",
|
|
856
|
+
files: [
|
|
857
|
+
{
|
|
858
|
+
path: "app/Services/ZyphrClient.php",
|
|
859
|
+
purpose: "Guzzle-backed Zyphr client",
|
|
860
|
+
contents: `<?php
|
|
861
|
+
|
|
862
|
+
namespace App\\Services;
|
|
863
|
+
|
|
864
|
+
use GuzzleHttp\\Client;
|
|
865
|
+
|
|
866
|
+
class ZyphrClient
|
|
867
|
+
{
|
|
868
|
+
private Client $http;
|
|
869
|
+
|
|
870
|
+
public function __construct()
|
|
871
|
+
{
|
|
872
|
+
$this->http = new Client([
|
|
873
|
+
'base_uri' => 'https://api.zyphr.dev/v1/',
|
|
874
|
+
'headers' => [
|
|
875
|
+
'X-API-Key' => getenv('ZYPHR_API_KEY'),
|
|
876
|
+
'Content-Type' => 'application/json',
|
|
877
|
+
],
|
|
878
|
+
]);
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
public function sendWelcomeEmail(string $to, string $name): array
|
|
882
|
+
{
|
|
883
|
+
$response = $this->http->post('emails', [
|
|
884
|
+
'json' => [
|
|
885
|
+
'to' => [['email' => $to, 'name' => $name]],
|
|
886
|
+
'subject' => "Welcome, {$name}!",
|
|
887
|
+
'html' => "<h1>Welcome, {$name}!</h1>",
|
|
888
|
+
],
|
|
889
|
+
]);
|
|
890
|
+
return json_decode((string) $response->getBody(), true);
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
`,
|
|
894
|
+
overwrite: false
|
|
895
|
+
}
|
|
896
|
+
],
|
|
897
|
+
envVarsNeeded: ENV,
|
|
898
|
+
nextSteps: NEXT_STEPS,
|
|
899
|
+
docsUrl: DOCS
|
|
900
|
+
},
|
|
901
|
+
frameworks: {
|
|
902
|
+
laravel: {
|
|
903
|
+
channel: "email",
|
|
904
|
+
language: "php",
|
|
905
|
+
framework: "laravel",
|
|
906
|
+
variant: "sdk",
|
|
907
|
+
files: [
|
|
908
|
+
{
|
|
909
|
+
path: "app/Services/ZyphrClient.php",
|
|
910
|
+
purpose: "Laravel-friendly Zyphr client",
|
|
911
|
+
contents: `<?php
|
|
912
|
+
|
|
913
|
+
namespace App\\Services;
|
|
914
|
+
|
|
915
|
+
use Illuminate\\Support\\Facades\\Http;
|
|
916
|
+
|
|
917
|
+
class ZyphrClient
|
|
918
|
+
{
|
|
919
|
+
public function sendWelcomeEmail(string $to, string $name): array
|
|
920
|
+
{
|
|
921
|
+
$response = Http::withHeaders([
|
|
922
|
+
'X-API-Key' => config('services.zyphr.api_key'),
|
|
923
|
+
'Content-Type' => 'application/json',
|
|
924
|
+
])->post('https://api.zyphr.dev/v1/emails', [
|
|
925
|
+
'to' => [['email' => $to, 'name' => $name]],
|
|
926
|
+
'subject' => "Welcome, {$name}!",
|
|
927
|
+
'html' => "<h1>Welcome, {$name}!</h1>",
|
|
928
|
+
]);
|
|
929
|
+
$response->throw();
|
|
930
|
+
return $response->json();
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
`,
|
|
934
|
+
overwrite: false
|
|
935
|
+
},
|
|
936
|
+
{
|
|
937
|
+
path: "config/services.php (snippet)",
|
|
938
|
+
purpose: "Register the Zyphr API key under services config",
|
|
939
|
+
contents: "'zyphr' => [\n 'api_key' => env('ZYPHR_API_KEY'),\n],\n",
|
|
940
|
+
overwrite: false
|
|
941
|
+
}
|
|
942
|
+
],
|
|
943
|
+
envVarsNeeded: ENV,
|
|
944
|
+
nextSteps: NEXT_STEPS,
|
|
945
|
+
docsUrl: DOCS
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
},
|
|
949
|
+
csharp: {
|
|
950
|
+
sdk: {
|
|
951
|
+
channel: "email",
|
|
952
|
+
language: "csharp",
|
|
953
|
+
framework: null,
|
|
954
|
+
variant: "sdk",
|
|
955
|
+
files: [
|
|
956
|
+
{
|
|
957
|
+
path: "Services/ZyphrClient.cs",
|
|
958
|
+
purpose: "Singleton-style Zyphr client wrapper",
|
|
959
|
+
contents: 'using ZyphrDev.SDK.Api;\nusing ZyphrDev.SDK.Client;\nusing ZyphrDev.SDK.Model;\n\nnamespace YourApp.Services;\n\npublic class ZyphrClient\n{\n private readonly EmailsApi _emails;\n\n public ZyphrClient()\n {\n var config = new Configuration\n {\n ApiKey = new Dictionary<string, string>\n {\n { "X-API-Key", Environment.GetEnvironmentVariable("ZYPHR_API_KEY")! }\n }\n };\n _emails = new EmailsApi(config);\n }\n\n public async Task<SendEmailResponse> SendWelcomeEmailAsync(string to, string name)\n {\n return await _emails.SendEmailAsync(new SendEmailRequest(\n to: new List<EmailAddress> { new() { Email = to, Name = name } },\n subject: $"Welcome, {name}!",\n html: $"<h1>Welcome, {name}!</h1>"\n ));\n }\n}\n',
|
|
960
|
+
overwrite: false
|
|
961
|
+
}
|
|
962
|
+
],
|
|
963
|
+
envVarsNeeded: ENV,
|
|
964
|
+
nextSteps: NEXT_STEPS,
|
|
965
|
+
docsUrl: DOCS
|
|
966
|
+
},
|
|
967
|
+
frameworks: {
|
|
968
|
+
aspnetcore: {
|
|
969
|
+
channel: "email",
|
|
970
|
+
language: "csharp",
|
|
971
|
+
framework: "aspnetcore",
|
|
972
|
+
variant: "sdk",
|
|
973
|
+
files: [
|
|
974
|
+
{
|
|
975
|
+
path: "Services/ZyphrClient.cs",
|
|
976
|
+
purpose: "DI-friendly Zyphr client",
|
|
977
|
+
contents: 'using ZyphrDev.SDK.Api;\nusing ZyphrDev.SDK.Client;\nusing ZyphrDev.SDK.Model;\n\nnamespace YourApp.Services;\n\npublic class ZyphrClient\n{\n public EmailsApi Emails { get; }\n\n public ZyphrClient(IConfiguration cfg)\n {\n var config = new Configuration\n {\n ApiKey = new Dictionary<string, string>\n {\n { "X-API-Key", cfg["Zyphr:ApiKey"]! }\n }\n };\n Emails = new EmailsApi(config);\n }\n}\n',
|
|
978
|
+
overwrite: false
|
|
979
|
+
},
|
|
980
|
+
{
|
|
981
|
+
path: "Controllers/NotifyController.cs",
|
|
982
|
+
purpose: "ASP.NET Core controller that sends a welcome email",
|
|
983
|
+
contents: 'using Microsoft.AspNetCore.Mvc;\nusing YourApp.Services;\nusing ZyphrDev.SDK.Model;\n\n[ApiController]\n[Route("api/[controller]")]\npublic class NotifyController : ControllerBase\n{\n private readonly ZyphrClient _zyphr;\n public NotifyController(ZyphrClient zyphr) => _zyphr = zyphr;\n\n public record NotifyIn(string To, string Name);\n\n [HttpPost]\n public async Task<IActionResult> Post([FromBody] NotifyIn body)\n {\n var result = await _zyphr.Emails.SendEmailAsync(new SendEmailRequest(\n to: new List<EmailAddress> { new() { Email = body.To, Name = body.Name } },\n subject: $"Welcome, {body.Name}!",\n html: $"<h1>Welcome, {body.Name}!</h1>"\n ));\n return Ok(result);\n }\n}\n',
|
|
984
|
+
overwrite: false
|
|
985
|
+
}
|
|
986
|
+
],
|
|
987
|
+
envVarsNeeded: ENV,
|
|
988
|
+
nextSteps: [
|
|
989
|
+
...NEXT_STEPS,
|
|
990
|
+
"Register the client in Program.cs: builder.Services.AddSingleton<ZyphrClient>();"
|
|
991
|
+
],
|
|
992
|
+
docsUrl: DOCS
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
};
|
|
997
|
+
|
|
998
|
+
// src/integration/quickstart/inbox.ts
|
|
999
|
+
var DOCS2 = "https://docs.zyphr.dev/channels/in-app-messaging";
|
|
1000
|
+
var ENV2 = ["ZYPHR_API_KEY"];
|
|
1001
|
+
var NEXT_STEPS2 = [
|
|
1002
|
+
"Add ZYPHR_API_KEY to your .env file.",
|
|
1003
|
+
"Use the matching subscriberId on the client (e.g. @zyphr-dev/inbox-react) to display the message."
|
|
1004
|
+
];
|
|
1005
|
+
var inboxChannel = {
|
|
1006
|
+
node: {
|
|
1007
|
+
sdk: {
|
|
1008
|
+
channel: "inbox",
|
|
1009
|
+
language: "node",
|
|
1010
|
+
framework: null,
|
|
1011
|
+
variant: "sdk",
|
|
1012
|
+
files: [
|
|
1013
|
+
{
|
|
1014
|
+
path: "src/services/inbox.ts",
|
|
1015
|
+
purpose: "Send an in-app inbox message through Zyphr",
|
|
1016
|
+
contents: "import { Zyphr } from '@zyphr-dev/node-sdk';\n\nconst zyphr = new Zyphr({ apiKey: process.env.ZYPHR_API_KEY! });\n\nexport async function notifyReportReady(subscriberId: string, reportId: string) {\n return await zyphr.inbox.sendInApp({\n subscriberId,\n title: 'New report ready',\n body: 'Your report finished processing \u2014 click to view.',\n actionUrl: `/reports/${reportId}`,\n actionLabel: 'View report',\n });\n}\n",
|
|
1017
|
+
overwrite: false
|
|
1018
|
+
}
|
|
1019
|
+
],
|
|
1020
|
+
envVarsNeeded: ENV2,
|
|
1021
|
+
nextSteps: NEXT_STEPS2,
|
|
1022
|
+
docsUrl: DOCS2
|
|
1023
|
+
}
|
|
1024
|
+
},
|
|
1025
|
+
python: {
|
|
1026
|
+
sdk: {
|
|
1027
|
+
channel: "inbox",
|
|
1028
|
+
language: "python",
|
|
1029
|
+
framework: null,
|
|
1030
|
+
variant: "sdk",
|
|
1031
|
+
files: [
|
|
1032
|
+
{
|
|
1033
|
+
path: "app/inbox.py",
|
|
1034
|
+
purpose: "Send an in-app inbox message through Zyphr",
|
|
1035
|
+
contents: 'from .zyphr_client import zyphr_request\n\ndef notify_report_ready(subscriber_id: str, report_id: str) -> dict:\n return zyphr_request("POST", "/inbox", json={\n "subscriberId": subscriber_id,\n "title": "New report ready",\n "body": "Your report finished processing \u2014 click to view.",\n "actionUrl": f"/reports/{report_id}",\n "actionLabel": "View report",\n })\n',
|
|
1036
|
+
overwrite: false
|
|
1037
|
+
}
|
|
1038
|
+
],
|
|
1039
|
+
envVarsNeeded: ENV2,
|
|
1040
|
+
nextSteps: NEXT_STEPS2,
|
|
1041
|
+
docsUrl: DOCS2
|
|
1042
|
+
}
|
|
1043
|
+
},
|
|
1044
|
+
ruby: {
|
|
1045
|
+
sdk: {
|
|
1046
|
+
channel: "inbox",
|
|
1047
|
+
language: "ruby",
|
|
1048
|
+
framework: null,
|
|
1049
|
+
variant: "sdk",
|
|
1050
|
+
files: [
|
|
1051
|
+
{
|
|
1052
|
+
path: "app/services/inbox_service.rb",
|
|
1053
|
+
purpose: "Send an in-app inbox message through Zyphr",
|
|
1054
|
+
contents: `class InboxService
|
|
1055
|
+
def self.notify_report_ready(subscriber_id:, report_id:)
|
|
1056
|
+
Zyphr::InboxApi.new.send_in_app(
|
|
1057
|
+
Zyphr::SendInAppRequest.new(
|
|
1058
|
+
subscriber_id: subscriber_id,
|
|
1059
|
+
title: 'New report ready',
|
|
1060
|
+
body: 'Your report finished processing \u2014 click to view.',
|
|
1061
|
+
action_url: "/reports/#{report_id}",
|
|
1062
|
+
action_label: 'View report'
|
|
1063
|
+
)
|
|
1064
|
+
)
|
|
1065
|
+
end
|
|
1066
|
+
end
|
|
1067
|
+
`,
|
|
1068
|
+
overwrite: false
|
|
1069
|
+
}
|
|
1070
|
+
],
|
|
1071
|
+
envVarsNeeded: ENV2,
|
|
1072
|
+
nextSteps: NEXT_STEPS2,
|
|
1073
|
+
docsUrl: DOCS2
|
|
1074
|
+
}
|
|
1075
|
+
},
|
|
1076
|
+
go: {
|
|
1077
|
+
sdk: {
|
|
1078
|
+
channel: "inbox",
|
|
1079
|
+
language: "go",
|
|
1080
|
+
framework: null,
|
|
1081
|
+
variant: "sdk",
|
|
1082
|
+
files: [
|
|
1083
|
+
{
|
|
1084
|
+
path: "internal/notify/inbox.go",
|
|
1085
|
+
purpose: "Send an in-app inbox message through Zyphr",
|
|
1086
|
+
contents: 'package notify\n\nimport "yourapp/internal/zyphr"\n\nfunc NotifyReportReady(client *zyphr.Client, subscriberID, reportID string) ([]byte, error) {\n return client.Do("POST", "/inbox", map[string]any{\n "subscriberId": subscriberID,\n "title": "New report ready",\n "body": "Your report finished processing \u2014 click to view.",\n "actionUrl": "/reports/" + reportID,\n "actionLabel": "View report",\n })\n}\n',
|
|
1087
|
+
overwrite: false
|
|
1088
|
+
}
|
|
1089
|
+
],
|
|
1090
|
+
envVarsNeeded: ENV2,
|
|
1091
|
+
nextSteps: NEXT_STEPS2,
|
|
1092
|
+
docsUrl: DOCS2
|
|
1093
|
+
}
|
|
1094
|
+
},
|
|
1095
|
+
php: {
|
|
1096
|
+
sdk: {
|
|
1097
|
+
channel: "inbox",
|
|
1098
|
+
language: "php",
|
|
1099
|
+
framework: null,
|
|
1100
|
+
variant: "sdk",
|
|
1101
|
+
files: [
|
|
1102
|
+
{
|
|
1103
|
+
path: "app/Services/InboxService.php",
|
|
1104
|
+
purpose: "Send an in-app inbox message through Zyphr",
|
|
1105
|
+
contents: `<?php
|
|
1106
|
+
|
|
1107
|
+
namespace App\\Services;
|
|
1108
|
+
|
|
1109
|
+
use GuzzleHttp\\Client;
|
|
1110
|
+
|
|
1111
|
+
class InboxService
|
|
1112
|
+
{
|
|
1113
|
+
public function notifyReportReady(string $subscriberId, string $reportId): array
|
|
1114
|
+
{
|
|
1115
|
+
$http = new Client([
|
|
1116
|
+
'base_uri' => 'https://api.zyphr.dev/v1/',
|
|
1117
|
+
'headers' => [
|
|
1118
|
+
'X-API-Key' => getenv('ZYPHR_API_KEY'),
|
|
1119
|
+
'Content-Type' => 'application/json',
|
|
1120
|
+
],
|
|
1121
|
+
]);
|
|
1122
|
+
$r = $http->post('inbox', ['json' => [
|
|
1123
|
+
'subscriberId' => $subscriberId,
|
|
1124
|
+
'title' => 'New report ready',
|
|
1125
|
+
'body' => 'Your report finished processing \u2014 click to view.',
|
|
1126
|
+
'actionUrl' => "/reports/{$reportId}",
|
|
1127
|
+
'actionLabel' => 'View report',
|
|
1128
|
+
]]);
|
|
1129
|
+
return json_decode((string) $r->getBody(), true);
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
`,
|
|
1133
|
+
overwrite: false
|
|
1134
|
+
}
|
|
1135
|
+
],
|
|
1136
|
+
envVarsNeeded: ENV2,
|
|
1137
|
+
nextSteps: NEXT_STEPS2,
|
|
1138
|
+
docsUrl: DOCS2
|
|
1139
|
+
}
|
|
1140
|
+
},
|
|
1141
|
+
csharp: {
|
|
1142
|
+
sdk: {
|
|
1143
|
+
channel: "inbox",
|
|
1144
|
+
language: "csharp",
|
|
1145
|
+
framework: null,
|
|
1146
|
+
variant: "sdk",
|
|
1147
|
+
files: [
|
|
1148
|
+
{
|
|
1149
|
+
path: "Services/InboxService.cs",
|
|
1150
|
+
purpose: "Send an in-app inbox message through Zyphr",
|
|
1151
|
+
contents: 'using ZyphrDev.SDK.Api;\nusing ZyphrDev.SDK.Client;\nusing ZyphrDev.SDK.Model;\n\nnamespace YourApp.Services;\n\npublic class InboxService\n{\n private readonly InboxApi _inbox;\n public InboxService()\n {\n var config = new Configuration\n {\n ApiKey = new Dictionary<string, string>\n {\n { "X-API-Key", Environment.GetEnvironmentVariable("ZYPHR_API_KEY")! }\n }\n };\n _inbox = new InboxApi(config);\n }\n\n public Task<SendInAppResponse> NotifyReportReadyAsync(string subscriberId, string reportId)\n {\n return _inbox.SendInAppAsync(new SendInAppRequest(\n subscriberId: subscriberId,\n title: "New report ready",\n body: "Your report finished processing \u2014 click to view.",\n actionUrl: $"/reports/{reportId}",\n actionLabel: "View report"\n ));\n }\n}\n',
|
|
1152
|
+
overwrite: false
|
|
1153
|
+
}
|
|
1154
|
+
],
|
|
1155
|
+
envVarsNeeded: ENV2,
|
|
1156
|
+
nextSteps: NEXT_STEPS2,
|
|
1157
|
+
docsUrl: DOCS2
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
};
|
|
1161
|
+
|
|
1162
|
+
// src/integration/quickstart/push.ts
|
|
1163
|
+
var DOCS3 = "https://docs.zyphr.dev/channels/push-notifications";
|
|
1164
|
+
var ENV3 = ["ZYPHR_API_KEY"];
|
|
1165
|
+
var NEXT_STEPS3 = [
|
|
1166
|
+
"Add ZYPHR_API_KEY to your .env file.",
|
|
1167
|
+
"Register at least one device for the target subscriber (via the SDK or dashboard) before sending.",
|
|
1168
|
+
"Verify your push provider credentials (APNs/FCM) are configured in the Zyphr dashboard."
|
|
1169
|
+
];
|
|
1170
|
+
var pushChannel = {
|
|
1171
|
+
node: {
|
|
1172
|
+
sdk: {
|
|
1173
|
+
channel: "push",
|
|
1174
|
+
language: "node",
|
|
1175
|
+
framework: null,
|
|
1176
|
+
variant: "sdk",
|
|
1177
|
+
files: [
|
|
1178
|
+
{
|
|
1179
|
+
path: "src/services/push.ts",
|
|
1180
|
+
purpose: "Send a push notification through Zyphr",
|
|
1181
|
+
contents: "import { Zyphr } from '@zyphr-dev/node-sdk';\n\nconst zyphr = new Zyphr({ apiKey: process.env.ZYPHR_API_KEY! });\n\nexport async function pushOrderShipped(subscriberId: string, orderId: string) {\n return await zyphr.push.sendPush({\n subscriberId,\n title: 'Order shipped',\n body: `Order ${orderId} is on its way.`,\n data: { orderId },\n });\n}\n",
|
|
1182
|
+
overwrite: false
|
|
1183
|
+
}
|
|
1184
|
+
],
|
|
1185
|
+
envVarsNeeded: ENV3,
|
|
1186
|
+
nextSteps: NEXT_STEPS3,
|
|
1187
|
+
docsUrl: DOCS3
|
|
1188
|
+
}
|
|
1189
|
+
},
|
|
1190
|
+
python: {
|
|
1191
|
+
sdk: {
|
|
1192
|
+
channel: "push",
|
|
1193
|
+
language: "python",
|
|
1194
|
+
framework: null,
|
|
1195
|
+
variant: "sdk",
|
|
1196
|
+
files: [
|
|
1197
|
+
{
|
|
1198
|
+
path: "app/push.py",
|
|
1199
|
+
purpose: "Send a push notification through Zyphr",
|
|
1200
|
+
contents: 'from .zyphr_client import zyphr_request\n\ndef push_order_shipped(subscriber_id: str, order_id: str) -> dict:\n return zyphr_request("POST", "/push", json={\n "subscriberId": subscriber_id,\n "title": "Order shipped",\n "body": f"Order {order_id} is on its way.",\n "data": {"orderId": order_id},\n })\n',
|
|
1201
|
+
overwrite: false
|
|
1202
|
+
}
|
|
1203
|
+
],
|
|
1204
|
+
envVarsNeeded: ENV3,
|
|
1205
|
+
nextSteps: NEXT_STEPS3,
|
|
1206
|
+
docsUrl: DOCS3
|
|
1207
|
+
}
|
|
1208
|
+
},
|
|
1209
|
+
ruby: {
|
|
1210
|
+
sdk: {
|
|
1211
|
+
channel: "push",
|
|
1212
|
+
language: "ruby",
|
|
1213
|
+
framework: null,
|
|
1214
|
+
variant: "sdk",
|
|
1215
|
+
files: [
|
|
1216
|
+
{
|
|
1217
|
+
path: "app/services/push_service.rb",
|
|
1218
|
+
purpose: "Send a push notification through Zyphr",
|
|
1219
|
+
contents: `class PushService
|
|
1220
|
+
def self.order_shipped(subscriber_id:, order_id:)
|
|
1221
|
+
Zyphr::PushApi.new.send_push(
|
|
1222
|
+
Zyphr::SendPushRequest.new(
|
|
1223
|
+
subscriber_id: subscriber_id,
|
|
1224
|
+
title: 'Order shipped',
|
|
1225
|
+
body: "Order #{order_id} is on its way.",
|
|
1226
|
+
data: { orderId: order_id }
|
|
1227
|
+
)
|
|
1228
|
+
)
|
|
1229
|
+
end
|
|
1230
|
+
end
|
|
1231
|
+
`,
|
|
1232
|
+
overwrite: false
|
|
1233
|
+
}
|
|
1234
|
+
],
|
|
1235
|
+
envVarsNeeded: ENV3,
|
|
1236
|
+
nextSteps: NEXT_STEPS3,
|
|
1237
|
+
docsUrl: DOCS3
|
|
1238
|
+
}
|
|
1239
|
+
},
|
|
1240
|
+
go: {
|
|
1241
|
+
sdk: {
|
|
1242
|
+
channel: "push",
|
|
1243
|
+
language: "go",
|
|
1244
|
+
framework: null,
|
|
1245
|
+
variant: "sdk",
|
|
1246
|
+
files: [
|
|
1247
|
+
{
|
|
1248
|
+
path: "internal/notify/push.go",
|
|
1249
|
+
purpose: "Send a push notification through Zyphr",
|
|
1250
|
+
contents: 'package notify\n\nimport "yourapp/internal/zyphr"\n\nfunc PushOrderShipped(client *zyphr.Client, subscriberID, orderID string) ([]byte, error) {\n return client.Do("POST", "/push", map[string]any{\n "subscriberId": subscriberID,\n "title": "Order shipped",\n "body": "Order " + orderID + " is on its way.",\n "data": map[string]string{"orderId": orderID},\n })\n}\n',
|
|
1251
|
+
overwrite: false
|
|
1252
|
+
}
|
|
1253
|
+
],
|
|
1254
|
+
envVarsNeeded: ENV3,
|
|
1255
|
+
nextSteps: NEXT_STEPS3,
|
|
1256
|
+
docsUrl: DOCS3
|
|
1257
|
+
}
|
|
1258
|
+
},
|
|
1259
|
+
php: {
|
|
1260
|
+
sdk: {
|
|
1261
|
+
channel: "push",
|
|
1262
|
+
language: "php",
|
|
1263
|
+
framework: null,
|
|
1264
|
+
variant: "sdk",
|
|
1265
|
+
files: [
|
|
1266
|
+
{
|
|
1267
|
+
path: "app/Services/PushService.php",
|
|
1268
|
+
purpose: "Send a push notification through Zyphr",
|
|
1269
|
+
contents: `<?php
|
|
1270
|
+
|
|
1271
|
+
namespace App\\Services;
|
|
1272
|
+
|
|
1273
|
+
use GuzzleHttp\\Client;
|
|
1274
|
+
|
|
1275
|
+
class PushService
|
|
1276
|
+
{
|
|
1277
|
+
public function orderShipped(string $subscriberId, string $orderId): array
|
|
1278
|
+
{
|
|
1279
|
+
$http = new Client([
|
|
1280
|
+
'base_uri' => 'https://api.zyphr.dev/v1/',
|
|
1281
|
+
'headers' => [
|
|
1282
|
+
'X-API-Key' => getenv('ZYPHR_API_KEY'),
|
|
1283
|
+
'Content-Type' => 'application/json',
|
|
1284
|
+
],
|
|
1285
|
+
]);
|
|
1286
|
+
$r = $http->post('push', ['json' => [
|
|
1287
|
+
'subscriberId' => $subscriberId,
|
|
1288
|
+
'title' => 'Order shipped',
|
|
1289
|
+
'body' => "Order {$orderId} is on its way.",
|
|
1290
|
+
'data' => ['orderId' => $orderId],
|
|
1291
|
+
]]);
|
|
1292
|
+
return json_decode((string) $r->getBody(), true);
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
`,
|
|
1296
|
+
overwrite: false
|
|
1297
|
+
}
|
|
1298
|
+
],
|
|
1299
|
+
envVarsNeeded: ENV3,
|
|
1300
|
+
nextSteps: NEXT_STEPS3,
|
|
1301
|
+
docsUrl: DOCS3
|
|
1302
|
+
}
|
|
1303
|
+
},
|
|
1304
|
+
csharp: {
|
|
1305
|
+
sdk: {
|
|
1306
|
+
channel: "push",
|
|
1307
|
+
language: "csharp",
|
|
1308
|
+
framework: null,
|
|
1309
|
+
variant: "sdk",
|
|
1310
|
+
files: [
|
|
1311
|
+
{
|
|
1312
|
+
path: "Services/PushService.cs",
|
|
1313
|
+
purpose: "Send a push notification through Zyphr",
|
|
1314
|
+
contents: 'using ZyphrDev.SDK.Api;\nusing ZyphrDev.SDK.Client;\nusing ZyphrDev.SDK.Model;\n\nnamespace YourApp.Services;\n\npublic class PushService\n{\n private readonly PushApi _push;\n public PushService()\n {\n var config = new Configuration\n {\n ApiKey = new Dictionary<string, string>\n {\n { "X-API-Key", Environment.GetEnvironmentVariable("ZYPHR_API_KEY")! }\n }\n };\n _push = new PushApi(config);\n }\n\n public Task<SendPushResponse> OrderShippedAsync(string subscriberId, string orderId)\n {\n return _push.SendPushAsync(new SendPushRequest(\n subscriberId: subscriberId,\n title: "Order shipped",\n body: $"Order {orderId} is on its way."\n ));\n }\n}\n',
|
|
1315
|
+
overwrite: false
|
|
1316
|
+
}
|
|
1317
|
+
],
|
|
1318
|
+
envVarsNeeded: ENV3,
|
|
1319
|
+
nextSteps: NEXT_STEPS3,
|
|
1320
|
+
docsUrl: DOCS3
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
};
|
|
1324
|
+
|
|
1325
|
+
// src/integration/quickstart/sms.ts
|
|
1326
|
+
var DOCS4 = "https://docs.zyphr.dev/channels/sms";
|
|
1327
|
+
var ENV4 = ["ZYPHR_API_KEY"];
|
|
1328
|
+
var NEXT_STEPS4 = [
|
|
1329
|
+
"Add ZYPHR_API_KEY to your .env file.",
|
|
1330
|
+
"Recipients MUST be in E.164 format (e.g. +14155551234).",
|
|
1331
|
+
"Provision your SMS sender (phone number or alphanumeric sender ID) in the Zyphr dashboard."
|
|
1332
|
+
];
|
|
1333
|
+
var smsChannel = {
|
|
1334
|
+
node: {
|
|
1335
|
+
sdk: {
|
|
1336
|
+
channel: "sms",
|
|
1337
|
+
language: "node",
|
|
1338
|
+
framework: null,
|
|
1339
|
+
variant: "sdk",
|
|
1340
|
+
files: [
|
|
1341
|
+
{
|
|
1342
|
+
path: "src/services/sms.ts",
|
|
1343
|
+
purpose: "Send an SMS through Zyphr",
|
|
1344
|
+
contents: "import { Zyphr } from '@zyphr-dev/node-sdk';\n\nconst zyphr = new Zyphr({ apiKey: process.env.ZYPHR_API_KEY! });\n\nexport async function sendOtp(to: string, code: string) {\n return await zyphr.sms.sendSms({\n to,\n body: `Your verification code is ${code}. Expires in 5 minutes.`,\n });\n}\n",
|
|
1345
|
+
overwrite: false
|
|
1346
|
+
}
|
|
1347
|
+
],
|
|
1348
|
+
envVarsNeeded: ENV4,
|
|
1349
|
+
nextSteps: NEXT_STEPS4,
|
|
1350
|
+
docsUrl: DOCS4
|
|
1351
|
+
}
|
|
1352
|
+
},
|
|
1353
|
+
python: {
|
|
1354
|
+
sdk: {
|
|
1355
|
+
channel: "sms",
|
|
1356
|
+
language: "python",
|
|
1357
|
+
framework: null,
|
|
1358
|
+
variant: "sdk",
|
|
1359
|
+
files: [
|
|
1360
|
+
{
|
|
1361
|
+
path: "app/sms.py",
|
|
1362
|
+
purpose: "Send an SMS through Zyphr",
|
|
1363
|
+
contents: 'from .zyphr_client import zyphr_request\n\ndef send_otp(to: str, code: str) -> dict:\n return zyphr_request("POST", "/sms", json={\n "to": to,\n "body": f"Your verification code is {code}. Expires in 5 minutes.",\n })\n',
|
|
1364
|
+
overwrite: false
|
|
1365
|
+
}
|
|
1366
|
+
],
|
|
1367
|
+
envVarsNeeded: ENV4,
|
|
1368
|
+
nextSteps: NEXT_STEPS4,
|
|
1369
|
+
docsUrl: DOCS4
|
|
1370
|
+
}
|
|
1371
|
+
},
|
|
1372
|
+
ruby: {
|
|
1373
|
+
sdk: {
|
|
1374
|
+
channel: "sms",
|
|
1375
|
+
language: "ruby",
|
|
1376
|
+
framework: null,
|
|
1377
|
+
variant: "sdk",
|
|
1378
|
+
files: [
|
|
1379
|
+
{
|
|
1380
|
+
path: "app/services/sms_service.rb",
|
|
1381
|
+
purpose: "Send an SMS through Zyphr",
|
|
1382
|
+
contents: 'class SmsService\n def self.send_otp(to:, code:)\n Zyphr::SMSApi.new.send_sms(\n Zyphr::SendSmsRequest.new(\n to: to,\n body: "Your verification code is #{code}. Expires in 5 minutes."\n )\n )\n end\nend\n',
|
|
1383
|
+
overwrite: false
|
|
1384
|
+
}
|
|
1385
|
+
],
|
|
1386
|
+
envVarsNeeded: ENV4,
|
|
1387
|
+
nextSteps: NEXT_STEPS4,
|
|
1388
|
+
docsUrl: DOCS4
|
|
1389
|
+
}
|
|
1390
|
+
},
|
|
1391
|
+
go: {
|
|
1392
|
+
sdk: {
|
|
1393
|
+
channel: "sms",
|
|
1394
|
+
language: "go",
|
|
1395
|
+
framework: null,
|
|
1396
|
+
variant: "sdk",
|
|
1397
|
+
files: [
|
|
1398
|
+
{
|
|
1399
|
+
path: "internal/notify/sms.go",
|
|
1400
|
+
purpose: "Send an SMS through Zyphr",
|
|
1401
|
+
contents: 'package notify\n\nimport "yourapp/internal/zyphr"\n\nfunc SendOtp(client *zyphr.Client, to, code string) ([]byte, error) {\n return client.Do("POST", "/sms", map[string]any{\n "to": to,\n "body": "Your verification code is " + code + ". Expires in 5 minutes.",\n })\n}\n',
|
|
1402
|
+
overwrite: false
|
|
1403
|
+
}
|
|
1404
|
+
],
|
|
1405
|
+
envVarsNeeded: ENV4,
|
|
1406
|
+
nextSteps: NEXT_STEPS4,
|
|
1407
|
+
docsUrl: DOCS4
|
|
1408
|
+
}
|
|
1409
|
+
},
|
|
1410
|
+
php: {
|
|
1411
|
+
sdk: {
|
|
1412
|
+
channel: "sms",
|
|
1413
|
+
language: "php",
|
|
1414
|
+
framework: null,
|
|
1415
|
+
variant: "sdk",
|
|
1416
|
+
files: [
|
|
1417
|
+
{
|
|
1418
|
+
path: "app/Services/SmsService.php",
|
|
1419
|
+
purpose: "Send an SMS through Zyphr",
|
|
1420
|
+
contents: `<?php
|
|
1421
|
+
|
|
1422
|
+
namespace App\\Services;
|
|
1423
|
+
|
|
1424
|
+
use GuzzleHttp\\Client;
|
|
1425
|
+
|
|
1426
|
+
class SmsService
|
|
1427
|
+
{
|
|
1428
|
+
public function sendOtp(string $to, string $code): array
|
|
1429
|
+
{
|
|
1430
|
+
$http = new Client([
|
|
1431
|
+
'base_uri' => 'https://api.zyphr.dev/v1/',
|
|
1432
|
+
'headers' => [
|
|
1433
|
+
'X-API-Key' => getenv('ZYPHR_API_KEY'),
|
|
1434
|
+
'Content-Type' => 'application/json',
|
|
1435
|
+
],
|
|
1436
|
+
]);
|
|
1437
|
+
$r = $http->post('sms', ['json' => [
|
|
1438
|
+
'to' => $to,
|
|
1439
|
+
'body' => "Your verification code is {$code}. Expires in 5 minutes.",
|
|
1440
|
+
]]);
|
|
1441
|
+
return json_decode((string) $r->getBody(), true);
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
`,
|
|
1445
|
+
overwrite: false
|
|
1446
|
+
}
|
|
1447
|
+
],
|
|
1448
|
+
envVarsNeeded: ENV4,
|
|
1449
|
+
nextSteps: NEXT_STEPS4,
|
|
1450
|
+
docsUrl: DOCS4
|
|
1451
|
+
}
|
|
1452
|
+
},
|
|
1453
|
+
csharp: {
|
|
1454
|
+
sdk: {
|
|
1455
|
+
channel: "sms",
|
|
1456
|
+
language: "csharp",
|
|
1457
|
+
framework: null,
|
|
1458
|
+
variant: "sdk",
|
|
1459
|
+
files: [
|
|
1460
|
+
{
|
|
1461
|
+
path: "Services/SmsService.cs",
|
|
1462
|
+
purpose: "Send an SMS through Zyphr",
|
|
1463
|
+
contents: 'using ZyphrDev.SDK.Api;\nusing ZyphrDev.SDK.Client;\nusing ZyphrDev.SDK.Model;\n\nnamespace YourApp.Services;\n\npublic class SmsService\n{\n private readonly SMSApi _sms;\n public SmsService()\n {\n var config = new Configuration\n {\n ApiKey = new Dictionary<string, string>\n {\n { "X-API-Key", Environment.GetEnvironmentVariable("ZYPHR_API_KEY")! }\n }\n };\n _sms = new SMSApi(config);\n }\n\n public Task<SendSmsResponse> SendOtpAsync(string to, string code)\n {\n return _sms.SendSmsAsync(new SendSmsRequest(\n to: to,\n body: $"Your verification code is {code}. Expires in 5 minutes."\n ));\n }\n}\n',
|
|
1464
|
+
overwrite: false
|
|
1465
|
+
}
|
|
1466
|
+
],
|
|
1467
|
+
envVarsNeeded: ENV4,
|
|
1468
|
+
nextSteps: NEXT_STEPS4,
|
|
1469
|
+
docsUrl: DOCS4
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
};
|
|
1473
|
+
|
|
1474
|
+
// src/integration/quickstart/webhook.ts
|
|
1475
|
+
var DOCS5 = "https://docs.zyphr.dev/features/webhooks-security";
|
|
1476
|
+
var ENV5 = ["ZYPHR_WEBHOOK_SECRET"];
|
|
1477
|
+
var NEXT_STEPS5 = [
|
|
1478
|
+
"Add ZYPHR_WEBHOOK_SECRET to your .env file \u2014 get it from `zyphr.webhooks.rotateWebhookSecret(id)` or the Zyphr dashboard.",
|
|
1479
|
+
"Configure the webhook endpoint URL in the Zyphr dashboard or via `create_webhook`.",
|
|
1480
|
+
"ALWAYS verify signatures before processing payloads \u2014 never trust an unverified webhook.",
|
|
1481
|
+
"Reject deliveries whose timestamp is more than 5 minutes from now to prevent replay attacks."
|
|
1482
|
+
];
|
|
1483
|
+
var webhookChannel = {
|
|
1484
|
+
node: {
|
|
1485
|
+
sdk: {
|
|
1486
|
+
channel: "webhook",
|
|
1487
|
+
language: "node",
|
|
1488
|
+
framework: null,
|
|
1489
|
+
variant: "webhook-handler",
|
|
1490
|
+
files: [
|
|
1491
|
+
{
|
|
1492
|
+
path: "src/lib/verifyZyphrWebhook.ts",
|
|
1493
|
+
purpose: "Standard Webhooks (HMAC-SHA256) signature + timestamp verification. Mirrors the canonical snippet in apps/docs/docs/features/webhooks-security.md.",
|
|
1494
|
+
contents: "import crypto from 'crypto';\n\nexport function verifyZyphrWebhook(\n payload: string,\n headers: { 'webhook-id': string; 'webhook-timestamp': string; 'webhook-signature': string },\n secret: string,\n): boolean {\n const msgId = headers['webhook-id'];\n const timestamp = parseInt(headers['webhook-timestamp'], 10);\n const signatures = headers['webhook-signature'];\n\n const now = Math.floor(Date.now() / 1000);\n if (Math.abs(now - timestamp) > 300) return false;\n\n const signedContent = `${msgId}.${timestamp}.${payload}`;\n const secretBytes = Buffer.from(\n secret.startsWith('whsec_') ? secret.slice(6) : secret,\n 'hex',\n );\n const expected = crypto\n .createHmac('sha256', secretBytes)\n .update(signedContent)\n .digest('base64');\n\n for (const sig of signatures.split(' ')) {\n const sigValue = sig.slice(3);\n if (\n sigValue.length === expected.length &&\n crypto.timingSafeEqual(Buffer.from(sigValue), Buffer.from(expected))\n ) {\n return true;\n }\n }\n return false;\n}\n",
|
|
1495
|
+
overwrite: false
|
|
1496
|
+
}
|
|
1497
|
+
],
|
|
1498
|
+
envVarsNeeded: ENV5,
|
|
1499
|
+
nextSteps: NEXT_STEPS5,
|
|
1500
|
+
docsUrl: DOCS5
|
|
1501
|
+
},
|
|
1502
|
+
frameworks: {
|
|
1503
|
+
express: {
|
|
1504
|
+
channel: "webhook",
|
|
1505
|
+
language: "node",
|
|
1506
|
+
framework: "express",
|
|
1507
|
+
variant: "webhook-handler",
|
|
1508
|
+
files: [
|
|
1509
|
+
{
|
|
1510
|
+
path: "src/lib/verifyZyphrWebhook.ts",
|
|
1511
|
+
purpose: "Standard Webhooks signature verification helper",
|
|
1512
|
+
contents: "import crypto from 'crypto';\n\nexport function verifyZyphrWebhook(payload: string, headers: Record<string,string>, secret: string): boolean {\n const msgId = headers['webhook-id'];\n const timestamp = parseInt(headers['webhook-timestamp'], 10);\n const signatures = headers['webhook-signature'] || '';\n const now = Math.floor(Date.now() / 1000);\n if (Math.abs(now - timestamp) > 300) return false;\n const signedContent = `${msgId}.${timestamp}.${payload}`;\n const secretBytes = Buffer.from(secret.startsWith('whsec_') ? secret.slice(6) : secret, 'hex');\n const expected = crypto.createHmac('sha256', secretBytes).update(signedContent).digest('base64');\n return signatures.split(' ').some((sig) => {\n const v = sig.slice(3);\n return v.length === expected.length && crypto.timingSafeEqual(Buffer.from(v), Buffer.from(expected));\n });\n}\n",
|
|
1513
|
+
overwrite: false
|
|
1514
|
+
},
|
|
1515
|
+
{
|
|
1516
|
+
path: "src/routes/zyphrWebhook.ts",
|
|
1517
|
+
purpose: "Express route that VERIFIES the signature before processing the webhook. Uses express.raw() so we can hash the exact bytes.",
|
|
1518
|
+
contents: "import { Router, raw } from 'express';\nimport { verifyZyphrWebhook } from '../lib/verifyZyphrWebhook.js';\n\nexport const zyphrWebhookRouter = Router();\n\nzyphrWebhookRouter.post('/zyphr', raw({ type: 'application/json' }), (req, res) => {\n const payload = (req.body as Buffer).toString();\n const headers = req.headers as Record<string, string>;\n if (!verifyZyphrWebhook(payload, headers, process.env.ZYPHR_WEBHOOK_SECRET!)) {\n return res.status(401).send('invalid signature');\n }\n const event = JSON.parse(payload) as { type: string; data: unknown };\n console.log('zyphr event', event.type);\n res.sendStatus(204);\n});\n",
|
|
1519
|
+
overwrite: false
|
|
1520
|
+
}
|
|
1521
|
+
],
|
|
1522
|
+
envVarsNeeded: ENV5,
|
|
1523
|
+
nextSteps: [
|
|
1524
|
+
...NEXT_STEPS5,
|
|
1525
|
+
"Mount the router BEFORE express.json(): app.use(zyphrWebhookRouter)"
|
|
1526
|
+
],
|
|
1527
|
+
docsUrl: DOCS5
|
|
1528
|
+
},
|
|
1529
|
+
nextjs: {
|
|
1530
|
+
channel: "webhook",
|
|
1531
|
+
language: "node",
|
|
1532
|
+
framework: "nextjs",
|
|
1533
|
+
variant: "webhook-handler",
|
|
1534
|
+
files: [
|
|
1535
|
+
{
|
|
1536
|
+
path: "src/lib/verifyZyphrWebhook.ts",
|
|
1537
|
+
purpose: "Standard Webhooks signature verification helper",
|
|
1538
|
+
contents: "import crypto from 'crypto';\n\nexport function verifyZyphrWebhook(payload: string, headers: Headers, secret: string): boolean {\n const msgId = headers.get('webhook-id') || '';\n const timestamp = parseInt(headers.get('webhook-timestamp') || '0', 10);\n const signatures = headers.get('webhook-signature') || '';\n const now = Math.floor(Date.now() / 1000);\n if (Math.abs(now - timestamp) > 300) return false;\n const signedContent = `${msgId}.${timestamp}.${payload}`;\n const secretBytes = Buffer.from(secret.startsWith('whsec_') ? secret.slice(6) : secret, 'hex');\n const expected = crypto.createHmac('sha256', secretBytes).update(signedContent).digest('base64');\n return signatures.split(' ').some((sig) => {\n const v = sig.slice(3);\n return v.length === expected.length && crypto.timingSafeEqual(Buffer.from(v), Buffer.from(expected));\n });\n}\n",
|
|
1539
|
+
overwrite: false
|
|
1540
|
+
},
|
|
1541
|
+
{
|
|
1542
|
+
path: "src/app/api/zyphr/webhook/route.ts",
|
|
1543
|
+
purpose: "Next.js App Router webhook handler with signature verification",
|
|
1544
|
+
contents: "import { NextResponse } from 'next/server';\nimport { verifyZyphrWebhook } from '@/lib/verifyZyphrWebhook';\n\nexport async function POST(req: Request) {\n const payload = await req.text();\n if (!verifyZyphrWebhook(payload, req.headers, process.env.ZYPHR_WEBHOOK_SECRET!)) {\n return new NextResponse('invalid signature', { status: 401 });\n }\n const event = JSON.parse(payload) as { type: string; data: unknown };\n console.log('zyphr event', event.type);\n return new NextResponse(null, { status: 204 });\n}\n",
|
|
1545
|
+
overwrite: false
|
|
1546
|
+
}
|
|
1547
|
+
],
|
|
1548
|
+
envVarsNeeded: ENV5,
|
|
1549
|
+
nextSteps: NEXT_STEPS5,
|
|
1550
|
+
docsUrl: DOCS5
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
},
|
|
1554
|
+
python: {
|
|
1555
|
+
sdk: {
|
|
1556
|
+
channel: "webhook",
|
|
1557
|
+
language: "python",
|
|
1558
|
+
framework: null,
|
|
1559
|
+
variant: "webhook-handler",
|
|
1560
|
+
files: [
|
|
1561
|
+
{
|
|
1562
|
+
path: "app/zyphr_webhook.py",
|
|
1563
|
+
purpose: "Standard Webhooks signature verification helper (verbatim from docs)",
|
|
1564
|
+
contents: 'import hashlib\nimport hmac\nimport base64\nimport time\n\ndef verify_zyphr_webhook(payload: str, headers: dict, secret: str) -> bool:\n msg_id = headers.get("webhook-id", "")\n timestamp = headers.get("webhook-timestamp", "")\n signature = headers.get("webhook-signature", "")\n\n now = int(time.time())\n if abs(now - int(timestamp)) > 300:\n return False\n\n signed_content = f"{msg_id}.{timestamp}.{payload}"\n secret_hex = secret.removeprefix("whsec_")\n secret_bytes = bytes.fromhex(secret_hex)\n expected = base64.b64encode(\n hmac.new(secret_bytes, signed_content.encode(), hashlib.sha256).digest()\n ).decode()\n\n for sig in signature.split(" "):\n sig_value = sig.removeprefix("v1,")\n if hmac.compare_digest(sig_value, expected):\n return True\n return False\n',
|
|
1565
|
+
overwrite: false
|
|
1566
|
+
}
|
|
1567
|
+
],
|
|
1568
|
+
envVarsNeeded: ENV5,
|
|
1569
|
+
nextSteps: NEXT_STEPS5,
|
|
1570
|
+
docsUrl: DOCS5
|
|
1571
|
+
},
|
|
1572
|
+
frameworks: {
|
|
1573
|
+
flask: {
|
|
1574
|
+
channel: "webhook",
|
|
1575
|
+
language: "python",
|
|
1576
|
+
framework: "flask",
|
|
1577
|
+
variant: "webhook-handler",
|
|
1578
|
+
files: [
|
|
1579
|
+
{
|
|
1580
|
+
path: "app/zyphr_webhook.py",
|
|
1581
|
+
purpose: "Standard Webhooks signature verification helper",
|
|
1582
|
+
contents: 'import hashlib, hmac, base64, time\n\ndef verify_zyphr_webhook(payload: str, headers, secret: str) -> bool:\n msg_id = headers.get("webhook-id", "")\n timestamp = headers.get("webhook-timestamp", "")\n signature = headers.get("webhook-signature", "")\n if abs(int(time.time()) - int(timestamp)) > 300:\n return False\n signed = f"{msg_id}.{timestamp}.{payload}"\n secret_bytes = bytes.fromhex(secret.removeprefix("whsec_"))\n expected = base64.b64encode(hmac.new(secret_bytes, signed.encode(), hashlib.sha256).digest()).decode()\n return any(hmac.compare_digest(sig.removeprefix("v1,"), expected) for sig in signature.split(" "))\n',
|
|
1583
|
+
overwrite: false
|
|
1584
|
+
},
|
|
1585
|
+
{
|
|
1586
|
+
path: "app/routes/zyphr_webhook.py",
|
|
1587
|
+
purpose: "Flask blueprint that verifies the webhook signature before processing",
|
|
1588
|
+
contents: 'import os, json\nfrom flask import Blueprint, request, abort\nfrom ..zyphr_webhook import verify_zyphr_webhook\n\nzyphr_webhook_bp = Blueprint("zyphr_webhook", __name__)\n\n@zyphr_webhook_bp.route("/webhooks/zyphr", methods=["POST"])\ndef handle():\n payload = request.get_data(as_text=True)\n if not verify_zyphr_webhook(payload, request.headers, os.environ["ZYPHR_WEBHOOK_SECRET"]):\n abort(401, "invalid signature")\n event = json.loads(payload)\n print("zyphr event", event.get("type"))\n return "", 204\n',
|
|
1589
|
+
overwrite: false
|
|
1590
|
+
}
|
|
1591
|
+
],
|
|
1592
|
+
envVarsNeeded: ENV5,
|
|
1593
|
+
nextSteps: NEXT_STEPS5,
|
|
1594
|
+
docsUrl: DOCS5
|
|
1595
|
+
},
|
|
1596
|
+
fastapi: {
|
|
1597
|
+
channel: "webhook",
|
|
1598
|
+
language: "python",
|
|
1599
|
+
framework: "fastapi",
|
|
1600
|
+
variant: "webhook-handler",
|
|
1601
|
+
files: [
|
|
1602
|
+
{
|
|
1603
|
+
path: "app/routers/zyphr_webhook.py",
|
|
1604
|
+
purpose: "FastAPI router that verifies the webhook signature before processing",
|
|
1605
|
+
contents: 'import os, json, hashlib, hmac, base64, time\nfrom fastapi import APIRouter, Request, HTTPException\n\nrouter = APIRouter()\n\ndef verify_zyphr_webhook(payload: str, headers, secret: str) -> bool:\n msg_id = headers.get("webhook-id", "")\n timestamp = headers.get("webhook-timestamp", "")\n signature = headers.get("webhook-signature", "")\n if abs(int(time.time()) - int(timestamp)) > 300:\n return False\n signed = f"{msg_id}.{timestamp}.{payload}"\n secret_bytes = bytes.fromhex(secret.removeprefix("whsec_"))\n expected = base64.b64encode(hmac.new(secret_bytes, signed.encode(), hashlib.sha256).digest()).decode()\n return any(hmac.compare_digest(sig.removeprefix("v1,"), expected) for sig in signature.split(" "))\n\n@router.post("/webhooks/zyphr")\nasync def handle(request: Request):\n body = (await request.body()).decode()\n if not verify_zyphr_webhook(body, request.headers, os.environ["ZYPHR_WEBHOOK_SECRET"]):\n raise HTTPException(status_code=401, detail="invalid signature")\n event = json.loads(body)\n print("zyphr event", event.get("type"))\n return {"ok": True}\n',
|
|
1606
|
+
overwrite: false
|
|
1607
|
+
}
|
|
1608
|
+
],
|
|
1609
|
+
envVarsNeeded: ENV5,
|
|
1610
|
+
nextSteps: NEXT_STEPS5,
|
|
1611
|
+
docsUrl: DOCS5
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
},
|
|
1615
|
+
ruby: {
|
|
1616
|
+
sdk: {
|
|
1617
|
+
channel: "webhook",
|
|
1618
|
+
language: "ruby",
|
|
1619
|
+
framework: null,
|
|
1620
|
+
variant: "webhook-handler",
|
|
1621
|
+
files: [
|
|
1622
|
+
{
|
|
1623
|
+
path: "app/services/zyphr_webhook.rb",
|
|
1624
|
+
purpose: "Standard Webhooks signature verification helper",
|
|
1625
|
+
contents: `require 'openssl'
|
|
1626
|
+
require 'base64'
|
|
1627
|
+
|
|
1628
|
+
class ZyphrWebhook
|
|
1629
|
+
def self.verify(payload:, headers:, secret:)
|
|
1630
|
+
msg_id = headers['webhook-id'].to_s
|
|
1631
|
+
timestamp = headers['webhook-timestamp'].to_i
|
|
1632
|
+
signatures = headers['webhook-signature'].to_s
|
|
1633
|
+
|
|
1634
|
+
return false if (Time.now.to_i - timestamp).abs > 300
|
|
1635
|
+
|
|
1636
|
+
signed = "#{msg_id}.#{timestamp}.#{payload}"
|
|
1637
|
+
hex = secret.start_with?('whsec_') ? secret[6..] : secret
|
|
1638
|
+
secret_bytes = [hex].pack('H*')
|
|
1639
|
+
expected = Base64.strict_encode64(OpenSSL::HMAC.digest('SHA256', secret_bytes, signed))
|
|
1640
|
+
|
|
1641
|
+
signatures.split(' ').any? do |sig|
|
|
1642
|
+
v = sig[3..]
|
|
1643
|
+
v && Rack::Utils.secure_compare(v, expected)
|
|
1644
|
+
end
|
|
1645
|
+
end
|
|
1646
|
+
end
|
|
1647
|
+
`,
|
|
1648
|
+
overwrite: false
|
|
1649
|
+
}
|
|
1650
|
+
],
|
|
1651
|
+
envVarsNeeded: ENV5,
|
|
1652
|
+
nextSteps: NEXT_STEPS5,
|
|
1653
|
+
docsUrl: DOCS5
|
|
1654
|
+
},
|
|
1655
|
+
frameworks: {
|
|
1656
|
+
rails: {
|
|
1657
|
+
channel: "webhook",
|
|
1658
|
+
language: "ruby",
|
|
1659
|
+
framework: "rails",
|
|
1660
|
+
variant: "webhook-handler",
|
|
1661
|
+
files: [
|
|
1662
|
+
{
|
|
1663
|
+
path: "app/services/zyphr_webhook.rb",
|
|
1664
|
+
purpose: "Standard Webhooks signature verification helper",
|
|
1665
|
+
contents: `require 'openssl'
|
|
1666
|
+
require 'base64'
|
|
1667
|
+
|
|
1668
|
+
class ZyphrWebhook
|
|
1669
|
+
def self.verify(payload:, headers:, secret:)
|
|
1670
|
+
msg_id = headers['webhook-id'].to_s
|
|
1671
|
+
timestamp = headers['webhook-timestamp'].to_i
|
|
1672
|
+
signatures = headers['webhook-signature'].to_s
|
|
1673
|
+
return false if (Time.now.to_i - timestamp).abs > 300
|
|
1674
|
+
signed = "#{msg_id}.#{timestamp}.#{payload}"
|
|
1675
|
+
hex = secret.start_with?('whsec_') ? secret[6..] : secret
|
|
1676
|
+
secret_bytes = [hex].pack('H*')
|
|
1677
|
+
expected = Base64.strict_encode64(OpenSSL::HMAC.digest('SHA256', secret_bytes, signed))
|
|
1678
|
+
signatures.split(' ').any? { |sig| sig[3..] && ActiveSupport::SecurityUtils.secure_compare(sig[3..], expected) }
|
|
1679
|
+
end
|
|
1680
|
+
end
|
|
1681
|
+
`,
|
|
1682
|
+
overwrite: false
|
|
1683
|
+
},
|
|
1684
|
+
{
|
|
1685
|
+
path: "app/controllers/zyphr_webhooks_controller.rb",
|
|
1686
|
+
purpose: "Rails controller that verifies the webhook signature before processing",
|
|
1687
|
+
contents: `class ZyphrWebhooksController < ApplicationController
|
|
1688
|
+
skip_before_action :verify_authenticity_token
|
|
1689
|
+
|
|
1690
|
+
def create
|
|
1691
|
+
payload = request.raw_post
|
|
1692
|
+
secret = ENV.fetch('ZYPHR_WEBHOOK_SECRET')
|
|
1693
|
+
unless ZyphrWebhook.verify(payload: payload, headers: request.headers, secret: secret)
|
|
1694
|
+
head :unauthorized and return
|
|
1695
|
+
end
|
|
1696
|
+
event = JSON.parse(payload)
|
|
1697
|
+
Rails.logger.info("zyphr event #{event['type']}")
|
|
1698
|
+
head :no_content
|
|
1699
|
+
end
|
|
1700
|
+
end
|
|
1701
|
+
`,
|
|
1702
|
+
overwrite: false
|
|
1703
|
+
}
|
|
1704
|
+
],
|
|
1705
|
+
envVarsNeeded: ENV5,
|
|
1706
|
+
nextSteps: [
|
|
1707
|
+
...NEXT_STEPS5,
|
|
1708
|
+
"Add route: post '/webhooks/zyphr', to: 'zyphr_webhooks#create'"
|
|
1709
|
+
],
|
|
1710
|
+
docsUrl: DOCS5
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
},
|
|
1714
|
+
go: {
|
|
1715
|
+
sdk: {
|
|
1716
|
+
channel: "webhook",
|
|
1717
|
+
language: "go",
|
|
1718
|
+
framework: null,
|
|
1719
|
+
variant: "webhook-handler",
|
|
1720
|
+
files: [
|
|
1721
|
+
{
|
|
1722
|
+
path: "internal/zyphr/verify.go",
|
|
1723
|
+
purpose: "Standard Webhooks signature verification helper (verbatim from docs).",
|
|
1724
|
+
contents: 'package zyphr\n\nimport (\n "crypto/hmac"\n "crypto/sha256"\n "encoding/base64"\n "encoding/hex"\n "math"\n "strconv"\n "strings"\n "time"\n)\n\nfunc VerifyWebhook(payload, msgID, timestamp, signature, secret string) bool {\n ts, err := strconv.ParseInt(timestamp, 10, 64)\n if err != nil { return false }\n if math.Abs(float64(time.Now().Unix()-ts)) > 300 { return false }\n\n signed := msgID + "." + timestamp + "." + payload\n secretHex := strings.TrimPrefix(secret, "whsec_")\n secretBytes, err := hex.DecodeString(secretHex)\n if err != nil { return false }\n mac := hmac.New(sha256.New, secretBytes)\n mac.Write([]byte(signed))\n expected := base64.StdEncoding.EncodeToString(mac.Sum(nil))\n\n for _, sig := range strings.Split(signature, " ") {\n v := strings.TrimPrefix(sig, "v1,")\n if hmac.Equal([]byte(v), []byte(expected)) { return true }\n }\n return false\n}\n',
|
|
1725
|
+
overwrite: false
|
|
1726
|
+
}
|
|
1727
|
+
],
|
|
1728
|
+
envVarsNeeded: ENV5,
|
|
1729
|
+
nextSteps: NEXT_STEPS5,
|
|
1730
|
+
docsUrl: DOCS5
|
|
1731
|
+
}
|
|
1732
|
+
},
|
|
1733
|
+
php: {
|
|
1734
|
+
sdk: {
|
|
1735
|
+
channel: "webhook",
|
|
1736
|
+
language: "php",
|
|
1737
|
+
framework: null,
|
|
1738
|
+
variant: "webhook-handler",
|
|
1739
|
+
files: [
|
|
1740
|
+
{
|
|
1741
|
+
path: "app/Webhooks/ZyphrWebhook.php",
|
|
1742
|
+
purpose: "Standard Webhooks signature verification helper (verbatim from docs)",
|
|
1743
|
+
contents: `<?php
|
|
1744
|
+
|
|
1745
|
+
namespace App\\Webhooks;
|
|
1746
|
+
|
|
1747
|
+
class ZyphrWebhook
|
|
1748
|
+
{
|
|
1749
|
+
public static function verify(string $payload, array $headers, string $secret): bool
|
|
1750
|
+
{
|
|
1751
|
+
$msgId = $headers['webhook-id'] ?? '';
|
|
1752
|
+
$timestamp = $headers['webhook-timestamp'] ?? '';
|
|
1753
|
+
$signature = $headers['webhook-signature'] ?? '';
|
|
1754
|
+
if (abs(time() - intval($timestamp)) > 300) {
|
|
1755
|
+
return false;
|
|
1756
|
+
}
|
|
1757
|
+
$signed = "{$msgId}.{$timestamp}.{$payload}";
|
|
1758
|
+
$hex = str_starts_with($secret, 'whsec_') ? substr($secret, 6) : $secret;
|
|
1759
|
+
$expected = base64_encode(hash_hmac('sha256', $signed, hex2bin($hex), true));
|
|
1760
|
+
foreach (explode(' ', $signature) as $sig) {
|
|
1761
|
+
if (hash_equals(substr($sig, 3), $expected)) {
|
|
1762
|
+
return true;
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
return false;
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
`,
|
|
1769
|
+
overwrite: false
|
|
1770
|
+
}
|
|
1771
|
+
],
|
|
1772
|
+
envVarsNeeded: ENV5,
|
|
1773
|
+
nextSteps: NEXT_STEPS5,
|
|
1774
|
+
docsUrl: DOCS5
|
|
1775
|
+
},
|
|
1776
|
+
frameworks: {
|
|
1777
|
+
laravel: {
|
|
1778
|
+
channel: "webhook",
|
|
1779
|
+
language: "php",
|
|
1780
|
+
framework: "laravel",
|
|
1781
|
+
variant: "webhook-handler",
|
|
1782
|
+
files: [
|
|
1783
|
+
{
|
|
1784
|
+
path: "app/Webhooks/ZyphrWebhook.php",
|
|
1785
|
+
purpose: "Standard Webhooks signature verification helper",
|
|
1786
|
+
contents: `<?php
|
|
1787
|
+
|
|
1788
|
+
namespace App\\Webhooks;
|
|
1789
|
+
|
|
1790
|
+
class ZyphrWebhook
|
|
1791
|
+
{
|
|
1792
|
+
public static function verify(string $payload, array $headers, string $secret): bool
|
|
1793
|
+
{
|
|
1794
|
+
$msgId = $headers['webhook-id'][0] ?? '';
|
|
1795
|
+
$timestamp = $headers['webhook-timestamp'][0] ?? '';
|
|
1796
|
+
$signature = $headers['webhook-signature'][0] ?? '';
|
|
1797
|
+
if (abs(time() - intval($timestamp)) > 300) {
|
|
1798
|
+
return false;
|
|
1799
|
+
}
|
|
1800
|
+
$signed = "{$msgId}.{$timestamp}.{$payload}";
|
|
1801
|
+
$hex = str_starts_with($secret, 'whsec_') ? substr($secret, 6) : $secret;
|
|
1802
|
+
$expected = base64_encode(hash_hmac('sha256', $signed, hex2bin($hex), true));
|
|
1803
|
+
foreach (explode(' ', $signature) as $sig) {
|
|
1804
|
+
if (hash_equals(substr($sig, 3), $expected)) {
|
|
1805
|
+
return true;
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
return false;
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
`,
|
|
1812
|
+
overwrite: false
|
|
1813
|
+
},
|
|
1814
|
+
{
|
|
1815
|
+
path: "app/Http/Controllers/ZyphrWebhookController.php",
|
|
1816
|
+
purpose: "Laravel controller that verifies the webhook signature before processing",
|
|
1817
|
+
contents: "<?php\n\nnamespace App\\Http\\Controllers;\n\nuse App\\Webhooks\\ZyphrWebhook;\nuse Illuminate\\Http\\Request;\n\nclass ZyphrWebhookController extends Controller\n{\n public function handle(Request $request)\n {\n $payload = $request->getContent();\n $secret = config('services.zyphr.webhook_secret');\n if (! ZyphrWebhook::verify($payload, $request->headers->all(), $secret)) {\n return response('invalid signature', 401);\n }\n $event = json_decode($payload, true);\n \\Log::info('zyphr event ' . ($event['type'] ?? 'unknown'));\n return response()->noContent();\n }\n}\n",
|
|
1818
|
+
overwrite: false
|
|
1819
|
+
}
|
|
1820
|
+
],
|
|
1821
|
+
envVarsNeeded: ENV5,
|
|
1822
|
+
nextSteps: [
|
|
1823
|
+
...NEXT_STEPS5,
|
|
1824
|
+
"Register route in routes/api.php: Route::post('/webhooks/zyphr', [ZyphrWebhookController::class, 'handle']);",
|
|
1825
|
+
"Exclude this route from CSRF (VerifyCsrfToken::$except)."
|
|
1826
|
+
],
|
|
1827
|
+
docsUrl: DOCS5
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
},
|
|
1831
|
+
csharp: {
|
|
1832
|
+
sdk: {
|
|
1833
|
+
channel: "webhook",
|
|
1834
|
+
language: "csharp",
|
|
1835
|
+
framework: null,
|
|
1836
|
+
variant: "webhook-handler",
|
|
1837
|
+
files: [
|
|
1838
|
+
{
|
|
1839
|
+
path: "Webhooks/ZyphrWebhookVerifier.cs",
|
|
1840
|
+
purpose: "Standard Webhooks signature verification helper",
|
|
1841
|
+
contents: `using System.Security.Cryptography;
|
|
1842
|
+
using System.Text;
|
|
1843
|
+
|
|
1844
|
+
namespace YourApp.Webhooks;
|
|
1845
|
+
|
|
1846
|
+
public static class ZyphrWebhookVerifier
|
|
1847
|
+
{
|
|
1848
|
+
public static bool Verify(string payload, IDictionary<string,string> headers, string secret)
|
|
1849
|
+
{
|
|
1850
|
+
var msgId = headers.TryGetValue("webhook-id", out var id) ? id : "";
|
|
1851
|
+
var timestamp = headers.TryGetValue("webhook-timestamp", out var ts) ? long.Parse(ts) : 0;
|
|
1852
|
+
var signatures = headers.TryGetValue("webhook-signature", out var sig) ? sig : "";
|
|
1853
|
+
|
|
1854
|
+
var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
|
1855
|
+
if (Math.Abs(now - timestamp) > 300) return false;
|
|
1856
|
+
|
|
1857
|
+
var signed = $"{msgId}.{timestamp}.{payload}";
|
|
1858
|
+
var hex = secret.StartsWith("whsec_") ? secret[6..] : secret;
|
|
1859
|
+
var secretBytes = Convert.FromHexString(hex);
|
|
1860
|
+
using var hmac = new HMACSHA256(secretBytes);
|
|
1861
|
+
var expected = Convert.ToBase64String(hmac.ComputeHash(Encoding.UTF8.GetBytes(signed)));
|
|
1862
|
+
|
|
1863
|
+
foreach (var s in signatures.Split(' '))
|
|
1864
|
+
{
|
|
1865
|
+
var v = s.Length > 3 ? s[3..] : "";
|
|
1866
|
+
if (CryptographicOperations.FixedTimeEquals(Encoding.UTF8.GetBytes(v), Encoding.UTF8.GetBytes(expected)))
|
|
1867
|
+
return true;
|
|
1868
|
+
}
|
|
1869
|
+
return false;
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
`,
|
|
1873
|
+
overwrite: false
|
|
1874
|
+
}
|
|
1875
|
+
],
|
|
1876
|
+
envVarsNeeded: ENV5,
|
|
1877
|
+
nextSteps: NEXT_STEPS5,
|
|
1878
|
+
docsUrl: DOCS5
|
|
1879
|
+
},
|
|
1880
|
+
frameworks: {
|
|
1881
|
+
aspnetcore: {
|
|
1882
|
+
channel: "webhook",
|
|
1883
|
+
language: "csharp",
|
|
1884
|
+
framework: "aspnetcore",
|
|
1885
|
+
variant: "webhook-handler",
|
|
1886
|
+
files: [
|
|
1887
|
+
{
|
|
1888
|
+
path: "Webhooks/ZyphrWebhookVerifier.cs",
|
|
1889
|
+
purpose: "Standard Webhooks signature verification helper",
|
|
1890
|
+
contents: `using System.Security.Cryptography;
|
|
1891
|
+
using System.Text;
|
|
1892
|
+
|
|
1893
|
+
namespace YourApp.Webhooks;
|
|
1894
|
+
|
|
1895
|
+
public static class ZyphrWebhookVerifier
|
|
1896
|
+
{
|
|
1897
|
+
public static bool Verify(string payload, IHeaderDictionary headers, string secret)
|
|
1898
|
+
{
|
|
1899
|
+
string msgId = headers["webhook-id"].ToString();
|
|
1900
|
+
long timestamp = long.TryParse(headers["webhook-timestamp"], out var t) ? t : 0;
|
|
1901
|
+
string signatures = headers["webhook-signature"].ToString();
|
|
1902
|
+
if (Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - timestamp) > 300) return false;
|
|
1903
|
+
var signed = $"{msgId}.{timestamp}.{payload}";
|
|
1904
|
+
var hex = secret.StartsWith("whsec_") ? secret[6..] : secret;
|
|
1905
|
+
using var hmac = new HMACSHA256(Convert.FromHexString(hex));
|
|
1906
|
+
var expected = Convert.ToBase64String(hmac.ComputeHash(Encoding.UTF8.GetBytes(signed)));
|
|
1907
|
+
foreach (var s in signatures.Split(' '))
|
|
1908
|
+
{
|
|
1909
|
+
var v = s.Length > 3 ? s[3..] : "";
|
|
1910
|
+
if (CryptographicOperations.FixedTimeEquals(Encoding.UTF8.GetBytes(v), Encoding.UTF8.GetBytes(expected)))
|
|
1911
|
+
return true;
|
|
1912
|
+
}
|
|
1913
|
+
return false;
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
`,
|
|
1917
|
+
overwrite: false
|
|
1918
|
+
},
|
|
1919
|
+
{
|
|
1920
|
+
path: "Controllers/ZyphrWebhookController.cs",
|
|
1921
|
+
purpose: "ASP.NET Core controller that verifies the webhook signature before processing",
|
|
1922
|
+
contents: 'using Microsoft.AspNetCore.Mvc;\nusing YourApp.Webhooks;\n\n[ApiController]\n[Route("webhooks/zyphr")]\npublic class ZyphrWebhookController : ControllerBase\n{\n [HttpPost]\n public async Task<IActionResult> Handle()\n {\n using var reader = new StreamReader(Request.Body);\n var payload = await reader.ReadToEndAsync();\n var secret = Environment.GetEnvironmentVariable("ZYPHR_WEBHOOK_SECRET")!;\n if (!ZyphrWebhookVerifier.Verify(payload, Request.Headers, secret))\n return Unauthorized("invalid signature");\n Console.WriteLine($"zyphr event {payload[..Math.Min(80, payload.Length)]}");\n return NoContent();\n }\n}\n',
|
|
1923
|
+
overwrite: false
|
|
1924
|
+
}
|
|
1925
|
+
],
|
|
1926
|
+
envVarsNeeded: ENV5,
|
|
1927
|
+
nextSteps: NEXT_STEPS5,
|
|
1928
|
+
docsUrl: DOCS5
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
};
|
|
1933
|
+
|
|
1934
|
+
// src/integration/quickstart/index.ts
|
|
1935
|
+
var REGISTRY = {
|
|
1936
|
+
email: emailChannel,
|
|
1937
|
+
push: pushChannel,
|
|
1938
|
+
sms: smsChannel,
|
|
1939
|
+
inbox: inboxChannel,
|
|
1940
|
+
webhook: webhookChannel
|
|
1941
|
+
};
|
|
1942
|
+
function resolveQuickstart(args) {
|
|
1943
|
+
const langMap = REGISTRY[args.channel]?.[args.language];
|
|
1944
|
+
if (!langMap) return null;
|
|
1945
|
+
if (args.framework) {
|
|
1946
|
+
const key = args.framework.trim().toLowerCase();
|
|
1947
|
+
const fw = langMap.frameworks?.[key];
|
|
1948
|
+
if (fw) return { result: fw, frameworkRecognized: true };
|
|
1949
|
+
return { result: langMap.sdk, frameworkRecognized: false };
|
|
1950
|
+
}
|
|
1951
|
+
return { result: langMap.sdk, frameworkRecognized: true };
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1954
|
+
// src/integration/sdk-snippets.ts
|
|
1955
|
+
var DOCS6 = "https://docs.zyphr.dev/sdks";
|
|
1956
|
+
var SDK_INSTALL_TABLE = {
|
|
1957
|
+
node: {
|
|
1958
|
+
language: "node",
|
|
1959
|
+
kind: "sdk",
|
|
1960
|
+
packageName: "@zyphr-dev/node-sdk",
|
|
1961
|
+
registry: "npm",
|
|
1962
|
+
registryUrl: "https://www.npmjs.com/package/@zyphr-dev/node-sdk",
|
|
1963
|
+
installCommands: [
|
|
1964
|
+
{ manager: "npm", command: "npm install @zyphr-dev/node-sdk" },
|
|
1965
|
+
{ manager: "yarn", command: "yarn add @zyphr-dev/node-sdk" },
|
|
1966
|
+
{ manager: "pnpm", command: "pnpm add @zyphr-dev/node-sdk" }
|
|
1967
|
+
],
|
|
1968
|
+
initSnippet: {
|
|
1969
|
+
imports: "import { Zyphr } from '@zyphr-dev/node-sdk';",
|
|
1970
|
+
init: "export const zyphr = new Zyphr({ apiKey: process.env.ZYPHR_API_KEY! });",
|
|
1971
|
+
fileExample: "src/lib/zyphr.ts"
|
|
1972
|
+
},
|
|
1973
|
+
envVarsNeeded: ["ZYPHR_API_KEY"],
|
|
1974
|
+
docsUrl: `${DOCS6}/node`
|
|
1975
|
+
},
|
|
1976
|
+
csharp: {
|
|
1977
|
+
language: "csharp",
|
|
1978
|
+
kind: "sdk",
|
|
1979
|
+
packageName: "ZyphrDev.SDK",
|
|
1980
|
+
registry: "NuGet",
|
|
1981
|
+
registryUrl: "https://www.nuget.org/packages/ZyphrDev.SDK",
|
|
1982
|
+
installCommands: [
|
|
1983
|
+
{ manager: "dotnet", command: "dotnet add package ZyphrDev.SDK" },
|
|
1984
|
+
{ manager: "nuget", command: "Install-Package ZyphrDev.SDK" }
|
|
1985
|
+
],
|
|
1986
|
+
initSnippet: {
|
|
1987
|
+
imports: [
|
|
1988
|
+
"using ZyphrDev.SDK.Api;",
|
|
1989
|
+
"using ZyphrDev.SDK.Client;",
|
|
1990
|
+
"using ZyphrDev.SDK.Model;"
|
|
1991
|
+
].join("\n"),
|
|
1992
|
+
init: [
|
|
1993
|
+
"var config = new Configuration",
|
|
1994
|
+
"{",
|
|
1995
|
+
" ApiKey = new Dictionary<string, string>",
|
|
1996
|
+
" {",
|
|
1997
|
+
' { "X-API-Key", Environment.GetEnvironmentVariable("ZYPHR_API_KEY")! }',
|
|
1998
|
+
" }",
|
|
1999
|
+
"};",
|
|
2000
|
+
"var emails = new EmailsApi(config);"
|
|
2001
|
+
].join("\n"),
|
|
2002
|
+
fileExample: "Services/ZyphrClient.cs"
|
|
2003
|
+
},
|
|
2004
|
+
envVarsNeeded: ["ZYPHR_API_KEY"],
|
|
2005
|
+
docsUrl: `${DOCS6}/csharp`
|
|
2006
|
+
},
|
|
2007
|
+
ruby: {
|
|
2008
|
+
language: "ruby",
|
|
2009
|
+
kind: "sdk",
|
|
2010
|
+
packageName: "zyphr",
|
|
2011
|
+
registry: "RubyGems",
|
|
2012
|
+
registryUrl: "https://rubygems.org/gems/zyphr",
|
|
2013
|
+
installCommands: [
|
|
2014
|
+
{ manager: "gem", command: "gem install zyphr" },
|
|
2015
|
+
{ manager: "bundler", command: "bundle add zyphr" }
|
|
2016
|
+
],
|
|
2017
|
+
initSnippet: {
|
|
2018
|
+
imports: "require 'zyphr'",
|
|
2019
|
+
init: [
|
|
2020
|
+
"Zyphr.configure do |config|",
|
|
2021
|
+
" config.api_key['X-API-Key'] = ENV.fetch('ZYPHR_API_KEY')",
|
|
2022
|
+
"end"
|
|
2023
|
+
].join("\n"),
|
|
2024
|
+
fileExample: "config/initializers/zyphr.rb"
|
|
2025
|
+
},
|
|
2026
|
+
envVarsNeeded: ["ZYPHR_API_KEY"],
|
|
2027
|
+
docsUrl: `${DOCS6}/ruby`
|
|
2028
|
+
},
|
|
2029
|
+
python: {
|
|
2030
|
+
language: "python",
|
|
2031
|
+
kind: "rest-client",
|
|
2032
|
+
packageName: "requests",
|
|
2033
|
+
registry: "PyPI",
|
|
2034
|
+
registryUrl: "https://pypi.org/project/requests/",
|
|
2035
|
+
installCommands: [
|
|
2036
|
+
{ manager: "pip", command: "pip install requests" },
|
|
2037
|
+
{ manager: "poetry", command: "poetry add requests" },
|
|
2038
|
+
{ manager: "uv", command: "uv add requests" }
|
|
2039
|
+
],
|
|
2040
|
+
initSnippet: {
|
|
2041
|
+
imports: "import os\nimport requests",
|
|
2042
|
+
init: [
|
|
2043
|
+
'ZYPHR_API_KEY = os.environ["ZYPHR_API_KEY"]',
|
|
2044
|
+
'BASE_URL = "https://api.zyphr.dev/v1"',
|
|
2045
|
+
"",
|
|
2046
|
+
"headers = {",
|
|
2047
|
+
' "X-API-Key": ZYPHR_API_KEY,',
|
|
2048
|
+
' "Content-Type": "application/json",',
|
|
2049
|
+
"}",
|
|
2050
|
+
"",
|
|
2051
|
+
"def zyphr_request(method, path, json=None, params=None):",
|
|
2052
|
+
" response = requests.request(",
|
|
2053
|
+
' method, f"{BASE_URL}{path}", headers=headers, json=json, params=params,',
|
|
2054
|
+
" )",
|
|
2055
|
+
" response.raise_for_status()",
|
|
2056
|
+
" return response.json()"
|
|
2057
|
+
].join("\n"),
|
|
2058
|
+
fileExample: "app/zyphr_client.py"
|
|
2059
|
+
},
|
|
2060
|
+
envVarsNeeded: ["ZYPHR_API_KEY"],
|
|
2061
|
+
docsUrl: `${DOCS6}/python`,
|
|
2062
|
+
notes: "There is no official Zyphr Python SDK yet \u2014 the canonical integration is a thin REST wrapper around the requests library."
|
|
2063
|
+
},
|
|
2064
|
+
go: {
|
|
2065
|
+
language: "go",
|
|
2066
|
+
kind: "rest-client",
|
|
2067
|
+
packageName: "net/http (stdlib)",
|
|
2068
|
+
registry: "stdlib",
|
|
2069
|
+
registryUrl: "https://pkg.go.dev/net/http",
|
|
2070
|
+
installCommands: [
|
|
2071
|
+
{ manager: "go", command: "# No install needed \u2014 net/http ships with Go." }
|
|
2072
|
+
],
|
|
2073
|
+
initSnippet: {
|
|
2074
|
+
imports: [
|
|
2075
|
+
"package zyphr",
|
|
2076
|
+
"",
|
|
2077
|
+
"import (",
|
|
2078
|
+
' "bytes"',
|
|
2079
|
+
' "encoding/json"',
|
|
2080
|
+
' "fmt"',
|
|
2081
|
+
' "io"',
|
|
2082
|
+
' "net/http"',
|
|
2083
|
+
' "os"',
|
|
2084
|
+
")"
|
|
2085
|
+
].join("\n"),
|
|
2086
|
+
init: [
|
|
2087
|
+
'const baseURL = "https://api.zyphr.dev/v1"',
|
|
2088
|
+
"",
|
|
2089
|
+
"type Client struct {",
|
|
2090
|
+
" APIKey string",
|
|
2091
|
+
" HTTPClient *http.Client",
|
|
2092
|
+
"}",
|
|
2093
|
+
"",
|
|
2094
|
+
"func NewClient() *Client {",
|
|
2095
|
+
' return &Client{APIKey: os.Getenv("ZYPHR_API_KEY"), HTTPClient: &http.Client{}}',
|
|
2096
|
+
"}",
|
|
2097
|
+
"",
|
|
2098
|
+
"func (c *Client) Do(method, path string, body any) ([]byte, error) {",
|
|
2099
|
+
" var buf io.Reader",
|
|
2100
|
+
" if body != nil {",
|
|
2101
|
+
" b, err := json.Marshal(body)",
|
|
2102
|
+
' if err != nil { return nil, fmt.Errorf("marshal: %w", err) }',
|
|
2103
|
+
" buf = bytes.NewReader(b)",
|
|
2104
|
+
" }",
|
|
2105
|
+
" req, err := http.NewRequest(method, baseURL+path, buf)",
|
|
2106
|
+
" if err != nil { return nil, err }",
|
|
2107
|
+
' req.Header.Set("X-API-Key", c.APIKey)',
|
|
2108
|
+
' req.Header.Set("Content-Type", "application/json")',
|
|
2109
|
+
" resp, err := c.HTTPClient.Do(req)",
|
|
2110
|
+
" if err != nil { return nil, err }",
|
|
2111
|
+
" defer resp.Body.Close()",
|
|
2112
|
+
" data, _ := io.ReadAll(resp.Body)",
|
|
2113
|
+
' if resp.StatusCode >= 400 { return nil, fmt.Errorf("zyphr %d: %s", resp.StatusCode, data) }',
|
|
2114
|
+
" return data, nil",
|
|
2115
|
+
"}"
|
|
2116
|
+
].join("\n"),
|
|
2117
|
+
fileExample: "internal/zyphr/client.go"
|
|
2118
|
+
},
|
|
2119
|
+
envVarsNeeded: ["ZYPHR_API_KEY"],
|
|
2120
|
+
docsUrl: `${DOCS6}/go`,
|
|
2121
|
+
notes: "There is no official Zyphr Go SDK yet \u2014 the canonical integration is a thin REST wrapper around net/http."
|
|
2122
|
+
},
|
|
2123
|
+
php: {
|
|
2124
|
+
language: "php",
|
|
2125
|
+
kind: "rest-client",
|
|
2126
|
+
packageName: "guzzlehttp/guzzle",
|
|
2127
|
+
registry: "Packagist",
|
|
2128
|
+
registryUrl: "https://packagist.org/packages/guzzlehttp/guzzle",
|
|
2129
|
+
installCommands: [
|
|
2130
|
+
{ manager: "composer", command: "composer require guzzlehttp/guzzle" }
|
|
2131
|
+
],
|
|
2132
|
+
initSnippet: {
|
|
2133
|
+
imports: [
|
|
2134
|
+
"<?php",
|
|
2135
|
+
"",
|
|
2136
|
+
"use GuzzleHttp\\Client;"
|
|
2137
|
+
].join("\n"),
|
|
2138
|
+
init: [
|
|
2139
|
+
"$client = new Client([",
|
|
2140
|
+
" 'base_uri' => 'https://api.zyphr.dev/v1/',",
|
|
2141
|
+
" 'headers' => [",
|
|
2142
|
+
" 'X-API-Key' => getenv('ZYPHR_API_KEY'),",
|
|
2143
|
+
" 'Content-Type' => 'application/json',",
|
|
2144
|
+
" ],",
|
|
2145
|
+
"]);"
|
|
2146
|
+
].join("\n"),
|
|
2147
|
+
fileExample: "app/Services/ZyphrClient.php"
|
|
2148
|
+
},
|
|
2149
|
+
envVarsNeeded: ["ZYPHR_API_KEY"],
|
|
2150
|
+
docsUrl: `${DOCS6}/php`,
|
|
2151
|
+
notes: "There is no official Zyphr PHP SDK yet \u2014 the canonical integration uses Guzzle (or raw cURL)."
|
|
2152
|
+
}
|
|
2153
|
+
};
|
|
2154
|
+
function resolveInstallEntry(language, packageManager) {
|
|
2155
|
+
const entry = SDK_INSTALL_TABLE[language];
|
|
2156
|
+
if (!packageManager) return entry;
|
|
2157
|
+
const normalized = packageManager.trim().toLowerCase();
|
|
2158
|
+
const match = entry.installCommands.find((c) => c.manager.toLowerCase() === normalized);
|
|
2159
|
+
if (!match) return entry;
|
|
2160
|
+
return { ...entry, installCommands: [match] };
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2163
|
+
// src/tools/integration.ts
|
|
2164
|
+
function registerIntegrationTools(server, guards) {
|
|
2165
|
+
if (isToolEnabled({ name: "get_sdk_install_for_language", mutates: false }, guards)) {
|
|
2166
|
+
server.registerTool(
|
|
2167
|
+
"get_sdk_install_for_language",
|
|
2168
|
+
{
|
|
2169
|
+
title: "Get SDK install instructions for a language",
|
|
2170
|
+
description: "Returns install commands, init snippet, env vars, and docs URL for the chosen language. Use this when wiring Zyphr into a new project so the AI can drop the correct package + client init code.",
|
|
2171
|
+
inputSchema: getSdkInstallShape
|
|
2172
|
+
},
|
|
2173
|
+
async (args) => {
|
|
2174
|
+
const entry = resolveInstallEntry(args.language, args.packageManager);
|
|
2175
|
+
return toolResult(entry);
|
|
2176
|
+
}
|
|
2177
|
+
);
|
|
2178
|
+
}
|
|
2179
|
+
if (isToolEnabled({ name: "get_quickstart_for_channel", mutates: false }, guards)) {
|
|
2180
|
+
server.registerTool(
|
|
2181
|
+
"get_quickstart_for_channel",
|
|
2182
|
+
{
|
|
2183
|
+
title: "Get quickstart code for a channel + language",
|
|
2184
|
+
description: "Returns drop-in service file(s) for the chosen Zyphr channel (email/push/sms/inbox/webhook), language, and optional framework (express, nextjs, flask, fastapi, rails, laravel, aspnetcore). Webhook handlers ALWAYS verify HMAC signatures. Unknown frameworks fall back to plain SDK code.",
|
|
2185
|
+
inputSchema: getQuickstartShape
|
|
2186
|
+
},
|
|
2187
|
+
async (args) => {
|
|
2188
|
+
const resolved = resolveQuickstart({
|
|
2189
|
+
channel: args.channel,
|
|
2190
|
+
language: args.language,
|
|
2191
|
+
framework: args.framework
|
|
2192
|
+
});
|
|
2193
|
+
if (!resolved) {
|
|
2194
|
+
return toolResult({
|
|
2195
|
+
error: `No quickstart available for channel=${args.channel} language=${args.language}`
|
|
2196
|
+
});
|
|
2197
|
+
}
|
|
2198
|
+
const { result, frameworkRecognized } = resolved;
|
|
2199
|
+
return toolResult({
|
|
2200
|
+
...result,
|
|
2201
|
+
frameworkRecognized,
|
|
2202
|
+
requestedFramework: args.framework ?? null
|
|
2203
|
+
});
|
|
2204
|
+
}
|
|
2205
|
+
);
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
|
|
2209
|
+
// src/tools/webhooks.ts
|
|
2210
|
+
function registerWebhookTools(server, guards) {
|
|
2211
|
+
if (isToolEnabled({ name: "list_webhooks", mutates: false }, guards)) {
|
|
2212
|
+
server.registerTool(
|
|
2213
|
+
"list_webhooks",
|
|
2214
|
+
{
|
|
2215
|
+
title: "List webhooks",
|
|
2216
|
+
description: "List webhook endpoints configured for the account.",
|
|
2217
|
+
inputSchema: listWebhooksShape
|
|
2218
|
+
},
|
|
2219
|
+
async (args) => {
|
|
2220
|
+
return runTool(async () => {
|
|
2221
|
+
const zyphr = getZyphrClient();
|
|
2222
|
+
return await zyphr.webhooks.listWebhooks(args.limit, args.offset);
|
|
2223
|
+
});
|
|
2224
|
+
}
|
|
2225
|
+
);
|
|
2226
|
+
}
|
|
2227
|
+
if (isToolEnabled({ name: "create_webhook", mutates: true }, guards)) {
|
|
2228
|
+
server.registerTool(
|
|
2229
|
+
"create_webhook",
|
|
2230
|
+
{
|
|
2231
|
+
title: "Create webhook",
|
|
2232
|
+
description: 'Register a new webhook endpoint. Subscribe to event types like "email.*", "subscriber.created", or "*".',
|
|
2233
|
+
inputSchema: createWebhookShape
|
|
2234
|
+
},
|
|
2235
|
+
async (args) => {
|
|
2236
|
+
return runTool(async () => {
|
|
2237
|
+
const zyphr = getZyphrClient();
|
|
2238
|
+
return await zyphr.webhooks.createWebhook({
|
|
2239
|
+
url: args.url,
|
|
2240
|
+
events: args.events,
|
|
2241
|
+
description: args.description,
|
|
2242
|
+
secret: args.secret,
|
|
2243
|
+
metadata: args.metadata,
|
|
2244
|
+
headers: args.headers,
|
|
2245
|
+
version: args.version,
|
|
2246
|
+
rateLimit: args.rateLimit
|
|
2247
|
+
});
|
|
2248
|
+
});
|
|
2249
|
+
}
|
|
2250
|
+
);
|
|
2251
|
+
}
|
|
2252
|
+
if (isToolEnabled({ name: "get_webhook_deliveries", mutates: false }, guards)) {
|
|
2253
|
+
server.registerTool(
|
|
2254
|
+
"get_webhook_deliveries",
|
|
2255
|
+
{
|
|
2256
|
+
title: "List webhook deliveries",
|
|
2257
|
+
description: "Inspect delivery history for a webhook endpoint. Filter by status (pending/delivering/delivered/failed/exhausted), event type, or date range. Useful for debugging why a webhook is not firing.",
|
|
2258
|
+
inputSchema: getWebhookDeliveriesShape
|
|
2259
|
+
},
|
|
2260
|
+
async (args) => {
|
|
2261
|
+
return runTool(async () => {
|
|
2262
|
+
const zyphr = getZyphrClient();
|
|
2263
|
+
return await zyphr.webhooks.listWebhookDeliveries(
|
|
2264
|
+
args.webhookId,
|
|
2265
|
+
args.status,
|
|
2266
|
+
args.eventType,
|
|
2267
|
+
args.search,
|
|
2268
|
+
args.startDate ? new Date(args.startDate) : void 0,
|
|
2269
|
+
args.endDate ? new Date(args.endDate) : void 0,
|
|
2270
|
+
args.limit,
|
|
2271
|
+
args.offset
|
|
2272
|
+
);
|
|
2273
|
+
});
|
|
2274
|
+
}
|
|
2275
|
+
);
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
|
|
2279
|
+
// src/server.ts
|
|
2280
|
+
var SERVER_NAME = "zyphr";
|
|
2281
|
+
var SERVER_VERSION = "0.1.0";
|
|
2282
|
+
function createServer() {
|
|
2283
|
+
const server = new McpServer(
|
|
2284
|
+
{ name: SERVER_NAME, version: SERVER_VERSION },
|
|
2285
|
+
{ capabilities: { tools: {} } }
|
|
2286
|
+
);
|
|
2287
|
+
const guards = loadToolGuards();
|
|
2288
|
+
registerSendTools(server, guards);
|
|
2289
|
+
registerTemplateTools(server, guards);
|
|
2290
|
+
registerSubscriberTools(server, guards);
|
|
2291
|
+
registerWebhookTools(server, guards);
|
|
2292
|
+
registerIntegrationTools(server, guards);
|
|
2293
|
+
return server;
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2296
|
+
// src/index.ts
|
|
2297
|
+
async function main() {
|
|
2298
|
+
if (!process.env.ZYPHR_API_KEY) {
|
|
2299
|
+
process.stderr.write(
|
|
2300
|
+
"[zyphr-mcp] ZYPHR_API_KEY is not set. Provide a zy_live_* or zy_test_* key via the `env` block of your MCP client config.\n"
|
|
2301
|
+
);
|
|
2302
|
+
process.exit(1);
|
|
2303
|
+
}
|
|
2304
|
+
const server = createServer();
|
|
2305
|
+
const transport = new StdioServerTransport();
|
|
2306
|
+
await server.connect(transport);
|
|
2307
|
+
process.stderr.write(`[zyphr-mcp] connected (base=${getBaseUrl()})
|
|
2308
|
+
`);
|
|
2309
|
+
}
|
|
2310
|
+
main().catch((err) => {
|
|
2311
|
+
process.stderr.write(`[zyphr-mcp] fatal: ${err instanceof Error ? err.stack ?? err.message : String(err)}
|
|
2312
|
+
`);
|
|
2313
|
+
process.exit(1);
|
|
2314
|
+
});
|
|
2315
|
+
//# sourceMappingURL=index.js.map
|