@hogsend/core 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,6 +4,17 @@ Core types, Zod schemas, the condition-evaluation engine, duration helpers, and
4
4
  the `JourneyRegistry` for [Hogsend](https://github.com/dougwithseismic/hogsend) —
5
5
  a code-first lifecycle orchestration engine for teams on PostHog + Resend.
6
6
 
7
+ ## Capability-provider contracts
8
+
9
+ Core also **owns** the capability-provider contracts — `EmailProvider` (with its
10
+ supporting `SendEmailOptions`, `BatchEmailItem`, `SendResult`, `WebhookEvent`,
11
+ `WebhookEventType`, `WebhookHandlerMap`) and `PostHogService` (with
12
+ `CaptureOptions`). These are the engine-owned contracts a swappable email/analytics
13
+ implementation satisfies; they live here and are re-exported by `@hogsend/engine`
14
+ (the canonical author import) and the vendor plugins (`@hogsend/plugin-resend`,
15
+ `@hogsend/plugin-posthog`) for back-compat. See
16
+ [docs/adr/0001-provider-boundary.md](https://github.com/dougwithseismic/hogsend/blob/main/docs/adr/0001-provider-boundary.md).
17
+
7
18
  This package ships raw TypeScript source; consumers bundle it via their own build
8
19
  (tsup `noExternal`). See the repo docs for the full architecture and the
9
20
  [release model](https://github.com/dougwithseismic/hogsend/blob/main/docs/RELEASING.md).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hogsend/core",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -14,6 +14,7 @@
14
14
  "exports": {
15
15
  ".": "./src/index.ts",
16
16
  "./types": "./src/types/index.ts",
17
+ "./providers": "./src/providers/index.ts",
17
18
  "./registry": "./src/registry/index.ts",
18
19
  "./conditions": "./src/conditions/index.ts",
19
20
  "./schedule": "./src/schedule/index.ts",
@@ -31,10 +32,11 @@
31
32
  "drizzle-orm": "^0.45.2",
32
33
  "iana-db-timezones": "^0.3.0",
33
34
  "zod": "^4.4.3",
34
- "@hogsend/db": "^0.3.0"
35
+ "@hogsend/db": "^0.5.0"
35
36
  },
36
37
  "devDependencies": {
37
38
  "@types/node": "latest",
39
+ "@types/react": "^19.2.16",
38
40
  "vitest": "^4.1.8",
39
41
  "@repo/typescript-config": "0.0.0"
40
42
  },
package/src/index.ts CHANGED
@@ -15,6 +15,7 @@ export {
15
15
  hours,
16
16
  minutes,
17
17
  } from "./duration.js";
18
+ export * from "./providers/index.js";
18
19
  export {
19
20
  BucketRegistry,
20
21
  collectEventNames,
@@ -0,0 +1,20 @@
1
+ export interface PostHogService {
2
+ getPersonProperties(distinctId: string): Promise<Record<string, unknown>>;
3
+
4
+ captureEvent(opts: CaptureOptions): void;
5
+
6
+ identify(distinctId: string, properties: Record<string, unknown>): void;
7
+
8
+ isFeatureEnabled(opts: {
9
+ distinctId: string;
10
+ flag: string;
11
+ }): Promise<boolean>;
12
+
13
+ shutdown(): Promise<void>;
14
+ }
15
+
16
+ export interface CaptureOptions {
17
+ distinctId: string;
18
+ event: string;
19
+ properties?: Record<string, unknown>;
20
+ }
@@ -0,0 +1,138 @@
1
+ import type { ReactElement } from "react";
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // Send options
5
+ // ---------------------------------------------------------------------------
6
+
7
+ export interface SendEmailOptions {
8
+ from: string;
9
+ to: string | string[];
10
+ subject: string;
11
+ react?: ReactElement;
12
+ html?: string;
13
+ replyTo?: string | string[];
14
+ cc?: string | string[];
15
+ bcc?: string | string[];
16
+ scheduledAt?: string;
17
+ tags?: Array<{ name: string; value: string }>;
18
+ headers?: Record<string, string>;
19
+ }
20
+
21
+ export interface BatchEmailItem {
22
+ from: string;
23
+ to: string | string[];
24
+ subject: string;
25
+ react: ReactElement;
26
+ replyTo?: string | string[];
27
+ cc?: string | string[];
28
+ bcc?: string | string[];
29
+ tags?: Array<{ name: string; value: string }>;
30
+ headers?: Record<string, string>;
31
+ }
32
+
33
+ export interface SendResult {
34
+ id: string;
35
+ }
36
+
37
+ // ---------------------------------------------------------------------------
38
+ // Webhook events
39
+ // ---------------------------------------------------------------------------
40
+
41
+ interface WebhookEventBase {
42
+ created_at: string;
43
+ data: {
44
+ email_id: string;
45
+ from: string;
46
+ to: string[];
47
+ subject: string;
48
+ created_at: string;
49
+ };
50
+ }
51
+
52
+ export interface EmailSentEvent extends WebhookEventBase {
53
+ type: "email.sent";
54
+ }
55
+
56
+ export interface EmailDeliveredEvent extends WebhookEventBase {
57
+ type: "email.delivered";
58
+ }
59
+
60
+ export interface EmailBouncedEvent extends WebhookEventBase {
61
+ type: "email.bounced";
62
+ data: WebhookEventBase["data"] & {
63
+ bounce: {
64
+ message: string;
65
+ type: string;
66
+ };
67
+ };
68
+ }
69
+
70
+ export interface EmailComplainedEvent extends WebhookEventBase {
71
+ type: "email.complained";
72
+ }
73
+
74
+ export interface EmailDeliveryDelayedEvent extends WebhookEventBase {
75
+ type: "email.delivery_delayed";
76
+ }
77
+
78
+ export interface EmailOpenedEvent extends WebhookEventBase {
79
+ type: "email.opened";
80
+ }
81
+
82
+ export interface EmailClickedEvent extends WebhookEventBase {
83
+ type: "email.clicked";
84
+ data: WebhookEventBase["data"] & {
85
+ click: {
86
+ link: string;
87
+ timestamp: string;
88
+ ipAddress: string;
89
+ userAgent: string;
90
+ };
91
+ };
92
+ }
93
+
94
+ export type WebhookEvent =
95
+ | EmailSentEvent
96
+ | EmailDeliveredEvent
97
+ | EmailBouncedEvent
98
+ | EmailComplainedEvent
99
+ | EmailDeliveryDelayedEvent
100
+ | EmailOpenedEvent
101
+ | EmailClickedEvent;
102
+
103
+ export type WebhookEventType = WebhookEvent["type"];
104
+
105
+ export type WebhookHandlerMap = {
106
+ [K in WebhookEventType]?: (
107
+ event: Extract<WebhookEvent, { type: K }>,
108
+ ) => void | Promise<void>;
109
+ };
110
+
111
+ // ---------------------------------------------------------------------------
112
+ // EmailProvider contract (the entire provider surface)
113
+ // ---------------------------------------------------------------------------
114
+
115
+ /**
116
+ * The dumb delivery + webhook parse/verify contract every email provider
117
+ * implements (Resend, Postmark, SES, …). All tracking, DB, preference, and
118
+ * render logic lives in the engine's `createTrackedMailer`, never here.
119
+ */
120
+ export interface EmailProvider {
121
+ /** Deliver a single message. Returns the provider message id. */
122
+ send(options: SendEmailOptions): Promise<SendResult>;
123
+
124
+ /** Deliver a batch of messages. */
125
+ sendBatch(emails: BatchEmailItem[]): Promise<{ results: SendResult[] }>;
126
+
127
+ /**
128
+ * Verify a provider webhook signature and return the parsed event. Throws
129
+ * if the signature is missing/invalid.
130
+ */
131
+ verifyWebhook(opts: {
132
+ payload: string;
133
+ headers: Record<string, string>;
134
+ }): WebhookEvent;
135
+
136
+ /** Parse an unsigned webhook payload (used in trusted contexts/tests). */
137
+ parseWebhook(payload: string): WebhookEvent;
138
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./analytics.js";
2
+ export * from "./email.js";
@@ -96,12 +96,47 @@ export interface EmailHistoryResult {
96
96
  count: number;
97
97
  }
98
98
 
99
+ export interface WaitForEventOptions {
100
+ /** Event name to wait for (use your `Events` constant). Matched verbatim. */
101
+ event: string;
102
+ /**
103
+ * Max time to wait before resolving as timed-out. Required: an unbounded wait
104
+ * is only capped by the task's execution timeout and would fail rather than
105
+ * resume. Keep it within the journey execution timeout (720h / 30 days).
106
+ */
107
+ timeout: DurationObject;
108
+ /** Optional observability label written to `currentNodeId` while waiting. */
109
+ label?: string;
110
+ }
111
+
112
+ export interface WaitForEventResult {
113
+ /** `true` when the `timeout` elapsed first; `false` when the event fired. */
114
+ timedOut: boolean;
115
+ }
116
+
99
117
  export interface JourneyContext {
100
118
  sleep(opts: SleepOptions): Promise<SleepResult>;
101
119
 
102
120
  /** Durable sleep until an absolute instant (`Date` or ISO string). */
103
121
  sleepUntil(at: Date | string, opts?: SleepUntilOptions): Promise<SleepResult>;
104
122
 
123
+ /**
124
+ * Durably wait until THIS user emits `event`, or `timeout` elapses —
125
+ * whichever comes first. The state is marked `"waiting"` while suspended and
126
+ * `"active"` again on resume. Returns `{ timedOut }` so the journey can branch
127
+ * (e.g. send a nudge on timeout, do nothing if the event arrived).
128
+ *
129
+ * Forward-looking: only events emitted AFTER the wait is established count —
130
+ * use `ctx.history.hasEvent` to check whether something already happened.
131
+ *
132
+ * If the journey exits (via `exitOn`) or is cancelled while waiting, the run
133
+ * is aborted cleanly (a `JourneyExitedError` is thrown and handled by the
134
+ * engine) so no post-wait side effects fire. After a long wait you should
135
+ * still re-check `ctx.guard.isSubscribed()` before sending, since an
136
+ * unsubscribe does not exit the journey.
137
+ */
138
+ waitForEvent(opts: WaitForEventOptions): Promise<WaitForEventResult>;
139
+
105
140
  /** Timezone-bound fluent scheduler. Always terminates in a `Date`. */
106
141
  when: WhenBuilder;
107
142