bitelio 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/dist/index.js ADDED
@@ -0,0 +1,302 @@
1
+ import { createHmac, timingSafeEqual, randomUUID } from 'crypto';
2
+
3
+ var __defProp = Object.defineProperty;
4
+ var __export = (target, all) => {
5
+ for (var name in all)
6
+ __defProp(target, name, { get: all[name], enumerable: true });
7
+ };
8
+
9
+ // src/errors.ts
10
+ var BitelioError = class _BitelioError extends Error {
11
+ /** HTTP status. `0` when the request never reached the API — a DNS failure, a timeout. */
12
+ status;
13
+ /** Machine-readable code, e.g. `VALIDATION_ERROR`. Absent on a transport failure. */
14
+ code;
15
+ /** Quote this when reporting a problem. */
16
+ requestId;
17
+ details;
18
+ constructor(message, init) {
19
+ super(message);
20
+ this.name = "BitelioError";
21
+ this.status = init.status;
22
+ this.code = init.code;
23
+ this.requestId = init.requestId;
24
+ this.details = init.details;
25
+ }
26
+ /** True for the failures a retry could plausibly fix. */
27
+ get retryable() {
28
+ return this.status === 0 || this.status === 429 || this.status >= 500;
29
+ }
30
+ static fromResponse(status, requestId, body) {
31
+ const envelope = body ?? {};
32
+ const error = envelope.error;
33
+ return new _BitelioError(error?.message ?? `Request failed with status ${status}`, {
34
+ status,
35
+ code: error?.code,
36
+ // The header wins: the body's copy is absent on responses the API did not build itself,
37
+ // such as a 502 from a proxy in front of it.
38
+ requestId: requestId ?? error?.requestId,
39
+ details: error?.details
40
+ });
41
+ }
42
+ };
43
+
44
+ // src/client.ts
45
+ var DEFAULTS = {
46
+ baseUrl: "https://api.bitelio.com",
47
+ timeoutMs: 3e4,
48
+ maxRetries: 2
49
+ };
50
+ function backoffMs(attempt, retryAfter) {
51
+ if (retryAfter !== null) return Math.min(retryAfter * 1e3, 6e4);
52
+ const ceiling = Math.min(500 * 2 ** attempt, 8e3);
53
+ return Math.random() * ceiling;
54
+ }
55
+ function retryAfterSeconds(response) {
56
+ const header = response.headers.get("retry-after");
57
+ if (header === null) return null;
58
+ const seconds = Number(header);
59
+ return Number.isFinite(seconds) && seconds >= 0 ? seconds : null;
60
+ }
61
+ var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
62
+ var HttpClient = class {
63
+ apiKey;
64
+ publicKey;
65
+ baseUrl;
66
+ timeoutMs;
67
+ maxRetries;
68
+ fetchImpl;
69
+ constructor(apiKey, options = {}) {
70
+ if (!apiKey) {
71
+ throw new BitelioError("An API key is required. Create one in Settings \u2192 General \u2192 API Keys.", { status: 0 });
72
+ }
73
+ this.apiKey = apiKey;
74
+ this.publicKey = options.publicKey;
75
+ this.baseUrl = (options.baseUrl ?? DEFAULTS.baseUrl).replace(/\/+$/, "");
76
+ this.timeoutMs = options.timeoutMs ?? DEFAULTS.timeoutMs;
77
+ this.maxRetries = options.maxRetries ?? DEFAULTS.maxRetries;
78
+ this.fetchImpl = options.fetch ?? globalThis.fetch;
79
+ }
80
+ async request(method, path, options = {}) {
81
+ const url = new URL(this.baseUrl + path);
82
+ for (const [key, value] of Object.entries(options.query ?? {})) {
83
+ if (value !== void 0) url.searchParams.set(key, String(value));
84
+ }
85
+ const headers = {
86
+ authorization: `Bearer ${options.authToken ?? this.apiKey}`,
87
+ accept: "application/json"
88
+ };
89
+ if (options.body !== void 0) headers["content-type"] = "application/json";
90
+ if (options.idempotencyKey !== void 0) headers["idempotency-key"] = options.idempotencyKey;
91
+ let lastError;
92
+ for (let attempt = 0; ; attempt += 1) {
93
+ let response;
94
+ const controller = new AbortController();
95
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
96
+ try {
97
+ response = await this.fetchImpl(url, {
98
+ method,
99
+ headers,
100
+ body: options.body === void 0 ? void 0 : JSON.stringify(options.body),
101
+ signal: controller.signal
102
+ });
103
+ } catch (cause) {
104
+ lastError = new BitelioError(
105
+ cause instanceof Error && cause.name === "AbortError" ? `Request to ${method} ${path} timed out after ${this.timeoutMs}ms` : `Could not reach the Bitelio API: ${cause instanceof Error ? cause.message : String(cause)}`,
106
+ { status: 0 }
107
+ );
108
+ } finally {
109
+ clearTimeout(timer);
110
+ }
111
+ if (response !== void 0) {
112
+ const requestId = response.headers.get("x-request-id") ?? void 0;
113
+ if (response.ok) {
114
+ if (response.status === 204) return void 0;
115
+ return await response.json();
116
+ }
117
+ const body = await response.json().catch(() => void 0);
118
+ lastError = BitelioError.fromResponse(response.status, requestId, body);
119
+ if (!lastError.retryable) throw lastError;
120
+ }
121
+ if (attempt >= this.maxRetries) throw lastError;
122
+ await sleep(backoffMs(attempt, response ? retryAfterSeconds(response) : null));
123
+ }
124
+ }
125
+ /** A key per call, so the retry above is safe by default rather than by remembering. */
126
+ static newIdempotencyKey() {
127
+ return randomUUID();
128
+ }
129
+ };
130
+
131
+ // src/resources/contacts.ts
132
+ var Contacts = class {
133
+ constructor(http) {
134
+ this.http = http;
135
+ }
136
+ http;
137
+ async create(params) {
138
+ return this.http.request("POST", "/contacts", { body: params });
139
+ }
140
+ async get(id) {
141
+ return this.http.request("GET", `/contacts/${encodeURIComponent(id)}`);
142
+ }
143
+ async update(id, params) {
144
+ return this.http.request("PATCH", `/contacts/${encodeURIComponent(id)}`, { body: params });
145
+ }
146
+ /** Irreversible, and it takes the contact's send history with it. */
147
+ async delete(id) {
148
+ await this.http.request("DELETE", `/contacts/${encodeURIComponent(id)}`);
149
+ }
150
+ };
151
+
152
+ // src/resources/emails.ts
153
+ function isoOrUndefined(value) {
154
+ if (value === void 0) return void 0;
155
+ return value instanceof Date ? value.toISOString() : value;
156
+ }
157
+ var Emails = class {
158
+ constructor(http) {
159
+ this.http = http;
160
+ }
161
+ http;
162
+ /**
163
+ * Sends one transactional email, or one per recipient when `to` is an array.
164
+ *
165
+ * An idempotency key is generated for you unless you supply one. That is not a nicety: this
166
+ * client retries 429s and 5xx responses, and a retried send without a key is a duplicate in
167
+ * somebody's inbox.
168
+ */
169
+ async send(params) {
170
+ const { idempotencyKey, ...body } = params;
171
+ const response = await this.http.request("POST", "/v1/send", {
172
+ body,
173
+ idempotencyKey: idempotencyKey ?? HttpClient.newIdempotencyKey()
174
+ });
175
+ return response.data;
176
+ }
177
+ /**
178
+ * One page of your send history, newest first.
179
+ *
180
+ * Shows live sends only unless you pass `mode`. Page by handing `nextCursor` straight back —
181
+ * it is opaque, and `null` means you have reached the end.
182
+ */
183
+ async list(params = {}) {
184
+ return this.http.request("GET", "/v1/emails", {
185
+ query: {
186
+ limit: params.limit,
187
+ cursor: params.cursor,
188
+ mode: params.mode,
189
+ status: params.status,
190
+ to: params.to,
191
+ since: isoOrUndefined(params.since),
192
+ until: isoOrUndefined(params.until)
193
+ }
194
+ });
195
+ }
196
+ /**
197
+ * One email, including the HTML that actually went out — after variables were substituted,
198
+ * blocks resolved and your brand applied. Most "the email looks wrong" reports end here.
199
+ */
200
+ async get(id) {
201
+ const response = await this.http.request("GET", `/v1/emails/${encodeURIComponent(id)}`);
202
+ return response.data;
203
+ }
204
+ /**
205
+ * Every send matching the filter, one page at a time.
206
+ *
207
+ * An async iterator rather than an array: a project's history does not fit in memory, and the
208
+ * shape of this method is what stops somebody discovering that in production.
209
+ */
210
+ async *iterate(params = {}) {
211
+ let cursor;
212
+ do {
213
+ const page = await this.list({ ...params, cursor });
214
+ for (const email of page.data) yield email;
215
+ cursor = page.nextCursor ?? void 0;
216
+ } while (cursor !== void 0);
217
+ }
218
+ };
219
+
220
+ // src/resources/events.ts
221
+ var Events = class {
222
+ constructor(http) {
223
+ this.http = http;
224
+ }
225
+ http;
226
+ /**
227
+ * Records an event against a contact, creating them if they are new. This is what triggers a
228
+ * workflow, so it is the call a SaaS makes from its own lifecycle code.
229
+ *
230
+ * Values in `data` are saved onto the contact and are available to every later message. To pass
231
+ * something for this event only, wrap it: `{orderId: {value: '123', persistent: false}}`.
232
+ *
233
+ * Authenticates with your PUBLIC key, which is why the client has to be given one. This endpoint
234
+ * is the same one browsers post to, and today it has no secret-key equivalent that identifies a
235
+ * contact by email address — `POST /events/track` does exist for secret keys, but it requires a
236
+ * contact id you would have to go and look up first. Supplying the public key is not a
237
+ * concession: it ships in every page of your own site already.
238
+ */
239
+ async track(params) {
240
+ if (this.http.publicKey === void 0) {
241
+ throw new BitelioError(
242
+ `events.track needs your project's public key: new Bitelio(secretKey, {publicKey: "pk_\u2026"}). Find it in Settings \u2192 General. It is not a secret \u2014 it already ships in your website's HTML.`,
243
+ { status: 0 }
244
+ );
245
+ }
246
+ const response = await this.http.request("POST", "/v1/track", {
247
+ body: params,
248
+ authToken: this.http.publicKey
249
+ });
250
+ return response.data;
251
+ }
252
+ };
253
+
254
+ // src/webhooks.ts
255
+ var webhooks_exports = {};
256
+ __export(webhooks_exports, {
257
+ assertValid: () => assertValid,
258
+ verify: () => verify
259
+ });
260
+ function constantTimeEquals(a, b) {
261
+ const left = Buffer.from(a, "utf8");
262
+ const right = Buffer.from(b, "utf8");
263
+ if (left.length !== right.length) return false;
264
+ return timingSafeEqual(left, right);
265
+ }
266
+ function verify(params) {
267
+ const { payload, signature, timestamp, secret, toleranceSeconds = 300 } = params;
268
+ const sentAt = Number(timestamp);
269
+ if (!Number.isFinite(sentAt)) return false;
270
+ if (Number.isFinite(toleranceSeconds)) {
271
+ const ageSeconds = Math.abs(Date.now() / 1e3 - sentAt);
272
+ if (ageSeconds > toleranceSeconds) return false;
273
+ }
274
+ const body = typeof payload === "string" ? payload : payload.toString("utf8");
275
+ const expected = createHmac("sha256", secret).update(`${sentAt}.${body}`).digest("hex");
276
+ return signature.split(",").map((part) => part.trim()).filter((part) => part.startsWith("v1=")).some((part) => constantTimeEquals(part.slice("v1=".length), expected));
277
+ }
278
+ function assertValid(params) {
279
+ if (!verify(params)) {
280
+ throw new Error("Webhook signature verification failed");
281
+ }
282
+ }
283
+
284
+ // src/index.ts
285
+ var Bitelio = class {
286
+ emails;
287
+ contacts;
288
+ events;
289
+ /** Verifying a webhook we sent you. Also exported standalone, for receivers with no client. */
290
+ static webhooks = webhooks_exports;
291
+ constructor(apiKey, options = {}) {
292
+ const http = new HttpClient(apiKey, options);
293
+ this.emails = new Emails(http);
294
+ this.contacts = new Contacts(http);
295
+ this.events = new Events(http);
296
+ }
297
+ };
298
+ var index_default = Bitelio;
299
+
300
+ export { Bitelio, BitelioError, index_default as default, webhooks_exports as webhooks };
301
+ //# sourceMappingURL=index.js.map
302
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/errors.ts","../src/client.ts","../src/resources/contacts.ts","../src/resources/emails.ts","../src/resources/events.ts","../src/webhooks.ts","../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/** The shape the API's global error handler returns. Every failure below is built from it. */\ninterface ErrorEnvelope {\n success?: false;\n error?: {\n code?: string;\n message?: string;\n statusCode?: number;\n requestId?: string;\n details?: Record<string, unknown>;\n };\n}\n\n/**\n * Every failure this client throws.\n *\n * `requestId` is the field worth knowing about: it comes back on every response, and quoting it in\n * a support conversation is the difference between \"I get a 500\" and something answerable.\n */\nexport class BitelioError extends Error {\n /** HTTP status. `0` when the request never reached the API — a DNS failure, a timeout. */\n readonly status: number;\n /** Machine-readable code, e.g. `VALIDATION_ERROR`. Absent on a transport failure. */\n readonly code: string | undefined;\n /** Quote this when reporting a problem. */\n readonly requestId: string | undefined;\n readonly details: Record<string, unknown> | undefined;\n\n constructor(\n message: string,\n init: {status: number; code?: string; requestId?: string; details?: Record<string, unknown>},\n ) {\n super(message);\n this.name = 'BitelioError';\n this.status = init.status;\n this.code = init.code;\n this.requestId = init.requestId;\n this.details = init.details;\n }\n\n /** True for the failures a retry could plausibly fix. */\n get retryable(): boolean {\n return this.status === 0 || this.status === 429 || this.status >= 500;\n }\n\n static fromResponse(status: number, requestId: string | undefined, body: unknown): BitelioError {\n const envelope = (body ?? {}) as ErrorEnvelope;\n const error = envelope.error;\n\n return new BitelioError(error?.message ?? `Request failed with status ${status}`, {\n status,\n code: error?.code,\n // The header wins: the body's copy is absent on responses the API did not build itself,\n // such as a 502 from a proxy in front of it.\n requestId: requestId ?? error?.requestId,\n details: error?.details,\n });\n }\n}\n","import {randomUUID} from 'node:crypto';\n\nimport {BitelioError} from './errors.js';\n\nexport interface BitelioOptions {\n /**\n * Your project's public key (`pk_…`), needed ONLY by `events.track`.\n *\n * That endpoint authenticates with the public key rather than the secret one — it is the same\n * endpoint browsers and mobile apps post to, and it has no secret-key equivalent that takes an\n * email address. Nothing is lost by supplying it here: a public key is not a secret, it ships in\n * every page of your site.\n */\n publicKey?: string;\n /** Defaults to `https://api.bitelio.com`. Point it at your own deployment if self-hosted. */\n baseUrl?: string;\n /** Per attempt, not for the whole call. Default 30s. */\n timeoutMs?: number;\n /** Retries AFTER the first attempt, on 429 and 5xx only. Default 2. */\n maxRetries?: number;\n /** Swap in a stub in tests, or a proxying fetch in production. */\n fetch?: typeof globalThis.fetch;\n}\n\nconst DEFAULTS = {\n baseUrl: 'https://api.bitelio.com',\n timeoutMs: 30_000,\n maxRetries: 2,\n};\n\n/** Full jitter, capped. Without the jitter a fleet retrying together retries together for ever. */\nfunction backoffMs(attempt: number, retryAfter: number | null): number {\n if (retryAfter !== null) return Math.min(retryAfter * 1000, 60_000);\n const ceiling = Math.min(500 * 2 ** attempt, 8_000);\n return Math.random() * ceiling;\n}\n\nfunction retryAfterSeconds(response: Response): number | null {\n const header = response.headers.get('retry-after');\n if (header === null) return null;\n const seconds = Number(header);\n\n return Number.isFinite(seconds) && seconds >= 0 ? seconds : null;\n}\n\nconst sleep = (ms: number) => new Promise<void>(resolve => setTimeout(resolve, ms));\n\nexport interface RequestOptions {\n query?: Record<string, string | number | boolean | undefined>;\n body?: unknown;\n idempotencyKey?: string;\n /** Overrides the bearer token for this call. Used by `events.track`, which needs the public key. */\n authToken?: string;\n}\n\n/**\n * The transport. Everything a resource does goes through `request`, so the retry policy, the\n * timeout and the error shape are decided in exactly one place.\n */\nexport class HttpClient {\n private readonly apiKey: string;\n readonly publicKey: string | undefined;\n private readonly baseUrl: string;\n private readonly timeoutMs: number;\n private readonly maxRetries: number;\n private readonly fetchImpl: typeof globalThis.fetch;\n\n constructor(apiKey: string, options: BitelioOptions = {}) {\n if (!apiKey) {\n throw new BitelioError('An API key is required. Create one in Settings → General → API Keys.', {status: 0});\n }\n\n this.apiKey = apiKey;\n this.publicKey = options.publicKey;\n this.baseUrl = (options.baseUrl ?? DEFAULTS.baseUrl).replace(/\\/+$/, '');\n this.timeoutMs = options.timeoutMs ?? DEFAULTS.timeoutMs;\n this.maxRetries = options.maxRetries ?? DEFAULTS.maxRetries;\n this.fetchImpl = options.fetch ?? globalThis.fetch;\n }\n\n async request<T>(method: string, path: string, options: RequestOptions = {}): Promise<T> {\n const url = new URL(this.baseUrl + path);\n for (const [key, value] of Object.entries(options.query ?? {})) {\n if (value !== undefined) url.searchParams.set(key, String(value));\n }\n\n const headers: Record<string, string> = {\n authorization: `Bearer ${options.authToken ?? this.apiKey}`,\n accept: 'application/json',\n };\n if (options.body !== undefined) headers['content-type'] = 'application/json';\n // Sent on the FIRST attempt and reused on every retry, which is the entire point: without it\n // the automatic retry below would be a duplicator of transactional email.\n if (options.idempotencyKey !== undefined) headers['idempotency-key'] = options.idempotencyKey;\n\n let lastError: BitelioError | undefined;\n\n for (let attempt = 0; ; attempt += 1) {\n let response: Response | undefined;\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), this.timeoutMs);\n try {\n response = await this.fetchImpl(url, {\n method,\n headers,\n body: options.body === undefined ? undefined : JSON.stringify(options.body),\n signal: controller.signal,\n });\n } catch (cause) {\n // Never reached the API: DNS, connection refused, or our own timeout. Status 0 marks the\n // difference, because \"the server said no\" and \"we never asked\" want different handling.\n lastError = new BitelioError(\n cause instanceof Error && cause.name === 'AbortError'\n ? `Request to ${method} ${path} timed out after ${this.timeoutMs}ms`\n : `Could not reach the Bitelio API: ${cause instanceof Error ? cause.message : String(cause)}`,\n {status: 0},\n );\n } finally {\n clearTimeout(timer);\n }\n\n if (response !== undefined) {\n const requestId = response.headers.get('x-request-id') ?? undefined;\n\n if (response.ok) {\n // 204 has no body to parse, and `response.json()` on an empty one throws.\n if (response.status === 204) return undefined as T;\n return (await response.json()) as T;\n }\n\n const body = await response.json().catch(() => undefined);\n lastError = BitelioError.fromResponse(response.status, requestId, body);\n\n // 4xx is never retried. A 403 retried three times is a 403 three times slower, and a 400\n // is a bug in the caller that another attempt cannot fix.\n if (!lastError.retryable) throw lastError;\n }\n\n if (attempt >= this.maxRetries) throw lastError;\n\n // `Retry-After` when the server sent one, jittered exponential backoff otherwise. One sleep,\n // decided here — an earlier draft of this had two, which silently doubled every wait.\n await sleep(backoffMs(attempt, response ? retryAfterSeconds(response) : null));\n }\n }\n\n /** A key per call, so the retry above is safe by default rather than by remembering. */\n static newIdempotencyKey(): string {\n return randomUUID();\n }\n}\n","import type {HttpClient} from '../client.js';\nimport type {Contact} from '../types.js';\n\nexport interface CreateContactParams {\n email: string;\n subscribed?: boolean;\n data?: Record<string, unknown>;\n}\n\nexport interface UpdateContactParams {\n email?: string;\n subscribed?: boolean;\n data?: Record<string, unknown>;\n}\n\nexport class Contacts {\n constructor(private readonly http: HttpClient) {}\n\n async create(params: CreateContactParams): Promise<Contact> {\n return this.http.request<Contact>('POST', '/contacts', {body: params});\n }\n\n async get(id: string): Promise<Contact> {\n return this.http.request<Contact>('GET', `/contacts/${encodeURIComponent(id)}`);\n }\n\n async update(id: string, params: UpdateContactParams): Promise<Contact> {\n return this.http.request<Contact>('PATCH', `/contacts/${encodeURIComponent(id)}`, {body: params});\n }\n\n /** Irreversible, and it takes the contact's send history with it. */\n async delete(id: string): Promise<void> {\n await this.http.request<void>('DELETE', `/contacts/${encodeURIComponent(id)}`);\n }\n}\n","import {HttpClient} from '../client.js';\nimport type {Email, EmailDetail, EmailListParams, Page, SendParams, SendResult} from '../types.js';\n\nfunction isoOrUndefined(value: Date | string | undefined): string | undefined {\n if (value === undefined) return undefined;\n return value instanceof Date ? value.toISOString() : value;\n}\n\nexport class Emails {\n constructor(private readonly http: HttpClient) {}\n\n /**\n * Sends one transactional email, or one per recipient when `to` is an array.\n *\n * An idempotency key is generated for you unless you supply one. That is not a nicety: this\n * client retries 429s and 5xx responses, and a retried send without a key is a duplicate in\n * somebody's inbox.\n */\n async send(params: SendParams): Promise<SendResult> {\n const {idempotencyKey, ...body} = params;\n\n const response = await this.http.request<{success: boolean; data: SendResult}>('POST', '/v1/send', {\n body,\n idempotencyKey: idempotencyKey ?? HttpClient.newIdempotencyKey(),\n });\n\n return response.data;\n }\n\n /**\n * One page of your send history, newest first.\n *\n * Shows live sends only unless you pass `mode`. Page by handing `nextCursor` straight back —\n * it is opaque, and `null` means you have reached the end.\n */\n async list(params: EmailListParams = {}): Promise<Page<Email>> {\n return this.http.request<Page<Email>>('GET', '/v1/emails', {\n query: {\n limit: params.limit,\n cursor: params.cursor,\n mode: params.mode,\n status: params.status,\n to: params.to,\n since: isoOrUndefined(params.since),\n until: isoOrUndefined(params.until),\n },\n });\n }\n\n /**\n * One email, including the HTML that actually went out — after variables were substituted,\n * blocks resolved and your brand applied. Most \"the email looks wrong\" reports end here.\n */\n async get(id: string): Promise<EmailDetail> {\n const response = await this.http.request<{data: EmailDetail}>('GET', `/v1/emails/${encodeURIComponent(id)}`);\n\n return response.data;\n }\n\n /**\n * Every send matching the filter, one page at a time.\n *\n * An async iterator rather than an array: a project's history does not fit in memory, and the\n * shape of this method is what stops somebody discovering that in production.\n */\n async *iterate(params: Omit<EmailListParams, 'cursor'> = {}): AsyncGenerator<Email> {\n let cursor: string | undefined;\n do {\n const page = await this.list({...params, cursor});\n for (const email of page.data) yield email;\n cursor = page.nextCursor ?? undefined;\n } while (cursor !== undefined);\n }\n}\n","import type {HttpClient} from '../client.js';\nimport {BitelioError} from '../errors.js';\nimport type {TrackParams} from '../types.js';\n\nexport interface TrackResult {\n contact: string;\n event: string;\n timestamp: string;\n}\n\nexport class Events {\n constructor(private readonly http: HttpClient) {}\n\n /**\n * Records an event against a contact, creating them if they are new. This is what triggers a\n * workflow, so it is the call a SaaS makes from its own lifecycle code.\n *\n * Values in `data` are saved onto the contact and are available to every later message. To pass\n * something for this event only, wrap it: `{orderId: {value: '123', persistent: false}}`.\n *\n * Authenticates with your PUBLIC key, which is why the client has to be given one. This endpoint\n * is the same one browsers post to, and today it has no secret-key equivalent that identifies a\n * contact by email address — `POST /events/track` does exist for secret keys, but it requires a\n * contact id you would have to go and look up first. Supplying the public key is not a\n * concession: it ships in every page of your own site already.\n */\n async track(params: TrackParams): Promise<TrackResult> {\n if (this.http.publicKey === undefined) {\n throw new BitelioError(\n 'events.track needs your project\\'s public key: new Bitelio(secretKey, {publicKey: \"pk_…\"}). ' +\n 'Find it in Settings → General. It is not a secret — it already ships in your website\\'s HTML.',\n {status: 0},\n );\n }\n\n const response = await this.http.request<{success: boolean; data: TrackResult}>('POST', '/v1/track', {\n body: params,\n authToken: this.http.publicKey,\n });\n\n return response.data;\n }\n}\n","import {createHmac, timingSafeEqual} from 'node:crypto';\n\n/**\n * Verifying a webhook Bitelio sent you.\n *\n * The signature is HMAC-SHA256 over `{timestamp}.{rawBody}`, sent as `Bitelio-Signature` beside\n * `Bitelio-Timestamp`. The timestamp is inside the signed string so a replayed old request can be\n * rejected — the same scheme Stripe uses, which is deliberate: it is the one developers already\n * know how to verify by hand.\n */\n\nexport interface VerifyParams {\n /**\n * The RAW request body, as a string or Buffer — NOT a parsed object.\n *\n * This is the mistake everyone makes. `JSON.parse` then `JSON.stringify` reorders keys and\n * changes whitespace, and the signature covers bytes. In Express, reach for\n * `express.raw({type: 'application/json'})` on the webhook route.\n */\n payload: string | Buffer;\n /** The `Bitelio-Signature` header, verbatim. */\n signature: string;\n /** The `Bitelio-Timestamp` header, verbatim. */\n timestamp: string | number;\n /** Your endpoint's signing secret. */\n secret: string;\n /**\n * How old a request may be, in seconds. Default 300.\n *\n * This is what makes the timestamp worth signing: without a bound, a request captured once can\n * be replayed for ever. Pass `Infinity` only if you have your own replay defence.\n */\n toleranceSeconds?: number;\n}\n\nfunction constantTimeEquals(a: string, b: string): boolean {\n const left = Buffer.from(a, 'utf8');\n const right = Buffer.from(b, 'utf8');\n // `timingSafeEqual` throws on a length mismatch, which would itself leak the length. Comparing\n // the lengths first is safe: a signature's length is not a secret, its content is.\n if (left.length !== right.length) return false;\n\n return timingSafeEqual(left, right);\n}\n\n/**\n * True when the request really came from Bitelio and is recent enough.\n *\n * Accepts the request if ANY `v1=` part verifies. There is more than one during a secret\n * rotation — the new secret and the still-live previous one — and a receiver mid-rollover that\n * insisted on a single part would drop half its traffic.\n */\nexport function verify(params: VerifyParams): boolean {\n const {payload, signature, timestamp, secret, toleranceSeconds = 300} = params;\n\n const sentAt = Number(timestamp);\n if (!Number.isFinite(sentAt)) return false;\n\n if (Number.isFinite(toleranceSeconds)) {\n const ageSeconds = Math.abs(Date.now() / 1000 - sentAt);\n if (ageSeconds > toleranceSeconds) return false;\n }\n\n const body = typeof payload === 'string' ? payload : payload.toString('utf8');\n const expected = createHmac('sha256', secret).update(`${sentAt}.${body}`).digest('hex');\n\n return signature\n .split(',')\n .map(part => part.trim())\n .filter(part => part.startsWith('v1='))\n .some(part => constantTimeEquals(part.slice('v1='.length), expected));\n}\n\n/** `verify`, but it throws instead of returning false — handy at the top of a handler. */\nexport function assertValid(params: VerifyParams): void {\n if (!verify(params)) {\n throw new Error('Webhook signature verification failed');\n }\n}\n","import type {BitelioOptions} from './client.js';\nimport {HttpClient} from './client.js';\nimport {Contacts} from './resources/contacts.js';\nimport {Emails} from './resources/emails.js';\nimport {Events} from './resources/events.js';\nimport * as webhooks from './webhooks.js';\n\nexport {BitelioError} from './errors.js';\nexport type {BitelioOptions} from './client.js';\nexport type {CreateContactParams, UpdateContactParams} from './resources/contacts.js';\nexport type {TrackResult} from './resources/events.js';\nexport type {VerifyParams} from './webhooks.js';\nexport type * from './types.js';\n\n/**\n * The Bitelio client.\n *\n * ```ts\n * import {Bitelio} from 'bitelio';\n *\n * const bitelio = new Bitelio(process.env.BITELIO_API_KEY!);\n *\n * await bitelio.emails.send({\n * from: 'onboarding@send.bitelio.com',\n * to: 'you@yourcompany.com',\n * subject: 'Hello',\n * body: '<p>It works.</p>',\n * });\n * ```\n *\n * Covers sending, reading your send history, contacts and events — what you drive from code.\n * Campaigns, segments, templates and workflows are designed in the dashboard and are deliberately\n * not here.\n */\nexport class Bitelio {\n readonly emails: Emails;\n readonly contacts: Contacts;\n readonly events: Events;\n\n /** Verifying a webhook we sent you. Also exported standalone, for receivers with no client. */\n static readonly webhooks = webhooks;\n\n constructor(apiKey: string, options: BitelioOptions = {}) {\n const http = new HttpClient(apiKey, options);\n this.emails = new Emails(http);\n this.contacts = new Contacts(http);\n this.events = new Events(http);\n }\n}\n\nexport {webhooks};\nexport default Bitelio;\n"]}
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "bitelio",
3
+ "version": "0.1.0",
4
+ "description": "Official Node.js client for the Bitelio email API",
5
+ "license": "MIT",
6
+ "author": "Overthings",
7
+ "homepage": "https://docs.bitelio.com",
8
+ "repository": {"type": "git", "url": "git+https://github.com/overthings/bitelio-node.git"},
9
+ "bugs": {"url": "https://github.com/overthings/bitelio-node/issues"},
10
+ "keywords": ["bitelio", "email", "transactional-email", "marketing-automation", "api-client"],
11
+ "type": "module",
12
+ "main": "./dist/index.cjs",
13
+ "module": "./dist/index.js",
14
+ "types": "./dist/index.d.ts",
15
+ "exports": {
16
+ ".": {
17
+ "types": "./dist/index.d.ts",
18
+ "import": "./dist/index.js",
19
+ "require": "./dist/index.cjs"
20
+ }
21
+ },
22
+ "files": ["dist"],
23
+ "engines": {"node": ">=20"},
24
+ "sideEffects": false,
25
+ "scripts": {
26
+ "build": "tsup",
27
+ "test": "vitest run",
28
+ "typecheck": "tsc --noEmit",
29
+ "lint": "eslint .",
30
+ "prepublishOnly": "npm run typecheck && npm run test && npm run build"
31
+ },
32
+ "dependencies": {},
33
+ "devDependencies": {
34
+ "@types/node": "^20.14.0",
35
+ "tsup": "^8.3.5",
36
+ "typescript": "^5.6.0",
37
+ "vitest": "^2.1.8"
38
+ },
39
+ "publishConfig": {"access": "public"}
40
+ }