skip to content
$worker

storage

v0.1.18

Object storage across AWS S3, GCS, Cloudflare R2, and a native local filesystem backend. Streamed uploads, signed URLs, and object change triggers.

iiiverified
242 installs0 in 7d0 today
install
$iii trigger compose::add worker=storage@0.1.18
binarylicense: Apache-2.0blobgcsobject-storager2s3
  • macOS: arm64
  • Linux: arm64 · armv7 · x64
  • Windows: arm64 · x64

exact versions are immutable; binary and bundle artifacts are digest-pinned.

agent-ready brief for v0.1.18
install + config + dependencies + readme + api reference, all in one place. fetch as agent-context.md for an llm to consume.
the same content rendered as discrete blocks below is exposed as a single markdown document at /workers/storage.md?version=0.1.18. paste it into an llm prompt or pipe it through curl from a worker.

install

install
$iii trigger compose::add worker=storage@0.1.18

configuration

iii-config.yaml
- buckets:
    scratch:
      provider: local
  providers:
    local:
      data_dir: data/storage
      http:
        bind_address: 127.0.0.1:0

dependencies

no dependencies for v0.1.18

readme

README.md

storage

Object storage for the iii engine over S3, GCS, R2, and a native local filesystem backend. Streamed uploads, signed downloads, and object-created / object-deleted triggers — all behind one bucket: name regardless of the cloud underneath.

Install

iii trigger compose::add worker=storage

iii trigger compose::add resolves the worker and its dependencies, writes exact declarations to worker-compose.yaml, and reconciles the Compose project.

Quickstart

Use inline RPCs only for genuinely small values. Normal files should go directly between the client and object storage through a signed upload or download endpoint.

import { registerWorker } from 'iii-sdk'

const iii = registerWorker(process.env.III_URL ?? 'ws://127.0.0.1:49134')

await iii.trigger({
  function_id: 'storage::putObject',
  payload: {
    bucket: 'uploads',
    key: 'u/1/profile.jpg',
    body_base64: tinyValueBase64,    // small values only; 10 MiB hard limit
    content_type: 'image/jpeg',
  },
})

const { url, expires_at } = await iii.trigger({
  function_id: 'storage::presignUrl',
  payload: {
    bucket: 'uploads',
    key: 'u/1/next.jpg',
    method: 'PUT',
    expires_in_seconds: 600,
    content_type: 'image/jpeg',      // pinned into the signature
  },
})

await fetch(url, {
  method: 'PUT',
  headers: { 'Content-Type': 'image/jpeg' },
  body: file,
})

const { body_base64, content_type } = await iii.trigger({
  function_id: 'storage::getObject',
  payload: {
    bucket: 'uploads',
    key: 'u/1/profile.jpg',
  },
})

await iii.trigger({
  function_id: 'storage::deleteObject',
  payload: {
    bucket: 'uploads',
    key: 'u/1/profile.jpg',
  },
})                                  // idempotent: returns { deleted: false } if absent

const { content_type, etag, size, last_modified } = await iii.trigger({
  function_id: 'storage::headObject',
  payload: {
    bucket: 'uploads',
    key: 'u/1/profile.jpg',
  },
})                                  // fetches metadata only — no body download

For a multipart upload to a native local bucket, use presignPost:

const { url, fields } = await iii.trigger({
  function_id: 'storage::presignPost',
  payload: {
    bucket: 'scratch',
    key: 'videos/demo.mp4',
    content_type: 'video/mp4',
    expires_in_seconds: 600,
  },
})

const form = new FormData()
for (const [name, value] of Object.entries(fields)) form.append(name, value)
form.append('file', file)
await fetch(url, { method: 'POST', body: form })

putObject and getObject serialize bytes as base64 inside an engine message. They buffer the complete value and base64 adds roughly 33% overhead. Both enforce a 10 MiB decoded hard limit, but they are intended for truly small values, not general file transfer. Use presignPost/a signed PUT for uploads and a signed GET from presignUrl for downloads.

From a Rust worker:

use iii_sdk::{register_worker, InitOptions, TriggerRequest};
use serde_json::json;

let iii = register_worker("ws://localhost:49134", InitOptions::default());

iii.trigger(TriggerRequest {
    function_id: "storage::putObject".into(),
    payload: json!({
        "bucket": "uploads",
        "key": "u/1/profile.jpg",
        "body_base64": file_b64,
        "content_type": "image/jpeg",
    }),
    action: None,
    timeout_ms: Some(5_000),
}).await?;

Configuration

The storage worker gets its live configuration from the configuration worker, not from a local file. On startup it:

  1. Registers its config schema with the configuration worker (configuration::register, id storage).
  2. Fetches the live, env-expanded config (configuration::get).
  3. Subscribes to configuration:updated events and hot-reloads.

--config is an optional seed: when given, the file is loaded and sent as initial_value the first time the schema is registered (no stored value yet). It is not the live source of truth — once a value exists in the configuration worker, that value wins.

Hot-reload scope

On a configuration:updated event the worker re-fetches the authoritative config from the configuration worker (it does not trust the event payload). Every setting is live: bucket additions/removals, providers, credentials, underlying names, notification sources, local data directory, and the local HTTP listener/public URL all apply without restarting the worker.

Application is transactional. The worker first prepares the candidate backends, notification clients, local service generation, and—when the bind address changes—the new TCP listener. Only after those fallible steps succeed does it publish the new runtime and gracefully retire the old listener and pollers. A failed backend build, authentication probe, or port bind leaves the previous configuration serving requests.

A fresh install with no configured buckets runs with zero backends until a bucket is configured.

Config shape

Each bucket pins a provider (s3 | gcs | r2 | local) and the credentials for that provider. Buckets without notifications: work fine for RPCs; they just don't fire triggers.

providers:
  local:
    data_dir: data/storage             # relative to III_COMPOSE_DIR
    http:
      bind_address: 0.0.0.0:49200      # omit `http` to disable direct transfers
      public_url: http://10.0.0.42:49200 # browser-visible LAN/VPN/proxy URL

buckets:
  uploads:
    provider: s3
    bucket: my-app-uploads             # underlying cloud bucket
    region: us-east-1
    notifications:
      sqs_queue_url: https://sqs.us-east-1.amazonaws.com/123/my-app-uploads-events

  documents:
    provider: gcs
    bucket: my-app-documents
    # credentials_file: /etc/iii/gcs-sa.json   # required for presignUrl

  avatars:
    provider: r2
    bucket: avatars
    account_id: ${R2_ACCOUNT_ID}
    access_key_id: ${R2_ACCESS_KEY_ID}
    secret_access_key: ${R2_SECRET_ACCESS_KEY}

  scratch:
    provider: local
    bucket: scratch

The map key (uploads) is the worker-facing bucket name handlers reference; the nested bucket: is the underlying cloud bucket. They can differ.

Per-provider notes

  • S3 — defaults to the AWS credential chain (env, ~/.aws, IMDS, IRSA). Override with access_key_id / secret_access_key / session_token only if the default chain doesn't fit.
  • GCS — defaults to ADC (GOOGLE_APPLICATION_CREDENTIALS, GCE metadata, gcloud auth application-default login). presignUrl requires a service-account JSON with a private key — supply credentials_file explicitly when running on metadata-server-only sources (e.g., GKE Workload Identity), otherwise GCS presigns return PRESIGN_UNSUPPORTED.
  • R2 — required: account_id, access_key_id, secret_access_key. Endpoint URL is derived automatically as https://{account_id}.r2.cloudflarestorage.com.
  • local — implemented directly by the worker. Object bytes and JSON metadata are persisted under data_dir; no sidecar or external binary is required. http is optional: omit it when only the small inline RPCs are needed. When enabled, bind_address controls the listener and public_url controls the base URL returned to clients. Set public_url whenever clients reach the worker through a LAN address, VPN, container port mapping, or reverse proxy. With port 0 and no public_url, the worker chooses a free loopback port and returns that address. The native listener includes browser CORS support for these signed transfers; cloud buckets need equivalent CORS rules on the provider when uploads originate in the Console.

Existing rustfs data directories are not imported automatically: the native backend uses its own object/metadata layout under data_dir. Export or copy existing objects before upgrading, then upload them into the native bucket.

Custom endpoints

S3, R2, and GCS bucket configs accept an optional endpoint_url field for self-hosted S3-compatible stores (MinIO, Ceph, SeaweedFS), staging environments, or local testing against fake-gcs-server.

buckets:
  scratch-self-hosted:
    provider: s3
    region: us-east-1
    endpoint_url: https://s3.internal.example.com
    bucket: scratch

R2 with endpoint_url set emits a tracing::warn! at startup — the field is fully functional but production R2 should omit it and let the worker derive the endpoint automatically.

Wiring notifications

Provider Config field(s) Setup
S3 notifications.sqs_queue_url SQS queue + bucket event config for s3:ObjectCreated:* / s3:ObjectRemoved:* + sqs:ReceiveMessage,DeleteMessage IAM on the queue ARN.
GCS notifications.pubsub_subscription gsutil notification create -t TOPIC -e OBJECT_FINALIZE,OBJECT_DELETE gs:// + roles/pubsub.subscriber on the subscription.
R2 notifications.queue_id + notifications.api_token Cloudflare Queue + R2 event notifications on the bucket + API token scoped queue:consume.
local (none) The worker dispatches events immediately after a native local write/delete commits.

Other config keys and their defaults live in src/config.rs; wire-stable error codes returned by every RPC live in src/error.rs.

Custom trigger types

Trigger type Fires when Payload to subscribers
storage::object-created An object is written (PUT, multipart complete, copy-in). { bucket, key, size, content_type, etag, version_id?, event_time }
storage::object-deleted An object (or version, on versioned buckets) is removed. { bucket, key, version_id?, event_time }

Delivery is at-least-once. Handlers must return { ack: true }; false, panic, or timeout (handler_timeout_ms, default 60 s) leaves the message in the upstream queue for redelivery.

triggers:
  - type: storage::object-created
    config:
      bucket: uploads
      # event_types: [ObjectCreated:Put, ObjectCreated:CompleteMultipartUpload]   # optional filter
      # handler_timeout_ms: 60000

  - type: storage::object-deleted
    config:
      bucket: uploads

R2 trigger v1 caveat: the Cloudflare Queues consume-from-outside REST API is the youngest of the four upstreams. The worker probes the consume endpoint at startup and surfaces CF_QUEUE_AUTH_FAILED for 401/403, so token misconfiguration is visible immediately. If you hit redelivery or auth-scope edge cases in production, file an issue — v1.1 will finalize the consume path.

Console explorer

When the worker is connected, it injects a storage page into the Console. Wide panels use three simultaneous levels—buckets, folders/objects, and object details—while narrow panels show one level at a time. The explorer supports folder breadcrumbs, pagination, metadata inspection, direct upload, signed download, copy-key, and delete actions. Uploads and downloads use the direct transfer endpoints instead of moving file bodies through an inline RPC.

For native local uploads, enable providers.local.http. Cloud uploads also require the provider bucket to permit the Console origin through CORS.

The page's configure action opens a storage-specific editor while the Console continues to own validation, dirty tracking, save, reset, and the unsaved-change guard. The editor separates native-local runtime settings from bucket mappings and presents only the fields relevant to Local, S3, GCS, or R2. Saved changes are applied live; no storage setting requires a worker restart.

RPC reference notes

Inline object calls are intentionally limited

storage::putObject and storage::getObject are convenience functions for small configuration fragments, thumbnails, test fixtures, and similar values. They are not streaming APIs: the full object crosses the iii engine as base64 and is held in memory. The decoded body is capped at 10 MiB. For files—even when a file happens to fit below that cap—prefer direct transfer endpoints:

  • upload: storage::presignPost for local multipart POST, or storage::presignUrl with method: "PUT";
  • download: storage::presignUrl with method: "GET".

storage::presignPost — multipart upload

Returns { url, fields, expires_at }. Add every returned field to a multipart/form-data body before the file part. Native local storage streams the file to disk and can enforce optional max_size_bytes without buffering. Other providers currently return PRESIGN_UNSUPPORTED; use a signed PUT there.

storage::presignUrl — GET-only response-override params

Two optional fields are accepted only when method is "GET". Passing either on a PUT presign returns INVALID_PRESIGN_PARAMS.

Field Type Description
response_content_disposition string (optional) Override Content-Disposition header on the served response (e.g. "attachment; filename=\"report.pdf\"").
response_content_type string (optional) Override Content-Type header on the served response (e.g. "application/pdf").
const { url } = await iii.trigger({
  function_id: 'storage::presignUrl',
  payload: {
    bucket: 'uploads',
    key: 'reports/q1.pdf',
    method: 'GET',
    expires_in_seconds: 300,
    response_content_disposition: 'attachment; filename="q1.pdf"',
    response_content_type: 'application/pdf',
  },
})

Local development & testing

The committed config.yaml declares a single scratch bucket served directly by the worker. Pass it as a seed so the configuration worker picks it up on first boot — zero cloud credentials required.

# In one terminal: start the engine (must include the configuration worker)
iii --config config.yaml

# In another: build & run the worker, seeding config.yaml on first registration
cargo run --release -- --url ws://127.0.0.1:49134 --config ./config.yaml

The worker registers its schema with the configuration worker (seeding config.yaml if no stored value exists), fetches the live config, initializes the native store and optional direct-transfer HTTP listener, and registers storage::putObject, storage::getObject, storage::deleteObject, storage::presignUrl, storage::presignPost, storage::headObject, storage::listBuckets, and storage::listObjects. Files land under data/storage/ under III_COMPOSE_DIR (configurable via providers.local.data_dir).

Running --manifest prints the registry-publish JSON without touching the engine — useful when testing CI flows:

cargo run -- --manifest | jq .

Tests

cargo test --lib                # unit tests (config, manifest, handlers, triggers)
cargo test --test schemas       # schema regression for every `storage::*` RPC
cargo test --test manifest      # `--manifest` subprocess contract
cargo test --test integration   # spec §9 pattern A: spawns engine + worker

tests/integration.rs self-skips when the iii engine is not available on PATH. Native local tests require no external storage dependency. The richer per-provider e2e suite under tests/e2e/ is env-var-gated — see tests/e2e/run-tests.sh for the orchestrator.

Verification before publishing

The full preflight checklist for binary workers (docs/sops/binary-worker.md §11):

cargo fmt --all -- --check
cargo clippy --all-targets --all-features -- -D warnings
cargo test --all-features
./target/debug/storage --manifest | jq .

api reference (json)

agent-api-reference.json
{
  "functions": [
    {
      "description": "Delete an object. No-op when the object does not exist.",
      "metadata": {},
      "name": "storage::deleteObject",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "bucket": {
            "type": "string"
          },
          "key": {
            "type": "string"
          },
          "version_id": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "bucket",
          "key"
        ],
        "title": "DeleteReq",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "deleted": {
            "type": "boolean"
          }
        },
        "required": [
          "deleted"
        ],
        "title": "DeleteResp",
        "type": "object"
      }
    },
    {
      "description": "Download a small object inline as base64 (10 MiB hard limit). This buffers and inflates the payload; use a presignUrl GET for files and large objects.",
      "metadata": {},
      "name": "storage::getObject",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "description": "Read a genuinely small object inline. The complete object is buffered and base64-encoded, with a 10 MiB decoded hard limit. Use a signed GET from `storage::presignUrl` for files and larger objects.",
        "properties": {
          "bucket": {
            "type": "string"
          },
          "key": {
            "type": "string"
          },
          "version_id": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "bucket",
          "key"
        ],
        "title": "GetReq",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "body_base64": {
            "type": "string"
          },
          "content_type": {
            "type": "string"
          },
          "etag": {
            "type": "string"
          },
          "last_modified": {
            "format": "date-time",
            "type": "string"
          },
          "size": {
            "format": "uint64",
            "minimum": 0,
            "type": "integer"
          }
        },
        "required": [
          "body_base64",
          "content_type",
          "etag",
          "last_modified",
          "size"
        ],
        "title": "GetResp",
        "type": "object"
      }
    },
    {
      "description": "Fetch object metadata (size, ETag, content-type, last-modified) without downloading the body.",
      "metadata": {},
      "name": "storage::headObject",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "bucket": {
            "type": "string"
          },
          "key": {
            "type": "string"
          },
          "version_id": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "bucket",
          "key"
        ],
        "title": "HeadReq",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "content_type": {
            "type": "string"
          },
          "etag": {
            "type": "string"
          },
          "last_modified": {
            "format": "date-time",
            "type": "string"
          },
          "size": {
            "format": "uint64",
            "minimum": 0,
            "type": "integer"
          }
        },
        "required": [
          "content_type",
          "etag",
          "last_modified",
          "size"
        ],
        "title": "HeadResp",
        "type": "object"
      }
    },
    {
      "description": "List configured worker-facing storage buckets and their providers.",
      "metadata": {},
      "name": "storage::listBuckets",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "title": "ListBucketsReq",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "BucketSummary": {
            "properties": {
              "name": {
                "type": "string"
              },
              "provider": {
                "type": "string"
              }
            },
            "required": [
              "name",
              "provider"
            ],
            "type": "object"
          }
        },
        "properties": {
          "buckets": {
            "items": {
              "$ref": "#/definitions/BucketSummary"
            },
            "type": "array"
          }
        },
        "required": [
          "buckets"
        ],
        "title": "ListBucketsResp",
        "type": "object"
      }
    },
    {
      "description": "List objects and common prefixes in a bucket with provider-native pagination.",
      "metadata": {},
      "name": "storage::listObjects",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "bucket": {
            "type": "string"
          },
          "cursor": {
            "type": [
              "string",
              "null"
            ]
          },
          "delimiter": {
            "description": "Directory separator. Use `/` for the bucket/folder explorer; omit it for a flat recursive listing.",
            "type": [
              "string",
              "null"
            ]
          },
          "limit": {
            "default": 250,
            "format": "uint32",
            "minimum": 0,
            "type": "integer"
          },
          "prefix": {
            "default": "",
            "type": "string"
          }
        },
        "required": [
          "bucket"
        ],
        "title": "ListObjectsReq",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "ObjectSummary": {
            "properties": {
              "content_type": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "etag": {
                "type": "string"
              },
              "key": {
                "type": "string"
              },
              "last_modified": {
                "format": "date-time",
                "type": "string"
              },
              "size": {
                "format": "uint64",
                "minimum": 0,
                "type": "integer"
              }
            },
            "required": [
              "etag",
              "key",
              "last_modified",
              "size"
            ],
            "type": "object"
          }
        },
        "properties": {
          "common_prefixes": {
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ]
          },
          "objects": {
            "items": {
              "$ref": "#/definitions/ObjectSummary"
            },
            "type": "array"
          }
        },
        "required": [
          "common_prefixes",
          "objects"
        ],
        "title": "ListObjectsResp",
        "type": "object"
      }
    },
    {
      "description": "Internal: transactionally apply the authoritative storage configuration without restarting the worker.",
      "metadata": {
        "internal": true
      },
      "name": "storage::on-config-change",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "id": {
            "default": null,
            "type": [
              "string",
              "null"
            ]
          }
        },
        "title": "OnConfigChangeEvent",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "ok": {
            "type": "boolean"
          }
        },
        "required": [
          "ok"
        ],
        "title": "OnConfigChangeResponse",
        "type": "object"
      }
    },
    {
      "description": "Issue a short-lived multipart/form-data POST for direct browser upload. The file never buffers in the engine or worker RPC path.",
      "metadata": {},
      "name": "storage::presignPost",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "bucket": {
            "type": "string"
          },
          "content_type": {
            "type": "string"
          },
          "expires_in_seconds": {
            "default": 600,
            "format": "uint64",
            "minimum": 0,
            "type": "integer"
          },
          "key": {
            "type": "string"
          },
          "max_size_bytes": {
            "description": "Optional upload cap enforced while the HTTP body is streamed.",
            "format": "uint64",
            "minimum": 0,
            "type": [
              "integer",
              "null"
            ]
          }
        },
        "required": [
          "bucket",
          "content_type",
          "key"
        ],
        "title": "PresignPostReq",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "expires_at": {
            "format": "date-time",
            "type": "string"
          },
          "fields": {
            "additionalProperties": {
              "type": "string"
            },
            "type": "object"
          },
          "url": {
            "type": "string"
          }
        },
        "required": [
          "expires_at",
          "fields",
          "url"
        ],
        "title": "PresignPostResp",
        "type": "object"
      }
    },
    {
      "description": "Issue a short-lived URL the browser can hit directly to PUT or GET an object.",
      "metadata": {},
      "name": "storage::presignUrl",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "HttpMethod": {
            "enum": [
              "GET",
              "PUT"
            ],
            "type": "string"
          }
        },
        "properties": {
          "bucket": {
            "type": "string"
          },
          "content_type": {
            "type": [
              "string",
              "null"
            ]
          },
          "expires_in_seconds": {
            "default": 600,
            "format": "uint64",
            "minimum": 0,
            "type": "integer"
          },
          "key": {
            "type": "string"
          },
          "method": {
            "$ref": "#/definitions/HttpMethod"
          },
          "response_content_disposition": {
            "description": "GET-only: override `Content-Disposition` header on the served response. Rejected with `INVALID_PRESIGN_PARAMS` if set on a PUT presign.",
            "type": [
              "string",
              "null"
            ]
          },
          "response_content_type": {
            "description": "GET-only: override `Content-Type` header on the served response. Rejected with `INVALID_PRESIGN_PARAMS` if set on a PUT presign.",
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "bucket",
          "key",
          "method"
        ],
        "title": "PresignReq",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "expires_at": {
            "format": "date-time",
            "type": "string"
          },
          "url": {
            "type": "string"
          }
        },
        "required": [
          "expires_at",
          "url"
        ],
        "title": "PresignResp",
        "type": "object"
      }
    },
    {
      "description": "Upload a small object inline as base64 (10 MiB hard limit). This buffers and inflates the payload; use presignPost or a presignUrl PUT for files and large objects.",
      "metadata": {},
      "name": "storage::putObject",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "description": "Write a genuinely small object inline. The complete base64 payload is buffered, with a 10 MiB decoded hard limit. Use `storage::presignPost` or a signed PUT from `storage::presignUrl` for files and larger objects.",
        "properties": {
          "body_base64": {
            "description": "Base64-encoded small body (10 MiB decoded hard limit). The legacy field name `body` is accepted as an alias for backward compatibility.",
            "type": "string"
          },
          "bucket": {
            "type": "string"
          },
          "cache_control": {
            "type": [
              "string",
              "null"
            ]
          },
          "content_type": {
            "type": [
              "string",
              "null"
            ]
          },
          "key": {
            "type": "string"
          },
          "metadata": {
            "additionalProperties": {
              "type": "string"
            },
            "default": {},
            "type": "object"
          }
        },
        "required": [
          "body_base64",
          "bucket",
          "key"
        ],
        "title": "PutReq",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "etag": {
            "type": "string"
          },
          "size": {
            "format": "uint64",
            "minimum": 0,
            "type": "integer"
          },
          "version_id": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "etag",
          "size"
        ],
        "title": "PutResp",
        "type": "object"
      }
    },
    {
      "description": "Serve the storage worker's injected console UI assets (content function for its console:script / console:style triggers).",
      "metadata": {
        "internal": true
      },
      "name": "storage::ui-content",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "description": "Input of the content function: the console asks for one asset by path.",
        "properties": {
          "path": {
            "description": "The asset path from the trigger config (e.g. `state/page.js`).",
            "type": "string"
          }
        },
        "required": [
          "path"
        ],
        "title": "UiContentInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "description": "Output of the content function.",
        "properties": {
          "content": {
            "description": "The asset source, verbatim.",
            "type": "string"
          },
          "content_type": {
            "description": "MIME type the console should serve the asset with.",
            "type": "string"
          }
        },
        "required": [
          "content",
          "content_type"
        ],
        "title": "UiContentResult",
        "type": "object"
      }
    }
  ],
  "triggers": [
    {
      "description": "Fires when an object is written to a configured bucket whose notifications source is wired up.",
      "invocation_schema": {},
      "metadata": {},
      "name": "storage::object-created",
      "return_schema": {}
    },
    {
      "description": "Fires when an object is removed from a configured bucket whose notifications source is wired up.",
      "invocation_schema": {},
      "metadata": {},
      "name": "storage::object-deleted",
      "return_schema": {}
    }
  ]
}