@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.
- package/package.json +7 -7
- package/src/container.ts +10 -3
- package/src/destinations/presets/posthog.ts +132 -30
- package/src/index.ts +31 -0
- package/src/lib/analytics-providers-from-env.ts +48 -5
- package/src/lib/oauth-token-manager.ts +353 -0
- package/src/lib/posthog-scopes.ts +29 -0
- package/src/lib/provider-credentials.ts +349 -0
- package/src/lib/provision-posthog-loop.ts +744 -0
- package/src/lib/seed-posthog-destination.ts +38 -7
- package/src/routes/admin/analytics.ts +335 -0
- package/src/routes/admin/index.ts +4 -0
- package/src/routes/admin/provider-credentials.ts +188 -0
- package/src/routes/webhooks/sources.ts +60 -1
|
@@ -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
|
-
|
|
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).
|