From cdb1a57614068fcfefa145bc6df45c9def7ccc6a Mon Sep 17 00:00:00 2001 From: Elizabeth Hunt Date: Sun, 14 Dec 2025 22:43:24 -0800 Subject: Updates --- src/activity/index.ts | 146 +++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 127 insertions(+), 19 deletions(-) (limited to 'src/activity') 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> { + ): 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({ 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 = {}; @@ -144,22 +177,95 @@ export class WebhookActivityImpl implements IWebhookActivity { obj[key] = value; } } - return Either.right({ body: obj, redirect, token }); + return Either.right({ 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 = {}; + const uploads: ParsedBody['uploads'] = []; + let redirect: string | undefined; + let token: string | undefined; - case ContentType.TEXT: - return Either.right({ 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({ 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)()); + uploads.push({ fieldName: key, filename, contentType, size, data: buf }); + } - default: - return Either.right({ body: rawBody, redirect: undefined, token: undefined }); + return Either.right({ 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({ + body: rawBody, + redirect: undefined, + token: undefined, + uploads: undefined, + }); + } + + case ContentType.RAW: { + const rawBody = await req.req.text(); + return Either.right({ + body: rawBody, + redirect: undefined, + token: undefined, + uploads: undefined, + }); + } + + default: { + const rawBody = await req.req.text(); + return Either.right({ + 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(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( @@ -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({ success: true, - stored: filename, + stored: storedPath, redirect, }), ); -- cgit v1.2.3-70-g09d2