@hogsend/engine 0.19.0 → 0.21.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.
@@ -1,10 +1,57 @@
1
+ import type { Database } from "@hogsend/db";
1
2
  import { createRoute, type OpenAPIHono, z } from "@hono/zod-openapi";
2
3
  import type { AppEnv } from "../../app.js";
3
4
  import { headersToRecord } from "../../lib/headers.js";
4
5
  import { ingestEvent } from "../../lib/ingestion.js";
6
+ import type { Logger } from "../../lib/logger.js";
7
+ import { getDerivedCredential } from "../../lib/provider-credentials.js";
5
8
  import type { DefinedWebhookSource } from "../../webhook-sources/define-webhook-source.js";
6
9
  import { verifySignature } from "../../webhook-sources/verify.js";
7
10
 
11
+ /** Negative-cache window for the stored PostHog secret (mirrors the token
12
+ * manager's ABSENT_RECHECK_MS) — caches present AND absent results so an
13
+ * inbound PostHog webhook POST does not hit the DB on every request. */
14
+ const STORED_SECRET_RECHECK_MS = 30_000;
15
+
16
+ let storedPosthogSecret: { value: string | undefined; at: number } | undefined;
17
+
18
+ /**
19
+ * The minted PostHog webhook secret falls back to the `kind="derived"` store
20
+ * when `POSTHOG_WEBHOOK_SECRET` is unset — so an inbound event verifies WITHOUT
21
+ * a redeploy after `hogsend connect posthog`. Cached (present and absent) for
22
+ * `STORED_SECRET_RECHECK_MS` to keep the hot webhook path off the DB.
23
+ *
24
+ * A store read failure (e.g. DB blip, or a derived row that no longer decrypts)
25
+ * resolves to `undefined` rather than throwing — the inbound webhook path must
26
+ * not 500 on a degraded store. `undefined` keeps match-auth in its pre-feature
27
+ * posture (OPEN when no secret is configured anywhere), and the failure is
28
+ * logged so the misconfiguration is still visible.
29
+ */
30
+ async function resolveStoredPosthogSecret(
31
+ db: Database,
32
+ logger: Logger,
33
+ ): Promise<string | undefined> {
34
+ const now = Date.now();
35
+ if (
36
+ storedPosthogSecret &&
37
+ now - storedPosthogSecret.at <= STORED_SECRET_RECHECK_MS
38
+ ) {
39
+ return storedPosthogSecret.value;
40
+ }
41
+
42
+ let value: string | undefined;
43
+ try {
44
+ value = (await getDerivedCredential(db, "posthog"))?.webhookSecret;
45
+ } catch (err) {
46
+ logger.warn("Failed to resolve stored PostHog webhook secret", {
47
+ error: err instanceof Error ? err.message : String(err),
48
+ });
49
+ value = undefined;
50
+ }
51
+ storedPosthogSecret = { value, at: now };
52
+ return value;
53
+ }
54
+
8
55
  export function registerWebhookSourceRoutes(
9
56
  app: OpenAPIHono<AppEnv>,
10
57
  sources: DefinedWebhookSource[],
@@ -72,10 +119,22 @@ export function registerWebhookSourceRoutes(
72
119
  const rawBody = await c.req.text();
73
120
  const headers = headersToRecord(c.req.raw.headers);
74
121
 
75
- const secret = env[source.auth.envKey as keyof typeof env] as
122
+ let secret = env[source.auth.envKey as keyof typeof env] as
76
123
  | string
77
124
  | undefined;
78
125
 
126
+ // For the inbound PostHog source, fall back to the secret minted by
127
+ // `hogsend connect` (kind="derived" store) when env has none — so an
128
+ // inbound event verifies WITHOUT a redeploy. Leaves match-auth OPEN when
129
+ // neither env nor the store has a secret (current behavior preserved).
130
+ if (
131
+ !secret &&
132
+ source.auth.type === "match" &&
133
+ source.auth.envKey === "POSTHOG_WEBHOOK_SECRET"
134
+ ) {
135
+ secret = await resolveStoredPosthogSecret(db, logger);
136
+ }
137
+
79
138
  if (source.auth.type === "signature") {
80
139
  // Signature sources FAIL CLOSED: an unset secret is a 401, never an open
81
140
  // pass-through (deliberate divergence from the "match" variant).