create-svc 0.1.8 → 0.1.10
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 +142 -13
- package/package.json +9 -4
- package/src/cli.test.ts +29 -8
- package/src/cli.ts +103 -70
- package/src/naming.test.ts +4 -2
- package/src/naming.ts +9 -1
- package/src/neon.ts +10 -8
- package/src/post-scaffold.ts +7 -28
- package/src/profiles.ts +28 -0
- package/src/scaffold.test.ts +126 -15
- package/src/scaffold.ts +94 -23
- package/src/vault.test.ts +62 -5
- package/src/vault.ts +24 -4
- package/templates/shared/README.md +143 -26
- package/templates/shared/docker-compose.yml +19 -0
- package/templates/shared/scripts/cloudrun/bootstrap.ts +15 -42
- package/templates/shared/scripts/cloudrun/cleanup.ts +17 -31
- package/templates/shared/scripts/cloudrun/config.ts +14 -19
- package/templates/shared/scripts/cloudrun/deploy.ts +19 -10
- package/templates/shared/scripts/cloudrun/integrations.ts +111 -0
- package/templates/shared/scripts/cloudrun/lib.ts +88 -112
- package/templates/shared/scripts/cloudrun/neon.ts +100 -14
- package/templates/shared/service.yaml +44 -1
- package/templates/variants/bun-connectrpc/Dockerfile +1 -0
- package/templates/variants/bun-connectrpc/Makefile +4 -1
- package/templates/variants/bun-connectrpc/gen/protos/chat/v1/chat_pb.ts +1078 -0
- package/templates/variants/bun-connectrpc/migrations/0000_init.sql +63 -0
- package/templates/variants/bun-connectrpc/package.json +17 -0
- package/templates/variants/bun-connectrpc/protos/chat/v1/chat.proto +228 -0
- package/templates/variants/bun-connectrpc/scripts/codegen.ts +31 -1
- package/templates/variants/bun-connectrpc/scripts/migrate.ts +46 -0
- package/templates/variants/bun-connectrpc/src/chat/service.ts +384 -0
- package/templates/variants/bun-connectrpc/src/chat/types.ts +142 -0
- package/templates/variants/bun-connectrpc/src/db/client.ts +15 -0
- package/templates/variants/bun-connectrpc/src/db/repository.ts +479 -0
- package/templates/variants/bun-connectrpc/src/db/schema.ts +75 -0
- package/templates/variants/bun-connectrpc/src/index.ts +294 -22
- package/templates/variants/bun-connectrpc/src/storage.ts +72 -0
- package/templates/variants/bun-connectrpc/src/webhooks.ts +35 -0
- package/templates/variants/bun-connectrpc/test/app.test.ts +14 -13
- package/templates/variants/bun-connectrpc/test/list-messages.integration.test.ts +182 -0
- package/templates/variants/bun-connectrpc/tsconfig.json +2 -1
- package/templates/variants/bun-hono/Makefile +4 -1
- package/templates/variants/bun-hono/migrations/0000_init.sql +63 -0
- package/templates/variants/bun-hono/package.json +13 -0
- package/templates/variants/bun-hono/scripts/migrate.ts +46 -0
- package/templates/variants/bun-hono/src/chat/service.ts +384 -0
- package/templates/variants/bun-hono/src/chat/types.ts +142 -0
- package/templates/variants/bun-hono/src/db/client.ts +15 -0
- package/templates/variants/bun-hono/src/db/repository.ts +479 -0
- package/templates/variants/bun-hono/src/db/schema.ts +75 -0
- package/templates/variants/bun-hono/src/index.ts +254 -8
- package/templates/variants/bun-hono/src/storage.ts +72 -0
- package/templates/variants/bun-hono/src/webhooks.ts +35 -0
- package/templates/variants/bun-hono/test/app.test.ts +60 -6
- package/templates/variants/bun-hono/test/list-messages.integration.test.ts +256 -0
- package/templates/variants/bun-hono/tsconfig.json +1 -0
- package/templates/variants/go-chi/Makefile +6 -2
- package/templates/variants/go-chi/buf.gen.yaml +2 -0
- package/templates/variants/go-chi/cmd/migrate/main.go +101 -0
- package/templates/variants/go-chi/cmd/server/main.go +16 -15
- package/templates/variants/go-chi/go.mod +3 -0
- package/templates/variants/go-chi/internal/app/service.go +763 -71
- package/templates/variants/go-chi/internal/config/config.go +22 -7
- package/templates/variants/go-chi/internal/httpapi/list_messages_integration_test.go +298 -0
- package/templates/variants/go-chi/internal/httpapi/routes.go +245 -43
- package/templates/variants/go-chi/migrations/0000_init.sql +63 -0
- package/templates/variants/go-chi/protos/chat/v1/chat.proto +219 -0
- package/templates/variants/go-chi/test/go.test.ts +4 -1
- package/templates/variants/go-connectrpc/Makefile +6 -2
- package/templates/variants/go-connectrpc/buf.gen.yaml +2 -0
- package/templates/variants/go-connectrpc/cmd/migrate/main.go +101 -0
- package/templates/variants/go-connectrpc/cmd/server/main.go +35 -11
- package/templates/variants/go-connectrpc/gen/chat/v1/chat.pb.go +2512 -0
- package/templates/variants/go-connectrpc/gen/chat/v1/chatv1connect/chat.connect.go +571 -0
- package/templates/variants/go-connectrpc/go.mod +4 -0
- package/templates/variants/go-connectrpc/internal/app/service.go +763 -71
- package/templates/variants/go-connectrpc/internal/config/config.go +22 -7
- package/templates/variants/go-connectrpc/internal/connectapi/handler.go +254 -42
- package/templates/variants/go-connectrpc/internal/connectapi/list_messages_integration_test.go +216 -0
- package/templates/variants/go-connectrpc/internal/httpapi/routes.go +41 -56
- package/templates/variants/go-connectrpc/migrations/0000_init.sql +63 -0
- package/templates/variants/go-connectrpc/protos/chat/v1/chat.proto +232 -0
- package/templates/shared/.env.example +0 -10
- package/templates/variants/go-chi/gen/dns/v1/dns.pb.go +0 -623
- package/templates/variants/go-chi/gen/dns/v1/dnsv1connect/dns.connect.go +0 -192
- package/templates/variants/go-chi/internal/connectapi/handler.go +0 -79
- package/templates/variants/go-chi/protos/dns/v1/dns.proto +0 -58
- package/templates/variants/go-connectrpc/gen/dns/v1/dns.pb.go +0 -623
- package/templates/variants/go-connectrpc/gen/dns/v1/dnsv1connect/dns.connect.go +0 -192
- package/templates/variants/go-connectrpc/protos/dns/v1/dns.proto +0 -58
|
@@ -1,22 +1,268 @@
|
|
|
1
1
|
import { Hono } from "hono";
|
|
2
|
+
import { AppError, createDefaultChatService, type ChatService } from "./chat/service";
|
|
2
3
|
|
|
3
|
-
export function createApp() {
|
|
4
|
+
export function createApp(service: ChatService) {
|
|
4
5
|
const app = new Hono();
|
|
5
6
|
|
|
6
|
-
app.get("/healthz", (context) => context.json({ status: "ok"
|
|
7
|
-
app.get("/", (context) => {
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
app.get("/healthz", (context) => context.json({ status: "ok" }));
|
|
8
|
+
app.get("/readyz", (context) => context.json({ status: "ok" }));
|
|
9
|
+
app.get("/", (context) =>
|
|
10
|
+
context.json({
|
|
10
11
|
service: "{{SERVICE_NAME}}",
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
domain: "chat",
|
|
13
|
+
apiOrigin: "https://api.{{SERVICE_NAME}}.anmho.com",
|
|
14
|
+
})
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
app.post("/v1/users", async (context) => {
|
|
18
|
+
try {
|
|
19
|
+
const body = await context.req.json();
|
|
20
|
+
const user = await service.createUser({
|
|
21
|
+
username: String(body.username ?? ""),
|
|
22
|
+
displayName: body.display_name ?? body.displayName ?? null,
|
|
23
|
+
});
|
|
24
|
+
return context.json({ user }, 201);
|
|
25
|
+
} catch (error) {
|
|
26
|
+
return writeError(context, error);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
app.get("/v1/users/:userId", async (context) => {
|
|
31
|
+
try {
|
|
32
|
+
return context.json({ user: await service.getUser(context.req.param("userId")) });
|
|
33
|
+
} catch (error) {
|
|
34
|
+
return writeError(context, error);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
app.get("/v1/users", async (context) => {
|
|
39
|
+
try {
|
|
40
|
+
const username = context.req.query("username") ?? "";
|
|
41
|
+
return context.json({ user: await service.getUserByUsername(username) });
|
|
42
|
+
} catch (error) {
|
|
43
|
+
return writeError(context, error);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
app.post("/v1/conversations", async (context) => {
|
|
48
|
+
try {
|
|
49
|
+
const body = await context.req.json();
|
|
50
|
+
const conversation = await service.createConversation({
|
|
51
|
+
createdByUserId: String(body.created_by_user_id ?? body.createdByUserId ?? ""),
|
|
52
|
+
title: body.title ?? null,
|
|
53
|
+
participantUserIds: Array.isArray(body.participant_user_ids ?? body.participantUserIds)
|
|
54
|
+
? (body.participant_user_ids ?? body.participantUserIds).map(String)
|
|
55
|
+
: [],
|
|
56
|
+
});
|
|
57
|
+
return context.json({ conversation }, 201);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
return writeError(context, error);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
app.get("/v1/conversations/:conversationId", async (context) => {
|
|
64
|
+
try {
|
|
65
|
+
return context.json({ conversation: await service.getConversation(context.req.param("conversationId")) });
|
|
66
|
+
} catch (error) {
|
|
67
|
+
return writeError(context, error);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
app.patch("/v1/conversations/:conversationId", async (context) => {
|
|
72
|
+
try {
|
|
73
|
+
const body = await context.req.json();
|
|
74
|
+
return context.json({
|
|
75
|
+
conversation: await service.updateConversation(context.req.param("conversationId"), {
|
|
76
|
+
title: body.title ?? null,
|
|
77
|
+
}),
|
|
78
|
+
});
|
|
79
|
+
} catch (error) {
|
|
80
|
+
return writeError(context, error);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
app.delete("/v1/conversations/:conversationId", async (context) => {
|
|
85
|
+
try {
|
|
86
|
+
await service.deleteConversation(context.req.param("conversationId"));
|
|
87
|
+
return context.body(null, 204);
|
|
88
|
+
} catch (error) {
|
|
89
|
+
return writeError(context, error);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
app.post("/v1/conversations/:conversationId/participants", async (context) => {
|
|
94
|
+
try {
|
|
95
|
+
const body = await context.req.json();
|
|
96
|
+
return context.json(
|
|
97
|
+
{
|
|
98
|
+
conversation: await service.addParticipant(
|
|
99
|
+
context.req.param("conversationId"),
|
|
100
|
+
String(body.user_id ?? body.userId ?? "")
|
|
101
|
+
),
|
|
102
|
+
},
|
|
103
|
+
201
|
|
104
|
+
);
|
|
105
|
+
} catch (error) {
|
|
106
|
+
return writeError(context, error);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
app.delete("/v1/conversations/:conversationId/participants/:userId", async (context) => {
|
|
111
|
+
try {
|
|
112
|
+
await service.removeParticipant(context.req.param("conversationId"), context.req.param("userId"));
|
|
113
|
+
return context.body(null, 204);
|
|
114
|
+
} catch (error) {
|
|
115
|
+
return writeError(context, error);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
app.get("/v1/conversations/:conversationId/messages", async (context) => {
|
|
120
|
+
try {
|
|
121
|
+
const limit = context.req.query("limit");
|
|
122
|
+
const result = await service.listMessages(context.req.param("conversationId"), {
|
|
123
|
+
cursor: context.req.query("cursor") ?? undefined,
|
|
124
|
+
limit: limit == null ? undefined : Number(limit),
|
|
125
|
+
});
|
|
126
|
+
return context.json({
|
|
127
|
+
messages: result.messages.map((message) => ({
|
|
128
|
+
id: message.id,
|
|
129
|
+
conversation_id: message.conversationId,
|
|
130
|
+
user_id: message.userId,
|
|
131
|
+
body: message.body,
|
|
132
|
+
edited_at: message.editedAt,
|
|
133
|
+
created_at: message.createdAt,
|
|
134
|
+
updated_at: message.updatedAt,
|
|
135
|
+
attachments: message.attachments.map((attachment) => ({
|
|
136
|
+
id: attachment.id,
|
|
137
|
+
filename: attachment.filename,
|
|
138
|
+
content_type: attachment.contentType,
|
|
139
|
+
byte_size: attachment.byteSize,
|
|
140
|
+
status: attachment.status,
|
|
141
|
+
public_url: attachment.publicUrl,
|
|
142
|
+
})),
|
|
143
|
+
})),
|
|
144
|
+
...(result.nextCursor ? { next_cursor: result.nextCursor } : {}),
|
|
145
|
+
});
|
|
146
|
+
} catch (error) {
|
|
147
|
+
return writeError(context, error);
|
|
148
|
+
}
|
|
13
149
|
});
|
|
14
150
|
|
|
151
|
+
app.post("/v1/conversations/:conversationId/messages", async (context) => {
|
|
152
|
+
try {
|
|
153
|
+
const body = await context.req.json();
|
|
154
|
+
return context.json(
|
|
155
|
+
{
|
|
156
|
+
message: await service.createMessage(context.req.param("conversationId"), {
|
|
157
|
+
userId: String(body.user_id ?? body.userId ?? ""),
|
|
158
|
+
body: String(body.body ?? ""),
|
|
159
|
+
}),
|
|
160
|
+
},
|
|
161
|
+
201
|
|
162
|
+
);
|
|
163
|
+
} catch (error) {
|
|
164
|
+
return writeError(context, error);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
app.patch("/v1/conversations/:conversationId/messages/:messageId", async (context) => {
|
|
169
|
+
try {
|
|
170
|
+
const body = await context.req.json();
|
|
171
|
+
return context.json({
|
|
172
|
+
message: await service.updateMessage(
|
|
173
|
+
context.req.param("conversationId"),
|
|
174
|
+
context.req.param("messageId"),
|
|
175
|
+
{ body: String(body.body ?? "") }
|
|
176
|
+
),
|
|
177
|
+
});
|
|
178
|
+
} catch (error) {
|
|
179
|
+
return writeError(context, error);
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
app.delete("/v1/conversations/:conversationId/messages/:messageId", async (context) => {
|
|
184
|
+
try {
|
|
185
|
+
await service.deleteMessage(context.req.param("conversationId"), context.req.param("messageId"));
|
|
186
|
+
return context.body(null, 204);
|
|
187
|
+
} catch (error) {
|
|
188
|
+
return writeError(context, error);
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
app.post("/v1/attachments/uploads", async (context) => {
|
|
193
|
+
try {
|
|
194
|
+
const body = await context.req.json();
|
|
195
|
+
return context.json(
|
|
196
|
+
{
|
|
197
|
+
result: await service.createAttachmentUpload({
|
|
198
|
+
conversationId: String(body.conversation_id ?? body.conversationId ?? ""),
|
|
199
|
+
uploadedByUserId: String(body.user_id ?? body.userId ?? ""),
|
|
200
|
+
filename: String(body.filename ?? ""),
|
|
201
|
+
contentType: String(body.content_type ?? body.contentType ?? ""),
|
|
202
|
+
byteSize: Number(body.byte_size ?? body.byteSize ?? 0),
|
|
203
|
+
}),
|
|
204
|
+
},
|
|
205
|
+
201
|
|
206
|
+
);
|
|
207
|
+
} catch (error) {
|
|
208
|
+
return writeError(context, error);
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
app.post("/v1/attachments/:attachmentId/finalize", async (context) => {
|
|
213
|
+
try {
|
|
214
|
+
const body = await context.req.json().catch(() => ({}));
|
|
215
|
+
return context.json({
|
|
216
|
+
attachment: await service.finalizeAttachment(context.req.param("attachmentId"), {
|
|
217
|
+
messageId: body.message_id ?? body.messageId ?? null,
|
|
218
|
+
}),
|
|
219
|
+
});
|
|
220
|
+
} catch (error) {
|
|
221
|
+
return writeError(context, error);
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
app.get("/v1/attachments/:attachmentId", async (context) => {
|
|
226
|
+
try {
|
|
227
|
+
return context.json({ attachment: await service.getAttachment(context.req.param("attachmentId")) });
|
|
228
|
+
} catch (error) {
|
|
229
|
+
return writeError(context, error);
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
app.delete("/v1/attachments/:attachmentId", async (context) => {
|
|
234
|
+
try {
|
|
235
|
+
await service.deleteAttachment(context.req.param("attachmentId"));
|
|
236
|
+
return context.body(null, 204);
|
|
237
|
+
} catch (error) {
|
|
238
|
+
return writeError(context, error);
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
app.post("/webhooks/:provider", async (context) => {
|
|
243
|
+
try {
|
|
244
|
+
const rawBody = await context.req.text();
|
|
245
|
+
const result = await service.processWebhook(context.req.param("provider"), context.req.raw.headers, rawBody);
|
|
246
|
+
return context.json(result, result.duplicate ? 200 : 202);
|
|
247
|
+
} catch (error) {
|
|
248
|
+
return writeError(context, error);
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
app.get("/webhooks/:provider/health", (context) => context.json({ status: "ok", provider: context.req.param("provider") }));
|
|
253
|
+
|
|
15
254
|
return app;
|
|
16
255
|
}
|
|
17
256
|
|
|
257
|
+
function writeError(context: any, error: unknown) {
|
|
258
|
+
if (error instanceof AppError) {
|
|
259
|
+
return context.json({ error: error.message, code: error.code }, error.status);
|
|
260
|
+
}
|
|
261
|
+
return context.json({ error: error instanceof Error ? error.message : String(error) }, 500);
|
|
262
|
+
}
|
|
263
|
+
|
|
18
264
|
if (import.meta.main) {
|
|
19
|
-
const app = createApp();
|
|
265
|
+
const app = createApp(createDefaultChatService());
|
|
20
266
|
Bun.serve({
|
|
21
267
|
port: Number(Bun.env.PORT ?? 8080),
|
|
22
268
|
fetch: app.fetch,
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { Storage } from "@google-cloud/storage";
|
|
2
|
+
import type { AttachmentObjectMetadata, AttachmentUploadTarget } from "./chat/types";
|
|
3
|
+
|
|
4
|
+
export type AttachmentStorage = {
|
|
5
|
+
createSignedUpload(input: {
|
|
6
|
+
attachmentId: string;
|
|
7
|
+
conversationId: string;
|
|
8
|
+
filename: string;
|
|
9
|
+
contentType: string;
|
|
10
|
+
}): Promise<{ bucket: string; key: string; upload: AttachmentUploadTarget; publicUrl: string }>;
|
|
11
|
+
getObjectMetadata(input: { bucket: string; key: string }): Promise<AttachmentObjectMetadata>;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export class GcsAttachmentStorage implements AttachmentStorage {
|
|
15
|
+
constructor(
|
|
16
|
+
private readonly bucketName = requireAttachmentBucket(),
|
|
17
|
+
private readonly publicBaseUrl = Bun.env.ATTACHMENT_PUBLIC_BASE_URL?.trim() || `https://storage.googleapis.com/${requireAttachmentBucket()}`
|
|
18
|
+
) {}
|
|
19
|
+
|
|
20
|
+
async createSignedUpload(input: { attachmentId: string; conversationId: string; filename: string; contentType: string }) {
|
|
21
|
+
const storage = new Storage();
|
|
22
|
+
const bucket = storage.bucket(this.bucketName);
|
|
23
|
+
const key = `attachments/${input.conversationId}/${input.attachmentId}/${sanitizeFilename(input.filename)}`;
|
|
24
|
+
const file = bucket.file(key);
|
|
25
|
+
const [url] = await file.getSignedUrl({
|
|
26
|
+
action: "write",
|
|
27
|
+
version: "v4",
|
|
28
|
+
expires: Date.now() + 15 * 60 * 1000,
|
|
29
|
+
contentType: input.contentType,
|
|
30
|
+
});
|
|
31
|
+
return {
|
|
32
|
+
bucket: this.bucketName,
|
|
33
|
+
key,
|
|
34
|
+
upload: {
|
|
35
|
+
method: "PUT" as const,
|
|
36
|
+
url,
|
|
37
|
+
headers: {
|
|
38
|
+
"Content-Type": input.contentType,
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
publicUrl: `${this.publicBaseUrl.replace(/\/+$/g, "")}/${key}`,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async getObjectMetadata(input: { bucket: string; key: string }): Promise<AttachmentObjectMetadata> {
|
|
46
|
+
const storage = new Storage();
|
|
47
|
+
const [metadata] = await storage.bucket(input.bucket).file(input.key).getMetadata();
|
|
48
|
+
return {
|
|
49
|
+
bucket: input.bucket,
|
|
50
|
+
key: input.key,
|
|
51
|
+
contentType: String(metadata.contentType ?? ""),
|
|
52
|
+
byteSize: Number(metadata.size ?? 0),
|
|
53
|
+
publicUrl: `${this.publicBaseUrl.replace(/\/+$/g, "")}/${input.key}`,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function createAttachmentStorage() {
|
|
59
|
+
return new GcsAttachmentStorage();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function requireAttachmentBucket() {
|
|
63
|
+
const bucket = Bun.env.ATTACHMENT_BUCKET?.trim();
|
|
64
|
+
if (!bucket) {
|
|
65
|
+
throw new Error("ATTACHMENT_BUCKET is required");
|
|
66
|
+
}
|
|
67
|
+
return bucket;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function sanitizeFilename(filename: string) {
|
|
71
|
+
return filename.trim().replace(/[^a-zA-Z0-9._-]+/g, "-") || "upload.bin";
|
|
72
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { NormalizedWebhookEvent } from "./chat/types";
|
|
2
|
+
|
|
3
|
+
export type WebhookAdapter = {
|
|
4
|
+
normalize(provider: string, headers: Headers, rawBody: string): Promise<NormalizedWebhookEvent>;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export class GenericJsonWebhookAdapter implements WebhookAdapter {
|
|
8
|
+
async normalize(provider: string, headers: Headers, rawBody: string): Promise<NormalizedWebhookEvent> {
|
|
9
|
+
const payload = parseJson(rawBody);
|
|
10
|
+
const secret = Bun.env[`WEBHOOK_${provider.toUpperCase()}_SECRET`]?.trim();
|
|
11
|
+
const incomingSecret = headers.get("x-webhook-secret")?.trim() ?? "";
|
|
12
|
+
const externalEventId = String(payload.id ?? headers.get("x-event-id") ?? crypto.randomUUID());
|
|
13
|
+
const eventType = String(payload.type ?? headers.get("x-event-type") ?? "generic.event");
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
provider,
|
|
17
|
+
externalEventId,
|
|
18
|
+
eventType,
|
|
19
|
+
signatureValid: secret ? incomingSecret === secret : true,
|
|
20
|
+
payloadJson: rawBody,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function createWebhookAdapter() {
|
|
26
|
+
return new GenericJsonWebhookAdapter();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function parseJson(rawBody: string) {
|
|
30
|
+
try {
|
|
31
|
+
return JSON.parse(rawBody) as Record<string, unknown>;
|
|
32
|
+
} catch {
|
|
33
|
+
return {};
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -1,12 +1,66 @@
|
|
|
1
1
|
import { expect, test } from "bun:test";
|
|
2
2
|
import { createApp } from "../src/index";
|
|
3
|
+
import type { ChatService } from "../src/chat/service";
|
|
3
4
|
|
|
4
5
|
test("health endpoint returns ok", async () => {
|
|
5
|
-
const response = await createApp().request("/healthz");
|
|
6
|
+
const response = await createApp(createMockService()).request("/healthz");
|
|
6
7
|
expect(response.status).toBe(200);
|
|
7
|
-
expect(await response.json()).toEqual({
|
|
8
|
-
status: "ok",
|
|
9
|
-
runtime: "bun",
|
|
10
|
-
framework: "hono",
|
|
11
|
-
});
|
|
8
|
+
expect(await response.json()).toEqual({ status: "ok" });
|
|
12
9
|
});
|
|
10
|
+
|
|
11
|
+
test("webhook health endpoint returns ok", async () => {
|
|
12
|
+
const response = await createApp(createMockService()).request("/webhooks/generic/health");
|
|
13
|
+
expect(response.status).toBe(200);
|
|
14
|
+
expect(await response.json()).toEqual({ status: "ok", provider: "generic" });
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
function createMockService(): ChatService {
|
|
18
|
+
return {
|
|
19
|
+
async createUser() {
|
|
20
|
+
throw new Error("not implemented");
|
|
21
|
+
},
|
|
22
|
+
async getUser() {
|
|
23
|
+
throw new Error("not implemented");
|
|
24
|
+
},
|
|
25
|
+
async getUserByUsername() {
|
|
26
|
+
throw new Error("not implemented");
|
|
27
|
+
},
|
|
28
|
+
async createConversation() {
|
|
29
|
+
throw new Error("not implemented");
|
|
30
|
+
},
|
|
31
|
+
async getConversation() {
|
|
32
|
+
throw new Error("not implemented");
|
|
33
|
+
},
|
|
34
|
+
async updateConversation() {
|
|
35
|
+
throw new Error("not implemented");
|
|
36
|
+
},
|
|
37
|
+
async deleteConversation() {},
|
|
38
|
+
async addParticipant() {
|
|
39
|
+
throw new Error("not implemented");
|
|
40
|
+
},
|
|
41
|
+
async removeParticipant() {},
|
|
42
|
+
async listMessages() {
|
|
43
|
+
return { messages: [] };
|
|
44
|
+
},
|
|
45
|
+
async createMessage() {
|
|
46
|
+
throw new Error("not implemented");
|
|
47
|
+
},
|
|
48
|
+
async updateMessage() {
|
|
49
|
+
throw new Error("not implemented");
|
|
50
|
+
},
|
|
51
|
+
async deleteMessage() {},
|
|
52
|
+
async createAttachmentUpload() {
|
|
53
|
+
throw new Error("not implemented");
|
|
54
|
+
},
|
|
55
|
+
async finalizeAttachment() {
|
|
56
|
+
throw new Error("not implemented");
|
|
57
|
+
},
|
|
58
|
+
async getAttachment() {
|
|
59
|
+
throw new Error("not implemented");
|
|
60
|
+
},
|
|
61
|
+
async deleteAttachment() {},
|
|
62
|
+
async processWebhook() {
|
|
63
|
+
throw new Error("not implemented");
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|