Skip to content

Commit 34488a4

Browse files
committed
create webhook ui
1 parent a15e51b commit 34488a4

22 files changed

+741
-161
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
"lint": "turbo lint",
2626
"prepare": "husky",
2727
"release": "node ./scripts/release.ts",
28-
"server": "NODE_ENV=development OTEL_DENO=true deno serve -REW --watch ./scripts/server.ts",
28+
"server": "NODE_ENV=development OTEL_DENO=true deno serve -RNEW --watch --unstable-raw-imports ./scripts/server.ts",
2929
"test": "turbo test",
3030
"ui:build": "yarn workspace @storybooker/ui build",
3131
"ui:dev": "yarn workspace @storybooker/ui dev",

packages/core/deno.json

Lines changed: 25 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -14,29 +14,30 @@
1414
"@azure/core-rest-pipeline": "npm:@azure/core-rest-pipeline@1.22.2"
1515
},
1616
"exports": {
17-
".": "./dist/index.mjs",
18-
"./_internal/adapter": "./dist/_internal/adapter.mjs",
19-
"./_internal/adapter/auth": "./dist/_internal/adapter/auth.mjs",
20-
"./_internal/adapter/database": "./dist/_internal/adapter/database.mjs",
21-
"./_internal/adapter/logger": "./dist/_internal/adapter/logger.mjs",
22-
"./_internal/adapter/storage": "./dist/_internal/adapter/storage.mjs",
23-
"./_internal/adapter/ui": "./dist/_internal/adapter/ui.mjs",
24-
"./_internal/constants": "./dist/_internal/constants.mjs",
25-
"./_internal/router": "./dist/_internal/router.mjs",
26-
"./_internal/types": "./dist/_internal/types.mjs",
27-
"./_internal/utils": "./dist/_internal/utils.mjs",
28-
"./aws-dynamodb": "./dist/aws-dynamodb.mjs",
29-
"./aws-s3": "./dist/aws-s3.mjs",
30-
"./azure-blob-storage": "./dist/azure-blob-storage.mjs",
31-
"./azure-cosmos-db": "./dist/azure-cosmos-db.mjs",
32-
"./azure-data-tables": "./dist/azure-data-tables.mjs",
33-
"./azure-easy-auth": "./dist/azure-easy-auth.mjs",
34-
"./azure-functions": "./dist/azure-functions.mjs",
35-
"./fs": "./dist/fs.mjs",
36-
"./gcp-big-table": "./dist/gcp-big-table.mjs",
37-
"./gcp-firestore": "./dist/gcp-firestore.mjs",
38-
"./gcp-storage": "./dist/gcp-storage.mjs",
39-
"./mysql": "./dist/mysql.mjs",
40-
"./redis": "./dist/redis.mjs"
17+
".": "./src/index.ts",
18+
"./_internal/adapter": "./src/adapters/_internal/index.ts",
19+
"./_internal/adapter/auth": "./src/adapters/_internal/auth.ts",
20+
"./_internal/adapter/database": "./src/adapters/_internal/database.ts",
21+
"./_internal/adapter/logger": "./src/adapters/_internal/logger.ts",
22+
"./_internal/adapter/storage": "./src/adapters/_internal/storage.ts",
23+
"./_internal/adapter/ui": "./src/adapters/_internal/ui.ts",
24+
"./_internal/constants": "./src/utils/constants.ts",
25+
"./_internal/router": "./src/routers/_app-router.ts",
26+
"./_internal/types": "./src/types.ts",
27+
"./_internal/utils": "./src/utils/index.ts",
28+
"./aws-dynamodb": "./src/adapters/aws-dynamodb.ts",
29+
"./aws-s3": "./src/adapters/aws-s3.ts",
30+
"./azure-blob-storage": "./src/adapters/azure-blob-storage.ts",
31+
"./azure-cosmos-db": "./src/adapters/azure-cosmos-db.ts",
32+
"./azure-data-tables": "./src/adapters/azure-data-tables.ts",
33+
"./azure-easy-auth": "./src/adapters/azure-easy-auth.ts",
34+
"./azure-functions": "./src/adapters/azure-functions.ts",
35+
"./fs": "./src/adapters/fs.ts",
36+
"./gcp-big-table": "./src/adapters/gcp-big-table.ts",
37+
"./gcp-firestore": "./src/adapters/gcp-firestore.ts",
38+
"./gcp-storage": "./src/adapters/gcp-storage.ts",
39+
"./mysql": "./src/adapters/mysql.ts",
40+
"./redis": "./src/adapters/redis.ts",
41+
"./openapi.json": "./openapi.json"
4142
}
4243
}

packages/core/package.json

Lines changed: 127 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -111,30 +111,132 @@
111111
"module": "./dist/index.mjs",
112112
"types": "./dist/index.d.mts",
113113
"exports": {
114-
".": "./dist/index.mjs",
115-
"./_internal/adapter": "./dist/_internal/adapter.mjs",
116-
"./_internal/adapter/auth": "./dist/_internal/adapter/auth.mjs",
117-
"./_internal/adapter/database": "./dist/_internal/adapter/database.mjs",
118-
"./_internal/adapter/logger": "./dist/_internal/adapter/logger.mjs",
119-
"./_internal/adapter/storage": "./dist/_internal/adapter/storage.mjs",
120-
"./_internal/adapter/ui": "./dist/_internal/adapter/ui.mjs",
121-
"./_internal/constants": "./dist/_internal/constants.mjs",
122-
"./_internal/router": "./dist/_internal/router.mjs",
123-
"./_internal/types": "./dist/_internal/types.mjs",
124-
"./_internal/utils": "./dist/_internal/utils.mjs",
125-
"./aws-dynamodb": "./dist/aws-dynamodb.mjs",
126-
"./aws-s3": "./dist/aws-s3.mjs",
127-
"./azure-blob-storage": "./dist/azure-blob-storage.mjs",
128-
"./azure-cosmos-db": "./dist/azure-cosmos-db.mjs",
129-
"./azure-data-tables": "./dist/azure-data-tables.mjs",
130-
"./azure-easy-auth": "./dist/azure-easy-auth.mjs",
131-
"./azure-functions": "./dist/azure-functions.mjs",
132-
"./fs": "./dist/fs.mjs",
133-
"./gcp-big-table": "./dist/gcp-big-table.mjs",
134-
"./gcp-firestore": "./dist/gcp-firestore.mjs",
135-
"./gcp-storage": "./dist/gcp-storage.mjs",
136-
"./mysql": "./dist/mysql.mjs",
137-
"./redis": "./dist/redis.mjs",
138-
"./package.json": "./package.json"
114+
".": {
115+
"source": "./src/index.ts",
116+
"default": "./dist/index.mjs"
117+
},
118+
"./_internal/adapter": {
119+
"source": "./src/adapters/_internal/index.ts",
120+
"default": "./dist/_internal/adapter.mjs"
121+
},
122+
"./_internal/adapter/auth": {
123+
"source": "./src/adapters/_internal/auth.ts",
124+
"default": "./dist/_internal/adapter/auth.mjs"
125+
},
126+
"./_internal/adapter/database": {
127+
"source": "./src/adapters/_internal/database.ts",
128+
"default": "./dist/_internal/adapter/database.mjs"
129+
},
130+
"./_internal/adapter/logger": {
131+
"source": "./src/adapters/_internal/logger.ts",
132+
"default": "./dist/_internal/adapter/logger.mjs"
133+
},
134+
"./_internal/adapter/storage": {
135+
"source": "./src/adapters/_internal/storage.ts",
136+
"default": "./dist/_internal/adapter/storage.mjs"
137+
},
138+
"./_internal/adapter/ui": {
139+
"source": "./src/adapters/_internal/ui.ts",
140+
"default": "./dist/_internal/adapter/ui.mjs"
141+
},
142+
"./_internal/constants": {
143+
"source": "./src/utils/constants.ts",
144+
"default": "./dist/_internal/constants.mjs"
145+
},
146+
"./_internal/router": {
147+
"source": "./src/routers/_app-router.ts",
148+
"default": "./dist/_internal/router.mjs"
149+
},
150+
"./_internal/types": {
151+
"source": "./src/types.ts",
152+
"default": "./dist/_internal/types.mjs"
153+
},
154+
"./_internal/utils": {
155+
"source": "./src/utils/index.ts",
156+
"default": "./dist/_internal/utils.mjs"
157+
},
158+
"./aws-dynamodb": {
159+
"source": "./src/adapters/aws-dynamodb.ts",
160+
"default": "./dist/aws-dynamodb.mjs"
161+
},
162+
"./aws-s3": {
163+
"source": "./src/adapters/aws-s3.ts",
164+
"default": "./dist/aws-s3.mjs"
165+
},
166+
"./azure-blob-storage": {
167+
"source": "./src/adapters/azure-blob-storage.ts",
168+
"default": "./dist/azure-blob-storage.mjs"
169+
},
170+
"./azure-cosmos-db": {
171+
"source": "./src/adapters/azure-cosmos-db.ts",
172+
"default": "./dist/azure-cosmos-db.mjs"
173+
},
174+
"./azure-data-tables": {
175+
"source": "./src/adapters/azure-data-tables.ts",
176+
"default": "./dist/azure-data-tables.mjs"
177+
},
178+
"./azure-easy-auth": {
179+
"source": "./src/adapters/azure-easy-auth.ts",
180+
"default": "./dist/azure-easy-auth.mjs"
181+
},
182+
"./azure-functions": {
183+
"source": "./src/adapters/azure-functions.ts",
184+
"default": "./dist/azure-functions.mjs"
185+
},
186+
"./fs": {
187+
"source": "./src/adapters/fs.ts",
188+
"default": "./dist/fs.mjs"
189+
},
190+
"./gcp-big-table": {
191+
"source": "./src/adapters/gcp-big-table.ts",
192+
"default": "./dist/gcp-big-table.mjs"
193+
},
194+
"./gcp-firestore": {
195+
"source": "./src/adapters/gcp-firestore.ts",
196+
"default": "./dist/gcp-firestore.mjs"
197+
},
198+
"./gcp-storage": {
199+
"source": "./src/adapters/gcp-storage.ts",
200+
"default": "./dist/gcp-storage.mjs"
201+
},
202+
"./mysql": {
203+
"source": "./src/adapters/mysql.ts",
204+
"default": "./dist/mysql.mjs"
205+
},
206+
"./redis": {
207+
"source": "./src/adapters/redis.ts",
208+
"default": "./dist/redis.mjs"
209+
},
210+
"./package.json": "./package.json",
211+
"./openapi.json": "./openapi.json"
212+
},
213+
"publishConfig": {
214+
"exports": {
215+
".": "./dist/index.mjs",
216+
"./_internal/adapter": "./dist/_internal/adapter.mjs",
217+
"./_internal/adapter/auth": "./dist/_internal/adapter/auth.mjs",
218+
"./_internal/adapter/database": "./dist/_internal/adapter/database.mjs",
219+
"./_internal/adapter/logger": "./dist/_internal/adapter/logger.mjs",
220+
"./_internal/adapter/storage": "./dist/_internal/adapter/storage.mjs",
221+
"./_internal/adapter/ui": "./dist/_internal/adapter/ui.mjs",
222+
"./_internal/constants": "./dist/_internal/constants.mjs",
223+
"./_internal/router": "./dist/_internal/router.mjs",
224+
"./_internal/types": "./dist/_internal/types.mjs",
225+
"./_internal/utils": "./dist/_internal/utils.mjs",
226+
"./aws-dynamodb": "./dist/aws-dynamodb.mjs",
227+
"./aws-s3": "./dist/aws-s3.mjs",
228+
"./azure-blob-storage": "./dist/azure-blob-storage.mjs",
229+
"./azure-cosmos-db": "./dist/azure-cosmos-db.mjs",
230+
"./azure-data-tables": "./dist/azure-data-tables.mjs",
231+
"./azure-easy-auth": "./dist/azure-easy-auth.mjs",
232+
"./azure-functions": "./dist/azure-functions.mjs",
233+
"./fs": "./dist/fs.mjs",
234+
"./gcp-big-table": "./dist/gcp-big-table.mjs",
235+
"./gcp-firestore": "./dist/gcp-firestore.mjs",
236+
"./gcp-storage": "./dist/gcp-storage.mjs",
237+
"./mysql": "./dist/mysql.mjs",
238+
"./redis": "./dist/redis.mjs",
239+
"./package.json": "./package.json"
240+
}
139241
}
140242
}

packages/core/src/models/webhooks-model.ts

Lines changed: 93 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import type { WebhookEvent } from "../types.ts";
22
import { generateDatabaseCollectionId } from "../utils/adapter-utils.ts";
33
import { checkAuthorisation } from "../utils/auth.ts";
4+
import { decrypt, encrypt, generateHMAC } from "../utils/crypto-utils.ts";
45
import { getStore } from "../utils/store.ts";
56
import {
6-
type WebhookCreateType,
77
WebhookSchema,
8+
type WebhookCreateType,
89
type WebhookUpdateType,
910
type WebhookType,
1011
} from "./webhooks-schema.ts";
@@ -30,7 +31,6 @@ export class WebhooksModel extends Model<WebhookType> {
3031
const now = new Date().toISOString();
3132
const webhook: WebhookType = {
3233
...data,
33-
id: crypto.randomUUID(),
3434
createdAt: now,
3535
updatedAt: now,
3636
};
@@ -43,7 +43,11 @@ export class WebhooksModel extends Model<WebhookType> {
4343
this.log("Get webhook '%s'...", id);
4444
const item = await this.database.getDocument(this.collectionId, id, this.dbOptions);
4545

46-
return WebhookSchema.parse(item);
46+
const { config } = getStore();
47+
const webhook = WebhookSchema.parse(item);
48+
webhook.secret = config?.secret ? decrypt(config.secret, webhook.secret) : webhook.secret;
49+
50+
return webhook;
4751
}
4852

4953
async has(id: string): Promise<boolean> {
@@ -87,7 +91,7 @@ export class WebhooksModel extends Model<WebhookType> {
8791

8892
async dispatchEvent(
8993
event: WebhookEvent,
90-
payload: Record<string, unknown>,
94+
payload: unknown,
9195
options?: { skipProjectHooks?: boolean; timeoutMs?: number },
9296
): Promise<void> {
9397
const { logger, config } = getStore();
@@ -110,23 +114,91 @@ export class WebhooksModel extends Model<WebhookType> {
110114

111115
// Run in parallel but don't throw — we only want best-effort delivery here
112116
await Promise.allSettled(
113-
allHooks.map(async (hook) => {
114-
const { url, headers } = hook;
115-
116-
try {
117-
const res = await fetch(url, {
118-
method: "POST",
119-
headers: { ...headers, "content-type": "application/json", "x-webhook-event": event },
120-
body: JSON.stringify({ event, projectId: this.projectId, payload }),
121-
signal: AbortSignal.timeout(timeoutMs),
122-
});
123-
logger?.log?.("[webhook] (%s) %s - %s", event, url, res.status);
124-
return { url, ok: res.ok, status: res.status };
125-
} catch (error) {
126-
logger?.error?.("[webhook] ERROR (%s) %s - $s", event, url, error);
127-
return { url, ok: false, error: error instanceof Error ? error.message : String(error) };
128-
}
129-
}),
117+
allHooks.map((hook) => this.dispatchEventToHook(event, hook, { payload, timeoutMs })),
130118
);
131119
}
120+
121+
async dispatchEventToHook(
122+
event: WebhookEvent,
123+
hook: WebhookCreateType,
124+
options: { payload: unknown; timeoutMs?: number },
125+
): Promise<{ url: string; ok: boolean; status: number; error?: string }> {
126+
const { logger } = getStore();
127+
const { timeoutMs = 5000, payload } = options;
128+
const { id, url, secret } = hook;
129+
const body = JSON.stringify({ event, projectId: this.projectId, payload });
130+
131+
try {
132+
const res = await fetch(url, {
133+
method: "POST",
134+
headers: {
135+
"content-type": "application/json",
136+
"x-webhook-id": id,
137+
"x-webhook-event": event,
138+
"x-webhook-project-id": this.projectId,
139+
"x-webhook-signature": generateHMAC(secret, body),
140+
},
141+
body,
142+
signal: AbortSignal.timeout(timeoutMs),
143+
});
144+
logger?.log?.("[webhook] (%s) %s - %s", event, id, res.status);
145+
return { url, ok: res.ok, status: res.status };
146+
} catch (error) {
147+
logger?.error?.("[webhook] ERROR (%s) %s - $s", event, id, error);
148+
return {
149+
url,
150+
ok: false,
151+
status: 500,
152+
error: error instanceof Error ? error.message : String(error),
153+
};
154+
}
155+
}
156+
157+
static sanitisePayload(payload: Record<string, unknown>): Record<string, unknown> {
158+
const store = getStore();
159+
const secret = store.config?.secret;
160+
161+
if (!payload || typeof payload !== "object") {
162+
return payload;
163+
}
164+
165+
const bodySecretValue = payload?.["secret"];
166+
if (bodySecretValue && typeof bodySecretValue === "string" && secret) {
167+
payload["secret"] = encrypt(secret, bodySecretValue);
168+
}
169+
170+
const bodyEventsValue = payload?.["events"];
171+
if (typeof bodyEventsValue === "string") {
172+
payload["events"] = [bodyEventsValue];
173+
}
174+
175+
/**
176+
* Convert headers from this format:
177+
```js
178+
headers: [Object: null prototype] {
179+
"0": [Object: null prototype] { name: "Authorization", value: "xas" },
180+
"1": [Object: null prototype] { name: "", value: "" },
181+
"2": [Object: null prototype] { name: "", value: "" }
182+
}
183+
```
184+
* to this format:
185+
`headers: { "Authorization": "xas" }`
186+
*/
187+
const bodyHeadersValue = payload?.["headers"];
188+
if (bodyHeadersValue && typeof bodyHeadersValue === "object") {
189+
const headersValue: Record<string, string> = {};
190+
for (const valueObj of Object.values(bodyHeadersValue)) {
191+
if (valueObj && typeof valueObj === "object" && "name" in valueObj && "value" in valueObj) {
192+
const { name, value } = valueObj;
193+
if (value && name) {
194+
headersValue[name] = secret ? encrypt(secret, value) : value;
195+
}
196+
}
197+
}
198+
199+
payload["headers"] = headersValue;
200+
}
201+
202+
return payload;
203+
}
132204
}

0 commit comments

Comments
 (0)