diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml index 36528d9..f819f89 100644 --- a/.github/workflows/python-ci.yml +++ b/.github/workflows/python-ci.yml @@ -10,13 +10,24 @@ on: jobs: build: - runs-on: ubuntu-latest + runs-on: forge-amd64-medium steps: - uses: actions/checkout@v4 - - name: Set Up Rust Toolchain + - name: Get wasiless submodule + # Org-internal submodules are very tricky to check out. + env: + SSHK: ${{ secrets.WASILESS_REPO_READ_KEY }} + run: | + mkdir -p $HOME/.ssh + echo "$SSHK" > $HOME/.ssh/ssh.key + chmod 600 $HOME/.ssh/ssh.key + export GIT_SSH_COMMAND="ssh -i $HOME/.ssh/ssh.key" + git submodule update --init --recursive + - name: Set up Rust toolchain uses: actions-rust-lang/setup-rust-toolchain@v1 with: toolchain: '1.86.0' + target: wasm32-unknown-unknown - name: Set up Python uses: actions/setup-python@v5 with: @@ -25,8 +36,20 @@ jobs: run: pip install uv - name: Install dependencies run: uv sync + - name: Cache viceroy, wasm-tools, etc. + id: cache-cargo + uses: actions/cache@v4 + with: + key: python-sdk-compatible-1 + path: | + ~/.cargo/bin/ + - name: Install wasm-tools and wac + if: steps.cache-cargo.outputs.cache-hit != 'true' + run: cargo install wasm-tools wac-cli - name: Install viceroy - run: cargo install --git https://github.com/fastly/Viceroy.git --branch sunfishcode/sync-wit viceroy + if: steps.cache-cargo.outputs.cache-hit != 'true' + # When you switch tags, update the cache key in the cache-cargo step. + run: cargo install --git https://github.com/fastly/Viceroy.git --tag erik/python-sdk-compatible-1 viceroy - name: Install dependencies run: uv sync --extra dev --extra test - name: Check formatting diff --git a/.gitignore b/.gitignore index 497a726..499f86d 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,3 @@ __pycache__ # Build artifacts /build/ -*.wasm diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..cc9a32c --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "vendor/wasiless"] + path = vendor/wasiless + url = git@github.com:fastly/wasiless.git diff --git a/Makefile b/Makefile index cd866fc..f8b63d3 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,8 @@ # Fastly Compute Python SDK +# Default Viceroy location. Set VICEROY env var to change. +VICEROY ?= viceroy + # Configuration STUBS_DIR := stubs BUILD_DIR := build @@ -10,20 +13,35 @@ EXAMPLES := wit-bottle flask-app # Default example for serve target EXAMPLE ?= wit-bottle -WASM_FILE := $(BUILD_DIR)/$(EXAMPLE).wasm +WASM_FILE := $(BUILD_DIR)/$(EXAMPLE).composed.wasm + +TARGET_WORLD := fastly:compute/service # Generate WASM file paths for all examples EXAMPLE_WASMS := $(foreach example,$(EXAMPLES),$(BUILD_DIR)/$(example).wasm) +# Composed wasm for each example +COMPOSED_WASMS := $(foreach example,$(EXAMPLES),$(BUILD_DIR)/$(example).composed.wasm) + +WASILESS_ROOT := vendor/wasiless +WASILESS_WASM := $(WASILESS_ROOT)/wasiless.wasm + # Default target builds all examples -all: $(EXAMPLE_WASMS) +all: $(COMPOSED_WASMS) + +$(BUILD_DIR)/%.composed.wasm: $(BUILD_DIR)/%.wasm $(WASILESS_WASM) + @echo "Composing $* example" + wac compose --dep fastly:wasiless=$(WASILESS_WASM) --dep app:component=$< -o $@ wrap_app_in_wasiless.wac # Pattern rule for building any example -$(BUILD_DIR)/%.wasm: $(EXAMPLES_DIR)/%.py wit/viceroy.wit wit/deps/fastly/compute.wit | $(BUILD_DIR) +$(BUILD_DIR)/%.wasm: $(EXAMPLES_DIR)/%.py wit/viceroy.wit wit/deps/fastly/compute.wit fastly_compute/wsgi.py | $(BUILD_DIR) @echo "Building $* example..." rm -rf $(STUBS_DIR) - uv run componentize-py -d wit -w viceroy bindings $(STUBS_DIR) - uv run componentize-py -d wit -w viceroy componentize $* -p $(EXAMPLES_DIR) -p . -o $@ + uv run componentize-py -d wit -w $(TARGET_WORLD) bindings $(STUBS_DIR) + uv run componentize-py -d wit -w $(TARGET_WORLD) componentize $* -p $(EXAMPLES_DIR) -p . -o $@ + +$(WASILESS_WASM): + $(MAKE) -C $(WASILESS_ROOT) wasiless.wasm # Create build directory $(BUILD_DIR): @@ -32,10 +50,10 @@ $(BUILD_DIR): # Serve the specified example (default: wit-bottle) serve: $(WASM_FILE) @echo "Serving $(EXAMPLE) example on http://127.0.0.1:7676" - viceroy serve $(WASM_FILE) + $(VICEROY) serve $(WASM_FILE) # Test all examples (requires all WASM files to be built) -test: $(EXAMPLE_WASMS) +test: $(COMPOSED_WASMS) uv run --extra test pytest # List available examples @@ -87,5 +105,4 @@ help: @echo "" @echo "Available examples: $(EXAMPLES)" -.PHONY: all serve test list-examples build-all clean lint lint-fix format format-check help - +.PHONY: all serve test list-examples build-all clean lint lint-fix format format-check help $(WASILESS_WASM) diff --git a/fastly_compute/testing.py b/fastly_compute/testing.py index c685abb..8243243 100644 --- a/fastly_compute/testing.py +++ b/fastly_compute/testing.py @@ -50,7 +50,7 @@ def test_my_endpoint(self): """ REQUEST_TIMEOUT = 10 - WASM_FILE = "build/wit-bottle.wasm" # Default to the main example + WASM_FILE = "build/wit-bottle.composed.wasm" # Default to the main example server: ViceroyServer = None # Will be set by the fixture @staticmethod diff --git a/fastly_compute/wsgi.py b/fastly_compute/wsgi.py index cccd636..a5bd7f7 100644 --- a/fastly_compute/wsgi.py +++ b/fastly_compute/wsgi.py @@ -40,7 +40,7 @@ def serve_wsgi_request( def write(body_data: bytes) -> None: """Write response body data (deprecated WSGI mechanism).""" - http_body.write(response_body, body_data, http_body.WriteEnd.BACK) + http_body.write(response_body, body_data) def start_response( status: str, headers: list[tuple[str, str]], exc_info: Any | None = None diff --git a/tests/test_flask_example.py b/tests/test_flask_example.py index 5d5540f..3dd19b7 100644 --- a/tests/test_flask_example.py +++ b/tests/test_flask_example.py @@ -1,14 +1,12 @@ """Tests for the Flask example application.""" -import pytest - from fastly_compute.testing import ViceroyTestBase class TestFlaskApp(ViceroyTestBase): """Integration tests for the Flask example application.""" - WASM_FILE = "build/flask-app.wasm" + WASM_FILE = "build/flask-app.composed.wasm" def test_hello_endpoint(self): """Test the hello endpoint returns expected content.""" diff --git a/vendor/wasiless b/vendor/wasiless new file mode 160000 index 0000000..4c9ac50 --- /dev/null +++ b/vendor/wasiless @@ -0,0 +1 @@ +Subproject commit 4c9ac507bae44e7964c0f689cb67c0f505e210a7 diff --git a/wit/deps/fastly/compute.wit b/wit/deps/fastly/compute.wit index 0346749..068584d 100644 --- a/wit/deps/fastly/compute.wit +++ b/wit/deps/fastly/compute.wit @@ -67,6 +67,31 @@ interface types { limit-exceeded, } + /// An error returned by `open`-like functions. + enum open-error { + /// The given name of the entity to open was invalid. + invalid-syntax, + /// The given name is longer the maximum permitted length. + name-too-long, + /// The given name is a reserved name that may not be opened. + reserved, + /// No entity by the given name was found. + not-found, + /// Unsupported operation error. + /// + /// This error is returned when some operation cannot be performed, because it is not supported. + unsupported, + /// Limit exceeded + /// + /// This is returned when an attempt to allocate a resource has exceeded the maximum number of + /// resources permitted. For example, creating too many response handles. + limit-exceeded, + /// Generic error value. + /// + /// This means that some unexpected error occurred. + generic-error, + } + /// IPv4 addresses. type ipv4-address = tuple; @@ -171,16 +196,15 @@ interface http-body { read: func(body: borrow, chunk-size: u32) -> result, error>; /// Writes to a body. - write: func(body: borrow, buf: list, end: write-end) -> result; - - /// Which side of a body to write to. - enum write-end { - /// Write to the back of the body; that is, append to it. - back, + /// + /// This function may write fewer bytes than requested; on success, the number of + /// bytes actually written is returned. + write: func(body: borrow, buf: list) -> result; - /// Write to the front of the body; that is, prepend to it. - front - } + /// Prepends bytes to the front of a body. + /// + /// On success, this function always writes all the bytes of `buf`. + write-front: func(body: borrow, buf: list) -> result<_, error>; /// Frees a body. /// @@ -224,7 +248,7 @@ interface http-body { body: borrow, max-len: u64, cursor: u32, - ) -> result>, error>; + ) -> result>, trailer-error>; /// Gets the value for the trailer with the given name, or `none` if the trailer is not present. /// @@ -239,7 +263,7 @@ interface http-body { body: borrow, name: string, max-len: u64, - ) -> result>, error>; + ) -> result>, trailer-error>; /// Gets multiple values associated with the trailer with the given name. /// @@ -255,15 +279,24 @@ interface http-body { name: string, max-len: u64, cursor: u32 - ) -> result, option>, error>; + ) -> result, option>, trailer-error>; + + /// Trailers aren't available until the body has been completely transmitted, so this error + /// type can either indicate that the errors aren't available yet, or that an error occurred. + variant trailer-error { + /// The trailers aren't available yet. + not-available-yet, + + /// An error occurred. + error(error), + } } /// Low-level interface to Fastly's [Real-Time Log Streaming] endpoints. /// /// [Real-Time Log Streaming]: https://docs.fastly.com/en/guides/about-fastlys-realtime-log-streaming-features interface log { - - use types.{error}; + use types.{error, open-error}; /// A logging endpoint. resource endpoint { @@ -278,7 +311,7 @@ interface log { /// logging endpoint available in your service will still return a usable endpoint, and writes /// to that endpoint will succeed. Refer to your service dashboard to diagnose missing log /// events. - get: static func(name: string) -> result; + open: static func(name: string) -> result; /// Writes a data to the given endpoint. /// @@ -294,7 +327,7 @@ interface log { interface http-downstream { use types.{error, ip-address}; use http-req.{ - request, client-cert-verify-result, error-with-detail, cache-override, request-promise, + request, client-cert-verify-result, error-with-detail, cache-override, pending-request, request-with-body, }; @@ -312,16 +345,18 @@ interface http-downstream { /// Starts waiting for the next request. next-request: func( options: next-request-options, - ) -> result; + ) -> result; /// Waits until the next request is available, and then returns the resulting /// request and body. - await-next-request: func( - pending: request-promise, - ) -> result; + /// + /// Returns `ok(none)` if there are no more requests for this session. + await-request: func( + pending: pending-request, + ) -> result, error>; next-request-abandon: func( - pending: request-promise, + pending: pending-request, ) -> result<_, error>; /// Returns the client request's header names exactly as they were originally received. @@ -376,67 +411,85 @@ interface http-downstream { /// Returns whether the request was tagged as contributing to a DDoS attack. downstream-client-ddos-detected: func( ds-request: borrow - ) -> result; + ) -> result; /// Gets the cipher suite used to secure the downstream client TLS connection. /// /// The value returned will be consistent with the [OpenSSL name] for the cipher suite. /// + /// Returns `ok(none)` if the downstream client connection is not a TLS connection. + /// /// [OpenSSL name]: https://testssl.sh/openssl-iana.mapping.html downstream-tls-cipher-openssl-name: func( ds-request: borrow, max-len: u64 - ) -> result, error>; + ) -> result>, error>; /// Gets the TLS protocol version used to secure the downstream client TLS connection. + /// + /// Returns `ok(none)` if the downstream client connection is not a TLS connection. downstream-tls-protocol: func( ds-request: borrow, max-len: u64 - ) -> result, error>; + ) -> result>, error>; /// Gets the raw bytes sent by the client in the TLS ClientHello message. /// /// See [RFC 5246] for details. /// + /// Returns `ok(none)` if the downstream client connection is not a TLS connection. + /// /// [RFC 5246]: https://www.rfc-editor.org/rfc/rfc5246#section-7.4.1.2 downstream-tls-client-hello: func( ds-request: borrow, max-len: u64 - ) -> result, error>; + ) -> result>, error>; /// Gets the raw client certificate used to secure the downstream client mTLS connection. /// /// The value returned will be based on PEM format. /// - /// Returns `error.optional-none` if the connection is not mTLS or is unavailable. + /// Returns `ok(none)` if the downstream client connection is not a TLS connection. downstream-tls-raw-client-certificate: func( ds-request: borrow, max-len: u64 - ) -> result, error>; + ) -> result>, error>; /// Returns the `client-cert-verify-result` from the downstream client mTLS handshake. /// - /// Returns `none` if not available. + /// Returns `ok(none)` if the downstream client connection is not a TLS connection. downstream-tls-client-cert-verify-result: func( ds-request: borrow - ) -> result; + ) -> result, error>; + + /// Returns the Server Name Indication from the downstream client TLS handshake. + /// + /// Returns `ok(none)` if not available. + downstream-tls-client-servername: func( + ds-request: borrow, + max-len: u64 + ) -> result, error>; /// Gets the JA3 hash of the TLS ClientHello message. + /// + /// Returns `ok(none)` if the downstream client connection is not a TLS connection. downstream-tls-ja3-md5: func( ds-request: borrow - ) -> result, error>; + ) -> result>, error>; /// Gets the JA4 hash of the TLS ClientHello message. + /// + /// Returns `ok(none)` if the downstream client connection is not a TLS connection. downstream-tls-ja4: func( ds-request: borrow, max-len: u64 - ) -> result; + ) -> result, error>; /// Gets the compliance region that the client IP address is in. downstream-compliance-region: func( ds-request: borrow, max-len: u64 - ) -> result; + ) -> result, error>; /// Returns whether or not the original client request arrived with a /// Fastly-Key belonging to a user with the rights to purge content on this @@ -453,14 +506,13 @@ interface http-req { use http-types.{http-version, content-encodings, framing-headers-mode, tls-version}; use http-resp.{response}; use http-body.{body}; - use secret-store.{secret}; use http-resp.{response-with-body}; - /// Handle that can be used to wait for a sent request. - use async-io.{pollable as pending-request}; + /// Handle that can be used to wait for a response from a sent request. + use async-io.{pollable as pending-response}; /// Handle that can be used to wait for incoming requests. - use async-io.{pollable as request-promise}; + use async-io.{pollable as pending-request}; /// An HTTP request. resource request { @@ -586,23 +638,6 @@ interface http-req { mode: framing-headers-mode, ) -> result<_, error>; - /// Inspects request HTTP traffic using the [NGWAF] lookaside service. - /// - /// Returns a JSON-encoded string. - /// - /// [NGWAF]: https://docs.fastly.com/en/ngwaf/ - inspect: func( - body: borrow, - options: inspect-options, - max-len: u64 - ) -> result; - - /// Instead of having this request cache in this service's space, use the - /// cache of the named service - on-behalf-of: func( - service: string, - ) -> result<_, error>; - redirect-to-grip-proxy: func( backend: string, ) -> result<_, error>; @@ -629,15 +664,15 @@ interface http-req { ) -> result; /// Begins sending the request to the given backend server, and returns a - /// `pending-request` that can yield the backend response or an error. + /// `pending-response` that can yield the backend response or an error. /// /// This method returns as soon as the request begins sending to the backend, /// and transmission of the request body and headers will continue in the /// background. /// /// This method allows for sending more than one request at once and receiving - /// their responses in arbitrary orders. See `pending-request` for more - /// details on how to wait on, poll, or select between pending requests. + /// their responses in arbitrary orders. See `pending-response` for more + /// details on how to wait on, poll, or select between pending responses. /// /// This method is also useful for sending requests where the response is /// unimportant, but the request may take longer than the Compute program is @@ -647,7 +682,7 @@ interface http-req { request: request, body: body, backend: string - ) -> result; + ) -> result; /// This is to `send-async` as `send-uncached` is to `send`. /// @@ -658,15 +693,15 @@ interface http-req { request: request, body: body, backend: string, - ) -> result; + ) -> result; /// Begins sending the request to the given backend server, and returns a - /// `pending-request` that can yield the backend response or an error. + /// `pending-response` that can yield the backend response or an error. /// /// The `body` argument is not consumed, so that it can accept further data to send. /// /// The backend connection is only closed once `http-body.close` is called. The - /// `pending-request` will not yield a `response` until the body is finished. + /// `pending-response` will not yield a `response` until the body is finished. /// /// This method is most useful for programs that do some sort of processing or /// inspection of a potentially-large client request body. Streaming allows the @@ -680,7 +715,7 @@ interface http-req { request: request, body: borrow, backend: string, - ) -> result; + ) -> result; /// This is to `send-async-streaming` as `send-uncached` is to `send`. /// @@ -691,7 +726,7 @@ interface http-req { request: request, body: borrow, backend: string, - ) -> result; + ) -> result; type request-with-body = tuple; @@ -761,18 +796,14 @@ interface http-req { certificate-unknown, } - enum send-error-detail-tag { - /// The $send_error_detail struct has not been populated. - uninitialized, - /// There was no send error. - ok, + /// Information about errors encountered by sent requests. + variant send-error-detail { /// The system encountered a timeout when trying to find an IP address for the backend /// hostname. dns-timeout, /// The system encountered a DNS error when trying to find an IP address for the backend - /// hostname. The fields `dns-error-rcode` and `dns-error-info-code` may be set in the - /// $send_error_detail. - dns-error, + /// hostname. + dns-error(dns-error-detail), /// The system cannot determine which backend to use, or the specified backend was invalid. destination-not-found, /// The system considers the backend to be unavailable, for example when recent attempts to @@ -819,21 +850,25 @@ interface http-req { http-request-uri-invalid, /// The system encountered an unexpected internal error. internal-error, - /// The system received a TLS alert from the backend. The field `tls-alert-id` may be set in - /// the $send_error_detail. - tls-alert-received, + /// The system received a TLS alert from the backend. + tls-alert-received(tls-alert-received-detail), /// The system encountered a TLS error when communicating with the backend, either during /// the handshake or afterwards. tls-protocol-error, } - record send-error-detail { - tag: send-error-detail-tag, - dns-error-rcode: option, - dns-error-info-code: option, - tls-alert-id: option, + /// Variant fields for `send-error.dns-error`. + record dns-error-detail { + rcode: option, + info-code: option, + } + + /// Variant fields for `send-error.tls-alert-received`. + record tls-alert-received-detail { + id: option, } + /// An `error` code, optionally with extra request error information. record error-with-detail { detail: option, error: error, @@ -843,6 +878,7 @@ interface http-req { record inspect-options { corp: option, workspace: option, + override-client-ip: option, /// Additional options may be added in the future via this resource type. extra: option>, @@ -853,8 +889,8 @@ interface http-req { /// Waits until the request is completed, and then returns the resulting /// response and body. - await-request: func( - pending: pending-request + await-response: func( + pending: pending-response ) -> result; /// Closes the `request`, releasing any associated resources. @@ -865,37 +901,25 @@ interface http-req { upgrade-websocket: func(backend: string) -> result<_, error>; - /// Create a backend for later use - register-dynamic-backend: func( - prefix: string, - target: string, - options: dynamic-backend-options, - ) -> result<_, error>; +} - /// Create a backend for later use - resource dynamic-backend-options { - constructor(); +/// [Fastly Next-Gen WAF] API. +/// +/// [Fastly Next-Gen WAF]: https://docs.fastly.com/en/ngwaf/ +interface security { + use http-req.{request, body, inspect-options, error}; - host-override: func(value: string); - connect-timeout: func(value: u32); - first-byte-timeout: func(value: u32); - between-bytes-timeout: func(value: u32); - use-tls: func(value: bool); - tls-min-version: func(value: tls-version); - tls-max-version: func(value: tls-version); - cert-hostname: func(value: string); - ca-cert: func(value: string); - ciphers: func(value: string); - sni-hostname: func(value: string); - client-cert: func(client-cert: string, key: borrow); - http-keepalive-time-ms: func(value: u32); - tcp-keepalive-enable: func(value: u32); - tcp-keepalive-interval-secs: func(value: u32); - tcp-keepalive-probes: func(value: u32); - tcp-keepalive-time-secs: func(value: u32); - pooling: func(value: bool); - grpc: func(value: bool); - } + /// Inspects request HTTP traffic using the [NGWAF] lookaside service. + /// + /// Returns a JSON-encoded string. + /// + /// [NGWAF]: https://docs.fastly.com/en/ngwaf/ + inspect: func( + request: borrow, + body: borrow, + options: inspect-options, + max-len: u64 + ) -> result; } /// HTTP responses. @@ -1047,21 +1071,20 @@ interface http-resp { /// /// [Compute Dictionaries]: https://www.fastly.com/documentation/guides/concepts/edge-state/dynamic-config/#dictionaries interface dictionary { - - use types.{error}; + use types.{error, open-error}; /// A Compute Dictionary. resource dictionary { /// Opens a dictionary, given its name. /// /// Names are case sensitive. - open: static func(name: string) -> result; + open: static func(name: string) -> result; /// Tries to look up a value in this dictionary. /// - /// If the lookup is successful, this function returns `ok(s)` containing the found string - /// `s`, or `err(error.optional-none)` if no entry with the given key was found. - get: func( + /// If the lookup is successful, this function returns `ok(some(s))` containing the found + /// string `s`, or `ok(none)` if no entry with the given key was found. + lookup: func( key: string, max-len: u64, ) -> result, error>; @@ -1154,58 +1177,6 @@ interface erl { ) -> result; } -/// Object Store (deprecated in favor of `kv-store`) -interface object-store { - - use types.{error}; - use http-body.{body}; - - /// (DEPRECATED) An Object Store. - resource store { - open: static func(name: string) -> result, error>; - - lookup: func( - key: string, - ) -> result, error>; - - lookup-async: func( - key: string, - ) -> result; - - insert: func( - key: string, - body: body, - ) -> result<_, error>; - - insert-async: func( - key: string, - body: body, - ) -> result; - - delete-async: func( - key: string, - ) -> result; - } - /// (DEPRECATED) A pending Object Store lookup. - resource pending-lookup {} - /// (DEPRECATED) A pending Object Store insert. - resource pending-insert {} - /// (DEPRECATED) A pending Object Store delete. - resource pending-delete {} - - await-pending-lookup: func( - handle: pending-lookup, - ) -> result, error>; - - await-pending-insert: func( - handle: pending-insert, - ) -> result<_, error>; - - await-pending-delete: func( - handle: pending-delete, - ) -> result<_, error>; -} - /// Interface to Fastly's [Compute KV Store]. /// /// For a high-level introduction to this feature, see this [blog post]. @@ -1213,16 +1184,13 @@ interface object-store { /// [Compute KV Store]: https://www.fastly.com/documentation/guides/concepts/edge-state/data-stores/#kv-stores /// [blog post]: https://www.fastly.com/blog/introducing-the-compute-edge-kv-store-global-persistent-storage-for-compute-functions interface kv-store { - - use types.{error}; + use types.{error, open-error}; use http-body.{body}; /// A KV Store. resource store { /// Opens the KV Store with the given name. - /// - /// If there is no store by that name, this returns `ok(none)`. - open: static func(name: string) -> result, error>; + open: static func(name: string) -> result; /// Looks up a value in the KV Store. /// @@ -1386,7 +1354,7 @@ interface kv-store { /// After calling this method, this entry will no longer have a body. take-body: func() -> option; - /// Read the metadata of the KV Store item. + /// Read the metadata of the KV Store item, if present. metadata: func(max-len: u64) -> result, error>; /// Read the current generation of the KV Store item. @@ -1484,8 +1452,7 @@ interface kv-store { /// /// [Secret Store]: https://www.fastly.com/documentation/reference/api/services/resources/secret-store/ interface secret-store { - - use types.{error}; + use types.{error, open-error}; /// An individual secret. resource secret { @@ -1507,13 +1474,13 @@ interface secret-store { /// Returns the plaintext value of this secret. plaintext: func( max-len: u64 - ) -> result>, error>; + ) -> result, error>; } /// A Secret Store. resource store { /// Opens the Secret Store with the given name. - open: static func(name: string) -> result; + open: static func(name: string) -> result; /// Tries to look up a Secret by name in this secret store. /// @@ -1529,35 +1496,34 @@ interface secret-store { /// /// [Access Control Lists]: https://www.fastly.com/documentation/reference/api/acls/ interface acl { - - use types.{error, ip-address}; + use types.{error, open-error, ip-address}; use http-body.{body}; /// An ACL. resource acl { /// Opens an ACL linked to the current service with the given link name. - open: static func(name: string) -> result; + open: static func(name: string) -> result; /// Performs a lookup of the given IP address in the ACL. /// - /// If no matches are found, then `ok(none)` is returned. + /// If no matches are found, then `ok(none)` is returned. This corresponds + /// to an HTTP error code of 204, “No Content”. lookup: func( ip-addr: ip-address, - ) -> result, acl-error>, error>; + ) -> result, acl-error>; } /// Errors returned on ACL lookup failure. enum acl-error { - /// The $acl_error has not been initialized. - uninitialized, - /// There was no error. - ok, - /// This will map to the api's 204 code. - /// It indicates that the request succeeded, yet returned nothing. - no-content, - /// This will map to the api's 429 code. /// Too many requests have been made. + /// + /// This corresponds to an HTTP error code of 429, “Too Many Requests”. too-many-requests, + + /// Generic error value. + /// + /// This means that some unexpected error occurred. + generic-error, } } @@ -1583,6 +1549,170 @@ interface acl { interface backend { use types.{error}; use http-types.{tls-version}; + use secret-store.{secret}; + + /// Creates a new dynamic backend. + /// + /// The arguments are the name of the new backend to use, along with a string describing the + /// backend host. The latter can be of the form: + /// + /// - "" + /// - "" + /// - ":" + /// - ":" + /// + /// The name can be whatever you would like, as long as it does not match the name of any of the + /// static service backends nor match any other dynamic backends built during this session. + /// (Names can overlap between different sessions of the same service—they will be treated as + /// completely separate entities and will not be pooled—but you cannot, for example, declare + /// a dynamic backend named “dynamic-backend” twice in the same session.) + /// + /// Dynamic backends must be enabled for the Compute service. You can determine whether or not + /// dynamic backends have been allowed for the current service by checking for the + /// `error.unsupported` error result. This error only arises when attempting to use dynamic + /// backends with a service that has not had dynamic backends enabled, or dynamic backends have + /// been administratively prohibited for the node in response to an ongoing incident. + register-dynamic-backend: func( + prefix: string, + target: string, + options: dynamic-backend-options, + ) -> result<_, error>; + + /// Options for `register-dynamic-backend`. + resource dynamic-backend-options { + /// Constructs an options resource with default values for all other possible fields for the + /// backend, which can be overridden using the other methods provided. + constructor(); + + /// Sets a host header override when contacting this backend. + /// + /// This will force the value of the “Host” header to the given string when sending out the + /// origin request. If this is not set and no header already exists, the “Host” header will + /// default to the target. + /// + /// For more information, see [the Fastly documentation on override hosts]. + /// + /// [the Fastly documentation on override hosts]: https://docs.fastly.com/en/guides/specifying-an-override-host> + override-host: func(value: string); + + /// Sets the connection timeout, in milliseconds, for this backend. + /// + /// Defaults to 1,000ms (1s). + connect-timeout: func(value: u32); + + /// Sets a timeout, in milliseconds, that applies between the time of connection and the time we + /// get the first byte back. + /// + /// Defaults to 15,000ms (15s). + first-byte-timeout: func(value: u32); + + /// Sets a timeout, in milliseconds, that applies between any two bytes we receive across the + /// wire. + /// + /// Defaults to 10,000ms (10s). + between-bytes-timeout: func(value: u32); + + /// Enables or disables TLS to connect to the backend. + /// + /// When using TLS, Fastly checks the validity of the backend’s certificate, and fails the + /// connection if the certificate is invalid. This check is not optional: an invalid + /// certificate will cause the backend connection to fail (but read on). + /// + /// By default, the validity check does not require that the certificate hostname matches the + /// hostname of your request. You can use check_certificate to request a check of the + /// certificate hostname. + /// + /// By default, certificate validity uses a set of public certificate authorities. You can + /// specify an alternative CA using ca_certificate. + use-tls: func(value: bool); + + /// Sets the minimum TLS version for connecting to the backend. + /// + /// Setting this will enable TLS for the connection as a side effect. + tls-min-version: func(value: tls-version); + + /// Sets the maximum TLS version for connecting to the backend. + /// + /// Setting this will enable TLS for the connection as a side effect. ( + tls-max-version: func(value: tls-version); + + /// Defines the hostname that the server certificate should declare, and turn on validation + /// during backend connections. + /// + /// You should enable this if you are using TLS, and setting this will enable TLS for the + /// connection as a side effect. + /// + /// If `check-certificate` is not provided (default), the server certificate’s hostname may + /// have any value. + cert-hostname: func(value: string); + + /// Sets the CA certificate to use when checking the validity of the backend. + /// + /// Setting this will enable TLS for the connection as a side effect. + /// + /// If `ca-certificate` is not provided (default), the backends’s certificate is validated + /// using a set of public root CAs. + ca-certificate: func(value: string); + + /// Sets the acceptable cipher suites to use for TLS 1.0 - 1.2 connections. + /// + /// Setting this will enable TLS for the connection as a side effect. + tls-ciphers: func(value: string); + + /// Sets the SNI hostname for the backend connection. + /// + /// Setting this will enable TLS for the connection as a side effect. + sni-hostname: func(value: string); + + /// Provides the given client certificate to the server as part of the TLS handshake. + /// + /// Setting this will enable TLS for the connection as a side effect. Both the certificate and + /// the key to use should be in standard PEM format; providing the information in another + /// format will lead to an error. We suggest that (at least the) key should be held in + /// something like the Fastly secret store for security, with the handle passed to this + /// function without unpacking it via Secret::plaintext; the certificate can be held in a less + /// secure medium. + /// + /// (If it is absolutely necessary to get the key from another source, we suggest the use of + /// `secret.from-bytes`. + client-cert: func(client-cert: string, key: borrow); + + /// Configures up to how long to allow HTTP keepalive connections to remain idle in the + /// connection pool. + http-keepalive-time-ms: func(value: u32); + + /// Configures whether or not to use TCP keepalive on the connection to the backend. + tcp-keepalive-enable: func(value: u32); + + /// Configures how long to wait in between each TCP keepalive probe sent to the backend. + tcp-keepalive-interval-secs: func(value: u32); + + /// Configures up to how many TCP keepalive probes to send to the backend before the connection + /// is considered dead. + tcp-keepalive-probes: func(value: u32); + + /// Configures how long to wait after the last sent data over the TCP connection before starting + /// to send TCP keepalive probes. + tcp-keepalive-time-secs: func(value: u32); + + /// Determines whether or not connections to the same backend should be pooled across different + /// sessions. + /// + /// Fastly considers two backends “the same” if they’re registered with the same name and + /// the exact same settings. In those cases, when pooling is enabled, if Session 1 opens a + /// connection to this backend it will be left open, and can be re-used by Session 2. This can + /// help improve backend latency, by removing the need for the initial + /// network / TLS handshake(s). + /// + /// By default, pooling is enabled for dynamic backends. + pooling: func(value: bool); + + /// Sets whether or not this backend will be used for gRPC traffic. + /// + /// Warning: Setting this for backends that will not be used with gRPC may have unpredictable + /// effects. Fastly only currently guarantees that this connection will work for gRPC traffic. + grpc: func(value: bool); + } type timeout-ms = u32; type timeout-secs = u32; @@ -1687,7 +1817,7 @@ interface backend { interface async-io { /// An object supporting generic async operations. /// - /// Can be a `http-body.body`, `http-req.pending-request`, `http-req.request-promise`, + /// Can be a `http-body.body`, `http-req.pending-response`, `http-req.pending-request`, /// `cache.pending-entry`. `kv-store.pending-lookup`, `kv-store.pending-insert`, /// `kv-store.pending-delete`, or `kv-store.pending-list`. /// @@ -1770,7 +1900,9 @@ interface purge { /// A surrogate key must contain only printable ASCII characters (those between `0x21` and `0x7E`, /// inclusive). /// - /// Never returns `error.optional-none`. + /// Returns a [JSON purge response]. + /// + /// [JSON purge response]: https://developer.fastly.com/reference/api/purging/#purge-tag purge-surrogate-key: func( surrogate-keys: string, purge-options: purge-options, @@ -1778,13 +1910,8 @@ interface purge { /// Purge a surrogate key for the current service, and return the purge id. /// - /// This is similar to `purge-surrogate-key`, but on success, returns a - /// [JSON purge response] containing an ASCII alphanumeric string identifying - /// a purging. - /// - /// Never returns `error.optional-none`. - /// - /// [JSON purge response]: https://developer.fastly.com/reference/api/purging/#purge-tag + /// This is similar to `purge-surrogate-key`, but on success, returns an + /// ASCII alphanumeric string identifying a purging. purge-surrogate-key-verbose: func( surrogate-keys: string, purge-options: purge-options, @@ -1865,11 +1992,11 @@ interface cache { get-state: func() -> result; - /// Gets the user metadata of the found object, returning `none` if no object + /// Gets the user metadata of the found object, returning `ok(none)` if no object /// was found. get-user-metadata: func(max-len: u64) -> result>, error>; - /// Gets a range of the found object body, returning the `optional-none` error if there + /// Gets a range of the found object body, returning `ok(none)` if there /// was no found object. /// /// The returned `body` must be closed before calling this function again on the same @@ -1882,25 +2009,25 @@ interface cache { options: get-body-options, ) -> result; - /// Gets the content length of the found object, returning the `error.optional-none` error if + /// Gets the content length of the found object, returning `ok(none)` if /// there was no found object, or no content length was provided. - get-length: func() -> result; + get-length: func() -> result, error>; - /// Gets the configured max age of the found object, returning the `error.optional-none` error + /// Gets the configured max age of the found object, returning `ok(none)` /// if there was no found object. - get-max-age-ns: func() -> result; + get-max-age-ns: func() -> result, error>; - /// Gets the configured stale-while-revalidate period of the found object, returning the - /// `error.optional-none` error if there was no found object. - get-stale-while-revalidate-ns: func() -> result; + /// Gets the configured stale-while-revalidate period of the found object, returning `ok(none)` + /// if there was no found object. + get-stale-while-revalidate-ns: func() -> result, error>; - /// Gets the age of the found object, returning the `error.optional-none` error if there + /// Gets the age of the found object, returning `ok(none)` if there /// was no found object. - get-age-ns: func() -> result; + get-age-ns: func() -> result, error>; - /// Gets the number of cache hits for the found object, returning the `error.optional-none` - /// error if there was no found object. - get-hits: func() -> result; + /// Gets the number of cache hits for the found object, returning `ok(none)` + /// if there was no found object. + get-hits: func() -> result, error>; /// Cancel an obligation to provide an object to the cache. /// @@ -1932,12 +2059,12 @@ interface cache { ) -> result; /// Gets the age of the existing object during replace, returning - /// `none` if there was no object. + /// `ok(none)` if there was no object. replace-get-age-ns: func( handle: borrow, ) -> result, error>; - /// Gets a range of the existing object body, returning `none` if there + /// Gets a range of the existing object body, returning `ok(none)` if there /// was no existing object. /// /// The returned `body` must be closed before calling this function @@ -1948,39 +2075,39 @@ interface cache { ) -> result, error>; /// Gets the number of cache hits for the existing object during replace, - /// returning `none` if there was no object. + /// returning `ok(none)` if there was no object. replace-get-hits: func( handle: borrow, ) -> result, error>; /// Gets the content length of the existing object during replace, - /// returning `none` if there was no object, or no content + /// returning `ok(none)` if there was no object, or no content /// length was provided. replace-get-length: func( handle: borrow, ) -> result, error>; /// Gets the configured max age of the existing object during replace, - /// returning the `error.optional-none` error if there was no object. + /// returning `ok(none)` if there was no object. replace-get-max-age-ns: func( handle: borrow, ) -> result, error>; /// Gets the configured stale-while-revalidate period of the existing - /// object during replace, returning the `error.optional-none` error if there was no + /// object during replace, returning `ok(none)` if there was no /// object. replace-get-stale-while-revalidate-ns: func( handle: borrow, ) -> result, error>; /// Gets the lookup state of the existing object during replace, returning - /// the `error.optional-none` error if there was no object. + /// `ok(none)` if there was no object. replace-get-state: func( handle: borrow, ) -> result, error>; /// Gets the user metadata of the existing object during replace, returning - /// the `error.optional-none` error if there was no object. + /// `ok(none)` if there was no object. replace-get-user-metadata: func( handle: borrow, max-len: u64, @@ -1999,8 +2126,6 @@ interface cache { /// request-headers: option>, - service-id: option, - always-use-requested-range: bool, /// Additional options may be added in the future via this resource type. @@ -2008,7 +2133,9 @@ interface cache { } /// Extensibility for `lookup-options` - resource extra-lookup-options {} + resource extra-lookup-options { + constructor(); + } /// Configuration for several functions that write to the cache: /// - `insert` @@ -2038,7 +2165,6 @@ interface cache { length: option, user-metadata: option>, edge-max-age-ns: option, - service-id: option, sensitive-data: bool, /// Additional options may be added in the future via this resource type. @@ -2046,7 +2172,9 @@ interface cache { } /// Extensibility for `write-options` - resource extra-write-options {} + resource extra-write-options { + constructor(); + } record get-body-options { %from: option, @@ -2096,14 +2224,13 @@ interface cache { /// choosing a new waiter to perform the insertion/update. /// /// This may be passed either an `entry` or a `replace-entry`. - close: func(handle: entry) -> result<_, error>; + close-entry: func(handle: entry) -> result<_, error>; /// Options for cache replace operations record replace-options { /// a full request handle, but used only for its headers request-headers: option>, replace-strategy: option, - service-id: option, always-use-requested-range: bool, /// Additional options may be added in the future via this resource type. @@ -2111,7 +2238,9 @@ interface cache { } /// Extensibility for `replace-options` - resource extra-replace-options {} + resource extra-replace-options { + constructor(); + } enum replace-strategy { /// Immediately start the replace and do not wait for any other pending requests for the same @@ -2169,17 +2298,11 @@ interface http-cache { use types.{error}; use http-body.{body}; use http-req.{request}; - use http-resp.{response}; + use http-resp.{response, response-with-body}; use cache.{lookup-state, object-length, duration-ns, cache-hit-count}; /// An HTTP Cache transaction. resource entry { - /// (DEPRECATED) Use transaction-lookup - lookup: static func( - req-handle: borrow, - options: lookup-options, - ) -> result; - /// Performs a cache lookup based on the given request. /// /// This operation always participates in request collapsing and may return an obligation to @@ -2278,7 +2401,7 @@ interface http-cache { response: borrow, ) -> result, error>; - /// Retrieves a stored response from the cache, returning the `error.optional-none` error if + /// Retrieves a stored response from the cache, returning `ok(none)` if /// there was no response found. /// /// If `transform-for-client` is set, the response will be adjusted according to the looked-up @@ -2286,7 +2409,7 @@ interface http-cache { /// `206 Partial Content` response with an appropriate `content-range` header. get-found-response: func( transform-for-client: u32, - ) -> result, error>; + ) -> result, error>; /// Gets the state of a cache transaction. /// @@ -2295,35 +2418,35 @@ interface http-cache { get-state: func( ) -> result; - /// Gets the length of the found response, returning the `error.optional-none` error if there + /// Gets the length of the found response, returning `ok(none)` if there /// was no response found or no length was provided. - get-length: func() -> result; + get-length: func() -> result, error>; - /// Gets the configured max age of the found response in nanoseconds, returning the - /// `error.optional-none` error if there was no response found. - get-max-age-ns: func() -> result; + /// Gets the configured max age of the found response in nanoseconds, returning `ok(none)` + /// if there was no response found. + get-max-age-ns: func() -> result, error>; /// Gets the configured stale-while-revalidate period of the found response in nanoseconds, - /// returning the `error.optional-none` error if there was no response found. + /// returning `ok(none)` if there was no response found. get-stale-while-revalidate-ns: func( - ) -> result; + ) -> result, error>; - /// Gets the age of the found response in nanoseconds, returning the `error.optional-none` error + /// Gets the age of the found response in nanoseconds, returning `ok(none)` /// if there was no response found. - get-age-ns: func() -> result; + get-age-ns: func() -> result, error>; - /// Gets the number of cache hits for the found response, returning the `error.optional-none` - /// error if there was no response found. + /// Gets the number of cache hits for the found response, returning `ok(none)` + /// if there was no response found. /// /// This figure only reflects hits for a stored response in a particular cache server /// or cluster, not the entire Fastly network. - get-hits: func() -> result; + get-hits: func() -> result, error>; - /// Gets whether a found response is marked as containing sensitive data, returning the - /// `error.optional-none` error if there was no response found. - get-sensitive-data: func() -> result; + /// Gets whether a found response is marked as containing sensitive data, returning `ok(none)` + /// if there was no response found. + get-sensitive-data: func() -> result, error>; - /// Gets the surrogate keys of the found response, returning the `error.optional-none` error if + /// Gets the surrogate keys of the found response, returning `ok(none)` if /// there was no response found. /// /// The output is a list of surrogate keys separated by spaces. @@ -2332,9 +2455,9 @@ interface http-cache { /// error is returned containing the required size. get-surrogate-keys: func( max-len: u64, - ) -> result; + ) -> result, error>; - /// Gets the vary rule of the found response, returning the `error.optional-none` error if there + /// Gets the vary rule of the found response, returning `ok(none)` if there /// was no response found. /// /// The output is a list of header names separated by spaces. @@ -2343,7 +2466,7 @@ interface http-cache { /// error is returned containing the required size. get-vary-rule: func( max-len: u64, - ) -> result; + ) -> result, error>; /// Abandons an obligation to provide a response to the cache. /// @@ -2474,7 +2597,7 @@ interface http-cache { /// If the cache handle state includes `must-insert-or-update` (and hence no insert or update /// has been performed), closing the handle cancels any request collapsing, potentially choosing /// a new waiter to perform the insertion/update. - close: func( + close-entry: func( handle: entry, ) -> result<_, error>; @@ -2505,16 +2628,16 @@ interface http-cache { /// /// [Config Store]: https://www.fastly.com/documentation/guides/concepts/edge-state/dynamic-config/#config-stores interface config-store { - use types.{error}; + use types.{error, open-error}; /// A Config Store. resource store { /// Attempts to open the named config store. /// /// Names are case sensitive. - open: static func(name: string) -> result; + open: static func(name: string) -> result; - /// Fetches a value from the config store, returning `none` if it doesn't exist. + /// Fetches a value from the config store, returning `ok(none)` if it doesn't exist. get: func( key: string, max-len: u64, @@ -2533,19 +2656,17 @@ interface shielding { max-len: u64, ) -> result; - record shield-backend-options { - cache-key: option, + /// Extensibility for `shield-backend-options` + resource shield-backend-options { + constructor(); - /// Additional options may be added in the future via this resource type. - extra: option>, + set-cache-key: func(cache-key: string); + set-first-byte-timeout: func(timeout-ms: u32); } - /// Extensibility for `shield-backend-options` - resource extra-shield-backend-options {} - backend-for-shield: func( name: string, - options: shield-backend-options, + options: option>, max-len: u64, ) -> result; } @@ -2572,24 +2693,11 @@ interface image-optimizer { /// Extensibility for `image-optimizer-transform-options` resource extra-image-optimizer-transform-options {} - enum image-optimizer-error-tag { - uninitialized, - ok, - error, - warning, - } - - record image-optimizer-error-detail { - tag: image-optimizer-error-tag, - message: list, - } - transform-image-optimizer-request: func( origin-image-request: borrow, origin-image-request-body: option, origin-image-request-backend: string, io-transform-options: image-optimizer-transform-options, - io-error-detail: image-optimizer-error-detail, ) -> result; } @@ -2619,7 +2727,7 @@ interface compute-runtime { /// A timestamp in milliseconds. type vcpu-ms = u64; - /// Gets the amount of vCPU time that has passed since this instance was started, in milliseconds. + /// Gets the amount of vCPU time that has passed since this session was started, in milliseconds. /// /// This function returns only time spent running on a vCPU, and does not include time spent /// performing any I/O operations. However, it is based on clock time passing, and so will include @@ -2629,24 +2737,112 @@ interface compute-runtime { /// As a result, this function *should not be used in benchmarking across runs*. It can be used, /// with caution, to compare the runtime of different operations within the same session. get-vcpu-ms: func() -> vcpu-ms; + + /// A UUID generated by Fastly for each session. + /// + /// This is often a useful value to include in log messages, and also to send to upstream + /// servers as an additional custom HTTP header, allowing for straightforward correlation of + /// which WebAssembly session processed a request to requests later processed by an origin + /// server. If a session is used to process multiple downstream requests, then you may wish to + /// use the per-request UUID associated with each individual request handle instead of this + /// field. + /// + /// Equivalent to the "FASTLY_TRACE_ID" environment variable. + get-session-id: func() -> string; + + /// The hostname of the Fastly cache server which is executing the current session, for + /// example, `cache-jfk1034`. + /// + /// Equivalent to the "FASTLY_HOSTNAME" environment variable and to [`server.hostname`] in VCL. + /// + /// [`server.hostname`]: https://www.fastly.com/documentation/reference/vcl/variables/server/server-hostname/ + get-hostname: func() -> string; + + /// The three-character identifying code of the [Fastly POP] in which the current session is + /// running. + /// + /// Equivalent to the "FASTLY_POP" environment variable and to [`server.datacenter`] in VCL. + /// + /// [Fastly POP]: https://www.fastly.com/documentation/guides/concepts/pop/ + /// [`server.datacenter`]: https://www.fastly.com/documentation/reference/vcl/variables/server/server-datacenter/ + get-pop: func() -> string; + + /// A code representing the general geographic region in which the [Fastly POP] processing the + /// current Compute session resides. + /// + /// Equivalent to the "FASTLY_REGION" environment variable and to [`server.region`] in VCL, and + /// has the same possible values. + /// + /// [`server.region`]: https://www.fastly.com/documentation/reference/vcl/variables/server/server-region/ + /// [Fastly POP]: https://www.fastly.com/documentation/guides/concepts/pop/ + get-region: func() -> string; + + /// The current cache generation value for this Fastly service. + /// + /// The cache generation value is incremented by [purge-all operations]. + /// + /// Equivalent to the "FASTLY_CACHE_GENERATION" environment variable and to + /// [`req.vcl.generation`] in VCL. + /// + /// [purge-all operations]: https://www.fastly.com/documentation/guides/concepts/edge-state/cache/purging/ + /// [`req.vcl.generation`]: https://www.fastly.com/documentation/reference/vcl/variables/miscellaneous/req-vcl-generation/ + get-cache-generation: func() -> u64; + + /// The customer ID of the Fastly customer account to which the currently executing Fastly + /// service belongs. + /// + /// Equivalent to the "FASTLY_CUSTOMER_ID" environment variable and to [`req.customer_id`] in VCL. + /// + /// [`req.customer_id`]: https://www.fastly.com/documentation/reference/vcl/variables/miscellaneous/req-customer-id/ + get-customer-id: func() -> string; + + /// Whether the request is running in the Fastly service's [staging environment]. + /// + /// `false` for production or `true` for staging. + /// + /// Equivalent to the "FASTLY_IS_STAGING" environment variable and to [`fastly.is_staging`] in VCL. + /// + /// [`fastly.is_staging`]: https://www.fastly.com/documentation/reference/vcl/variables/miscellaneous/fastly-is-staging/ + /// [staging environment]: https://docs.fastly.com/products/staging + get-is-staging: func() -> bool; + + /// The identifier for the Fastly service that is processing the current request. + /// + /// Equivalent to the "FASTLY_SERVICE_ID" environment variable and to [`req.service_id`] in VCL. + /// + /// [`req.service_id`]: https://www.fastly.com/documentation/reference/vcl/variables/miscellaneous/req-service-id/ + get-service-id: func() -> string; + + /// The version number for the Fastly service that is processing the current request. + /// + /// Equivalent to the "FASTLY_SERVICE_VERSION" environment variable and to [`req.vcl.version`] + /// in VCL. + /// + /// [`req.vcl.version`]: https://www.fastly.com/documentation/reference/vcl/variables/miscellaneous/req-vcl-version/ + get-service-version: func() -> u64; + + /// This function is not suitable for general-purpose use. + get-namespace-id: func() -> string; } -/// WASI interfaces used by `fastly:compute/service`. -world wasi-imports { +/// Interfaces that a Fastly Compute service may import. +/// +/// This contains the imports used in the `service` world, factored out into a +/// separate world so that it can be used by library components. Library components +/// are components that do not export anything themselves. +world service-imports { import wasi:clocks/wall-clock@0.2.6; import wasi:clocks/monotonic-clock@0.2.6; import wasi:io/error@0.2.6; import wasi:io/streams@0.2.6; + import wasi:io/poll@0.2.6; import wasi:random/random@0.2.6; import wasi:cli/environment@0.2.6; import wasi:cli/exit@0.2.6; import wasi:cli/stdout@0.2.6; import wasi:cli/stderr@0.2.6; import wasi:cli/stdin@0.2.6; -} -/// Custom interfaces used by `fastly:compute/service`. -world custom-imports { import acl; import async-io; import backend; @@ -2665,27 +2861,12 @@ world custom-imports { import image-optimizer; import log; import kv-store; - import object-store; import purge; import secret-store; + import security; import shielding; } -world custom-exports { - // Export the `http-incoming` interface. - export http-incoming; -} - -/// Interfaces that a Fastly Compute service may import. -/// -/// This contains the imports used in the `service` world, factored out into a -/// separate world so that it can be used by library components. Library components -/// are components that do not export anything themselves. -world service-imports { - include wasi-imports; - include custom-imports; -} - /// A Fastly Compute service. /// /// This defines the set of interfaces available to, and expected of, @@ -2695,5 +2876,7 @@ world service-imports { /// `http-incoming` exports. world service { include service-imports; - include custom-exports; + + // Export the `http-incoming` interface. + export http-incoming; } diff --git a/wit/deps/http/handler.wit b/wit/deps/http/handler.wit index a34a064..6a6c629 100644 --- a/wit/deps/http/handler.wit +++ b/wit/deps/http/handler.wit @@ -1,6 +1,8 @@ /// This interface defines a handler of incoming HTTP Requests. It should /// be exported by components which can respond to HTTP Requests. +@since(version = 0.2.0) interface incoming-handler { + @since(version = 0.2.0) use types.{incoming-request, response-outparam}; /// This function is invoked with an incoming HTTP Request, and a resource @@ -13,6 +15,7 @@ interface incoming-handler { /// The implementor of this function must write a response to the /// `response-outparam` before returning, or else the caller will respond /// with an error on its behalf. + @since(version = 0.2.0) handle: func( request: incoming-request, response-out: response-outparam @@ -21,7 +24,9 @@ interface incoming-handler { /// This interface defines a handler of outgoing HTTP Requests. It should be /// imported by components which wish to make HTTP Requests. +@since(version = 0.2.0) interface outgoing-handler { + @since(version = 0.2.0) use types.{ outgoing-request, request-options, future-incoming-response, error-code }; @@ -36,6 +41,7 @@ interface outgoing-handler { /// This function may return an error if the `outgoing-request` is invalid /// or not allowed to be made. Otherwise, protocol errors are reported /// through the `future-incoming-response`. + @since(version = 0.2.0) handle: func( request: outgoing-request, options: option diff --git a/wit/deps/http/types.wit b/wit/deps/http/types.wit index e174c3d..c9f3cc4 100644 --- a/wit/deps/http/types.wit +++ b/wit/deps/http/types.wit @@ -1,13 +1,19 @@ /// This interface defines all of the types and methods for implementing /// HTTP Requests and Responses, both incoming and outgoing, as well as /// their headers, trailers, and bodies. +@since(version = 0.2.0) interface types { + @since(version = 0.2.0) use wasi:clocks/monotonic-clock@0.2.6.{duration}; + @since(version = 0.2.0) use wasi:io/streams@0.2.6.{input-stream, output-stream}; + @since(version = 0.2.0) use wasi:io/error@0.2.6.{error as io-error}; + @since(version = 0.2.0) use wasi:io/poll@0.2.6.{pollable}; /// This type corresponds to HTTP standard Methods. + @since(version = 0.2.0) variant method { get, head, @@ -22,6 +28,7 @@ interface types { } /// This type corresponds to HTTP standard Related Schemes. + @since(version = 0.2.0) variant scheme { HTTP, HTTPS, @@ -29,7 +36,8 @@ interface types { } /// These cases are inspired by the IANA HTTP Proxy Error Types: - /// https://www.iana.org/assignments/http-proxy-status/http-proxy-status.xhtml#table-http-proxy-error-types + /// + @since(version = 0.2.0) variant error-code { DNS-timeout, DNS-error(DNS-error-payload), @@ -78,18 +86,21 @@ interface types { } /// Defines the case payload type for `DNS-error` above: + @since(version = 0.2.0) record DNS-error-payload { rcode: option, info-code: option } /// Defines the case payload type for `TLS-alert-received` above: + @since(version = 0.2.0) record TLS-alert-received-payload { alert-id: option, alert-message: option } /// Defines the case payload type for `HTTP-response-{header,trailer}-size` above: + @since(version = 0.2.0) record field-size-payload { field-name: option, field-size: option @@ -106,17 +117,19 @@ interface types { /// /// Note that this function is fallible because not all io-errors are /// http-related errors. + @since(version = 0.2.0) http-error-code: func(err: borrow) -> option; /// This type enumerates the different kinds of errors that may occur when /// setting or appending to a `fields` resource. + @since(version = 0.2.0) variant header-error { - /// This error indicates that a `field-key` or `field-value` was + /// This error indicates that a `field-name` or `field-value` was /// syntactically invalid when used with an operation that sets headers in a /// `fields`. invalid-syntax, - /// This error indicates that a forbidden `field-key` was used when trying + /// This error indicates that a forbidden `field-name` was used when trying /// to set a header in a `fields`. forbidden, @@ -125,12 +138,29 @@ interface types { immutable, } + /// Field names are always strings. + /// + /// Field names should always be treated as case insensitive by the `fields` + /// resource for the purposes of equality checking. + @since(version = 0.2.1) + type field-name = field-key; + /// Field keys are always strings. + /// + /// Field keys should always be treated as case insensitive by the `fields` + /// resource for the purposes of equality checking. + /// + /// # Deprecation + /// + /// This type has been deprecated in favor of the `field-name` type. + @since(version = 0.2.0) + @deprecated(version = 0.2.2) type field-key = string; /// Field values should always be ASCII strings. However, in /// reality, HTTP implementations often have to interpret malformed values, /// so they are provided as a list of bytes. + @since(version = 0.2.0) type field-value = list; /// This following block defines the `fields` resource which corresponds to @@ -140,96 +170,123 @@ interface types { /// A `fields` may be mutable or immutable. A `fields` created using the /// constructor, `from-list`, or `clone` will be mutable, but a `fields` /// resource given by other means (including, but not limited to, - /// `incoming-request.headers`, `outgoing-request.headers`) might be be + /// `incoming-request.headers`, `outgoing-request.headers`) might be /// immutable. In an immutable fields, the `set`, `append`, and `delete` /// operations will fail with `header-error.immutable`. + @since(version = 0.2.0) resource fields { /// Construct an empty HTTP Fields. /// /// The resulting `fields` is mutable. + @since(version = 0.2.0) constructor(); /// Construct an HTTP Fields. /// /// The resulting `fields` is mutable. /// - /// The list represents each key-value pair in the Fields. Keys + /// The list represents each name-value pair in the Fields. Names /// which have multiple values are represented by multiple entries in this - /// list with the same key. + /// list with the same name. /// - /// The tuple is a pair of the field key, represented as a string, and - /// Value, represented as a list of bytes. In a valid Fields, all keys - /// and values are valid UTF-8 strings. However, values are not always - /// well-formed, so they are represented as a raw list of bytes. + /// The tuple is a pair of the field name, represented as a string, and + /// Value, represented as a list of bytes. /// - /// An error result will be returned if any header or value was - /// syntactically invalid, or if a header was forbidden. + /// An error result will be returned if any `field-name` or `field-value` is + /// syntactically invalid, or if a field is forbidden. + @since(version = 0.2.0) from-list: static func( - entries: list> + entries: list> ) -> result; - /// Get all of the values corresponding to a key. If the key is not present - /// in this `fields`, an empty list is returned. However, if the key is - /// present but empty, this is represented by a list with one or more - /// empty field-values present. - get: func(name: field-key) -> list; + /// Get all of the values corresponding to a name. If the name is not present + /// in this `fields` or is syntactically invalid, an empty list is returned. + /// However, if the name is present but empty, this is represented by a list + /// with one or more empty field-values present. + @since(version = 0.2.0) + get: func(name: field-name) -> list; - /// Returns `true` when the key is present in this `fields`. If the key is + /// Returns `true` when the name is present in this `fields`. If the name is /// syntactically invalid, `false` is returned. - has: func(name: field-key) -> bool; + @since(version = 0.2.0) + has: func(name: field-name) -> bool; - /// Set all of the values for a key. Clears any existing values for that - /// key, if they have been set. + /// Set all of the values for a name. Clears any existing values for that + /// name, if they have been set. /// /// Fails with `header-error.immutable` if the `fields` are immutable. - set: func(name: field-key, value: list) -> result<_, header-error>; + /// + /// Fails with `header-error.invalid-syntax` if the `field-name` or any of + /// the `field-value`s are syntactically invalid. + @since(version = 0.2.0) + set: func(name: field-name, value: list) -> result<_, header-error>; - /// Delete all values for a key. Does nothing if no values for the key + /// Delete all values for a name. Does nothing if no values for the name /// exist. /// /// Fails with `header-error.immutable` if the `fields` are immutable. - delete: func(name: field-key) -> result<_, header-error>; + /// + /// Fails with `header-error.invalid-syntax` if the `field-name` is + /// syntactically invalid. + @since(version = 0.2.0) + delete: func(name: field-name) -> result<_, header-error>; - /// Append a value for a key. Does not change or delete any existing - /// values for that key. + /// Append a value for a name. Does not change or delete any existing + /// values for that name. /// /// Fails with `header-error.immutable` if the `fields` are immutable. - append: func(name: field-key, value: field-value) -> result<_, header-error>; + /// + /// Fails with `header-error.invalid-syntax` if the `field-name` or + /// `field-value` are syntactically invalid. + @since(version = 0.2.0) + append: func(name: field-name, value: field-value) -> result<_, header-error>; - /// Retrieve the full set of keys and values in the Fields. Like the - /// constructor, the list represents each key-value pair. + /// Retrieve the full set of names and values in the Fields. Like the + /// constructor, the list represents each name-value pair. /// - /// The outer list represents each key-value pair in the Fields. Keys + /// The outer list represents each name-value pair in the Fields. Names /// which have multiple values are represented by multiple entries in this - /// list with the same key. - entries: func() -> list>; + /// list with the same name. + /// + /// The names and values are always returned in the original casing and in + /// the order in which they will be serialized for transport. + @since(version = 0.2.0) + entries: func() -> list>; - /// Make a deep copy of the Fields. Equivelant in behavior to calling the + /// Make a deep copy of the Fields. Equivalent in behavior to calling the /// `fields` constructor on the return value of `entries`. The resulting /// `fields` is mutable. + @since(version = 0.2.0) clone: func() -> fields; } /// Headers is an alias for Fields. + @since(version = 0.2.0) type headers = fields; /// Trailers is an alias for Fields. + @since(version = 0.2.0) type trailers = fields; /// Represents an incoming HTTP Request. + @since(version = 0.2.0) resource incoming-request { /// Returns the method of the incoming request. + @since(version = 0.2.0) method: func() -> method; /// Returns the path with query parameters from the request, as a string. + @since(version = 0.2.0) path-with-query: func() -> option; /// Returns the protocol scheme from the request. + @since(version = 0.2.0) scheme: func() -> option; - /// Returns the authority from the request, if it was present. + /// Returns the authority of the Request's target URI, if present. + @since(version = 0.2.0) authority: func() -> option; /// Get the `headers` associated with the request. @@ -240,14 +297,17 @@ interface types { /// The `headers` returned are a child resource: it must be dropped before /// the parent `incoming-request` is dropped. Dropping this /// `incoming-request` before all children are dropped will trap. + @since(version = 0.2.0) headers: func() -> headers; /// Gives the `incoming-body` associated with this request. Will only /// return success at most once, and subsequent calls will return error. + @since(version = 0.2.0) consume: func() -> result; } /// Represents an outgoing HTTP Request. + @since(version = 0.2.0) resource outgoing-request { /// Construct a new `outgoing-request` with a default `method` of `GET`, and @@ -260,6 +320,7 @@ interface types { /// and `authority`, or `headers` which are not permitted to be sent. /// It is the obligation of the `outgoing-handler.handle` implementation /// to reject invalid constructions of `outgoing-request`. + @since(version = 0.2.0) constructor( headers: headers ); @@ -270,38 +331,47 @@ interface types { /// Returns success on the first call: the `outgoing-body` resource for /// this `outgoing-request` can be retrieved at most once. Subsequent /// calls will return error. + @since(version = 0.2.0) body: func() -> result; /// Get the Method for the Request. + @since(version = 0.2.0) method: func() -> method; /// Set the Method for the Request. Fails if the string present in a /// `method.other` argument is not a syntactically valid method. + @since(version = 0.2.0) set-method: func(method: method) -> result; /// Get the combination of the HTTP Path and Query for the Request. /// When `none`, this represents an empty Path and empty Query. + @since(version = 0.2.0) path-with-query: func() -> option; /// Set the combination of the HTTP Path and Query for the Request. /// When `none`, this represents an empty Path and empty Query. Fails is the /// string given is not a syntactically valid path and query uri component. + @since(version = 0.2.0) set-path-with-query: func(path-with-query: option) -> result; /// Get the HTTP Related Scheme for the Request. When `none`, the /// implementation may choose an appropriate default scheme. + @since(version = 0.2.0) scheme: func() -> option; /// Set the HTTP Related Scheme for the Request. When `none`, the /// implementation may choose an appropriate default scheme. Fails if the /// string given is not a syntactically valid uri scheme. + @since(version = 0.2.0) set-scheme: func(scheme: option) -> result; - /// Get the HTTP Authority for the Request. A value of `none` may be used - /// with Related Schemes which do not require an Authority. The HTTP and + /// Get the authority of the Request's target URI. A value of `none` may be used + /// with Related Schemes which do not require an authority. The HTTP and /// HTTPS schemes always require an authority. + @since(version = 0.2.0) authority: func() -> option; - /// Set the HTTP Authority for the Request. A value of `none` may be used - /// with Related Schemes which do not require an Authority. The HTTP and + /// Set the authority of the Request's target URI. A value of `none` may be used + /// with Related Schemes which do not require an authority. The HTTP and /// HTTPS schemes always require an authority. Fails if the string given is - /// not a syntactically valid uri authority. + /// not a syntactically valid URI authority. + @since(version = 0.2.0) set-authority: func(authority: option) -> result; /// Get the headers associated with the Request. @@ -310,8 +380,9 @@ interface types { /// `delete` operations will fail with `header-error.immutable`. /// /// This headers resource is a child: it must be dropped before the parent - /// `outgoing-request` is dropped, or its ownership is transfered to + /// `outgoing-request` is dropped, or its ownership is transferred to /// another component by e.g. `outgoing-handler.handle`. + @since(version = 0.2.0) headers: func() -> headers; } @@ -321,31 +392,39 @@ interface types { /// /// These timeouts are separate from any the user may use to bound a /// blocking call to `wasi:io/poll.poll`. + @since(version = 0.2.0) resource request-options { /// Construct a default `request-options` value. + @since(version = 0.2.0) constructor(); /// The timeout for the initial connect to the HTTP Server. + @since(version = 0.2.0) connect-timeout: func() -> option; /// Set the timeout for the initial connect to the HTTP Server. An error /// return value indicates that this timeout is not supported. + @since(version = 0.2.0) set-connect-timeout: func(duration: option) -> result; /// The timeout for receiving the first byte of the Response body. + @since(version = 0.2.0) first-byte-timeout: func() -> option; /// Set the timeout for receiving the first byte of the Response body. An /// error return value indicates that this timeout is not supported. + @since(version = 0.2.0) set-first-byte-timeout: func(duration: option) -> result; /// The timeout for receiving subsequent chunks of bytes in the Response /// body stream. + @since(version = 0.2.0) between-bytes-timeout: func() -> option; /// Set the timeout for receiving subsequent chunks of bytes in the Response /// body stream. An error return value indicates that this timeout is not /// supported. + @since(version = 0.2.0) set-between-bytes-timeout: func(duration: option) -> result; } @@ -354,7 +433,23 @@ interface types { /// This resource is used by the `wasi:http/incoming-handler` interface to /// allow a Response to be sent corresponding to the Request provided as the /// other argument to `incoming-handler.handle`. + @since(version = 0.2.0) resource response-outparam { + /// Send an HTTP 1xx response. + /// + /// Unlike `response-outparam.set`, this does not consume the + /// `response-outparam`, allowing the guest to send an arbitrary number of + /// informational responses before sending the final response using + /// `response-outparam.set`. + /// + /// This will return an `HTTP-protocol-error` if `status` is not in the + /// range [100-199], or an `internal-error` if the implementation does not + /// support informational responses. + @unstable(feature = informational-outbound-responses) + send-informational: func( + status: u16, + headers: headers + ) -> result<_, error-code>; /// Set the value of the `response-outparam` to either send a response, /// or indicate an error. @@ -365,6 +460,7 @@ interface types { /// /// The user may provide an `error` to `response` to allow the /// implementation determine how to respond with an HTTP error response. + @since(version = 0.2.0) set: static func( param: response-outparam, response: result, @@ -372,12 +468,15 @@ interface types { } /// This type corresponds to the HTTP standard Status Code. + @since(version = 0.2.0) type status-code = u16; /// Represents an incoming HTTP Response. + @since(version = 0.2.0) resource incoming-response { /// Returns the status code from the incoming response. + @since(version = 0.2.0) status: func() -> status-code; /// Returns the headers from the incoming response. @@ -387,10 +486,12 @@ interface types { /// /// This headers resource is a child: it must be dropped before the parent /// `incoming-response` is dropped. + @since(version = 0.2.0) headers: func() -> headers; /// Returns the incoming body. May be called at most once. Returns error /// if called additional times. + @since(version = 0.2.0) consume: func() -> result; } @@ -402,6 +503,7 @@ interface types { /// an `input-stream` and the delivery of trailers as a `future-trailers`, /// and ensures that the user of this interface may only be consuming either /// the body contents or waiting on trailers at any given time. + @since(version = 0.2.0) resource incoming-body { /// Returns the contents of the body, as a stream of bytes. @@ -419,26 +521,30 @@ interface types { /// backpressure is to be applied when the user is consuming the body, /// and for that backpressure to not inhibit delivery of the trailers if /// the user does not read the entire body. + @since(version = 0.2.0) %stream: func() -> result; /// Takes ownership of `incoming-body`, and returns a `future-trailers`. /// This function will trap if the `input-stream` child is still alive. + @since(version = 0.2.0) finish: static func(this: incoming-body) -> future-trailers; } - /// Represents a future which may eventaully return trailers, or an error. + /// Represents a future which may eventually return trailers, or an error. /// /// In the case that the incoming HTTP Request or Response did not have any /// trailers, this future will resolve to the empty set of trailers once the /// complete Request or Response body has been received. + @since(version = 0.2.0) resource future-trailers { /// Returns a pollable which becomes ready when either the trailers have - /// been received, or an error has occured. When this pollable is ready, + /// been received, or an error has occurred. When this pollable is ready, /// the `get` method will return `some`. + @since(version = 0.2.0) subscribe: func() -> pollable; - /// Returns the contents of the trailers, or an error which occured, + /// Returns the contents of the trailers, or an error which occurred, /// once the future is ready. /// /// The outer `option` represents future readiness. Users can wait on this @@ -450,17 +556,19 @@ interface types { /// /// The inner `result` represents that either the HTTP Request or Response /// body, as well as any trailers, were received successfully, or that an - /// error occured receiving them. The optional `trailers` indicates whether + /// error occurred receiving them. The optional `trailers` indicates whether /// or not trailers were present in the body. /// /// When some `trailers` are returned by this method, the `trailers` /// resource is immutable, and a child. Use of the `set`, `append`, or /// `delete` methods will return an error, and the resource must be /// dropped before the parent `future-trailers` is dropped. + @since(version = 0.2.0) get: func() -> option, error-code>>>; } /// Represents an outgoing HTTP Response. + @since(version = 0.2.0) resource outgoing-response { /// Construct an `outgoing-response`, with a default `status-code` of `200`. @@ -468,13 +576,16 @@ interface types { /// `set-status-code` method. /// /// * `headers` is the HTTP Headers for the Response. + @since(version = 0.2.0) constructor(headers: headers); /// Get the HTTP Status Code for the Response. + @since(version = 0.2.0) status-code: func() -> status-code; /// Set the HTTP Status Code for the Response. Fails if the status-code /// given is not a valid http status code. + @since(version = 0.2.0) set-status-code: func(status-code: status-code) -> result; /// Get the headers associated with the Request. @@ -483,8 +594,9 @@ interface types { /// `delete` operations will fail with `header-error.immutable`. /// /// This headers resource is a child: it must be dropped before the parent - /// `outgoing-request` is dropped, or its ownership is transfered to + /// `outgoing-request` is dropped, or its ownership is transferred to /// another component by e.g. `outgoing-handler.handle`. + @since(version = 0.2.0) headers: func() -> headers; /// Returns the resource corresponding to the outgoing Body for this Response. @@ -492,6 +604,7 @@ interface types { /// Returns success on the first call: the `outgoing-body` resource for /// this `outgoing-response` can be retrieved at most once. Subsequent /// calls will return error. + @since(version = 0.2.0) body: func() -> result; } @@ -507,10 +620,11 @@ interface types { /// /// If the user code drops this resource, as opposed to calling the static /// method `finish`, the implementation should treat the body as incomplete, - /// and that an error has occured. The implementation should propogate this + /// and that an error has occurred. The implementation should propagate this /// error to the HTTP protocol by whatever means it has available, /// including: corrupting the body on the wire, aborting the associated /// Request, or sending a late status code for the Response. + @since(version = 0.2.0) resource outgoing-body { /// Returns a stream for writing the body contents. @@ -522,6 +636,7 @@ interface types { /// Returns success on the first call: the `output-stream` resource for /// this `outgoing-body` may be retrieved at most once. Subsequent calls /// will return error. + @since(version = 0.2.0) write: func() -> result; /// Finalize an outgoing body, optionally providing trailers. This must be @@ -533,21 +648,24 @@ interface types { /// constructed with a Content-Length header, and the contents written /// to the body (via `write`) does not match the value given in the /// Content-Length. + @since(version = 0.2.0) finish: static func( this: outgoing-body, trailers: option ) -> result<_, error-code>; } - /// Represents a future which may eventaully return an incoming HTTP + /// Represents a future which may eventually return an incoming HTTP /// Response, or an error. /// /// This resource is returned by the `wasi:http/outgoing-handler` interface to /// provide the HTTP Response corresponding to the sent Request. + @since(version = 0.2.0) resource future-incoming-response { /// Returns a pollable which becomes ready when either the Response has - /// been received, or an error has occured. When this pollable is ready, + /// been received, or an error has occurred. When this pollable is ready, /// the `get` method will return `some`. + @since(version = 0.2.0) subscribe: func() -> pollable; /// Returns the incoming HTTP Response, or an error, once one is ready. @@ -560,11 +678,11 @@ interface types { /// is `some`, and error on subsequent calls. /// /// The inner `result` represents that either the incoming HTTP Response - /// status and headers have recieved successfully, or that an error - /// occured. Errors may also occur while consuming the response body, + /// status and headers have received successfully, or that an error + /// occurred. Errors may also occur while consuming the response body, /// but those will be reported by the `incoming-body` and its /// `output-stream` child. + @since(version = 0.2.0) get: func() -> option>>; - } } diff --git a/wit/viceroy.wit b/wit/viceroy.wit index 7c68bd5..ccaa46f 100644 --- a/wit/viceroy.wit +++ b/wit/viceroy.wit @@ -1,7 +1 @@ package fastly:viceroy; - -world viceroy { - include fastly:compute/custom-imports; - include fastly:adapter/adapter-imports; - include fastly:compute/custom-exports; -} diff --git a/wrap_app_in_wasiless.wac b/wrap_app_in_wasiless.wac new file mode 100644 index 0000000..5295e7f --- /dev/null +++ b/wrap_app_in_wasiless.wac @@ -0,0 +1,9 @@ +package fastly:python-wasiless; // TODO: Append "targets fastly:compute/service" as an additional check. + +// Instantiate wasiless to satisfy irrelevant WASI interfaces: +let wasiless = new fastly:wasiless { ... }; + +let app = new app:component { ...wasiless, ... }; + +// Export only the HTTP handler, not the extraneous `exports` bundle: +export app["fastly:compute/http-incoming"];