@hogsend/engine 0.21.0 → 0.21.1

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hogsend/engine",
3
- "version": "0.21.0",
3
+ "version": "0.21.1",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -40,14 +40,14 @@
40
40
  "svix": "^1.95.1",
41
41
  "winston": "^3.19.0",
42
42
  "zod": "^4.4.3",
43
- "@hogsend/core": "^0.21.0",
44
- "@hogsend/db": "^0.21.0",
45
- "@hogsend/email": "^0.21.0",
46
- "@hogsend/plugin-posthog": "^0.21.0",
47
- "@hogsend/plugin-resend": "^0.21.0"
43
+ "@hogsend/core": "^0.21.1",
44
+ "@hogsend/db": "^0.21.1",
45
+ "@hogsend/email": "^0.21.1",
46
+ "@hogsend/plugin-posthog": "^0.21.1",
47
+ "@hogsend/plugin-resend": "^0.21.1"
48
48
  },
49
49
  "optionalDependencies": {
50
- "@hogsend/plugin-postmark": "^0.21.0"
50
+ "@hogsend/plugin-postmark": "^0.21.1"
51
51
  },
52
52
  "devDependencies": {
53
53
  "@types/node": "^22.15.3",
@@ -297,6 +297,25 @@ export async function deleteProviderCredential(
297
297
  return deleted.length > 0;
298
298
  }
299
299
 
300
+ /**
301
+ * Purge EVERY stored credential for a provider — the oauth grant AND the
302
+ * server-derived config (minted webhook secret + grabbed phc_). Disconnect
303
+ * must leave no orphaned rows; the single-kind `deleteProviderCredential`
304
+ * stays unchanged so token-lifecycle callers can still target one kind.
305
+ * Returns which kinds were removed. Never decrypts.
306
+ */
307
+ export async function deleteAllProviderCredentials(
308
+ db: Database,
309
+ providerId: string,
310
+ ): Promise<{ oauth: boolean; derived: boolean }> {
311
+ const [oauth, derived] = await Promise.all([
312
+ deleteProviderCredential(db, providerId, "oauth"),
313
+ deleteProviderCredential(db, providerId, "derived"),
314
+ ]);
315
+
316
+ return { oauth, derived };
317
+ }
318
+
300
319
  /**
301
320
  * Read + decrypt the kind="derived" config for a provider. `null` when none
302
321
  * stored; throws `ProviderCredentialDecryptError` when a row exists but cannot
@@ -14,6 +14,7 @@ import {
14
14
  provisionPostHogLoop,
15
15
  } from "../../lib/provision-posthog-loop.js";
16
16
  import { errorSchema } from "../../lib/schemas.js";
17
+ import { invalidateStoredPosthogSecret } from "../webhooks/sources.js";
17
18
 
18
19
  /**
19
20
  * Admin analytics-connection routes — the server half of
@@ -170,9 +171,8 @@ const provisionLoopRoute = createRoute({
170
171
  description:
171
172
  "Refused: `no_posthog_credential` (no OAuth credential and no " +
172
173
  "personal API key), `posthog_not_configured` (no PostHog env signal " +
173
- "at all), `webhook_secret_missing` (POSTHOG_WEBHOOK_SECRET unset), " +
174
- "or `api_public_url_unreachable` (API_PUBLIC_URL is loopback " +
175
- "PostHog cannot deliver to it)",
174
+ "at all), or `api_public_url_unreachable` (API_PUBLIC_URL is loopback " +
175
+ " PostHog cannot deliver to it)",
176
176
  },
177
177
  502: {
178
178
  content: { "application/json": { schema: provisionFailureSchema } },
@@ -273,6 +273,9 @@ export const analyticsAdminRouter = new OpenAPIHono<AppEnv>()
273
273
  ...(storedDerived ?? {}),
274
274
  webhookSecret,
275
275
  });
276
+ // Bust the inbound posthog webhook source's cached secret so it enforces
277
+ // the freshly-minted value immediately instead of after the ~30s TTL.
278
+ invalidateStoredPosthogSecret();
276
279
  }
277
280
 
278
281
  try {
@@ -318,9 +321,6 @@ export const analyticsAdminRouter = new OpenAPIHono<AppEnv>()
318
321
  );
319
322
  } catch (error) {
320
323
  if (error instanceof ProvisionPostHogLoopError) {
321
- if (error.code === "missing-webhook-secret") {
322
- return c.json({ error: "webhook_secret_missing" }, 409);
323
- }
324
324
  return c.json(
325
325
  {
326
326
  error: error.code,
@@ -1,7 +1,7 @@
1
1
  import { createRoute, OpenAPIHono, z } from "@hono/zod-openapi";
2
2
  import type { AppEnv } from "../../app.js";
3
3
  import {
4
- deleteProviderCredential,
4
+ deleteAllProviderCredentials,
5
5
  getProviderCredential,
6
6
  ProviderCredentialDecryptError,
7
7
  type ProviderCredentialMeta,
@@ -108,7 +108,11 @@ const deleteRoute = createRoute({
108
108
  method: "delete",
109
109
  path: "/{providerId}",
110
110
  tags: ["Admin — Provider Credentials"],
111
- summary: "Delete a provider credential",
111
+ summary: "Purge a provider's stored credentials (oauth + derived)",
112
+ description:
113
+ "Disconnect: hard-deletes EVERY stored credential for the provider — " +
114
+ "the oauth grant AND the server-derived config (minted webhook secret + " +
115
+ "grabbed phc_) — so no orphaned rows remain.",
112
116
  request: {
113
117
  params: providerIdParam,
114
118
  },
@@ -117,11 +121,11 @@ const deleteRoute = createRoute({
117
121
  content: {
118
122
  "application/json": { schema: z.object({ deleted: z.boolean() }) },
119
123
  },
120
- description: "Credential hard-deleted",
124
+ description: "Credentials hard-deleted (at least one row removed)",
121
125
  },
122
126
  404: {
123
127
  content: { "application/json": { schema: errorSchema } },
124
- description: "No credential stored for this provider",
128
+ description: "No credentials stored for this provider",
125
129
  },
126
130
  },
127
131
  });
@@ -180,8 +184,13 @@ export const providerCredentialsRouter = new OpenAPIHono<AppEnv>()
180
184
 
181
185
  // Never decrypts — DELETE must succeed even when the payload is
182
186
  // undecryptable (the operator's escape hatch after a secret rotation).
183
- const deleted = await deleteProviderCredential(db, providerId);
184
- if (!deleted) {
187
+ // Disconnect purges BOTH kinds (oauth grant + derived config) so the
188
+ // minted webhook secret + grabbed phc_ never linger orphaned.
189
+ const { oauth, derived } = await deleteAllProviderCredentials(
190
+ db,
191
+ providerId,
192
+ );
193
+ if (!oauth && !derived) {
185
194
  return c.json({ error: "Provider credential not found" }, 404);
186
195
  }
187
196
  return c.json({ deleted: true }, 200);
@@ -52,6 +52,16 @@ async function resolveStoredPosthogSecret(
52
52
  return value;
53
53
  }
54
54
 
55
+ /**
56
+ * Drop the module-level stored-secret cache so the next inbound PostHog webhook
57
+ * re-reads from the `kind="derived"` store. Called right after `hogsend connect`
58
+ * mints + persists a secret, so the freshly-minted value is enforced
59
+ * immediately instead of waiting out the `STORED_SECRET_RECHECK_MS` window.
60
+ */
61
+ export function invalidateStoredPosthogSecret(): void {
62
+ storedPosthogSecret = undefined;
63
+ }
64
+
55
65
  export function registerWebhookSourceRoutes(
56
66
  app: OpenAPIHono<AppEnv>,
57
67
  sources: DefinedWebhookSource[],