aboutsummaryrefslogtreecommitdiff
path: root/src/activity
diff options
context:
space:
mode:
authorElizabeth Hunt <me@liz.coffee>2025-12-14 22:43:24 -0800
committerElizabeth Hunt <me@liz.coffee>2025-12-14 22:43:24 -0800
commitcdb1a57614068fcfefa145bc6df45c9def7ccc6a (patch)
tree92cadbecda8658c143b7625d5925e3411976a892 /src/activity
parent6d318665a08c0d4564d8de23cc39425d2c0bac49 (diff)
downloadposthook-cdb1a57614068fcfefa145bc6df45c9def7ccc6a.tar.gz
posthook-cdb1a57614068fcfefa145bc6df45c9def7ccc6a.zip
Updates
Diffstat (limited to 'src/activity')
-rw-r--r--src/activity/index.ts146
1 files changed, 127 insertions, 19 deletions
diff --git a/src/activity/index.ts b/src/activity/index.ts
index 20d123c..c3ee821 100644
--- a/src/activity/index.ts
+++ b/src/activity/index.ts
@@ -110,25 +110,58 @@ export class WebhookActivityImpl implements IWebhookActivity {
private async parseBody(
req: PenguenoRequest,
contentType: ContentType,
- ): Promise<IEither<PenguenoError, { body: unknown; redirect: string | undefined; token: string | undefined }>> {
+ ): Promise<
+ IEither<
+ PenguenoError,
+ {
+ body: unknown;
+ redirect: string | undefined;
+ token: string | undefined;
+ uploads:
+ | Array<{
+ fieldName: string;
+ filename: string;
+ contentType: string;
+ size: number;
+ data: Uint8Array;
+ }>
+ | undefined;
+ }
+ >
+ > {
try {
- const rawBody = await req.req.text();
-
- type ParsedBody = { body: unknown; redirect: string | undefined; token: string | undefined };
+ type ParsedBody = {
+ body: unknown;
+ redirect: string | undefined;
+ token: string | undefined;
+ uploads:
+ | Array<{
+ fieldName: string;
+ filename: string;
+ contentType: string;
+ size: number;
+ data: Uint8Array;
+ }>
+ | undefined;
+ };
switch (contentType) {
- case ContentType.JSON:
+ case ContentType.JSON: {
+ const rawBody = await req.req.text();
try {
return Either.right(<ParsedBody>{
body: JSON.parse(rawBody),
redirect: undefined,
token: undefined,
+ uploads: undefined,
});
} catch {
return Either.left(new PenguenoError('Invalid JSON', 400));
}
+ }
- case ContentType.FORM:
+ case ContentType.FORM: {
+ const rawBody = await req.req.text();
try {
const formData = new URLSearchParams(rawBody);
const obj: Record<string, string> = {};
@@ -144,22 +177,95 @@ export class WebhookActivityImpl implements IWebhookActivity {
obj[key] = value;
}
}
- return Either.right(<ParsedBody>{ body: obj, redirect, token });
+ return Either.right(<ParsedBody>{ body: obj, redirect, token, uploads: undefined });
} catch {
return Either.left(new PenguenoError('Invalid form data', 400));
}
+ }
+
+ case ContentType.MULTIPART: {
+ try {
+ const formData = await req.req.formData();
+ const obj: Record<string, string | string[]> = {};
+ const uploads: ParsedBody['uploads'] = [];
+ let redirect: string | undefined;
+ let token: string | undefined;
- case ContentType.TEXT:
- return Either.right(<ParsedBody>{ body: rawBody, redirect: undefined, token: undefined });
+ for (const [key, value] of formData.entries()) {
+ if (typeof value === 'string') {
+ if (key === '_redirect') {
+ redirect = value;
+ } else if (key === '_token') {
+ token = value;
+ } else {
+ const existing = obj[key];
+ if (existing === undefined) {
+ obj[key] = value;
+ } else if (Array.isArray(existing)) {
+ existing.push(value);
+ } else {
+ obj[key] = [existing, value];
+ }
+ }
+ continue;
+ }
- case ContentType.RAW:
- return Either.right(<ParsedBody>{ body: rawBody, redirect: undefined, token: undefined });
+ // Avoid DOM typings; treat as a File-like object.
+ const maybeFile = value as unknown as {
+ name?: unknown;
+ type?: unknown;
+ size?: unknown;
+ arrayBuffer?: unknown;
+ };
+
+ const filename = typeof maybeFile.name === 'string' ? maybeFile.name : 'upload.bin';
+ const contentType =
+ typeof maybeFile.type === 'string' ? maybeFile.type : 'application/octet-stream';
+ const size = typeof maybeFile.size === 'number' ? maybeFile.size : 0;
+
+ if (typeof maybeFile.arrayBuffer !== 'function') {
+ return Either.left(new PenguenoError('Invalid multipart file upload', 400));
+ }
- case ContentType.MULTIPART:
- return Either.left(new PenguenoError('Multipart not yet implemented', 501));
+ const buf = new Uint8Array(await (maybeFile.arrayBuffer as () => Promise<ArrayBuffer>)());
+ uploads.push({ fieldName: key, filename, contentType, size, data: buf });
+ }
- default:
- return Either.right(<ParsedBody>{ body: rawBody, redirect: undefined, token: undefined });
+ return Either.right(<ParsedBody>{ body: obj, redirect, token, uploads });
+ } catch {
+ return Either.left(new PenguenoError('Invalid multipart form data', 400));
+ }
+ }
+
+ case ContentType.TEXT: {
+ const rawBody = await req.req.text();
+ return Either.right(<ParsedBody>{
+ body: rawBody,
+ redirect: undefined,
+ token: undefined,
+ uploads: undefined,
+ });
+ }
+
+ case ContentType.RAW: {
+ const rawBody = await req.req.text();
+ return Either.right(<ParsedBody>{
+ body: rawBody,
+ redirect: undefined,
+ token: undefined,
+ uploads: undefined,
+ });
+ }
+
+ default: {
+ const rawBody = await req.req.text();
+ return Either.right(<ParsedBody>{
+ body: rawBody,
+ redirect: undefined,
+ token: undefined,
+ uploads: undefined,
+ });
+ }
}
} catch (err) {
return Either.left(new PenguenoError(err instanceof Error ? err.message : String(err), 500));
@@ -227,7 +333,7 @@ export class WebhookActivityImpl implements IWebhookActivity {
return tReq.move(Either.left<PenguenoError, WebhookResult>(bodyResult.left().get()));
}
- const { body, redirect, token: bodyToken } = bodyResult.right().get();
+ const { body, redirect, token: bodyToken, uploads } = bodyResult.right().get();
// Validate token if required
if (route.requireToken) {
@@ -252,7 +358,7 @@ export class WebhookActivityImpl implements IWebhookActivity {
}
// Store the request
- const storeResult = await this.storage.storeRequest(routeName, req.method, headers, body);
+ const storeResult = await this.storage.storeRequest(routeName, req.method, headers, body, uploads);
if (storeResult.left().present()) {
return tReq.move(
Either.left<PenguenoError, WebhookResult>(
@@ -274,11 +380,13 @@ export class WebhookActivityImpl implements IWebhookActivity {
}
}
- const filename = `${storedRequest.timestamp}_${storedRequest.uuid}.json`;
+ const baseName = `${storedRequest.timestamp}_${storedRequest.uuid}`;
+ const storedPath = `${baseName}/request.json`;
+
return tReq.move(
Either.right<PenguenoError, WebhookResult>({
success: true,
- stored: filename,
+ stored: storedPath,
redirect,
}),
);