Hookway

Multipart s3 lambda — multipart-s3-lambda

# Multipart split with S3 object storage + Lambda notify: one upload stores
# the file as raw bytes in S3, then Event-invokes Lambda with metadata after
# PutObject returns (publish success continuation).
#
# Requires UPLOAD_SOURCE_SECRET and ARCHIVE_BUCKET.
#
# Why not payload: object on Lambda?
#   Lambda Invoke has hard payload limits (256 KiB Event / 6 MiB RequestResponse)
#   and is not an object store. The established file path is: PutObject the
#   bytes to S3, then Event-invoke Lambda with metadata so the function can
#   GetObject by bucket/key (or process the form fields alone).
#
# How it works:
#   1. envelope body_format: multipart parses the request into {fields, files}.
#   2. for_s3 reshapes body to the single file object for payload: object.
#   3. out_s3 PutObjects the decoded bytes.
#   4. After S3 returns, for_lambda strips content so the Lambda envelope is
#      metadata only; out_lambda Event-invokes the worker.
#
# Object payload body contract: {content: "<base64>", filename?, content_type?}.
# The 4 MiB code-node limits account for base64 expansion of the 2 MiB file cap.
#
# IAM (Lambda): lambda:InvokeFunction (+ lambda:GetFunction for /ready).
# IAM (S3): s3:PutObject (and s3:CreateBucket when create: true).
# The function itself typically needs s3:GetObject on ARCHIVE_BUCKET if it
# reads the uploaded object.

server:
  addr: ":8080"
  max_body_bytes: 3145728

# Both destinations below talk to the same AWS account, so the region and
# credentials are declared once here and referenced by name. See
# examples/connections.yaml for the full walkthrough.
connections:
  aws:
    type: aws
    region: "us-east-1"

destinations:
  archive:
    type: s3
    connection: aws
    s3:
      bucket: "${ARCHIVE_BUCKET}"
      create: true
  workers:
    type: lambda
    connection: aws
    lambda:
      # Event (default): succeed when AWS accepts the invoke. Use
      # RequestResponse only when the inbound sender should see function errors.
      invocation_type: Event

flows:
  upload_s3_lambda:
    nodes:
      - {id: in, type: http_trigger, description: "Vendor posts multipart/form-data (fields + file).", path: /webhooks/upload-s3-lambda, 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 publish."
        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 as body; stash form fields in vars for the post-S3 Lambda step."
        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: Object.assign({}, input.vars || {}, {
              fields: (input.body && input.body.fields) || {}
            })
          };
      - id: out_s3
        type: publish
        description: "Stores decoded file bytes under uploads/…/<id>-<filename> in S3."
        destination: archive
        target: uploads
        payload: object
      - id: for_lambda
        type: code
        description: "After S3 succeeds: strip file bytes; Lambda gets fields + file metadata."
        timeout: 50ms
        max_input_bytes: 4194304
        script: |
          const file = input.body || {};
          return {
            body: {
              // Object key layout: uploads/yyyy/mm/dd/<timestamp>-<id>[-<filename>]
              // under destinations.archive.s3.bucket (see docs/reference/configuration.md § S3).
              archive_prefix: 'uploads',
              fields: (input.vars && input.vars.fields) || {},
              files: {
                document: {
                  filename: file.filename || '',
                  content_type: file.content_type || '',
                  size: file.size || 0
                }
              }
            },
            vars: input.vars || {}
          };
      - id: out_lambda
        type: publish
        description: "Event-invokes upload-worker with fields + file metadata (no bytes)."
        destination: workers
        target: upload-worker
    edges:
      - in>body
      - body>auth
      - auth>env
      - env>for_s3
      - for_s3>out_s3
      - out_s3>for_lambda
      - for_lambda>out_lambda