Multipart split — multipart-split
# Multipart split with object payload: one upload fans out so the file is
# stored as raw bytes in S3 and the form fields land in RabbitMQ.
#
# Requires UPLOAD_SOURCE_SECRET, ARCHIVE_BUCKET, and RABBITMQ_URL.
#
# How it works:
# 1. envelope body_format: multipart parses the request into {fields, files}.
# 2. Fan-out from `env` clones the envelope onto two branches.
# 3. The S3 branch reshapes body to the single file object and publishes
# with payload: object — S3 PutObject writes decoded bytes with the
# vendor Content-Type and a filename suffix on the key.
# 4. The info branch publishes fields + file metadata (no bytes) as the
# normal JSON envelope to RabbitMQ.
#
# Object payload body contract: {content: "<base64>", filename?, content_type?}.
# Supported destinations: s3, redis_kv. Default payload (envelope) still writes
# the JSON Message for every destination type.
# The 4 MiB code-node limits account for base64 expansion of the 2 MiB file cap.
server:
addr: ":8080"
max_body_bytes: 3145728
destinations:
archive:
type: s3
s3:
region: "us-east-1"
bucket: "${ARCHIVE_BUCKET}"
create: true
events:
type: rabbitmq
rabbitmq:
url: "${RABBITMQ_URL}"
exchange: "hookway.uploads"
exchange_type: "direct"
durable: true
publish_timeout: "5s"
declare_queues: true
flows:
upload_split:
nodes:
- {id: in, type: http_trigger, description: "Vendor posts multipart/form-data (fields + file).", path: /webhooks/upload-split, methods: [POST]}
- {id: body, type: read_body, description: "Buffers the whole request so HMAC can cover file bytes.", max_bytes: 3145728}
- {id: auth, type: auth, description: "Verifies HMAC over the raw multipart bytes before parsing.", scheme: hmac_sha256_hex, secret: "${UPLOAD_SOURCE_SECRET}", signature_header: X-Hub-Signature-256}
- id: env
type: envelope
description: "Parses parts into {fields, files}. include_content keeps file bytes for the S3 branch."
body_format: multipart
multipart:
max_parts: 2
max_part_bytes: 2097152
include_content: true
- id: for_s3
type: code
description: "S3 object payload needs the file object itself as body (content + metadata)."
timeout: 50ms
max_input_bytes: 4194304
max_output_bytes: 4194304
script: |
const files = (input.body && input.body.files) || {};
const doc = files.document;
if (!doc) {
throw new Error('missing files.document');
}
return {body: doc, vars: input.vars || {}};
- id: for_info
type: code
description: "Info branch body: form fields + file metadata, no base64 payload."
timeout: 50ms
max_input_bytes: 4194304
script: |
const files = (input.body && input.body.files) || {};
const meta = {};
for (const [name, file] of Object.entries(files)) {
meta[name] = {
filename: file.filename || '',
content_type: file.content_type || '',
size: file.size || 0
};
}
return {
body: {
fields: (input.body && input.body.fields) || {},
files: meta
},
vars: input.vars || {}
};
- id: out_s3
type: publish
description: "Stores decoded file bytes under uploads/…/<id>-<filename> in S3."
destination: archive
target: uploads
payload: object
- {id: out_info, type: publish, description: "Publishes fields + file metadata to the events queue.", destination: events, target: uploads-meta, create: true}
edges:
- in>body
- body>auth
- auth>env
- env>for_s3
- env>for_info
- for_s3>out_s3
- for_info>out_info