@hogsend/client 0.6.0 → 0.8.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.cjs +156 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +172 -1
- package/dist/index.d.ts +172 -1
- package/dist/index.js +154 -1
- package/dist/index.js.map +1 -1
- package/package.json +6 -11
package/dist/index.cjs
CHANGED
|
@@ -22,7 +22,8 @@ var index_exports = {};
|
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
Hogsend: () => Hogsend,
|
|
24
24
|
HogsendAPIError: () => HogsendAPIError,
|
|
25
|
-
RateLimitError: () => RateLimitError
|
|
25
|
+
RateLimitError: () => RateLimitError,
|
|
26
|
+
verifyHogsendWebhook: () => verifyHogsendWebhook
|
|
26
27
|
});
|
|
27
28
|
module.exports = __toCommonJS(index_exports);
|
|
28
29
|
|
|
@@ -144,6 +145,7 @@ function createHttpClient(config) {
|
|
|
144
145
|
get: (path, query) => request("GET", path, { query }),
|
|
145
146
|
post: (path, body, extras) => request("POST", path, { body, extras }),
|
|
146
147
|
put: (path, body, extras) => request("PUT", path, { body, extras }),
|
|
148
|
+
patch: (path, body, extras) => request("PATCH", path, { body, extras }),
|
|
147
149
|
del: (path, body) => request("DELETE", path, { body })
|
|
148
150
|
};
|
|
149
151
|
}
|
|
@@ -335,6 +337,87 @@ var ListsResource = class {
|
|
|
335
337
|
}
|
|
336
338
|
};
|
|
337
339
|
|
|
340
|
+
// src/resources/webhooks.ts
|
|
341
|
+
var BASE = "/v1/admin/webhooks";
|
|
342
|
+
var WebhooksResource = class {
|
|
343
|
+
constructor(http) {
|
|
344
|
+
this.http = http;
|
|
345
|
+
}
|
|
346
|
+
http;
|
|
347
|
+
/**
|
|
348
|
+
* Register a new endpoint subscribed to one or more outbound event types.
|
|
349
|
+
* Returns the endpoint INCLUDING the full signing `secret` — this is the
|
|
350
|
+
* only time (besides rotate) the secret is returned. Store it now.
|
|
351
|
+
*/
|
|
352
|
+
create(input) {
|
|
353
|
+
return this.http.post(BASE, {
|
|
354
|
+
url: input.url,
|
|
355
|
+
eventTypes: input.eventTypes,
|
|
356
|
+
description: input.description,
|
|
357
|
+
disabled: input.disabled
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* List endpoints (newest first). Disabled endpoints are hidden unless
|
|
362
|
+
* `includeDisabled` is set. Returns the endpoints array (unwrapped from the
|
|
363
|
+
* `{ endpoints, total, limit, offset }` envelope).
|
|
364
|
+
*/
|
|
365
|
+
async list(opts) {
|
|
366
|
+
const res = await this.http.get(BASE, {
|
|
367
|
+
limit: opts?.limit,
|
|
368
|
+
offset: opts?.offset,
|
|
369
|
+
includeDisabled: opts?.includeDisabled === void 0 ? void 0 : String(opts.includeDisabled)
|
|
370
|
+
});
|
|
371
|
+
return res.endpoints;
|
|
372
|
+
}
|
|
373
|
+
/** Fetch one endpoint by id (404 → {@link HogsendAPIError}). */
|
|
374
|
+
get(id) {
|
|
375
|
+
return this.http.get(`${BASE}/${encodeURIComponent(id)}`);
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Patch an endpoint. Only the provided fields change; `description: null`
|
|
379
|
+
* clears the description. Does NOT return or rotate the secret.
|
|
380
|
+
*/
|
|
381
|
+
update(id, input) {
|
|
382
|
+
return this.http.patch(
|
|
383
|
+
`${BASE}/${encodeURIComponent(id)}`,
|
|
384
|
+
{
|
|
385
|
+
url: input.url,
|
|
386
|
+
eventTypes: input.eventTypes,
|
|
387
|
+
description: input.description,
|
|
388
|
+
disabled: input.disabled
|
|
389
|
+
}
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
/** Hard-delete an endpoint (cascade drops its deliveries). */
|
|
393
|
+
delete(id) {
|
|
394
|
+
return this.http.del(
|
|
395
|
+
`${BASE}/${encodeURIComponent(id)}`
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Rotate the signing secret. The OLD secret is invalidated immediately (hard
|
|
400
|
+
* cutover) — update every subscriber with the returned new `secret` (returned
|
|
401
|
+
* ONCE).
|
|
402
|
+
*/
|
|
403
|
+
rotateSecret(id) {
|
|
404
|
+
return this.http.post(
|
|
405
|
+
`${BASE}/${encodeURIComponent(id)}/rotate-secret`,
|
|
406
|
+
{}
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Enqueue an out-of-band `webhook.test` delivery to the endpoint, delivered
|
|
411
|
+
* regardless of its subscribed `eventTypes`. Returns the 202 enqueue ack.
|
|
412
|
+
*/
|
|
413
|
+
sendTest(id) {
|
|
414
|
+
return this.http.post(
|
|
415
|
+
`${BASE}/${encodeURIComponent(id)}/test`,
|
|
416
|
+
{}
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
|
|
338
421
|
// src/hogsend.ts
|
|
339
422
|
var Hogsend = class {
|
|
340
423
|
contacts;
|
|
@@ -342,6 +425,13 @@ var Hogsend = class {
|
|
|
342
425
|
emails;
|
|
343
426
|
lists;
|
|
344
427
|
campaigns;
|
|
428
|
+
/**
|
|
429
|
+
* Manage outbound webhook endpoints (the signed event stream Hogsend emits to
|
|
430
|
+
* subscriber URLs). REQUIRES a full-admin `apiKey` — this resource hits the
|
|
431
|
+
* admin plane (`/v1/admin/webhooks`), NOT the ingest data plane the other
|
|
432
|
+
* resources use. See {@link WebhooksResource}.
|
|
433
|
+
*/
|
|
434
|
+
webhooks;
|
|
345
435
|
constructor(opts) {
|
|
346
436
|
if (!opts.baseUrl) {
|
|
347
437
|
throw new TypeError("Hogsend: `baseUrl` is required.");
|
|
@@ -361,12 +451,76 @@ var Hogsend = class {
|
|
|
361
451
|
this.emails = new EmailsResource(http);
|
|
362
452
|
this.lists = new ListsResource(http);
|
|
363
453
|
this.campaigns = new CampaignsResource(http);
|
|
454
|
+
this.webhooks = new WebhooksResource(http);
|
|
364
455
|
}
|
|
365
456
|
};
|
|
457
|
+
|
|
458
|
+
// src/internal/verify.ts
|
|
459
|
+
var import_node_crypto = require("crypto");
|
|
460
|
+
var import_svix = require("svix");
|
|
461
|
+
var TOLERANCE_SECONDS = 5 * 60;
|
|
462
|
+
function normalizeHeaders(headers) {
|
|
463
|
+
const out = {};
|
|
464
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
465
|
+
out[key.toLowerCase()] = value;
|
|
466
|
+
}
|
|
467
|
+
return out;
|
|
468
|
+
}
|
|
469
|
+
function verifyHogsendWebhook(opts) {
|
|
470
|
+
const headers = normalizeHeaders(opts.headers);
|
|
471
|
+
const id = headers["webhook-id"] ?? headers["svix-id"];
|
|
472
|
+
const timestamp = headers["webhook-timestamp"] ?? headers["svix-timestamp"];
|
|
473
|
+
const signature = headers["webhook-signature"] ?? headers["svix-signature"];
|
|
474
|
+
if (!id || !timestamp || !signature) {
|
|
475
|
+
throw new Error(
|
|
476
|
+
"verifyHogsendWebhook: missing Webhook-Id / Webhook-Timestamp / Webhook-Signature header"
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
try {
|
|
480
|
+
const wh = new import_svix.Webhook(opts.secret);
|
|
481
|
+
return wh.verify(opts.payload, {
|
|
482
|
+
"svix-id": id,
|
|
483
|
+
"svix-timestamp": timestamp,
|
|
484
|
+
"svix-signature": signature
|
|
485
|
+
});
|
|
486
|
+
} catch {
|
|
487
|
+
return verifyWithNodeCrypto({
|
|
488
|
+
payload: opts.payload,
|
|
489
|
+
secret: opts.secret,
|
|
490
|
+
id,
|
|
491
|
+
timestamp,
|
|
492
|
+
signature
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
function verifyWithNodeCrypto(opts) {
|
|
497
|
+
const ts = Number.parseInt(opts.timestamp, 10);
|
|
498
|
+
if (!Number.isFinite(ts)) {
|
|
499
|
+
throw new Error("verifyHogsendWebhook: invalid Webhook-Timestamp");
|
|
500
|
+
}
|
|
501
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
502
|
+
if (Math.abs(now - ts) > TOLERANCE_SECONDS) {
|
|
503
|
+
throw new Error("verifyHogsendWebhook: timestamp outside tolerance window");
|
|
504
|
+
}
|
|
505
|
+
const key = opts.secret.startsWith("whsec_") ? Buffer.from(opts.secret.slice(6), "base64") : Buffer.from(opts.secret, "base64");
|
|
506
|
+
const signedContent = `${opts.id}.${opts.timestamp}.${opts.payload}`;
|
|
507
|
+
const expected = (0, import_node_crypto.createHmac)("sha256", key).update(signedContent).digest("base64");
|
|
508
|
+
const expectedBuf = Buffer.from(expected);
|
|
509
|
+
const matched = opts.signature.split(" ").some((part) => {
|
|
510
|
+
const sig = part.startsWith("v1,") ? part.slice(3) : part;
|
|
511
|
+
const candidate = Buffer.from(sig);
|
|
512
|
+
return candidate.length === expectedBuf.length && (0, import_node_crypto.timingSafeEqual)(candidate, expectedBuf);
|
|
513
|
+
});
|
|
514
|
+
if (!matched) {
|
|
515
|
+
throw new Error("verifyHogsendWebhook: signature verification failed");
|
|
516
|
+
}
|
|
517
|
+
return JSON.parse(opts.payload);
|
|
518
|
+
}
|
|
366
519
|
// Annotate the CommonJS export names for ESM import in node:
|
|
367
520
|
0 && (module.exports = {
|
|
368
521
|
Hogsend,
|
|
369
522
|
HogsendAPIError,
|
|
370
|
-
RateLimitError
|
|
523
|
+
RateLimitError,
|
|
524
|
+
verifyHogsendWebhook
|
|
371
525
|
});
|
|
372
526
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/errors.ts","../src/internal/http.ts","../src/resources/campaigns.ts","../src/internal/identity.ts","../src/resources/contacts.ts","../src/resources/emails.ts","../src/resources/events.ts","../src/resources/lists.ts","../src/hogsend.ts"],"sourcesContent":["export { HogsendAPIError, RateLimitError } from \"./errors.js\";\nexport { Hogsend } from \"./hogsend.js\";\nexport type {\n Campaign,\n CampaignAudienceKind,\n CampaignStatus,\n Contact,\n DeleteContactInput,\n DeleteContactResult,\n ExitResult,\n FindContactsInput,\n HogsendOptions,\n Identity,\n IngestResult,\n ListSummary,\n SendCampaignInput,\n SendCampaignResult,\n SendEmailInput,\n SendEmailResult,\n SendEventInput,\n SubscribeInput,\n SubscribeResult,\n UnsubscribeResult,\n UpsertContactInput,\n UpsertContactResult,\n} from \"./types.js\";\n","/**\n * A non-2xx response — or a transport-level failure — from the Hogsend data\n * plane. `status` is the HTTP status code, or `0` when the request never\n * reached the server (DNS/connect/timeout). `body` is the parsed JSON body when\n * available, else the raw text, else `undefined`.\n */\nexport class HogsendAPIError extends Error {\n readonly status: number;\n readonly body: unknown;\n\n constructor(message: string, status: number, body: unknown) {\n super(message);\n this.name = \"HogsendAPIError\";\n this.status = status;\n this.body = body;\n // Restore prototype chain for instanceof across transpile targets.\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n/**\n * A `429 Too Many Requests` response. `retryAfter` is the parsed `Retry-After`\n * header in seconds when present (the server sends it on 429 for `/v1/emails`).\n */\nexport class RateLimitError extends HogsendAPIError {\n readonly retryAfter?: number;\n\n constructor(message: string, body: unknown, retryAfter?: number) {\n super(message, 429, body);\n this.name = \"RateLimitError\";\n this.retryAfter = retryAfter;\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n","import { HogsendAPIError, RateLimitError } from \"../errors.js\";\n\n/** Query params accepted by `get` — undefined values are dropped. */\nexport type Query = Record<string, string | number | undefined>;\n\n/** Per-request extras (currently just an idempotency header passthrough). */\nexport interface RequestExtras {\n /** Sent as the `Idempotency-Key` header when set. */\n idempotencyKey?: string;\n}\n\nexport interface HttpClientConfig {\n baseUrl: string;\n apiKey: string;\n fetch?: typeof fetch;\n timeoutMs?: number;\n headers?: Record<string, string>;\n}\n\n/** A minimal, self-contained data-plane HTTP client over native fetch. */\nexport interface HttpClient {\n get<T = unknown>(path: string, query?: Query): Promise<T>;\n post<T = unknown>(\n path: string,\n body: unknown,\n extras?: RequestExtras,\n ): Promise<T>;\n put<T = unknown>(\n path: string,\n body: unknown,\n extras?: RequestExtras,\n ): Promise<T>;\n del<T = unknown>(path: string, body?: unknown): Promise<T>;\n}\n\nconst DEFAULT_TIMEOUT_MS = 30_000;\n\nfunction buildUrl(baseUrl: string, path: string, query?: Query): string {\n const url = new URL(path.startsWith(\"/\") ? path : `/${path}`, `${baseUrl}/`);\n if (query) {\n for (const [key, value] of Object.entries(query)) {\n if (value === undefined) continue;\n url.searchParams.set(key, String(value));\n }\n }\n return url.toString();\n}\n\nfunction bodyMessage(status: number, body: unknown): string {\n if (body && typeof body === \"object\") {\n // Application-handler envelope: `{ error: \"human message\" }`.\n const errField = (body as { error?: unknown }).error;\n if (typeof errField === \"string\") {\n return `${status}: ${errField}`;\n }\n // @hono/zod-openapi default-hook validation envelope:\n // `{ success: false, error: <ZodError> }` (no defaultHook configured). The\n // structured ZodError is preserved on `err.body`; surface a short, readable\n // summary for `err.message` instead of the generic fallback.\n if (\n (body as { success?: unknown }).success === false &&\n errField &&\n typeof errField === \"object\"\n ) {\n const summary = JSON.stringify(errField).slice(0, 200);\n return `${status}: validation failed ${summary}`;\n }\n }\n return `request failed with status ${status}`;\n}\n\n/** Parse a `Retry-After` header (seconds form) into a number, else undefined. */\nfunction parseRetryAfter(value: string | null): number | undefined {\n if (!value) return undefined;\n const seconds = Number.parseInt(value, 10);\n return Number.isFinite(seconds) ? seconds : undefined;\n}\n\n/**\n * Builds an {@link HttpClient} bound to a config. Native `fetch`, JSON in/out,\n * `Authorization: Bearer <apiKey>`. Throws typed errors:\n * - {@link RateLimitError} on 429 (with parsed `Retry-After`),\n * - {@link HogsendAPIError} on any other non-2xx,\n * - {@link HogsendAPIError} with `status === 0` on a transport failure\n * (DNS/connect/abort/timeout).\n */\nexport function createHttpClient(config: HttpClientConfig): HttpClient {\n const doFetch = config.fetch ?? fetch;\n const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const extraHeaders = config.headers ?? {};\n\n async function request<T>(\n method: string,\n path: string,\n opts: { query?: Query; body?: unknown; extras?: RequestExtras },\n ): Promise<T> {\n const headers: Record<string, string> = {\n Accept: \"application/json\",\n Authorization: `Bearer ${config.apiKey}`,\n ...extraHeaders,\n };\n if (opts.body !== undefined) {\n headers[\"Content-Type\"] = \"application/json\";\n }\n if (opts.extras?.idempotencyKey) {\n headers[\"Idempotency-Key\"] = opts.extras.idempotencyKey;\n }\n\n const url = buildUrl(config.baseUrl, path, opts.query);\n\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n\n let res: Response;\n try {\n res = await doFetch(url, {\n method,\n headers,\n body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined,\n signal: controller.signal,\n });\n } catch (cause) {\n const msg = cause instanceof Error ? cause.message : String(cause);\n throw new HogsendAPIError(\n `cannot reach ${config.baseUrl} (${msg})`,\n 0,\n undefined,\n );\n } finally {\n clearTimeout(timer);\n }\n\n const text = await res.text();\n let parsed: unknown;\n if (text.length > 0) {\n try {\n parsed = JSON.parse(text);\n } catch {\n parsed = text;\n }\n }\n\n if (!res.ok) {\n if (res.status === 429) {\n throw new RateLimitError(\n bodyMessage(res.status, parsed),\n parsed,\n parseRetryAfter(res.headers.get(\"Retry-After\")),\n );\n }\n throw new HogsendAPIError(\n bodyMessage(res.status, parsed),\n res.status,\n parsed,\n );\n }\n\n return parsed as T;\n }\n\n return {\n get: <T>(path: string, query?: Query) => request<T>(\"GET\", path, { query }),\n post: <T>(path: string, body: unknown, extras?: RequestExtras) =>\n request<T>(\"POST\", path, { body, extras }),\n put: <T>(path: string, body: unknown, extras?: RequestExtras) =>\n request<T>(\"PUT\", path, { body, extras }),\n del: <T>(path: string, body?: unknown) =>\n request<T>(\"DELETE\", path, { body }),\n };\n}\n","import type { HttpClient } from \"../internal/http.js\";\nimport type {\n Campaign,\n SendCampaignInput,\n SendCampaignResult,\n} from \"../types.js\";\n\n/** The `campaigns.*` resource bound to an {@link HttpClient}. */\nexport class CampaignsResource {\n constructor(private readonly http: HttpClient) {}\n\n /**\n * Queue a broadcast: durably send one template to every subscribed member of\n * a `list` (or every active member of a `bucket`). Exactly one of `list` /\n * `bucket` must be set; `template`/`props` are type-checked against the\n * augmented `TemplateRegistryMap` when `@hogsend/email` is installed, else\n * degrade to `{ template: string; props? }`.\n *\n * Returns the 202 enqueue ack (`{ campaignId, status }`); the actual sends run\n * asynchronously in the worker. Poll {@link CampaignsResource.get} for counts.\n */\n send(input: SendCampaignInput): Promise<SendCampaignResult> {\n // The discriminated union narrows `template`/`props` and the audience; index\n // into the input via a permissive view to build the wire body without\n // re-discriminating.\n const body = input as SendCampaignInput & {\n list?: string;\n bucket?: string;\n props?: Record<string, unknown>;\n };\n return this.http.post<SendCampaignResult>(\"/v1/campaigns\", {\n name: body.name,\n list: body.list,\n bucket: body.bucket,\n template: body.template,\n props: body.props,\n from: body.from,\n subject: body.subject,\n });\n }\n\n /** Fetch a campaign's current status + send counts. */\n get(id: string): Promise<Campaign> {\n return this.http.get<Campaign>(`/v1/campaigns/${encodeURIComponent(id)}`);\n }\n}\n","/**\n * Runtime guard mirroring the `Identity` union: at least one of `email` /\n * `userId` must be a non-empty string. The type system enforces this at the\n * call site, but runtime callers (plain JS, untyped data) can still violate it,\n * so we fail fast with a clear message before issuing the request.\n */\nexport function assertIdentity(input: {\n email?: string;\n userId?: string;\n}): void {\n const hasEmail = typeof input.email === \"string\" && input.email.length > 0;\n const hasUserId = typeof input.userId === \"string\" && input.userId.length > 0;\n if (!hasEmail && !hasUserId) {\n throw new TypeError(\n \"Hogsend: an identity is required — pass `email`, `userId`, or both.\",\n );\n }\n}\n","import type { HttpClient } from \"../internal/http.js\";\nimport { assertIdentity } from \"../internal/identity.js\";\nimport type {\n Contact,\n DeleteContactInput,\n DeleteContactResult,\n FindContactsInput,\n UpsertContactInput,\n UpsertContactResult,\n} from \"../types.js\";\n\n/** The `contacts.*` resource bound to an {@link HttpClient}. */\nexport class ContactsResource {\n constructor(private readonly http: HttpClient) {}\n\n /**\n * Upsert a contact by identity. Resolves/merges server-side and optionally\n * applies list membership. Returns `{ id, created, linked }`.\n */\n async upsert(input: UpsertContactInput): Promise<UpsertContactResult> {\n assertIdentity(input);\n return this.http.put<UpsertContactResult>(\"/v1/contacts\", {\n email: input.email,\n userId: input.userId,\n properties: input.properties,\n lists: input.lists,\n });\n }\n\n /** Find non-deleted contacts by `email` or `userId`. */\n async find(input: FindContactsInput): Promise<Contact[]> {\n const query: Record<string, string | undefined> = {\n email: \"email\" in input ? input.email : undefined,\n userId: \"userId\" in input ? input.userId : undefined,\n };\n const res = await this.http.get<{ contacts: Contact[] }>(\n \"/v1/contacts/find\",\n query,\n );\n return res.contacts;\n }\n\n /** Soft-delete a contact by identity. */\n async delete(input: DeleteContactInput): Promise<DeleteContactResult> {\n assertIdentity(input);\n return this.http.del<DeleteContactResult>(\"/v1/contacts\", {\n email: input.email,\n userId: input.userId,\n });\n }\n}\n","import type { HttpClient } from \"../internal/http.js\";\nimport type { SendEmailInput, SendEmailResult } from \"../types.js\";\n\n/** The `emails.*` resource bound to an {@link HttpClient}. */\nexport class EmailsResource {\n constructor(private readonly http: HttpClient) {}\n\n /**\n * Send a transactional email by template. Recipient is `to` (raw address) or\n * `userId` (resolved server-side). `template`/`props` are type-checked against\n * the augmented `TemplateRegistryMap` when `@hogsend/email` is installed,\n * else degrade to `{ template: string; props? }`.\n */\n send(input: SendEmailInput): Promise<SendEmailResult> {\n // The discriminated union narrows `template`/`props`; index into the input\n // via a permissive view to build the wire body without re-discriminating.\n const body = input as SendEmailInput & {\n props?: Record<string, unknown>;\n };\n return this.http.post<SendEmailResult>(\n \"/v1/emails\",\n {\n to: body.to,\n userId: body.userId,\n template: body.template,\n props: body.props,\n from: body.from,\n subject: body.subject,\n replyTo: body.replyTo,\n category: body.category,\n skipPreferenceCheck: body.skipPreferenceCheck,\n idempotencyKey: body.idempotencyKey,\n },\n body.idempotencyKey ? { idempotencyKey: body.idempotencyKey } : undefined,\n );\n }\n}\n","import type { HttpClient } from \"../internal/http.js\";\nimport { assertIdentity } from \"../internal/identity.js\";\nimport type { IngestResult, SendEventInput } from \"../types.js\";\n\n/** The `events.*` resource bound to an {@link HttpClient}. */\nexport class EventsResource {\n constructor(private readonly http: HttpClient) {}\n\n /**\n * Send an event through the ingestion pipeline. The two property bags are\n * kept distinct: `eventProperties` feed `trigger.where`/`exitOn`,\n * `contactProperties` merge onto the contact. Optionally apply list\n * membership. Returns the ingest result (stored + exit evaluations).\n *\n * `idempotencyKey` is sent both as the `Idempotency-Key` header (which wins\n * server-side) and in the body, matching `POST /v1/events`.\n */\n send(input: SendEventInput): Promise<IngestResult> {\n assertIdentity(input);\n return this.http.post<IngestResult>(\n \"/v1/events\",\n {\n name: input.name,\n email: input.email,\n userId: input.userId,\n eventProperties: input.eventProperties,\n contactProperties: input.contactProperties,\n lists: input.lists,\n idempotencyKey: input.idempotencyKey,\n },\n input.idempotencyKey\n ? { idempotencyKey: input.idempotencyKey }\n : undefined,\n );\n }\n\n /** Alias of {@link EventsResource.send}. */\n track(input: SendEventInput): Promise<IngestResult> {\n return this.send(input);\n }\n}\n","import type { HttpClient } from \"../internal/http.js\";\nimport { assertIdentity } from \"../internal/identity.js\";\nimport type {\n ListSummary,\n SubscribeInput,\n SubscribeResult,\n UnsubscribeResult,\n} from \"../types.js\";\n\n/** The `lists.*` resource bound to an {@link HttpClient}. */\nexport class ListsResource {\n constructor(private readonly http: HttpClient) {}\n\n /** List all code-defined lists. */\n async list(): Promise<ListSummary[]> {\n const res = await this.http.get<{ lists: ListSummary[] }>(\"/v1/lists\");\n return res.lists;\n }\n\n /** Subscribe an identity to a list. */\n async subscribe(input: SubscribeInput): Promise<SubscribeResult> {\n assertIdentity(input);\n const res = await this.http.post<{ list: string; subscribed: boolean }>(\n `/v1/lists/${encodeURIComponent(input.list)}/subscribe`,\n { email: input.email, userId: input.userId },\n );\n return { subscribed: res.subscribed };\n }\n\n /** Unsubscribe an identity from a list. */\n async unsubscribe(input: SubscribeInput): Promise<UnsubscribeResult> {\n assertIdentity(input);\n const res = await this.http.post<{ list: string; subscribed: boolean }>(\n `/v1/lists/${encodeURIComponent(input.list)}/unsubscribe`,\n { email: input.email, userId: input.userId },\n );\n return { unsubscribed: res.subscribed === false };\n }\n}\n","import { createHttpClient } from \"./internal/http.js\";\nimport { CampaignsResource } from \"./resources/campaigns.js\";\nimport { ContactsResource } from \"./resources/contacts.js\";\nimport { EmailsResource } from \"./resources/emails.js\";\nimport { EventsResource } from \"./resources/events.js\";\nimport { ListsResource } from \"./resources/lists.js\";\nimport type { HogsendOptions } from \"./types.js\";\n\n/**\n * Typed HTTP client for the Hogsend data plane.\n *\n * ```ts\n * const hs = new Hogsend({ baseUrl: \"https://api.example.com\", apiKey: \"hsk_…\" });\n * await hs.contacts.upsert({ email: \"a@b.com\", properties: { plan: \"pro\" } });\n * await hs.events.send({ userId: \"u_1\", name: \"signup\" });\n * ```\n */\nexport class Hogsend {\n readonly contacts: ContactsResource;\n readonly events: EventsResource;\n readonly emails: EmailsResource;\n readonly lists: ListsResource;\n readonly campaigns: CampaignsResource;\n\n constructor(opts: HogsendOptions) {\n if (!opts.baseUrl) {\n throw new TypeError(\"Hogsend: `baseUrl` is required.\");\n }\n if (!opts.apiKey) {\n throw new TypeError(\"Hogsend: `apiKey` is required.\");\n }\n\n const http = createHttpClient({\n baseUrl: opts.baseUrl.replace(/\\/+$/, \"\"),\n apiKey: opts.apiKey,\n fetch: opts.fetch,\n timeoutMs: opts.timeoutMs,\n headers: opts.headers,\n });\n\n this.contacts = new ContactsResource(http);\n this.events = new EventsResource(http);\n this.emails = new EmailsResource(http);\n this.lists = new ListsResource(http);\n this.campaigns = new CampaignsResource(http);\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/errors.ts","../src/internal/http.ts","../src/resources/campaigns.ts","../src/internal/identity.ts","../src/resources/contacts.ts","../src/resources/emails.ts","../src/resources/events.ts","../src/resources/lists.ts","../src/resources/webhooks.ts","../src/hogsend.ts","../src/internal/verify.ts"],"sourcesContent":["export { HogsendAPIError, RateLimitError } from \"./errors.js\";\nexport { Hogsend } from \"./hogsend.js\";\nexport { verifyHogsendWebhook } from \"./internal/verify.js\";\nexport type {\n Campaign,\n CampaignAudienceKind,\n CampaignStatus,\n Contact,\n CreatedWebhookEndpoint,\n CreateWebhookInput,\n DeleteContactInput,\n DeleteContactResult,\n ExitResult,\n FindContactsInput,\n HogsendOptions,\n Identity,\n IngestResult,\n ListSummary,\n OutboundEventType,\n RotateWebhookSecretResult,\n SendCampaignInput,\n SendCampaignResult,\n SendEmailInput,\n SendEmailResult,\n SendEventInput,\n SubscribeInput,\n SubscribeResult,\n UnsubscribeResult,\n UpdateWebhookInput,\n UpsertContactInput,\n UpsertContactResult,\n WebhookEndpoint,\n} from \"./types.js\";\n","/**\n * A non-2xx response — or a transport-level failure — from the Hogsend data\n * plane. `status` is the HTTP status code, or `0` when the request never\n * reached the server (DNS/connect/timeout). `body` is the parsed JSON body when\n * available, else the raw text, else `undefined`.\n */\nexport class HogsendAPIError extends Error {\n readonly status: number;\n readonly body: unknown;\n\n constructor(message: string, status: number, body: unknown) {\n super(message);\n this.name = \"HogsendAPIError\";\n this.status = status;\n this.body = body;\n // Restore prototype chain for instanceof across transpile targets.\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n/**\n * A `429 Too Many Requests` response. `retryAfter` is the parsed `Retry-After`\n * header in seconds when present (the server sends it on 429 for `/v1/emails`).\n */\nexport class RateLimitError extends HogsendAPIError {\n readonly retryAfter?: number;\n\n constructor(message: string, body: unknown, retryAfter?: number) {\n super(message, 429, body);\n this.name = \"RateLimitError\";\n this.retryAfter = retryAfter;\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n","import { HogsendAPIError, RateLimitError } from \"../errors.js\";\n\n/** Query params accepted by `get` — undefined values are dropped. */\nexport type Query = Record<string, string | number | undefined>;\n\n/** Per-request extras (currently just an idempotency header passthrough). */\nexport interface RequestExtras {\n /** Sent as the `Idempotency-Key` header when set. */\n idempotencyKey?: string;\n}\n\nexport interface HttpClientConfig {\n baseUrl: string;\n apiKey: string;\n fetch?: typeof fetch;\n timeoutMs?: number;\n headers?: Record<string, string>;\n}\n\n/** A minimal, self-contained data-plane HTTP client over native fetch. */\nexport interface HttpClient {\n get<T = unknown>(path: string, query?: Query): Promise<T>;\n post<T = unknown>(\n path: string,\n body: unknown,\n extras?: RequestExtras,\n ): Promise<T>;\n put<T = unknown>(\n path: string,\n body: unknown,\n extras?: RequestExtras,\n ): Promise<T>;\n patch<T = unknown>(\n path: string,\n body: unknown,\n extras?: RequestExtras,\n ): Promise<T>;\n del<T = unknown>(path: string, body?: unknown): Promise<T>;\n}\n\nconst DEFAULT_TIMEOUT_MS = 30_000;\n\nfunction buildUrl(baseUrl: string, path: string, query?: Query): string {\n const url = new URL(path.startsWith(\"/\") ? path : `/${path}`, `${baseUrl}/`);\n if (query) {\n for (const [key, value] of Object.entries(query)) {\n if (value === undefined) continue;\n url.searchParams.set(key, String(value));\n }\n }\n return url.toString();\n}\n\nfunction bodyMessage(status: number, body: unknown): string {\n if (body && typeof body === \"object\") {\n // Application-handler envelope: `{ error: \"human message\" }`.\n const errField = (body as { error?: unknown }).error;\n if (typeof errField === \"string\") {\n return `${status}: ${errField}`;\n }\n // @hono/zod-openapi default-hook validation envelope:\n // `{ success: false, error: <ZodError> }` (no defaultHook configured). The\n // structured ZodError is preserved on `err.body`; surface a short, readable\n // summary for `err.message` instead of the generic fallback.\n if (\n (body as { success?: unknown }).success === false &&\n errField &&\n typeof errField === \"object\"\n ) {\n const summary = JSON.stringify(errField).slice(0, 200);\n return `${status}: validation failed ${summary}`;\n }\n }\n return `request failed with status ${status}`;\n}\n\n/** Parse a `Retry-After` header (seconds form) into a number, else undefined. */\nfunction parseRetryAfter(value: string | null): number | undefined {\n if (!value) return undefined;\n const seconds = Number.parseInt(value, 10);\n return Number.isFinite(seconds) ? seconds : undefined;\n}\n\n/**\n * Builds an {@link HttpClient} bound to a config. Native `fetch`, JSON in/out,\n * `Authorization: Bearer <apiKey>`. Throws typed errors:\n * - {@link RateLimitError} on 429 (with parsed `Retry-After`),\n * - {@link HogsendAPIError} on any other non-2xx,\n * - {@link HogsendAPIError} with `status === 0` on a transport failure\n * (DNS/connect/abort/timeout).\n */\nexport function createHttpClient(config: HttpClientConfig): HttpClient {\n const doFetch = config.fetch ?? fetch;\n const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const extraHeaders = config.headers ?? {};\n\n async function request<T>(\n method: string,\n path: string,\n opts: { query?: Query; body?: unknown; extras?: RequestExtras },\n ): Promise<T> {\n const headers: Record<string, string> = {\n Accept: \"application/json\",\n Authorization: `Bearer ${config.apiKey}`,\n ...extraHeaders,\n };\n if (opts.body !== undefined) {\n headers[\"Content-Type\"] = \"application/json\";\n }\n if (opts.extras?.idempotencyKey) {\n headers[\"Idempotency-Key\"] = opts.extras.idempotencyKey;\n }\n\n const url = buildUrl(config.baseUrl, path, opts.query);\n\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n\n let res: Response;\n try {\n res = await doFetch(url, {\n method,\n headers,\n body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined,\n signal: controller.signal,\n });\n } catch (cause) {\n const msg = cause instanceof Error ? cause.message : String(cause);\n throw new HogsendAPIError(\n `cannot reach ${config.baseUrl} (${msg})`,\n 0,\n undefined,\n );\n } finally {\n clearTimeout(timer);\n }\n\n const text = await res.text();\n let parsed: unknown;\n if (text.length > 0) {\n try {\n parsed = JSON.parse(text);\n } catch {\n parsed = text;\n }\n }\n\n if (!res.ok) {\n if (res.status === 429) {\n throw new RateLimitError(\n bodyMessage(res.status, parsed),\n parsed,\n parseRetryAfter(res.headers.get(\"Retry-After\")),\n );\n }\n throw new HogsendAPIError(\n bodyMessage(res.status, parsed),\n res.status,\n parsed,\n );\n }\n\n return parsed as T;\n }\n\n return {\n get: <T>(path: string, query?: Query) => request<T>(\"GET\", path, { query }),\n post: <T>(path: string, body: unknown, extras?: RequestExtras) =>\n request<T>(\"POST\", path, { body, extras }),\n put: <T>(path: string, body: unknown, extras?: RequestExtras) =>\n request<T>(\"PUT\", path, { body, extras }),\n patch: <T>(path: string, body: unknown, extras?: RequestExtras) =>\n request<T>(\"PATCH\", path, { body, extras }),\n del: <T>(path: string, body?: unknown) =>\n request<T>(\"DELETE\", path, { body }),\n };\n}\n","import type { HttpClient } from \"../internal/http.js\";\nimport type {\n Campaign,\n SendCampaignInput,\n SendCampaignResult,\n} from \"../types.js\";\n\n/** The `campaigns.*` resource bound to an {@link HttpClient}. */\nexport class CampaignsResource {\n constructor(private readonly http: HttpClient) {}\n\n /**\n * Queue a broadcast: durably send one template to every subscribed member of\n * a `list` (or every active member of a `bucket`). Exactly one of `list` /\n * `bucket` must be set; `template`/`props` are type-checked against the\n * augmented `TemplateRegistryMap` when `@hogsend/email` is installed, else\n * degrade to `{ template: string; props? }`.\n *\n * Returns the 202 enqueue ack (`{ campaignId, status }`); the actual sends run\n * asynchronously in the worker. Poll {@link CampaignsResource.get} for counts.\n */\n send(input: SendCampaignInput): Promise<SendCampaignResult> {\n // The discriminated union narrows `template`/`props` and the audience; index\n // into the input via a permissive view to build the wire body without\n // re-discriminating.\n const body = input as SendCampaignInput & {\n list?: string;\n bucket?: string;\n props?: Record<string, unknown>;\n };\n return this.http.post<SendCampaignResult>(\"/v1/campaigns\", {\n name: body.name,\n list: body.list,\n bucket: body.bucket,\n template: body.template,\n props: body.props,\n from: body.from,\n subject: body.subject,\n });\n }\n\n /** Fetch a campaign's current status + send counts. */\n get(id: string): Promise<Campaign> {\n return this.http.get<Campaign>(`/v1/campaigns/${encodeURIComponent(id)}`);\n }\n}\n","/**\n * Runtime guard mirroring the `Identity` union: at least one of `email` /\n * `userId` must be a non-empty string. The type system enforces this at the\n * call site, but runtime callers (plain JS, untyped data) can still violate it,\n * so we fail fast with a clear message before issuing the request.\n */\nexport function assertIdentity(input: {\n email?: string;\n userId?: string;\n}): void {\n const hasEmail = typeof input.email === \"string\" && input.email.length > 0;\n const hasUserId = typeof input.userId === \"string\" && input.userId.length > 0;\n if (!hasEmail && !hasUserId) {\n throw new TypeError(\n \"Hogsend: an identity is required — pass `email`, `userId`, or both.\",\n );\n }\n}\n","import type { HttpClient } from \"../internal/http.js\";\nimport { assertIdentity } from \"../internal/identity.js\";\nimport type {\n Contact,\n DeleteContactInput,\n DeleteContactResult,\n FindContactsInput,\n UpsertContactInput,\n UpsertContactResult,\n} from \"../types.js\";\n\n/** The `contacts.*` resource bound to an {@link HttpClient}. */\nexport class ContactsResource {\n constructor(private readonly http: HttpClient) {}\n\n /**\n * Upsert a contact by identity. Resolves/merges server-side and optionally\n * applies list membership. Returns `{ id, created, linked }`.\n */\n async upsert(input: UpsertContactInput): Promise<UpsertContactResult> {\n assertIdentity(input);\n return this.http.put<UpsertContactResult>(\"/v1/contacts\", {\n email: input.email,\n userId: input.userId,\n properties: input.properties,\n lists: input.lists,\n });\n }\n\n /** Find non-deleted contacts by `email` or `userId`. */\n async find(input: FindContactsInput): Promise<Contact[]> {\n const query: Record<string, string | undefined> = {\n email: \"email\" in input ? input.email : undefined,\n userId: \"userId\" in input ? input.userId : undefined,\n };\n const res = await this.http.get<{ contacts: Contact[] }>(\n \"/v1/contacts/find\",\n query,\n );\n return res.contacts;\n }\n\n /** Soft-delete a contact by identity. */\n async delete(input: DeleteContactInput): Promise<DeleteContactResult> {\n assertIdentity(input);\n return this.http.del<DeleteContactResult>(\"/v1/contacts\", {\n email: input.email,\n userId: input.userId,\n });\n }\n}\n","import type { HttpClient } from \"../internal/http.js\";\nimport type { SendEmailInput, SendEmailResult } from \"../types.js\";\n\n/** The `emails.*` resource bound to an {@link HttpClient}. */\nexport class EmailsResource {\n constructor(private readonly http: HttpClient) {}\n\n /**\n * Send a transactional email by template. Recipient is `to` (raw address) or\n * `userId` (resolved server-side). `template`/`props` are type-checked against\n * the augmented `TemplateRegistryMap` when `@hogsend/email` is installed,\n * else degrade to `{ template: string; props? }`.\n */\n send(input: SendEmailInput): Promise<SendEmailResult> {\n // The discriminated union narrows `template`/`props`; index into the input\n // via a permissive view to build the wire body without re-discriminating.\n const body = input as SendEmailInput & {\n props?: Record<string, unknown>;\n };\n return this.http.post<SendEmailResult>(\n \"/v1/emails\",\n {\n to: body.to,\n userId: body.userId,\n template: body.template,\n props: body.props,\n from: body.from,\n subject: body.subject,\n replyTo: body.replyTo,\n category: body.category,\n skipPreferenceCheck: body.skipPreferenceCheck,\n idempotencyKey: body.idempotencyKey,\n },\n body.idempotencyKey ? { idempotencyKey: body.idempotencyKey } : undefined,\n );\n }\n}\n","import type { HttpClient } from \"../internal/http.js\";\nimport { assertIdentity } from \"../internal/identity.js\";\nimport type { IngestResult, SendEventInput } from \"../types.js\";\n\n/** The `events.*` resource bound to an {@link HttpClient}. */\nexport class EventsResource {\n constructor(private readonly http: HttpClient) {}\n\n /**\n * Send an event through the ingestion pipeline. The two property bags are\n * kept distinct: `eventProperties` feed `trigger.where`/`exitOn`,\n * `contactProperties` merge onto the contact. Optionally apply list\n * membership. Returns the ingest result (stored + exit evaluations).\n *\n * `idempotencyKey` is sent both as the `Idempotency-Key` header (which wins\n * server-side) and in the body, matching `POST /v1/events`.\n */\n send(input: SendEventInput): Promise<IngestResult> {\n assertIdentity(input);\n return this.http.post<IngestResult>(\n \"/v1/events\",\n {\n name: input.name,\n email: input.email,\n userId: input.userId,\n eventProperties: input.eventProperties,\n contactProperties: input.contactProperties,\n lists: input.lists,\n idempotencyKey: input.idempotencyKey,\n },\n input.idempotencyKey\n ? { idempotencyKey: input.idempotencyKey }\n : undefined,\n );\n }\n\n /** Alias of {@link EventsResource.send}. */\n track(input: SendEventInput): Promise<IngestResult> {\n return this.send(input);\n }\n}\n","import type { HttpClient } from \"../internal/http.js\";\nimport { assertIdentity } from \"../internal/identity.js\";\nimport type {\n ListSummary,\n SubscribeInput,\n SubscribeResult,\n UnsubscribeResult,\n} from \"../types.js\";\n\n/** The `lists.*` resource bound to an {@link HttpClient}. */\nexport class ListsResource {\n constructor(private readonly http: HttpClient) {}\n\n /** List all code-defined lists. */\n async list(): Promise<ListSummary[]> {\n const res = await this.http.get<{ lists: ListSummary[] }>(\"/v1/lists\");\n return res.lists;\n }\n\n /** Subscribe an identity to a list. */\n async subscribe(input: SubscribeInput): Promise<SubscribeResult> {\n assertIdentity(input);\n const res = await this.http.post<{ list: string; subscribed: boolean }>(\n `/v1/lists/${encodeURIComponent(input.list)}/subscribe`,\n { email: input.email, userId: input.userId },\n );\n return { subscribed: res.subscribed };\n }\n\n /** Unsubscribe an identity from a list. */\n async unsubscribe(input: SubscribeInput): Promise<UnsubscribeResult> {\n assertIdentity(input);\n const res = await this.http.post<{ list: string; subscribed: boolean }>(\n `/v1/lists/${encodeURIComponent(input.list)}/unsubscribe`,\n { email: input.email, userId: input.userId },\n );\n return { unsubscribed: res.subscribed === false };\n }\n}\n","import type { HttpClient } from \"../internal/http.js\";\nimport type {\n CreatedWebhookEndpoint,\n CreateWebhookInput,\n RotateWebhookSecretResult,\n UpdateWebhookInput,\n WebhookEndpoint,\n} from \"../types.js\";\n\nconst BASE = \"/v1/admin/webhooks\";\n\n/**\n * The `webhooks.*` resource — manage outbound webhook endpoints (the\n * Svix-style signed event stream Hogsend emits to subscriber URLs).\n *\n * IMPORTANT: unlike the rest of the client (which uses an `ingest`-scoped data\n * key), this resource targets the ADMIN plane (`/v1/admin/webhooks`) and\n * REQUIRES a full-admin key. Signing-secret management is the same trust class\n * as API-key management — a leaked ingest key must never register an\n * exfiltration endpoint. Construct the client with an admin `apiKey`.\n *\n * The full signing `secret` (`whsec_…`) is returned ONCE — on\n * {@link WebhooksResource.create} and {@link WebhooksResource.rotateSecret}.\n * `list`/`get` only ever expose the display `secretPrefix`. Store it on create.\n */\nexport class WebhooksResource {\n constructor(private readonly http: HttpClient) {}\n\n /**\n * Register a new endpoint subscribed to one or more outbound event types.\n * Returns the endpoint INCLUDING the full signing `secret` — this is the\n * only time (besides rotate) the secret is returned. Store it now.\n */\n create(input: CreateWebhookInput): Promise<CreatedWebhookEndpoint> {\n return this.http.post<CreatedWebhookEndpoint>(BASE, {\n url: input.url,\n eventTypes: input.eventTypes,\n description: input.description,\n disabled: input.disabled,\n });\n }\n\n /**\n * List endpoints (newest first). Disabled endpoints are hidden unless\n * `includeDisabled` is set. Returns the endpoints array (unwrapped from the\n * `{ endpoints, total, limit, offset }` envelope).\n */\n async list(opts?: {\n limit?: number;\n offset?: number;\n includeDisabled?: boolean;\n }): Promise<WebhookEndpoint[]> {\n const res = await this.http.get<{ endpoints: WebhookEndpoint[] }>(BASE, {\n limit: opts?.limit,\n offset: opts?.offset,\n includeDisabled:\n opts?.includeDisabled === undefined\n ? undefined\n : String(opts.includeDisabled),\n });\n return res.endpoints;\n }\n\n /** Fetch one endpoint by id (404 → {@link HogsendAPIError}). */\n get(id: string): Promise<WebhookEndpoint> {\n return this.http.get<WebhookEndpoint>(`${BASE}/${encodeURIComponent(id)}`);\n }\n\n /**\n * Patch an endpoint. Only the provided fields change; `description: null`\n * clears the description. Does NOT return or rotate the secret.\n */\n update(id: string, input: UpdateWebhookInput): Promise<WebhookEndpoint> {\n return this.http.patch<WebhookEndpoint>(\n `${BASE}/${encodeURIComponent(id)}`,\n {\n url: input.url,\n eventTypes: input.eventTypes,\n description: input.description,\n disabled: input.disabled,\n },\n );\n }\n\n /** Hard-delete an endpoint (cascade drops its deliveries). */\n delete(id: string): Promise<{ deleted: boolean }> {\n return this.http.del<{ deleted: boolean }>(\n `${BASE}/${encodeURIComponent(id)}`,\n );\n }\n\n /**\n * Rotate the signing secret. The OLD secret is invalidated immediately (hard\n * cutover) — update every subscriber with the returned new `secret` (returned\n * ONCE).\n */\n rotateSecret(id: string): Promise<RotateWebhookSecretResult> {\n return this.http.post<RotateWebhookSecretResult>(\n `${BASE}/${encodeURIComponent(id)}/rotate-secret`,\n {},\n );\n }\n\n /**\n * Enqueue an out-of-band `webhook.test` delivery to the endpoint, delivered\n * regardless of its subscribed `eventTypes`. Returns the 202 enqueue ack.\n */\n sendTest(\n id: string,\n ): Promise<{ enqueued: boolean; eventType: \"webhook.test\" }> {\n return this.http.post<{ enqueued: boolean; eventType: \"webhook.test\" }>(\n `${BASE}/${encodeURIComponent(id)}/test`,\n {},\n );\n }\n}\n","import { createHttpClient } from \"./internal/http.js\";\nimport { CampaignsResource } from \"./resources/campaigns.js\";\nimport { ContactsResource } from \"./resources/contacts.js\";\nimport { EmailsResource } from \"./resources/emails.js\";\nimport { EventsResource } from \"./resources/events.js\";\nimport { ListsResource } from \"./resources/lists.js\";\nimport { WebhooksResource } from \"./resources/webhooks.js\";\nimport type { HogsendOptions } from \"./types.js\";\n\n/**\n * Typed HTTP client for the Hogsend data plane.\n *\n * ```ts\n * const hs = new Hogsend({ baseUrl: \"https://api.example.com\", apiKey: \"hsk_…\" });\n * await hs.contacts.upsert({ email: \"a@b.com\", properties: { plan: \"pro\" } });\n * await hs.events.send({ userId: \"u_1\", name: \"signup\" });\n * ```\n */\nexport class Hogsend {\n readonly contacts: ContactsResource;\n readonly events: EventsResource;\n readonly emails: EmailsResource;\n readonly lists: ListsResource;\n readonly campaigns: CampaignsResource;\n /**\n * Manage outbound webhook endpoints (the signed event stream Hogsend emits to\n * subscriber URLs). REQUIRES a full-admin `apiKey` — this resource hits the\n * admin plane (`/v1/admin/webhooks`), NOT the ingest data plane the other\n * resources use. See {@link WebhooksResource}.\n */\n readonly webhooks: WebhooksResource;\n\n constructor(opts: HogsendOptions) {\n if (!opts.baseUrl) {\n throw new TypeError(\"Hogsend: `baseUrl` is required.\");\n }\n if (!opts.apiKey) {\n throw new TypeError(\"Hogsend: `apiKey` is required.\");\n }\n\n const http = createHttpClient({\n baseUrl: opts.baseUrl.replace(/\\/+$/, \"\"),\n apiKey: opts.apiKey,\n fetch: opts.fetch,\n timeoutMs: opts.timeoutMs,\n headers: opts.headers,\n });\n\n this.contacts = new ContactsResource(http);\n this.events = new EventsResource(http);\n this.emails = new EmailsResource(http);\n this.lists = new ListsResource(http);\n this.campaigns = new CampaignsResource(http);\n this.webhooks = new WebhooksResource(http);\n }\n}\n","import { createHmac, timingSafeEqual } from \"node:crypto\";\nimport { Webhook } from \"svix\";\n\n/** Default tolerance (seconds) for the timestamp freshness check. */\nconst TOLERANCE_SECONDS = 5 * 60;\n\n/**\n * Lowercase every header key so callers can pass the Title-Case headers Hogsend\n * sends (`Webhook-Id`/`Webhook-Timestamp`/`Webhook-Signature`) OR the lowercase\n * form a framework may hand back. Svix expects lowercase `svix-*`/`webhook-*`.\n */\nfunction normalizeHeaders(\n headers: Record<string, string>,\n): Record<string, string> {\n const out: Record<string, string> = {};\n for (const [key, value] of Object.entries(headers)) {\n out[key.toLowerCase()] = value;\n }\n return out;\n}\n\n/**\n * Verify and parse an INBOUND Hogsend outbound-webhook delivery, the\n * subscriber-side counterpart of the engine's signing. Use this in a handler\n * that receives Hogsend's signed POSTs to confirm authenticity before trusting\n * the body.\n *\n * Pass the RAW request body bytes (the exact string Hogsend signed — never a\n * re-stringified object), the request headers, and the endpoint's `whsec_…`\n * signing secret (from create / rotate-secret). Returns the parsed event\n * envelope (`{ id, type, timestamp, data }`) on success.\n *\n * Throws on a bad signature, a missing signature header, or a timestamp outside\n * the 5-minute tolerance window.\n *\n * Implementation: wraps svix's `Webhook.verify` (constant-time, tolerance-\n * checked); if svix cannot run for any reason it falls back to a pure\n * `node:crypto` HMAC-SHA256 check over `${id}.${timestamp}.${body}` with a\n * `timingSafeEqual` compare against the `v1,<base64>` signature(s).\n *\n * @example\n * ```ts\n * import { verifyHogsendWebhook } from \"@hogsend/client\";\n *\n * app.post(\"/webhooks/hogsend\", async (req, res) => {\n * const body = await readRawBody(req); // the exact bytes\n * try {\n * const event = verifyHogsendWebhook({\n * payload: body,\n * headers: req.headers,\n * secret: process.env.HOGSEND_WEBHOOK_SECRET!,\n * });\n * // handle event.type ...\n * res.sendStatus(200);\n * } catch {\n * res.sendStatus(401);\n * }\n * });\n * ```\n */\nexport function verifyHogsendWebhook(opts: {\n payload: string;\n headers: Record<string, string>;\n secret: string;\n}): unknown {\n const headers = normalizeHeaders(opts.headers);\n const id = headers[\"webhook-id\"] ?? headers[\"svix-id\"];\n const timestamp = headers[\"webhook-timestamp\"] ?? headers[\"svix-timestamp\"];\n const signature = headers[\"webhook-signature\"] ?? headers[\"svix-signature\"];\n\n if (!id || !timestamp || !signature) {\n throw new Error(\n \"verifyHogsendWebhook: missing Webhook-Id / Webhook-Timestamp / Webhook-Signature header\",\n );\n }\n\n try {\n const wh = new Webhook(opts.secret);\n return wh.verify(opts.payload, {\n \"svix-id\": id,\n \"svix-timestamp\": timestamp,\n \"svix-signature\": signature,\n });\n } catch {\n // svix unavailable (tree-shaken) or threw — fall back to node:crypto. We\n // re-run the SAME canonical check so a genuine signature/timestamp failure\n // still throws; only a svix-internal/import failure is \"rescued\".\n return verifyWithNodeCrypto({\n payload: opts.payload,\n secret: opts.secret,\n id,\n timestamp,\n signature,\n });\n }\n}\n\n/**\n * Pure `node:crypto` verification of the Svix signature scheme. Mirrors the\n * documented fallback in the engine's webhook-signing lib:\n * `HMAC_SHA256(base64-decoded secret, `${id}.${ts}.${body}`)` → `v1,<base64>`,\n * `timingSafeEqual` against each space-separated signature in the header.\n */\nfunction verifyWithNodeCrypto(opts: {\n payload: string;\n secret: string;\n id: string;\n timestamp: string;\n signature: string;\n}): unknown {\n const ts = Number.parseInt(opts.timestamp, 10);\n if (!Number.isFinite(ts)) {\n throw new Error(\"verifyHogsendWebhook: invalid Webhook-Timestamp\");\n }\n const now = Math.floor(Date.now() / 1000);\n if (Math.abs(now - ts) > TOLERANCE_SECONDS) {\n throw new Error(\"verifyHogsendWebhook: timestamp outside tolerance window\");\n }\n\n // The secret is `whsec_<base64>`; the signing key is the base64-decoded body.\n const key = opts.secret.startsWith(\"whsec_\")\n ? Buffer.from(opts.secret.slice(6), \"base64\")\n : Buffer.from(opts.secret, \"base64\");\n const signedContent = `${opts.id}.${opts.timestamp}.${opts.payload}`;\n const expected = createHmac(\"sha256\", key)\n .update(signedContent)\n .digest(\"base64\");\n const expectedBuf = Buffer.from(expected);\n\n // The header is space-separated `v1,<sig>` pairs; accept if ANY matches.\n const matched = opts.signature.split(\" \").some((part) => {\n const sig = part.startsWith(\"v1,\") ? part.slice(3) : part;\n const candidate = Buffer.from(sig);\n return (\n candidate.length === expectedBuf.length &&\n timingSafeEqual(candidate, expectedBuf)\n );\n });\n\n if (!matched) {\n throw new Error(\"verifyHogsendWebhook: signature verification failed\");\n }\n\n return JSON.parse(opts.payload);\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.d.cts
CHANGED
|
@@ -32,6 +32,7 @@ interface HttpClient {
|
|
|
32
32
|
get<T = unknown>(path: string, query?: Query): Promise<T>;
|
|
33
33
|
post<T = unknown>(path: string, body: unknown, extras?: RequestExtras): Promise<T>;
|
|
34
34
|
put<T = unknown>(path: string, body: unknown, extras?: RequestExtras): Promise<T>;
|
|
35
|
+
patch<T = unknown>(path: string, body: unknown, extras?: RequestExtras): Promise<T>;
|
|
35
36
|
del<T = unknown>(path: string, body?: unknown): Promise<T>;
|
|
36
37
|
}
|
|
37
38
|
|
|
@@ -161,6 +162,64 @@ interface SubscribeResult {
|
|
|
161
162
|
interface UnsubscribeResult {
|
|
162
163
|
unsubscribed: boolean;
|
|
163
164
|
}
|
|
165
|
+
/**
|
|
166
|
+
* The 12-event outbound catalog. MIRRORS the engine's `WEBHOOK_EVENT_TYPES`
|
|
167
|
+
* (`@hogsend/engine` lib/webhook-signing.ts) — the client cannot import the
|
|
168
|
+
* engine, so the union is re-declared here. A drift check keeps them in sync.
|
|
169
|
+
* The `webhook.test` sentinel is NOT a member (out-of-band).
|
|
170
|
+
*/
|
|
171
|
+
type OutboundEventType = "contact.created" | "contact.updated" | "contact.deleted" | "contact.unsubscribed" | "email.sent" | "email.delivered" | "email.opened" | "email.clicked" | "email.bounced" | "journey.completed" | "bucket.entered" | "bucket.left";
|
|
172
|
+
/**
|
|
173
|
+
* A managed outbound webhook endpoint as returned by `/v1/admin/webhooks` list
|
|
174
|
+
* + get. NEVER carries the full signing `secret` — only its display
|
|
175
|
+
* `secretPrefix`. The full secret is returned exactly once, on create and
|
|
176
|
+
* rotate-secret, as {@link CreatedWebhookEndpoint} / {@link RotateWebhookSecretResult}.
|
|
177
|
+
*/
|
|
178
|
+
interface WebhookEndpoint {
|
|
179
|
+
id: string;
|
|
180
|
+
url: string;
|
|
181
|
+
description: string | null;
|
|
182
|
+
eventTypes: OutboundEventType[];
|
|
183
|
+
/** Safe-to-display prefix, e.g. `whsec_AbCd`. The full secret is never here. */
|
|
184
|
+
secretPrefix: string;
|
|
185
|
+
status: "enabled" | "disabled";
|
|
186
|
+
organizationId: string | null;
|
|
187
|
+
/** ISO string of the last delivery attempt, or null if never delivered. */
|
|
188
|
+
lastDeliveryAt: string | null;
|
|
189
|
+
createdAt: string;
|
|
190
|
+
updatedAt: string;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* The create / rotate response: a {@link WebhookEndpoint} PLUS the full signing
|
|
194
|
+
* `secret` (`whsec_…`). Returned ONCE — store it now, it is never recoverable
|
|
195
|
+
* from list/get.
|
|
196
|
+
*/
|
|
197
|
+
type CreatedWebhookEndpoint = WebhookEndpoint & {
|
|
198
|
+
secret: string;
|
|
199
|
+
};
|
|
200
|
+
/** Body for `hs.webhooks.create`. At least one event type is required. */
|
|
201
|
+
interface CreateWebhookInput {
|
|
202
|
+
url: string;
|
|
203
|
+
eventTypes: OutboundEventType[];
|
|
204
|
+
description?: string;
|
|
205
|
+
disabled?: boolean;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Body for `hs.webhooks.update` (PATCH semantics — only provided fields change).
|
|
209
|
+
* `description: null` clears the description.
|
|
210
|
+
*/
|
|
211
|
+
interface UpdateWebhookInput {
|
|
212
|
+
url?: string;
|
|
213
|
+
eventTypes?: OutboundEventType[];
|
|
214
|
+
description?: string | null;
|
|
215
|
+
disabled?: boolean;
|
|
216
|
+
}
|
|
217
|
+
/** Result of `hs.webhooks.rotateSecret` — the NEW full secret, returned once. */
|
|
218
|
+
interface RotateWebhookSecretResult {
|
|
219
|
+
id: string;
|
|
220
|
+
secret: string;
|
|
221
|
+
secretPrefix: string;
|
|
222
|
+
}
|
|
164
223
|
/**
|
|
165
224
|
* `true` when `TemplateRegistryMap` carries no augmented keys (consumer has not
|
|
166
225
|
* declared their templates). `[keyof TemplateRegistryMap] extends [never]` is
|
|
@@ -314,6 +373,66 @@ declare class ListsResource {
|
|
|
314
373
|
unsubscribe(input: SubscribeInput): Promise<UnsubscribeResult>;
|
|
315
374
|
}
|
|
316
375
|
|
|
376
|
+
/**
|
|
377
|
+
* The `webhooks.*` resource — manage outbound webhook endpoints (the
|
|
378
|
+
* Svix-style signed event stream Hogsend emits to subscriber URLs).
|
|
379
|
+
*
|
|
380
|
+
* IMPORTANT: unlike the rest of the client (which uses an `ingest`-scoped data
|
|
381
|
+
* key), this resource targets the ADMIN plane (`/v1/admin/webhooks`) and
|
|
382
|
+
* REQUIRES a full-admin key. Signing-secret management is the same trust class
|
|
383
|
+
* as API-key management — a leaked ingest key must never register an
|
|
384
|
+
* exfiltration endpoint. Construct the client with an admin `apiKey`.
|
|
385
|
+
*
|
|
386
|
+
* The full signing `secret` (`whsec_…`) is returned ONCE — on
|
|
387
|
+
* {@link WebhooksResource.create} and {@link WebhooksResource.rotateSecret}.
|
|
388
|
+
* `list`/`get` only ever expose the display `secretPrefix`. Store it on create.
|
|
389
|
+
*/
|
|
390
|
+
declare class WebhooksResource {
|
|
391
|
+
private readonly http;
|
|
392
|
+
constructor(http: HttpClient);
|
|
393
|
+
/**
|
|
394
|
+
* Register a new endpoint subscribed to one or more outbound event types.
|
|
395
|
+
* Returns the endpoint INCLUDING the full signing `secret` — this is the
|
|
396
|
+
* only time (besides rotate) the secret is returned. Store it now.
|
|
397
|
+
*/
|
|
398
|
+
create(input: CreateWebhookInput): Promise<CreatedWebhookEndpoint>;
|
|
399
|
+
/**
|
|
400
|
+
* List endpoints (newest first). Disabled endpoints are hidden unless
|
|
401
|
+
* `includeDisabled` is set. Returns the endpoints array (unwrapped from the
|
|
402
|
+
* `{ endpoints, total, limit, offset }` envelope).
|
|
403
|
+
*/
|
|
404
|
+
list(opts?: {
|
|
405
|
+
limit?: number;
|
|
406
|
+
offset?: number;
|
|
407
|
+
includeDisabled?: boolean;
|
|
408
|
+
}): Promise<WebhookEndpoint[]>;
|
|
409
|
+
/** Fetch one endpoint by id (404 → {@link HogsendAPIError}). */
|
|
410
|
+
get(id: string): Promise<WebhookEndpoint>;
|
|
411
|
+
/**
|
|
412
|
+
* Patch an endpoint. Only the provided fields change; `description: null`
|
|
413
|
+
* clears the description. Does NOT return or rotate the secret.
|
|
414
|
+
*/
|
|
415
|
+
update(id: string, input: UpdateWebhookInput): Promise<WebhookEndpoint>;
|
|
416
|
+
/** Hard-delete an endpoint (cascade drops its deliveries). */
|
|
417
|
+
delete(id: string): Promise<{
|
|
418
|
+
deleted: boolean;
|
|
419
|
+
}>;
|
|
420
|
+
/**
|
|
421
|
+
* Rotate the signing secret. The OLD secret is invalidated immediately (hard
|
|
422
|
+
* cutover) — update every subscriber with the returned new `secret` (returned
|
|
423
|
+
* ONCE).
|
|
424
|
+
*/
|
|
425
|
+
rotateSecret(id: string): Promise<RotateWebhookSecretResult>;
|
|
426
|
+
/**
|
|
427
|
+
* Enqueue an out-of-band `webhook.test` delivery to the endpoint, delivered
|
|
428
|
+
* regardless of its subscribed `eventTypes`. Returns the 202 enqueue ack.
|
|
429
|
+
*/
|
|
430
|
+
sendTest(id: string): Promise<{
|
|
431
|
+
enqueued: boolean;
|
|
432
|
+
eventType: "webhook.test";
|
|
433
|
+
}>;
|
|
434
|
+
}
|
|
435
|
+
|
|
317
436
|
/**
|
|
318
437
|
* Typed HTTP client for the Hogsend data plane.
|
|
319
438
|
*
|
|
@@ -329,7 +448,59 @@ declare class Hogsend {
|
|
|
329
448
|
readonly emails: EmailsResource;
|
|
330
449
|
readonly lists: ListsResource;
|
|
331
450
|
readonly campaigns: CampaignsResource;
|
|
451
|
+
/**
|
|
452
|
+
* Manage outbound webhook endpoints (the signed event stream Hogsend emits to
|
|
453
|
+
* subscriber URLs). REQUIRES a full-admin `apiKey` — this resource hits the
|
|
454
|
+
* admin plane (`/v1/admin/webhooks`), NOT the ingest data plane the other
|
|
455
|
+
* resources use. See {@link WebhooksResource}.
|
|
456
|
+
*/
|
|
457
|
+
readonly webhooks: WebhooksResource;
|
|
332
458
|
constructor(opts: HogsendOptions);
|
|
333
459
|
}
|
|
334
460
|
|
|
335
|
-
|
|
461
|
+
/**
|
|
462
|
+
* Verify and parse an INBOUND Hogsend outbound-webhook delivery, the
|
|
463
|
+
* subscriber-side counterpart of the engine's signing. Use this in a handler
|
|
464
|
+
* that receives Hogsend's signed POSTs to confirm authenticity before trusting
|
|
465
|
+
* the body.
|
|
466
|
+
*
|
|
467
|
+
* Pass the RAW request body bytes (the exact string Hogsend signed — never a
|
|
468
|
+
* re-stringified object), the request headers, and the endpoint's `whsec_…`
|
|
469
|
+
* signing secret (from create / rotate-secret). Returns the parsed event
|
|
470
|
+
* envelope (`{ id, type, timestamp, data }`) on success.
|
|
471
|
+
*
|
|
472
|
+
* Throws on a bad signature, a missing signature header, or a timestamp outside
|
|
473
|
+
* the 5-minute tolerance window.
|
|
474
|
+
*
|
|
475
|
+
* Implementation: wraps svix's `Webhook.verify` (constant-time, tolerance-
|
|
476
|
+
* checked); if svix cannot run for any reason it falls back to a pure
|
|
477
|
+
* `node:crypto` HMAC-SHA256 check over `${id}.${timestamp}.${body}` with a
|
|
478
|
+
* `timingSafeEqual` compare against the `v1,<base64>` signature(s).
|
|
479
|
+
*
|
|
480
|
+
* @example
|
|
481
|
+
* ```ts
|
|
482
|
+
* import { verifyHogsendWebhook } from "@hogsend/client";
|
|
483
|
+
*
|
|
484
|
+
* app.post("/webhooks/hogsend", async (req, res) => {
|
|
485
|
+
* const body = await readRawBody(req); // the exact bytes
|
|
486
|
+
* try {
|
|
487
|
+
* const event = verifyHogsendWebhook({
|
|
488
|
+
* payload: body,
|
|
489
|
+
* headers: req.headers,
|
|
490
|
+
* secret: process.env.HOGSEND_WEBHOOK_SECRET!,
|
|
491
|
+
* });
|
|
492
|
+
* // handle event.type ...
|
|
493
|
+
* res.sendStatus(200);
|
|
494
|
+
* } catch {
|
|
495
|
+
* res.sendStatus(401);
|
|
496
|
+
* }
|
|
497
|
+
* });
|
|
498
|
+
* ```
|
|
499
|
+
*/
|
|
500
|
+
declare function verifyHogsendWebhook(opts: {
|
|
501
|
+
payload: string;
|
|
502
|
+
headers: Record<string, string>;
|
|
503
|
+
secret: string;
|
|
504
|
+
}): unknown;
|
|
505
|
+
|
|
506
|
+
export { type Campaign, type CampaignAudienceKind, type CampaignStatus, type Contact, type CreateWebhookInput, type CreatedWebhookEndpoint, type DeleteContactInput, type DeleteContactResult, type ExitResult, type FindContactsInput, Hogsend, HogsendAPIError, type HogsendOptions, type Identity, type IngestResult, type ListSummary, type OutboundEventType, RateLimitError, type RotateWebhookSecretResult, type SendCampaignInput, type SendCampaignResult, type SendEmailInput, type SendEmailResult, type SendEventInput, type SubscribeInput, type SubscribeResult, type UnsubscribeResult, type UpdateWebhookInput, type UpsertContactInput, type UpsertContactResult, type WebhookEndpoint, verifyHogsendWebhook };
|
package/dist/index.d.ts
CHANGED
|
@@ -32,6 +32,7 @@ interface HttpClient {
|
|
|
32
32
|
get<T = unknown>(path: string, query?: Query): Promise<T>;
|
|
33
33
|
post<T = unknown>(path: string, body: unknown, extras?: RequestExtras): Promise<T>;
|
|
34
34
|
put<T = unknown>(path: string, body: unknown, extras?: RequestExtras): Promise<T>;
|
|
35
|
+
patch<T = unknown>(path: string, body: unknown, extras?: RequestExtras): Promise<T>;
|
|
35
36
|
del<T = unknown>(path: string, body?: unknown): Promise<T>;
|
|
36
37
|
}
|
|
37
38
|
|
|
@@ -161,6 +162,64 @@ interface SubscribeResult {
|
|
|
161
162
|
interface UnsubscribeResult {
|
|
162
163
|
unsubscribed: boolean;
|
|
163
164
|
}
|
|
165
|
+
/**
|
|
166
|
+
* The 12-event outbound catalog. MIRRORS the engine's `WEBHOOK_EVENT_TYPES`
|
|
167
|
+
* (`@hogsend/engine` lib/webhook-signing.ts) — the client cannot import the
|
|
168
|
+
* engine, so the union is re-declared here. A drift check keeps them in sync.
|
|
169
|
+
* The `webhook.test` sentinel is NOT a member (out-of-band).
|
|
170
|
+
*/
|
|
171
|
+
type OutboundEventType = "contact.created" | "contact.updated" | "contact.deleted" | "contact.unsubscribed" | "email.sent" | "email.delivered" | "email.opened" | "email.clicked" | "email.bounced" | "journey.completed" | "bucket.entered" | "bucket.left";
|
|
172
|
+
/**
|
|
173
|
+
* A managed outbound webhook endpoint as returned by `/v1/admin/webhooks` list
|
|
174
|
+
* + get. NEVER carries the full signing `secret` — only its display
|
|
175
|
+
* `secretPrefix`. The full secret is returned exactly once, on create and
|
|
176
|
+
* rotate-secret, as {@link CreatedWebhookEndpoint} / {@link RotateWebhookSecretResult}.
|
|
177
|
+
*/
|
|
178
|
+
interface WebhookEndpoint {
|
|
179
|
+
id: string;
|
|
180
|
+
url: string;
|
|
181
|
+
description: string | null;
|
|
182
|
+
eventTypes: OutboundEventType[];
|
|
183
|
+
/** Safe-to-display prefix, e.g. `whsec_AbCd`. The full secret is never here. */
|
|
184
|
+
secretPrefix: string;
|
|
185
|
+
status: "enabled" | "disabled";
|
|
186
|
+
organizationId: string | null;
|
|
187
|
+
/** ISO string of the last delivery attempt, or null if never delivered. */
|
|
188
|
+
lastDeliveryAt: string | null;
|
|
189
|
+
createdAt: string;
|
|
190
|
+
updatedAt: string;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* The create / rotate response: a {@link WebhookEndpoint} PLUS the full signing
|
|
194
|
+
* `secret` (`whsec_…`). Returned ONCE — store it now, it is never recoverable
|
|
195
|
+
* from list/get.
|
|
196
|
+
*/
|
|
197
|
+
type CreatedWebhookEndpoint = WebhookEndpoint & {
|
|
198
|
+
secret: string;
|
|
199
|
+
};
|
|
200
|
+
/** Body for `hs.webhooks.create`. At least one event type is required. */
|
|
201
|
+
interface CreateWebhookInput {
|
|
202
|
+
url: string;
|
|
203
|
+
eventTypes: OutboundEventType[];
|
|
204
|
+
description?: string;
|
|
205
|
+
disabled?: boolean;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Body for `hs.webhooks.update` (PATCH semantics — only provided fields change).
|
|
209
|
+
* `description: null` clears the description.
|
|
210
|
+
*/
|
|
211
|
+
interface UpdateWebhookInput {
|
|
212
|
+
url?: string;
|
|
213
|
+
eventTypes?: OutboundEventType[];
|
|
214
|
+
description?: string | null;
|
|
215
|
+
disabled?: boolean;
|
|
216
|
+
}
|
|
217
|
+
/** Result of `hs.webhooks.rotateSecret` — the NEW full secret, returned once. */
|
|
218
|
+
interface RotateWebhookSecretResult {
|
|
219
|
+
id: string;
|
|
220
|
+
secret: string;
|
|
221
|
+
secretPrefix: string;
|
|
222
|
+
}
|
|
164
223
|
/**
|
|
165
224
|
* `true` when `TemplateRegistryMap` carries no augmented keys (consumer has not
|
|
166
225
|
* declared their templates). `[keyof TemplateRegistryMap] extends [never]` is
|
|
@@ -314,6 +373,66 @@ declare class ListsResource {
|
|
|
314
373
|
unsubscribe(input: SubscribeInput): Promise<UnsubscribeResult>;
|
|
315
374
|
}
|
|
316
375
|
|
|
376
|
+
/**
|
|
377
|
+
* The `webhooks.*` resource — manage outbound webhook endpoints (the
|
|
378
|
+
* Svix-style signed event stream Hogsend emits to subscriber URLs).
|
|
379
|
+
*
|
|
380
|
+
* IMPORTANT: unlike the rest of the client (which uses an `ingest`-scoped data
|
|
381
|
+
* key), this resource targets the ADMIN plane (`/v1/admin/webhooks`) and
|
|
382
|
+
* REQUIRES a full-admin key. Signing-secret management is the same trust class
|
|
383
|
+
* as API-key management — a leaked ingest key must never register an
|
|
384
|
+
* exfiltration endpoint. Construct the client with an admin `apiKey`.
|
|
385
|
+
*
|
|
386
|
+
* The full signing `secret` (`whsec_…`) is returned ONCE — on
|
|
387
|
+
* {@link WebhooksResource.create} and {@link WebhooksResource.rotateSecret}.
|
|
388
|
+
* `list`/`get` only ever expose the display `secretPrefix`. Store it on create.
|
|
389
|
+
*/
|
|
390
|
+
declare class WebhooksResource {
|
|
391
|
+
private readonly http;
|
|
392
|
+
constructor(http: HttpClient);
|
|
393
|
+
/**
|
|
394
|
+
* Register a new endpoint subscribed to one or more outbound event types.
|
|
395
|
+
* Returns the endpoint INCLUDING the full signing `secret` — this is the
|
|
396
|
+
* only time (besides rotate) the secret is returned. Store it now.
|
|
397
|
+
*/
|
|
398
|
+
create(input: CreateWebhookInput): Promise<CreatedWebhookEndpoint>;
|
|
399
|
+
/**
|
|
400
|
+
* List endpoints (newest first). Disabled endpoints are hidden unless
|
|
401
|
+
* `includeDisabled` is set. Returns the endpoints array (unwrapped from the
|
|
402
|
+
* `{ endpoints, total, limit, offset }` envelope).
|
|
403
|
+
*/
|
|
404
|
+
list(opts?: {
|
|
405
|
+
limit?: number;
|
|
406
|
+
offset?: number;
|
|
407
|
+
includeDisabled?: boolean;
|
|
408
|
+
}): Promise<WebhookEndpoint[]>;
|
|
409
|
+
/** Fetch one endpoint by id (404 → {@link HogsendAPIError}). */
|
|
410
|
+
get(id: string): Promise<WebhookEndpoint>;
|
|
411
|
+
/**
|
|
412
|
+
* Patch an endpoint. Only the provided fields change; `description: null`
|
|
413
|
+
* clears the description. Does NOT return or rotate the secret.
|
|
414
|
+
*/
|
|
415
|
+
update(id: string, input: UpdateWebhookInput): Promise<WebhookEndpoint>;
|
|
416
|
+
/** Hard-delete an endpoint (cascade drops its deliveries). */
|
|
417
|
+
delete(id: string): Promise<{
|
|
418
|
+
deleted: boolean;
|
|
419
|
+
}>;
|
|
420
|
+
/**
|
|
421
|
+
* Rotate the signing secret. The OLD secret is invalidated immediately (hard
|
|
422
|
+
* cutover) — update every subscriber with the returned new `secret` (returned
|
|
423
|
+
* ONCE).
|
|
424
|
+
*/
|
|
425
|
+
rotateSecret(id: string): Promise<RotateWebhookSecretResult>;
|
|
426
|
+
/**
|
|
427
|
+
* Enqueue an out-of-band `webhook.test` delivery to the endpoint, delivered
|
|
428
|
+
* regardless of its subscribed `eventTypes`. Returns the 202 enqueue ack.
|
|
429
|
+
*/
|
|
430
|
+
sendTest(id: string): Promise<{
|
|
431
|
+
enqueued: boolean;
|
|
432
|
+
eventType: "webhook.test";
|
|
433
|
+
}>;
|
|
434
|
+
}
|
|
435
|
+
|
|
317
436
|
/**
|
|
318
437
|
* Typed HTTP client for the Hogsend data plane.
|
|
319
438
|
*
|
|
@@ -329,7 +448,59 @@ declare class Hogsend {
|
|
|
329
448
|
readonly emails: EmailsResource;
|
|
330
449
|
readonly lists: ListsResource;
|
|
331
450
|
readonly campaigns: CampaignsResource;
|
|
451
|
+
/**
|
|
452
|
+
* Manage outbound webhook endpoints (the signed event stream Hogsend emits to
|
|
453
|
+
* subscriber URLs). REQUIRES a full-admin `apiKey` — this resource hits the
|
|
454
|
+
* admin plane (`/v1/admin/webhooks`), NOT the ingest data plane the other
|
|
455
|
+
* resources use. See {@link WebhooksResource}.
|
|
456
|
+
*/
|
|
457
|
+
readonly webhooks: WebhooksResource;
|
|
332
458
|
constructor(opts: HogsendOptions);
|
|
333
459
|
}
|
|
334
460
|
|
|
335
|
-
|
|
461
|
+
/**
|
|
462
|
+
* Verify and parse an INBOUND Hogsend outbound-webhook delivery, the
|
|
463
|
+
* subscriber-side counterpart of the engine's signing. Use this in a handler
|
|
464
|
+
* that receives Hogsend's signed POSTs to confirm authenticity before trusting
|
|
465
|
+
* the body.
|
|
466
|
+
*
|
|
467
|
+
* Pass the RAW request body bytes (the exact string Hogsend signed — never a
|
|
468
|
+
* re-stringified object), the request headers, and the endpoint's `whsec_…`
|
|
469
|
+
* signing secret (from create / rotate-secret). Returns the parsed event
|
|
470
|
+
* envelope (`{ id, type, timestamp, data }`) on success.
|
|
471
|
+
*
|
|
472
|
+
* Throws on a bad signature, a missing signature header, or a timestamp outside
|
|
473
|
+
* the 5-minute tolerance window.
|
|
474
|
+
*
|
|
475
|
+
* Implementation: wraps svix's `Webhook.verify` (constant-time, tolerance-
|
|
476
|
+
* checked); if svix cannot run for any reason it falls back to a pure
|
|
477
|
+
* `node:crypto` HMAC-SHA256 check over `${id}.${timestamp}.${body}` with a
|
|
478
|
+
* `timingSafeEqual` compare against the `v1,<base64>` signature(s).
|
|
479
|
+
*
|
|
480
|
+
* @example
|
|
481
|
+
* ```ts
|
|
482
|
+
* import { verifyHogsendWebhook } from "@hogsend/client";
|
|
483
|
+
*
|
|
484
|
+
* app.post("/webhooks/hogsend", async (req, res) => {
|
|
485
|
+
* const body = await readRawBody(req); // the exact bytes
|
|
486
|
+
* try {
|
|
487
|
+
* const event = verifyHogsendWebhook({
|
|
488
|
+
* payload: body,
|
|
489
|
+
* headers: req.headers,
|
|
490
|
+
* secret: process.env.HOGSEND_WEBHOOK_SECRET!,
|
|
491
|
+
* });
|
|
492
|
+
* // handle event.type ...
|
|
493
|
+
* res.sendStatus(200);
|
|
494
|
+
* } catch {
|
|
495
|
+
* res.sendStatus(401);
|
|
496
|
+
* }
|
|
497
|
+
* });
|
|
498
|
+
* ```
|
|
499
|
+
*/
|
|
500
|
+
declare function verifyHogsendWebhook(opts: {
|
|
501
|
+
payload: string;
|
|
502
|
+
headers: Record<string, string>;
|
|
503
|
+
secret: string;
|
|
504
|
+
}): unknown;
|
|
505
|
+
|
|
506
|
+
export { type Campaign, type CampaignAudienceKind, type CampaignStatus, type Contact, type CreateWebhookInput, type CreatedWebhookEndpoint, type DeleteContactInput, type DeleteContactResult, type ExitResult, type FindContactsInput, Hogsend, HogsendAPIError, type HogsendOptions, type Identity, type IngestResult, type ListSummary, type OutboundEventType, RateLimitError, type RotateWebhookSecretResult, type SendCampaignInput, type SendCampaignResult, type SendEmailInput, type SendEmailResult, type SendEventInput, type SubscribeInput, type SubscribeResult, type UnsubscribeResult, type UpdateWebhookInput, type UpsertContactInput, type UpsertContactResult, type WebhookEndpoint, verifyHogsendWebhook };
|
package/dist/index.js
CHANGED
|
@@ -116,6 +116,7 @@ function createHttpClient(config) {
|
|
|
116
116
|
get: (path, query) => request("GET", path, { query }),
|
|
117
117
|
post: (path, body, extras) => request("POST", path, { body, extras }),
|
|
118
118
|
put: (path, body, extras) => request("PUT", path, { body, extras }),
|
|
119
|
+
patch: (path, body, extras) => request("PATCH", path, { body, extras }),
|
|
119
120
|
del: (path, body) => request("DELETE", path, { body })
|
|
120
121
|
};
|
|
121
122
|
}
|
|
@@ -307,6 +308,87 @@ var ListsResource = class {
|
|
|
307
308
|
}
|
|
308
309
|
};
|
|
309
310
|
|
|
311
|
+
// src/resources/webhooks.ts
|
|
312
|
+
var BASE = "/v1/admin/webhooks";
|
|
313
|
+
var WebhooksResource = class {
|
|
314
|
+
constructor(http) {
|
|
315
|
+
this.http = http;
|
|
316
|
+
}
|
|
317
|
+
http;
|
|
318
|
+
/**
|
|
319
|
+
* Register a new endpoint subscribed to one or more outbound event types.
|
|
320
|
+
* Returns the endpoint INCLUDING the full signing `secret` — this is the
|
|
321
|
+
* only time (besides rotate) the secret is returned. Store it now.
|
|
322
|
+
*/
|
|
323
|
+
create(input) {
|
|
324
|
+
return this.http.post(BASE, {
|
|
325
|
+
url: input.url,
|
|
326
|
+
eventTypes: input.eventTypes,
|
|
327
|
+
description: input.description,
|
|
328
|
+
disabled: input.disabled
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* List endpoints (newest first). Disabled endpoints are hidden unless
|
|
333
|
+
* `includeDisabled` is set. Returns the endpoints array (unwrapped from the
|
|
334
|
+
* `{ endpoints, total, limit, offset }` envelope).
|
|
335
|
+
*/
|
|
336
|
+
async list(opts) {
|
|
337
|
+
const res = await this.http.get(BASE, {
|
|
338
|
+
limit: opts?.limit,
|
|
339
|
+
offset: opts?.offset,
|
|
340
|
+
includeDisabled: opts?.includeDisabled === void 0 ? void 0 : String(opts.includeDisabled)
|
|
341
|
+
});
|
|
342
|
+
return res.endpoints;
|
|
343
|
+
}
|
|
344
|
+
/** Fetch one endpoint by id (404 → {@link HogsendAPIError}). */
|
|
345
|
+
get(id) {
|
|
346
|
+
return this.http.get(`${BASE}/${encodeURIComponent(id)}`);
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Patch an endpoint. Only the provided fields change; `description: null`
|
|
350
|
+
* clears the description. Does NOT return or rotate the secret.
|
|
351
|
+
*/
|
|
352
|
+
update(id, input) {
|
|
353
|
+
return this.http.patch(
|
|
354
|
+
`${BASE}/${encodeURIComponent(id)}`,
|
|
355
|
+
{
|
|
356
|
+
url: input.url,
|
|
357
|
+
eventTypes: input.eventTypes,
|
|
358
|
+
description: input.description,
|
|
359
|
+
disabled: input.disabled
|
|
360
|
+
}
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
/** Hard-delete an endpoint (cascade drops its deliveries). */
|
|
364
|
+
delete(id) {
|
|
365
|
+
return this.http.del(
|
|
366
|
+
`${BASE}/${encodeURIComponent(id)}`
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Rotate the signing secret. The OLD secret is invalidated immediately (hard
|
|
371
|
+
* cutover) — update every subscriber with the returned new `secret` (returned
|
|
372
|
+
* ONCE).
|
|
373
|
+
*/
|
|
374
|
+
rotateSecret(id) {
|
|
375
|
+
return this.http.post(
|
|
376
|
+
`${BASE}/${encodeURIComponent(id)}/rotate-secret`,
|
|
377
|
+
{}
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Enqueue an out-of-band `webhook.test` delivery to the endpoint, delivered
|
|
382
|
+
* regardless of its subscribed `eventTypes`. Returns the 202 enqueue ack.
|
|
383
|
+
*/
|
|
384
|
+
sendTest(id) {
|
|
385
|
+
return this.http.post(
|
|
386
|
+
`${BASE}/${encodeURIComponent(id)}/test`,
|
|
387
|
+
{}
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
|
|
310
392
|
// src/hogsend.ts
|
|
311
393
|
var Hogsend = class {
|
|
312
394
|
contacts;
|
|
@@ -314,6 +396,13 @@ var Hogsend = class {
|
|
|
314
396
|
emails;
|
|
315
397
|
lists;
|
|
316
398
|
campaigns;
|
|
399
|
+
/**
|
|
400
|
+
* Manage outbound webhook endpoints (the signed event stream Hogsend emits to
|
|
401
|
+
* subscriber URLs). REQUIRES a full-admin `apiKey` — this resource hits the
|
|
402
|
+
* admin plane (`/v1/admin/webhooks`), NOT the ingest data plane the other
|
|
403
|
+
* resources use. See {@link WebhooksResource}.
|
|
404
|
+
*/
|
|
405
|
+
webhooks;
|
|
317
406
|
constructor(opts) {
|
|
318
407
|
if (!opts.baseUrl) {
|
|
319
408
|
throw new TypeError("Hogsend: `baseUrl` is required.");
|
|
@@ -333,11 +422,75 @@ var Hogsend = class {
|
|
|
333
422
|
this.emails = new EmailsResource(http);
|
|
334
423
|
this.lists = new ListsResource(http);
|
|
335
424
|
this.campaigns = new CampaignsResource(http);
|
|
425
|
+
this.webhooks = new WebhooksResource(http);
|
|
336
426
|
}
|
|
337
427
|
};
|
|
428
|
+
|
|
429
|
+
// src/internal/verify.ts
|
|
430
|
+
import { createHmac, timingSafeEqual } from "crypto";
|
|
431
|
+
import { Webhook } from "svix";
|
|
432
|
+
var TOLERANCE_SECONDS = 5 * 60;
|
|
433
|
+
function normalizeHeaders(headers) {
|
|
434
|
+
const out = {};
|
|
435
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
436
|
+
out[key.toLowerCase()] = value;
|
|
437
|
+
}
|
|
438
|
+
return out;
|
|
439
|
+
}
|
|
440
|
+
function verifyHogsendWebhook(opts) {
|
|
441
|
+
const headers = normalizeHeaders(opts.headers);
|
|
442
|
+
const id = headers["webhook-id"] ?? headers["svix-id"];
|
|
443
|
+
const timestamp = headers["webhook-timestamp"] ?? headers["svix-timestamp"];
|
|
444
|
+
const signature = headers["webhook-signature"] ?? headers["svix-signature"];
|
|
445
|
+
if (!id || !timestamp || !signature) {
|
|
446
|
+
throw new Error(
|
|
447
|
+
"verifyHogsendWebhook: missing Webhook-Id / Webhook-Timestamp / Webhook-Signature header"
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
try {
|
|
451
|
+
const wh = new Webhook(opts.secret);
|
|
452
|
+
return wh.verify(opts.payload, {
|
|
453
|
+
"svix-id": id,
|
|
454
|
+
"svix-timestamp": timestamp,
|
|
455
|
+
"svix-signature": signature
|
|
456
|
+
});
|
|
457
|
+
} catch {
|
|
458
|
+
return verifyWithNodeCrypto({
|
|
459
|
+
payload: opts.payload,
|
|
460
|
+
secret: opts.secret,
|
|
461
|
+
id,
|
|
462
|
+
timestamp,
|
|
463
|
+
signature
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
function verifyWithNodeCrypto(opts) {
|
|
468
|
+
const ts = Number.parseInt(opts.timestamp, 10);
|
|
469
|
+
if (!Number.isFinite(ts)) {
|
|
470
|
+
throw new Error("verifyHogsendWebhook: invalid Webhook-Timestamp");
|
|
471
|
+
}
|
|
472
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
473
|
+
if (Math.abs(now - ts) > TOLERANCE_SECONDS) {
|
|
474
|
+
throw new Error("verifyHogsendWebhook: timestamp outside tolerance window");
|
|
475
|
+
}
|
|
476
|
+
const key = opts.secret.startsWith("whsec_") ? Buffer.from(opts.secret.slice(6), "base64") : Buffer.from(opts.secret, "base64");
|
|
477
|
+
const signedContent = `${opts.id}.${opts.timestamp}.${opts.payload}`;
|
|
478
|
+
const expected = createHmac("sha256", key).update(signedContent).digest("base64");
|
|
479
|
+
const expectedBuf = Buffer.from(expected);
|
|
480
|
+
const matched = opts.signature.split(" ").some((part) => {
|
|
481
|
+
const sig = part.startsWith("v1,") ? part.slice(3) : part;
|
|
482
|
+
const candidate = Buffer.from(sig);
|
|
483
|
+
return candidate.length === expectedBuf.length && timingSafeEqual(candidate, expectedBuf);
|
|
484
|
+
});
|
|
485
|
+
if (!matched) {
|
|
486
|
+
throw new Error("verifyHogsendWebhook: signature verification failed");
|
|
487
|
+
}
|
|
488
|
+
return JSON.parse(opts.payload);
|
|
489
|
+
}
|
|
338
490
|
export {
|
|
339
491
|
Hogsend,
|
|
340
492
|
HogsendAPIError,
|
|
341
|
-
RateLimitError
|
|
493
|
+
RateLimitError,
|
|
494
|
+
verifyHogsendWebhook
|
|
342
495
|
};
|
|
343
496
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/errors.ts","../src/internal/http.ts","../src/resources/campaigns.ts","../src/internal/identity.ts","../src/resources/contacts.ts","../src/resources/emails.ts","../src/resources/events.ts","../src/resources/lists.ts","../src/hogsend.ts"],"sourcesContent":["/**\n * A non-2xx response — or a transport-level failure — from the Hogsend data\n * plane. `status` is the HTTP status code, or `0` when the request never\n * reached the server (DNS/connect/timeout). `body` is the parsed JSON body when\n * available, else the raw text, else `undefined`.\n */\nexport class HogsendAPIError extends Error {\n readonly status: number;\n readonly body: unknown;\n\n constructor(message: string, status: number, body: unknown) {\n super(message);\n this.name = \"HogsendAPIError\";\n this.status = status;\n this.body = body;\n // Restore prototype chain for instanceof across transpile targets.\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n/**\n * A `429 Too Many Requests` response. `retryAfter` is the parsed `Retry-After`\n * header in seconds when present (the server sends it on 429 for `/v1/emails`).\n */\nexport class RateLimitError extends HogsendAPIError {\n readonly retryAfter?: number;\n\n constructor(message: string, body: unknown, retryAfter?: number) {\n super(message, 429, body);\n this.name = \"RateLimitError\";\n this.retryAfter = retryAfter;\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n","import { HogsendAPIError, RateLimitError } from \"../errors.js\";\n\n/** Query params accepted by `get` — undefined values are dropped. */\nexport type Query = Record<string, string | number | undefined>;\n\n/** Per-request extras (currently just an idempotency header passthrough). */\nexport interface RequestExtras {\n /** Sent as the `Idempotency-Key` header when set. */\n idempotencyKey?: string;\n}\n\nexport interface HttpClientConfig {\n baseUrl: string;\n apiKey: string;\n fetch?: typeof fetch;\n timeoutMs?: number;\n headers?: Record<string, string>;\n}\n\n/** A minimal, self-contained data-plane HTTP client over native fetch. */\nexport interface HttpClient {\n get<T = unknown>(path: string, query?: Query): Promise<T>;\n post<T = unknown>(\n path: string,\n body: unknown,\n extras?: RequestExtras,\n ): Promise<T>;\n put<T = unknown>(\n path: string,\n body: unknown,\n extras?: RequestExtras,\n ): Promise<T>;\n del<T = unknown>(path: string, body?: unknown): Promise<T>;\n}\n\nconst DEFAULT_TIMEOUT_MS = 30_000;\n\nfunction buildUrl(baseUrl: string, path: string, query?: Query): string {\n const url = new URL(path.startsWith(\"/\") ? path : `/${path}`, `${baseUrl}/`);\n if (query) {\n for (const [key, value] of Object.entries(query)) {\n if (value === undefined) continue;\n url.searchParams.set(key, String(value));\n }\n }\n return url.toString();\n}\n\nfunction bodyMessage(status: number, body: unknown): string {\n if (body && typeof body === \"object\") {\n // Application-handler envelope: `{ error: \"human message\" }`.\n const errField = (body as { error?: unknown }).error;\n if (typeof errField === \"string\") {\n return `${status}: ${errField}`;\n }\n // @hono/zod-openapi default-hook validation envelope:\n // `{ success: false, error: <ZodError> }` (no defaultHook configured). The\n // structured ZodError is preserved on `err.body`; surface a short, readable\n // summary for `err.message` instead of the generic fallback.\n if (\n (body as { success?: unknown }).success === false &&\n errField &&\n typeof errField === \"object\"\n ) {\n const summary = JSON.stringify(errField).slice(0, 200);\n return `${status}: validation failed ${summary}`;\n }\n }\n return `request failed with status ${status}`;\n}\n\n/** Parse a `Retry-After` header (seconds form) into a number, else undefined. */\nfunction parseRetryAfter(value: string | null): number | undefined {\n if (!value) return undefined;\n const seconds = Number.parseInt(value, 10);\n return Number.isFinite(seconds) ? seconds : undefined;\n}\n\n/**\n * Builds an {@link HttpClient} bound to a config. Native `fetch`, JSON in/out,\n * `Authorization: Bearer <apiKey>`. Throws typed errors:\n * - {@link RateLimitError} on 429 (with parsed `Retry-After`),\n * - {@link HogsendAPIError} on any other non-2xx,\n * - {@link HogsendAPIError} with `status === 0` on a transport failure\n * (DNS/connect/abort/timeout).\n */\nexport function createHttpClient(config: HttpClientConfig): HttpClient {\n const doFetch = config.fetch ?? fetch;\n const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const extraHeaders = config.headers ?? {};\n\n async function request<T>(\n method: string,\n path: string,\n opts: { query?: Query; body?: unknown; extras?: RequestExtras },\n ): Promise<T> {\n const headers: Record<string, string> = {\n Accept: \"application/json\",\n Authorization: `Bearer ${config.apiKey}`,\n ...extraHeaders,\n };\n if (opts.body !== undefined) {\n headers[\"Content-Type\"] = \"application/json\";\n }\n if (opts.extras?.idempotencyKey) {\n headers[\"Idempotency-Key\"] = opts.extras.idempotencyKey;\n }\n\n const url = buildUrl(config.baseUrl, path, opts.query);\n\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n\n let res: Response;\n try {\n res = await doFetch(url, {\n method,\n headers,\n body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined,\n signal: controller.signal,\n });\n } catch (cause) {\n const msg = cause instanceof Error ? cause.message : String(cause);\n throw new HogsendAPIError(\n `cannot reach ${config.baseUrl} (${msg})`,\n 0,\n undefined,\n );\n } finally {\n clearTimeout(timer);\n }\n\n const text = await res.text();\n let parsed: unknown;\n if (text.length > 0) {\n try {\n parsed = JSON.parse(text);\n } catch {\n parsed = text;\n }\n }\n\n if (!res.ok) {\n if (res.status === 429) {\n throw new RateLimitError(\n bodyMessage(res.status, parsed),\n parsed,\n parseRetryAfter(res.headers.get(\"Retry-After\")),\n );\n }\n throw new HogsendAPIError(\n bodyMessage(res.status, parsed),\n res.status,\n parsed,\n );\n }\n\n return parsed as T;\n }\n\n return {\n get: <T>(path: string, query?: Query) => request<T>(\"GET\", path, { query }),\n post: <T>(path: string, body: unknown, extras?: RequestExtras) =>\n request<T>(\"POST\", path, { body, extras }),\n put: <T>(path: string, body: unknown, extras?: RequestExtras) =>\n request<T>(\"PUT\", path, { body, extras }),\n del: <T>(path: string, body?: unknown) =>\n request<T>(\"DELETE\", path, { body }),\n };\n}\n","import type { HttpClient } from \"../internal/http.js\";\nimport type {\n Campaign,\n SendCampaignInput,\n SendCampaignResult,\n} from \"../types.js\";\n\n/** The `campaigns.*` resource bound to an {@link HttpClient}. */\nexport class CampaignsResource {\n constructor(private readonly http: HttpClient) {}\n\n /**\n * Queue a broadcast: durably send one template to every subscribed member of\n * a `list` (or every active member of a `bucket`). Exactly one of `list` /\n * `bucket` must be set; `template`/`props` are type-checked against the\n * augmented `TemplateRegistryMap` when `@hogsend/email` is installed, else\n * degrade to `{ template: string; props? }`.\n *\n * Returns the 202 enqueue ack (`{ campaignId, status }`); the actual sends run\n * asynchronously in the worker. Poll {@link CampaignsResource.get} for counts.\n */\n send(input: SendCampaignInput): Promise<SendCampaignResult> {\n // The discriminated union narrows `template`/`props` and the audience; index\n // into the input via a permissive view to build the wire body without\n // re-discriminating.\n const body = input as SendCampaignInput & {\n list?: string;\n bucket?: string;\n props?: Record<string, unknown>;\n };\n return this.http.post<SendCampaignResult>(\"/v1/campaigns\", {\n name: body.name,\n list: body.list,\n bucket: body.bucket,\n template: body.template,\n props: body.props,\n from: body.from,\n subject: body.subject,\n });\n }\n\n /** Fetch a campaign's current status + send counts. */\n get(id: string): Promise<Campaign> {\n return this.http.get<Campaign>(`/v1/campaigns/${encodeURIComponent(id)}`);\n }\n}\n","/**\n * Runtime guard mirroring the `Identity` union: at least one of `email` /\n * `userId` must be a non-empty string. The type system enforces this at the\n * call site, but runtime callers (plain JS, untyped data) can still violate it,\n * so we fail fast with a clear message before issuing the request.\n */\nexport function assertIdentity(input: {\n email?: string;\n userId?: string;\n}): void {\n const hasEmail = typeof input.email === \"string\" && input.email.length > 0;\n const hasUserId = typeof input.userId === \"string\" && input.userId.length > 0;\n if (!hasEmail && !hasUserId) {\n throw new TypeError(\n \"Hogsend: an identity is required — pass `email`, `userId`, or both.\",\n );\n }\n}\n","import type { HttpClient } from \"../internal/http.js\";\nimport { assertIdentity } from \"../internal/identity.js\";\nimport type {\n Contact,\n DeleteContactInput,\n DeleteContactResult,\n FindContactsInput,\n UpsertContactInput,\n UpsertContactResult,\n} from \"../types.js\";\n\n/** The `contacts.*` resource bound to an {@link HttpClient}. */\nexport class ContactsResource {\n constructor(private readonly http: HttpClient) {}\n\n /**\n * Upsert a contact by identity. Resolves/merges server-side and optionally\n * applies list membership. Returns `{ id, created, linked }`.\n */\n async upsert(input: UpsertContactInput): Promise<UpsertContactResult> {\n assertIdentity(input);\n return this.http.put<UpsertContactResult>(\"/v1/contacts\", {\n email: input.email,\n userId: input.userId,\n properties: input.properties,\n lists: input.lists,\n });\n }\n\n /** Find non-deleted contacts by `email` or `userId`. */\n async find(input: FindContactsInput): Promise<Contact[]> {\n const query: Record<string, string | undefined> = {\n email: \"email\" in input ? input.email : undefined,\n userId: \"userId\" in input ? input.userId : undefined,\n };\n const res = await this.http.get<{ contacts: Contact[] }>(\n \"/v1/contacts/find\",\n query,\n );\n return res.contacts;\n }\n\n /** Soft-delete a contact by identity. */\n async delete(input: DeleteContactInput): Promise<DeleteContactResult> {\n assertIdentity(input);\n return this.http.del<DeleteContactResult>(\"/v1/contacts\", {\n email: input.email,\n userId: input.userId,\n });\n }\n}\n","import type { HttpClient } from \"../internal/http.js\";\nimport type { SendEmailInput, SendEmailResult } from \"../types.js\";\n\n/** The `emails.*` resource bound to an {@link HttpClient}. */\nexport class EmailsResource {\n constructor(private readonly http: HttpClient) {}\n\n /**\n * Send a transactional email by template. Recipient is `to` (raw address) or\n * `userId` (resolved server-side). `template`/`props` are type-checked against\n * the augmented `TemplateRegistryMap` when `@hogsend/email` is installed,\n * else degrade to `{ template: string; props? }`.\n */\n send(input: SendEmailInput): Promise<SendEmailResult> {\n // The discriminated union narrows `template`/`props`; index into the input\n // via a permissive view to build the wire body without re-discriminating.\n const body = input as SendEmailInput & {\n props?: Record<string, unknown>;\n };\n return this.http.post<SendEmailResult>(\n \"/v1/emails\",\n {\n to: body.to,\n userId: body.userId,\n template: body.template,\n props: body.props,\n from: body.from,\n subject: body.subject,\n replyTo: body.replyTo,\n category: body.category,\n skipPreferenceCheck: body.skipPreferenceCheck,\n idempotencyKey: body.idempotencyKey,\n },\n body.idempotencyKey ? { idempotencyKey: body.idempotencyKey } : undefined,\n );\n }\n}\n","import type { HttpClient } from \"../internal/http.js\";\nimport { assertIdentity } from \"../internal/identity.js\";\nimport type { IngestResult, SendEventInput } from \"../types.js\";\n\n/** The `events.*` resource bound to an {@link HttpClient}. */\nexport class EventsResource {\n constructor(private readonly http: HttpClient) {}\n\n /**\n * Send an event through the ingestion pipeline. The two property bags are\n * kept distinct: `eventProperties` feed `trigger.where`/`exitOn`,\n * `contactProperties` merge onto the contact. Optionally apply list\n * membership. Returns the ingest result (stored + exit evaluations).\n *\n * `idempotencyKey` is sent both as the `Idempotency-Key` header (which wins\n * server-side) and in the body, matching `POST /v1/events`.\n */\n send(input: SendEventInput): Promise<IngestResult> {\n assertIdentity(input);\n return this.http.post<IngestResult>(\n \"/v1/events\",\n {\n name: input.name,\n email: input.email,\n userId: input.userId,\n eventProperties: input.eventProperties,\n contactProperties: input.contactProperties,\n lists: input.lists,\n idempotencyKey: input.idempotencyKey,\n },\n input.idempotencyKey\n ? { idempotencyKey: input.idempotencyKey }\n : undefined,\n );\n }\n\n /** Alias of {@link EventsResource.send}. */\n track(input: SendEventInput): Promise<IngestResult> {\n return this.send(input);\n }\n}\n","import type { HttpClient } from \"../internal/http.js\";\nimport { assertIdentity } from \"../internal/identity.js\";\nimport type {\n ListSummary,\n SubscribeInput,\n SubscribeResult,\n UnsubscribeResult,\n} from \"../types.js\";\n\n/** The `lists.*` resource bound to an {@link HttpClient}. */\nexport class ListsResource {\n constructor(private readonly http: HttpClient) {}\n\n /** List all code-defined lists. */\n async list(): Promise<ListSummary[]> {\n const res = await this.http.get<{ lists: ListSummary[] }>(\"/v1/lists\");\n return res.lists;\n }\n\n /** Subscribe an identity to a list. */\n async subscribe(input: SubscribeInput): Promise<SubscribeResult> {\n assertIdentity(input);\n const res = await this.http.post<{ list: string; subscribed: boolean }>(\n `/v1/lists/${encodeURIComponent(input.list)}/subscribe`,\n { email: input.email, userId: input.userId },\n );\n return { subscribed: res.subscribed };\n }\n\n /** Unsubscribe an identity from a list. */\n async unsubscribe(input: SubscribeInput): Promise<UnsubscribeResult> {\n assertIdentity(input);\n const res = await this.http.post<{ list: string; subscribed: boolean }>(\n `/v1/lists/${encodeURIComponent(input.list)}/unsubscribe`,\n { email: input.email, userId: input.userId },\n );\n return { unsubscribed: res.subscribed === false };\n }\n}\n","import { createHttpClient } from \"./internal/http.js\";\nimport { CampaignsResource } from \"./resources/campaigns.js\";\nimport { ContactsResource } from \"./resources/contacts.js\";\nimport { EmailsResource } from \"./resources/emails.js\";\nimport { EventsResource } from \"./resources/events.js\";\nimport { ListsResource } from \"./resources/lists.js\";\nimport type { HogsendOptions } from \"./types.js\";\n\n/**\n * Typed HTTP client for the Hogsend data plane.\n *\n * ```ts\n * const hs = new Hogsend({ baseUrl: \"https://api.example.com\", apiKey: \"hsk_…\" });\n * await hs.contacts.upsert({ email: \"a@b.com\", properties: { plan: \"pro\" } });\n * await hs.events.send({ userId: \"u_1\", name: \"signup\" });\n * ```\n */\nexport class Hogsend {\n readonly contacts: ContactsResource;\n readonly events: EventsResource;\n readonly emails: EmailsResource;\n readonly lists: ListsResource;\n readonly campaigns: CampaignsResource;\n\n constructor(opts: HogsendOptions) {\n if (!opts.baseUrl) {\n throw new TypeError(\"Hogsend: `baseUrl` is required.\");\n }\n if (!opts.apiKey) {\n throw new TypeError(\"Hogsend: `apiKey` is required.\");\n }\n\n const http = createHttpClient({\n baseUrl: opts.baseUrl.replace(/\\/+$/, \"\"),\n apiKey: opts.apiKey,\n fetch: opts.fetch,\n timeoutMs: opts.timeoutMs,\n headers: opts.headers,\n });\n\n this.contacts = new ContactsResource(http);\n this.events = new EventsResource(http);\n this.emails = new EmailsResource(http);\n this.lists = new ListsResource(http);\n this.campaigns = new CampaignsResource(http);\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/errors.ts","../src/internal/http.ts","../src/resources/campaigns.ts","../src/internal/identity.ts","../src/resources/contacts.ts","../src/resources/emails.ts","../src/resources/events.ts","../src/resources/lists.ts","../src/resources/webhooks.ts","../src/hogsend.ts","../src/internal/verify.ts"],"sourcesContent":["/**\n * A non-2xx response — or a transport-level failure — from the Hogsend data\n * plane. `status` is the HTTP status code, or `0` when the request never\n * reached the server (DNS/connect/timeout). `body` is the parsed JSON body when\n * available, else the raw text, else `undefined`.\n */\nexport class HogsendAPIError extends Error {\n readonly status: number;\n readonly body: unknown;\n\n constructor(message: string, status: number, body: unknown) {\n super(message);\n this.name = \"HogsendAPIError\";\n this.status = status;\n this.body = body;\n // Restore prototype chain for instanceof across transpile targets.\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n/**\n * A `429 Too Many Requests` response. `retryAfter` is the parsed `Retry-After`\n * header in seconds when present (the server sends it on 429 for `/v1/emails`).\n */\nexport class RateLimitError extends HogsendAPIError {\n readonly retryAfter?: number;\n\n constructor(message: string, body: unknown, retryAfter?: number) {\n super(message, 429, body);\n this.name = \"RateLimitError\";\n this.retryAfter = retryAfter;\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n","import { HogsendAPIError, RateLimitError } from \"../errors.js\";\n\n/** Query params accepted by `get` — undefined values are dropped. */\nexport type Query = Record<string, string | number | undefined>;\n\n/** Per-request extras (currently just an idempotency header passthrough). */\nexport interface RequestExtras {\n /** Sent as the `Idempotency-Key` header when set. */\n idempotencyKey?: string;\n}\n\nexport interface HttpClientConfig {\n baseUrl: string;\n apiKey: string;\n fetch?: typeof fetch;\n timeoutMs?: number;\n headers?: Record<string, string>;\n}\n\n/** A minimal, self-contained data-plane HTTP client over native fetch. */\nexport interface HttpClient {\n get<T = unknown>(path: string, query?: Query): Promise<T>;\n post<T = unknown>(\n path: string,\n body: unknown,\n extras?: RequestExtras,\n ): Promise<T>;\n put<T = unknown>(\n path: string,\n body: unknown,\n extras?: RequestExtras,\n ): Promise<T>;\n patch<T = unknown>(\n path: string,\n body: unknown,\n extras?: RequestExtras,\n ): Promise<T>;\n del<T = unknown>(path: string, body?: unknown): Promise<T>;\n}\n\nconst DEFAULT_TIMEOUT_MS = 30_000;\n\nfunction buildUrl(baseUrl: string, path: string, query?: Query): string {\n const url = new URL(path.startsWith(\"/\") ? path : `/${path}`, `${baseUrl}/`);\n if (query) {\n for (const [key, value] of Object.entries(query)) {\n if (value === undefined) continue;\n url.searchParams.set(key, String(value));\n }\n }\n return url.toString();\n}\n\nfunction bodyMessage(status: number, body: unknown): string {\n if (body && typeof body === \"object\") {\n // Application-handler envelope: `{ error: \"human message\" }`.\n const errField = (body as { error?: unknown }).error;\n if (typeof errField === \"string\") {\n return `${status}: ${errField}`;\n }\n // @hono/zod-openapi default-hook validation envelope:\n // `{ success: false, error: <ZodError> }` (no defaultHook configured). The\n // structured ZodError is preserved on `err.body`; surface a short, readable\n // summary for `err.message` instead of the generic fallback.\n if (\n (body as { success?: unknown }).success === false &&\n errField &&\n typeof errField === \"object\"\n ) {\n const summary = JSON.stringify(errField).slice(0, 200);\n return `${status}: validation failed ${summary}`;\n }\n }\n return `request failed with status ${status}`;\n}\n\n/** Parse a `Retry-After` header (seconds form) into a number, else undefined. */\nfunction parseRetryAfter(value: string | null): number | undefined {\n if (!value) return undefined;\n const seconds = Number.parseInt(value, 10);\n return Number.isFinite(seconds) ? seconds : undefined;\n}\n\n/**\n * Builds an {@link HttpClient} bound to a config. Native `fetch`, JSON in/out,\n * `Authorization: Bearer <apiKey>`. Throws typed errors:\n * - {@link RateLimitError} on 429 (with parsed `Retry-After`),\n * - {@link HogsendAPIError} on any other non-2xx,\n * - {@link HogsendAPIError} with `status === 0` on a transport failure\n * (DNS/connect/abort/timeout).\n */\nexport function createHttpClient(config: HttpClientConfig): HttpClient {\n const doFetch = config.fetch ?? fetch;\n const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const extraHeaders = config.headers ?? {};\n\n async function request<T>(\n method: string,\n path: string,\n opts: { query?: Query; body?: unknown; extras?: RequestExtras },\n ): Promise<T> {\n const headers: Record<string, string> = {\n Accept: \"application/json\",\n Authorization: `Bearer ${config.apiKey}`,\n ...extraHeaders,\n };\n if (opts.body !== undefined) {\n headers[\"Content-Type\"] = \"application/json\";\n }\n if (opts.extras?.idempotencyKey) {\n headers[\"Idempotency-Key\"] = opts.extras.idempotencyKey;\n }\n\n const url = buildUrl(config.baseUrl, path, opts.query);\n\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n\n let res: Response;\n try {\n res = await doFetch(url, {\n method,\n headers,\n body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined,\n signal: controller.signal,\n });\n } catch (cause) {\n const msg = cause instanceof Error ? cause.message : String(cause);\n throw new HogsendAPIError(\n `cannot reach ${config.baseUrl} (${msg})`,\n 0,\n undefined,\n );\n } finally {\n clearTimeout(timer);\n }\n\n const text = await res.text();\n let parsed: unknown;\n if (text.length > 0) {\n try {\n parsed = JSON.parse(text);\n } catch {\n parsed = text;\n }\n }\n\n if (!res.ok) {\n if (res.status === 429) {\n throw new RateLimitError(\n bodyMessage(res.status, parsed),\n parsed,\n parseRetryAfter(res.headers.get(\"Retry-After\")),\n );\n }\n throw new HogsendAPIError(\n bodyMessage(res.status, parsed),\n res.status,\n parsed,\n );\n }\n\n return parsed as T;\n }\n\n return {\n get: <T>(path: string, query?: Query) => request<T>(\"GET\", path, { query }),\n post: <T>(path: string, body: unknown, extras?: RequestExtras) =>\n request<T>(\"POST\", path, { body, extras }),\n put: <T>(path: string, body: unknown, extras?: RequestExtras) =>\n request<T>(\"PUT\", path, { body, extras }),\n patch: <T>(path: string, body: unknown, extras?: RequestExtras) =>\n request<T>(\"PATCH\", path, { body, extras }),\n del: <T>(path: string, body?: unknown) =>\n request<T>(\"DELETE\", path, { body }),\n };\n}\n","import type { HttpClient } from \"../internal/http.js\";\nimport type {\n Campaign,\n SendCampaignInput,\n SendCampaignResult,\n} from \"../types.js\";\n\n/** The `campaigns.*` resource bound to an {@link HttpClient}. */\nexport class CampaignsResource {\n constructor(private readonly http: HttpClient) {}\n\n /**\n * Queue a broadcast: durably send one template to every subscribed member of\n * a `list` (or every active member of a `bucket`). Exactly one of `list` /\n * `bucket` must be set; `template`/`props` are type-checked against the\n * augmented `TemplateRegistryMap` when `@hogsend/email` is installed, else\n * degrade to `{ template: string; props? }`.\n *\n * Returns the 202 enqueue ack (`{ campaignId, status }`); the actual sends run\n * asynchronously in the worker. Poll {@link CampaignsResource.get} for counts.\n */\n send(input: SendCampaignInput): Promise<SendCampaignResult> {\n // The discriminated union narrows `template`/`props` and the audience; index\n // into the input via a permissive view to build the wire body without\n // re-discriminating.\n const body = input as SendCampaignInput & {\n list?: string;\n bucket?: string;\n props?: Record<string, unknown>;\n };\n return this.http.post<SendCampaignResult>(\"/v1/campaigns\", {\n name: body.name,\n list: body.list,\n bucket: body.bucket,\n template: body.template,\n props: body.props,\n from: body.from,\n subject: body.subject,\n });\n }\n\n /** Fetch a campaign's current status + send counts. */\n get(id: string): Promise<Campaign> {\n return this.http.get<Campaign>(`/v1/campaigns/${encodeURIComponent(id)}`);\n }\n}\n","/**\n * Runtime guard mirroring the `Identity` union: at least one of `email` /\n * `userId` must be a non-empty string. The type system enforces this at the\n * call site, but runtime callers (plain JS, untyped data) can still violate it,\n * so we fail fast with a clear message before issuing the request.\n */\nexport function assertIdentity(input: {\n email?: string;\n userId?: string;\n}): void {\n const hasEmail = typeof input.email === \"string\" && input.email.length > 0;\n const hasUserId = typeof input.userId === \"string\" && input.userId.length > 0;\n if (!hasEmail && !hasUserId) {\n throw new TypeError(\n \"Hogsend: an identity is required — pass `email`, `userId`, or both.\",\n );\n }\n}\n","import type { HttpClient } from \"../internal/http.js\";\nimport { assertIdentity } from \"../internal/identity.js\";\nimport type {\n Contact,\n DeleteContactInput,\n DeleteContactResult,\n FindContactsInput,\n UpsertContactInput,\n UpsertContactResult,\n} from \"../types.js\";\n\n/** The `contacts.*` resource bound to an {@link HttpClient}. */\nexport class ContactsResource {\n constructor(private readonly http: HttpClient) {}\n\n /**\n * Upsert a contact by identity. Resolves/merges server-side and optionally\n * applies list membership. Returns `{ id, created, linked }`.\n */\n async upsert(input: UpsertContactInput): Promise<UpsertContactResult> {\n assertIdentity(input);\n return this.http.put<UpsertContactResult>(\"/v1/contacts\", {\n email: input.email,\n userId: input.userId,\n properties: input.properties,\n lists: input.lists,\n });\n }\n\n /** Find non-deleted contacts by `email` or `userId`. */\n async find(input: FindContactsInput): Promise<Contact[]> {\n const query: Record<string, string | undefined> = {\n email: \"email\" in input ? input.email : undefined,\n userId: \"userId\" in input ? input.userId : undefined,\n };\n const res = await this.http.get<{ contacts: Contact[] }>(\n \"/v1/contacts/find\",\n query,\n );\n return res.contacts;\n }\n\n /** Soft-delete a contact by identity. */\n async delete(input: DeleteContactInput): Promise<DeleteContactResult> {\n assertIdentity(input);\n return this.http.del<DeleteContactResult>(\"/v1/contacts\", {\n email: input.email,\n userId: input.userId,\n });\n }\n}\n","import type { HttpClient } from \"../internal/http.js\";\nimport type { SendEmailInput, SendEmailResult } from \"../types.js\";\n\n/** The `emails.*` resource bound to an {@link HttpClient}. */\nexport class EmailsResource {\n constructor(private readonly http: HttpClient) {}\n\n /**\n * Send a transactional email by template. Recipient is `to` (raw address) or\n * `userId` (resolved server-side). `template`/`props` are type-checked against\n * the augmented `TemplateRegistryMap` when `@hogsend/email` is installed,\n * else degrade to `{ template: string; props? }`.\n */\n send(input: SendEmailInput): Promise<SendEmailResult> {\n // The discriminated union narrows `template`/`props`; index into the input\n // via a permissive view to build the wire body without re-discriminating.\n const body = input as SendEmailInput & {\n props?: Record<string, unknown>;\n };\n return this.http.post<SendEmailResult>(\n \"/v1/emails\",\n {\n to: body.to,\n userId: body.userId,\n template: body.template,\n props: body.props,\n from: body.from,\n subject: body.subject,\n replyTo: body.replyTo,\n category: body.category,\n skipPreferenceCheck: body.skipPreferenceCheck,\n idempotencyKey: body.idempotencyKey,\n },\n body.idempotencyKey ? { idempotencyKey: body.idempotencyKey } : undefined,\n );\n }\n}\n","import type { HttpClient } from \"../internal/http.js\";\nimport { assertIdentity } from \"../internal/identity.js\";\nimport type { IngestResult, SendEventInput } from \"../types.js\";\n\n/** The `events.*` resource bound to an {@link HttpClient}. */\nexport class EventsResource {\n constructor(private readonly http: HttpClient) {}\n\n /**\n * Send an event through the ingestion pipeline. The two property bags are\n * kept distinct: `eventProperties` feed `trigger.where`/`exitOn`,\n * `contactProperties` merge onto the contact. Optionally apply list\n * membership. Returns the ingest result (stored + exit evaluations).\n *\n * `idempotencyKey` is sent both as the `Idempotency-Key` header (which wins\n * server-side) and in the body, matching `POST /v1/events`.\n */\n send(input: SendEventInput): Promise<IngestResult> {\n assertIdentity(input);\n return this.http.post<IngestResult>(\n \"/v1/events\",\n {\n name: input.name,\n email: input.email,\n userId: input.userId,\n eventProperties: input.eventProperties,\n contactProperties: input.contactProperties,\n lists: input.lists,\n idempotencyKey: input.idempotencyKey,\n },\n input.idempotencyKey\n ? { idempotencyKey: input.idempotencyKey }\n : undefined,\n );\n }\n\n /** Alias of {@link EventsResource.send}. */\n track(input: SendEventInput): Promise<IngestResult> {\n return this.send(input);\n }\n}\n","import type { HttpClient } from \"../internal/http.js\";\nimport { assertIdentity } from \"../internal/identity.js\";\nimport type {\n ListSummary,\n SubscribeInput,\n SubscribeResult,\n UnsubscribeResult,\n} from \"../types.js\";\n\n/** The `lists.*` resource bound to an {@link HttpClient}. */\nexport class ListsResource {\n constructor(private readonly http: HttpClient) {}\n\n /** List all code-defined lists. */\n async list(): Promise<ListSummary[]> {\n const res = await this.http.get<{ lists: ListSummary[] }>(\"/v1/lists\");\n return res.lists;\n }\n\n /** Subscribe an identity to a list. */\n async subscribe(input: SubscribeInput): Promise<SubscribeResult> {\n assertIdentity(input);\n const res = await this.http.post<{ list: string; subscribed: boolean }>(\n `/v1/lists/${encodeURIComponent(input.list)}/subscribe`,\n { email: input.email, userId: input.userId },\n );\n return { subscribed: res.subscribed };\n }\n\n /** Unsubscribe an identity from a list. */\n async unsubscribe(input: SubscribeInput): Promise<UnsubscribeResult> {\n assertIdentity(input);\n const res = await this.http.post<{ list: string; subscribed: boolean }>(\n `/v1/lists/${encodeURIComponent(input.list)}/unsubscribe`,\n { email: input.email, userId: input.userId },\n );\n return { unsubscribed: res.subscribed === false };\n }\n}\n","import type { HttpClient } from \"../internal/http.js\";\nimport type {\n CreatedWebhookEndpoint,\n CreateWebhookInput,\n RotateWebhookSecretResult,\n UpdateWebhookInput,\n WebhookEndpoint,\n} from \"../types.js\";\n\nconst BASE = \"/v1/admin/webhooks\";\n\n/**\n * The `webhooks.*` resource — manage outbound webhook endpoints (the\n * Svix-style signed event stream Hogsend emits to subscriber URLs).\n *\n * IMPORTANT: unlike the rest of the client (which uses an `ingest`-scoped data\n * key), this resource targets the ADMIN plane (`/v1/admin/webhooks`) and\n * REQUIRES a full-admin key. Signing-secret management is the same trust class\n * as API-key management — a leaked ingest key must never register an\n * exfiltration endpoint. Construct the client with an admin `apiKey`.\n *\n * The full signing `secret` (`whsec_…`) is returned ONCE — on\n * {@link WebhooksResource.create} and {@link WebhooksResource.rotateSecret}.\n * `list`/`get` only ever expose the display `secretPrefix`. Store it on create.\n */\nexport class WebhooksResource {\n constructor(private readonly http: HttpClient) {}\n\n /**\n * Register a new endpoint subscribed to one or more outbound event types.\n * Returns the endpoint INCLUDING the full signing `secret` — this is the\n * only time (besides rotate) the secret is returned. Store it now.\n */\n create(input: CreateWebhookInput): Promise<CreatedWebhookEndpoint> {\n return this.http.post<CreatedWebhookEndpoint>(BASE, {\n url: input.url,\n eventTypes: input.eventTypes,\n description: input.description,\n disabled: input.disabled,\n });\n }\n\n /**\n * List endpoints (newest first). Disabled endpoints are hidden unless\n * `includeDisabled` is set. Returns the endpoints array (unwrapped from the\n * `{ endpoints, total, limit, offset }` envelope).\n */\n async list(opts?: {\n limit?: number;\n offset?: number;\n includeDisabled?: boolean;\n }): Promise<WebhookEndpoint[]> {\n const res = await this.http.get<{ endpoints: WebhookEndpoint[] }>(BASE, {\n limit: opts?.limit,\n offset: opts?.offset,\n includeDisabled:\n opts?.includeDisabled === undefined\n ? undefined\n : String(opts.includeDisabled),\n });\n return res.endpoints;\n }\n\n /** Fetch one endpoint by id (404 → {@link HogsendAPIError}). */\n get(id: string): Promise<WebhookEndpoint> {\n return this.http.get<WebhookEndpoint>(`${BASE}/${encodeURIComponent(id)}`);\n }\n\n /**\n * Patch an endpoint. Only the provided fields change; `description: null`\n * clears the description. Does NOT return or rotate the secret.\n */\n update(id: string, input: UpdateWebhookInput): Promise<WebhookEndpoint> {\n return this.http.patch<WebhookEndpoint>(\n `${BASE}/${encodeURIComponent(id)}`,\n {\n url: input.url,\n eventTypes: input.eventTypes,\n description: input.description,\n disabled: input.disabled,\n },\n );\n }\n\n /** Hard-delete an endpoint (cascade drops its deliveries). */\n delete(id: string): Promise<{ deleted: boolean }> {\n return this.http.del<{ deleted: boolean }>(\n `${BASE}/${encodeURIComponent(id)}`,\n );\n }\n\n /**\n * Rotate the signing secret. The OLD secret is invalidated immediately (hard\n * cutover) — update every subscriber with the returned new `secret` (returned\n * ONCE).\n */\n rotateSecret(id: string): Promise<RotateWebhookSecretResult> {\n return this.http.post<RotateWebhookSecretResult>(\n `${BASE}/${encodeURIComponent(id)}/rotate-secret`,\n {},\n );\n }\n\n /**\n * Enqueue an out-of-band `webhook.test` delivery to the endpoint, delivered\n * regardless of its subscribed `eventTypes`. Returns the 202 enqueue ack.\n */\n sendTest(\n id: string,\n ): Promise<{ enqueued: boolean; eventType: \"webhook.test\" }> {\n return this.http.post<{ enqueued: boolean; eventType: \"webhook.test\" }>(\n `${BASE}/${encodeURIComponent(id)}/test`,\n {},\n );\n }\n}\n","import { createHttpClient } from \"./internal/http.js\";\nimport { CampaignsResource } from \"./resources/campaigns.js\";\nimport { ContactsResource } from \"./resources/contacts.js\";\nimport { EmailsResource } from \"./resources/emails.js\";\nimport { EventsResource } from \"./resources/events.js\";\nimport { ListsResource } from \"./resources/lists.js\";\nimport { WebhooksResource } from \"./resources/webhooks.js\";\nimport type { HogsendOptions } from \"./types.js\";\n\n/**\n * Typed HTTP client for the Hogsend data plane.\n *\n * ```ts\n * const hs = new Hogsend({ baseUrl: \"https://api.example.com\", apiKey: \"hsk_…\" });\n * await hs.contacts.upsert({ email: \"a@b.com\", properties: { plan: \"pro\" } });\n * await hs.events.send({ userId: \"u_1\", name: \"signup\" });\n * ```\n */\nexport class Hogsend {\n readonly contacts: ContactsResource;\n readonly events: EventsResource;\n readonly emails: EmailsResource;\n readonly lists: ListsResource;\n readonly campaigns: CampaignsResource;\n /**\n * Manage outbound webhook endpoints (the signed event stream Hogsend emits to\n * subscriber URLs). REQUIRES a full-admin `apiKey` — this resource hits the\n * admin plane (`/v1/admin/webhooks`), NOT the ingest data plane the other\n * resources use. See {@link WebhooksResource}.\n */\n readonly webhooks: WebhooksResource;\n\n constructor(opts: HogsendOptions) {\n if (!opts.baseUrl) {\n throw new TypeError(\"Hogsend: `baseUrl` is required.\");\n }\n if (!opts.apiKey) {\n throw new TypeError(\"Hogsend: `apiKey` is required.\");\n }\n\n const http = createHttpClient({\n baseUrl: opts.baseUrl.replace(/\\/+$/, \"\"),\n apiKey: opts.apiKey,\n fetch: opts.fetch,\n timeoutMs: opts.timeoutMs,\n headers: opts.headers,\n });\n\n this.contacts = new ContactsResource(http);\n this.events = new EventsResource(http);\n this.emails = new EmailsResource(http);\n this.lists = new ListsResource(http);\n this.campaigns = new CampaignsResource(http);\n this.webhooks = new WebhooksResource(http);\n }\n}\n","import { createHmac, timingSafeEqual } from \"node:crypto\";\nimport { Webhook } from \"svix\";\n\n/** Default tolerance (seconds) for the timestamp freshness check. */\nconst TOLERANCE_SECONDS = 5 * 60;\n\n/**\n * Lowercase every header key so callers can pass the Title-Case headers Hogsend\n * sends (`Webhook-Id`/`Webhook-Timestamp`/`Webhook-Signature`) OR the lowercase\n * form a framework may hand back. Svix expects lowercase `svix-*`/`webhook-*`.\n */\nfunction normalizeHeaders(\n headers: Record<string, string>,\n): Record<string, string> {\n const out: Record<string, string> = {};\n for (const [key, value] of Object.entries(headers)) {\n out[key.toLowerCase()] = value;\n }\n return out;\n}\n\n/**\n * Verify and parse an INBOUND Hogsend outbound-webhook delivery, the\n * subscriber-side counterpart of the engine's signing. Use this in a handler\n * that receives Hogsend's signed POSTs to confirm authenticity before trusting\n * the body.\n *\n * Pass the RAW request body bytes (the exact string Hogsend signed — never a\n * re-stringified object), the request headers, and the endpoint's `whsec_…`\n * signing secret (from create / rotate-secret). Returns the parsed event\n * envelope (`{ id, type, timestamp, data }`) on success.\n *\n * Throws on a bad signature, a missing signature header, or a timestamp outside\n * the 5-minute tolerance window.\n *\n * Implementation: wraps svix's `Webhook.verify` (constant-time, tolerance-\n * checked); if svix cannot run for any reason it falls back to a pure\n * `node:crypto` HMAC-SHA256 check over `${id}.${timestamp}.${body}` with a\n * `timingSafeEqual` compare against the `v1,<base64>` signature(s).\n *\n * @example\n * ```ts\n * import { verifyHogsendWebhook } from \"@hogsend/client\";\n *\n * app.post(\"/webhooks/hogsend\", async (req, res) => {\n * const body = await readRawBody(req); // the exact bytes\n * try {\n * const event = verifyHogsendWebhook({\n * payload: body,\n * headers: req.headers,\n * secret: process.env.HOGSEND_WEBHOOK_SECRET!,\n * });\n * // handle event.type ...\n * res.sendStatus(200);\n * } catch {\n * res.sendStatus(401);\n * }\n * });\n * ```\n */\nexport function verifyHogsendWebhook(opts: {\n payload: string;\n headers: Record<string, string>;\n secret: string;\n}): unknown {\n const headers = normalizeHeaders(opts.headers);\n const id = headers[\"webhook-id\"] ?? headers[\"svix-id\"];\n const timestamp = headers[\"webhook-timestamp\"] ?? headers[\"svix-timestamp\"];\n const signature = headers[\"webhook-signature\"] ?? headers[\"svix-signature\"];\n\n if (!id || !timestamp || !signature) {\n throw new Error(\n \"verifyHogsendWebhook: missing Webhook-Id / Webhook-Timestamp / Webhook-Signature header\",\n );\n }\n\n try {\n const wh = new Webhook(opts.secret);\n return wh.verify(opts.payload, {\n \"svix-id\": id,\n \"svix-timestamp\": timestamp,\n \"svix-signature\": signature,\n });\n } catch {\n // svix unavailable (tree-shaken) or threw — fall back to node:crypto. We\n // re-run the SAME canonical check so a genuine signature/timestamp failure\n // still throws; only a svix-internal/import failure is \"rescued\".\n return verifyWithNodeCrypto({\n payload: opts.payload,\n secret: opts.secret,\n id,\n timestamp,\n signature,\n });\n }\n}\n\n/**\n * Pure `node:crypto` verification of the Svix signature scheme. Mirrors the\n * documented fallback in the engine's webhook-signing lib:\n * `HMAC_SHA256(base64-decoded secret, `${id}.${ts}.${body}`)` → `v1,<base64>`,\n * `timingSafeEqual` against each space-separated signature in the header.\n */\nfunction verifyWithNodeCrypto(opts: {\n payload: string;\n secret: string;\n id: string;\n timestamp: string;\n signature: string;\n}): unknown {\n const ts = Number.parseInt(opts.timestamp, 10);\n if (!Number.isFinite(ts)) {\n throw new Error(\"verifyHogsendWebhook: invalid Webhook-Timestamp\");\n }\n const now = Math.floor(Date.now() / 1000);\n if (Math.abs(now - ts) > TOLERANCE_SECONDS) {\n throw new Error(\"verifyHogsendWebhook: timestamp outside tolerance window\");\n }\n\n // The secret is `whsec_<base64>`; the signing key is the base64-decoded body.\n const key = opts.secret.startsWith(\"whsec_\")\n ? Buffer.from(opts.secret.slice(6), \"base64\")\n : Buffer.from(opts.secret, \"base64\");\n const signedContent = `${opts.id}.${opts.timestamp}.${opts.payload}`;\n const expected = createHmac(\"sha256\", key)\n .update(signedContent)\n .digest(\"base64\");\n const expectedBuf = Buffer.from(expected);\n\n // The header is space-separated `v1,<sig>` pairs; accept if ANY matches.\n const matched = opts.signature.split(\" \").some((part) => {\n const sig = part.startsWith(\"v1,\") ? part.slice(3) : part;\n const candidate = Buffer.from(sig);\n return (\n candidate.length === expectedBuf.length &&\n timingSafeEqual(candidate, expectedBuf)\n );\n });\n\n if (!matched) {\n throw new Error(\"verifyHogsendWebhook: signature verification failed\");\n }\n\n return JSON.parse(opts.payload);\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hogsend/client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "Typed HTTP client for the Hogsend data plane (contacts, events, emails, lists).",
|
|
@@ -27,24 +27,19 @@
|
|
|
27
27
|
"publishConfig": {
|
|
28
28
|
"access": "public"
|
|
29
29
|
},
|
|
30
|
-
"peerDependencies": {
|
|
31
|
-
"@hogsend/email": "^0.6.0"
|
|
32
|
-
},
|
|
33
|
-
"peerDependenciesMeta": {
|
|
34
|
-
"@hogsend/email": {
|
|
35
|
-
"optional": true
|
|
36
|
-
}
|
|
37
|
-
},
|
|
38
30
|
"devDependencies": {
|
|
39
31
|
"@types/node": "^22.15.3",
|
|
40
32
|
"tsup": "^8.5.1",
|
|
41
33
|
"vitest": "^4.1.7",
|
|
42
|
-
"@
|
|
43
|
-
"@
|
|
34
|
+
"@hogsend/email": "^0.8.0",
|
|
35
|
+
"@repo/typescript-config": "0.0.0"
|
|
44
36
|
},
|
|
45
37
|
"engines": {
|
|
46
38
|
"node": ">=22"
|
|
47
39
|
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"svix": "^1.95.1"
|
|
42
|
+
},
|
|
48
43
|
"scripts": {
|
|
49
44
|
"build": "tsup",
|
|
50
45
|
"check-types": "tsc --noEmit",
|