Repository: cloudflare/cloudflare-go
Pull request #1330 (2023-07-07 18:10:22)

dispatch namespace features
## Description

Adds support for several Workers for Platforms features:
- dispatch namespace bindings
- uploading workers 'to' a dispatch namespace

Also includes a few internal-ish items:
- 'unsafe' bindings, similar to wrangler's: https://github.com/cloudflare/workers-sdk/pull/411
- 'pipeline_hash', which is used internally and in a few experimental features. This is also present in wrangler: https://github.com/cloudflare/workers-sdk/blob/1ce32968b990fef59953b8cd61172b98fb2386e5/packages/wrangler/src/deploy/deploy.ts#L635

## Has your change been tested?

Yes, besides `pipeline_hash`, but they've only been tested against the mock handlers in the included tests.

## Types of changes

What sort of change does your code introduce/modify?

- [❌ ] Bug fix (non-breaking change which fixes an issue)
- [✅ ] New feature (non-breaking change which adds functionality)
- [❌ ] Breaking change (fix or feature that would cause existing functionality to change)

## Checklist:

- [✅] My code follows the code style of this project.
- [❓] My change requires a change to the documentation.
  - Possibly. The public features are documented on ` developers.cloudflare.com `, but I don't believe the library changes are.
- [❌] I have updated the documentation accordingly.
- [✅] I have added tests to cover my changes.
- [✅] All new and existing tests passed.
- [❓] This change is using publicly documented in [cloudflare/api-schemas](https://github.com/cloudflare/api-schemas) 
      and relies on stable APIs.
  - The WFP APIs should be stable.
  - The 'unsafe' binding API is specifically to avoid adding support for some unstable features.
  - The `pipeline_hash` field is internal and unstable, but has been requested by some users.



Patch:
diff --git a/.changelog/1330.txt b/.changelog/1330.txt
new file mode 100644
index 000000000..630d7ee25
--- /dev/null
+++ b/.changelog/1330.txt
@@ -0,0 +1,15 @@
+```release-note:enhancement
+workers: Add support for uploading scripts to a Workers for Platforms namespace.
+```
+
+```release-note:enhancement
+workers: Add support for declaring arbitrary bindings with UnsafeBinding.
+```
+
+```release-note:enhancement
+workers: Add support for uploading workers with Workers for Platforms namespace bindings.
+```
+
+```release-note:enhancement
+workers: Add `pipeline_hash` field to Workers script response struct.
+```
diff --git a/.changelog/1333.txt b/.changelog/1333.txt
deleted file mode 100644
index 81026e815..000000000
--- a/.changelog/1333.txt
+++ /dev/null
@@ -1,47 +0,0 @@
-```release-note:breaking-change
-rulesets: `GetZoneRuleset` is removed in favour of `GetRuleset`
-```
-
-```release-note:breaking-change
-rulesets: `GetAccountRuleset` is removed in favour of `GetRuleset`
-```
-
-```release-note:breaking-change
-rulesets: `CreateZoneRuleset` is removed in favour of `CreateRuleset`
-```
-
-```release-note:breaking-change
-rulesets: `CreateAccountRuleset` is removed in favour of `CreateRuleset`
-```
-
-```release-note:breaking-change
-rulesets: `DeleteZoneRuleset` is removed in favour of `DeleteRuleset`
-```
-
-```release-note:breaking-change
-rulesets: `DeleteAccountRuleset` is removed in favour of `DeleteRuleset`
-```
-
-```release-note:breaking-change
-rulesets: `UpdateZoneRuleset` is removed in favour of `UpdateRuleset`
-```
-
-```release-note:breaking-change
-rulesets: `UpdateAccountRuleset` is removed in favour of `UpdateRuleset`
-```
-
-```release-note:breaking-change
-rulesets: `GetZoneRulesetPhase` is removed in favour of `GetEntrypointRuleset`
-```
-
-```release-note:breaking-change
-rulesets: `GetAccountRulesetPhase` is removed in favour of `GetEntrypointRuleset`
-```
-
-```release-note:breaking-change
-rulesets: `UpdateZoneRulesetPhase` is removed in favour of `UpdateEntrypointRuleset`
-```
-
-```release-note:breaking-change
-rulesets: `UpdateAccountRulesetPhase` is removed in favour of `UpdateEntrypointRuleset`
-```
diff --git a/.changelog/1335.txt b/.changelog/1335.txt
deleted file mode 100644
index 13b520854..000000000
--- a/.changelog/1335.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-```release-note:enhancement
-images: adds ability to upload image by url
-```
diff --git a/.changelog/1336.txt b/.changelog/1336.txt
deleted file mode 100644
index f949bdc25..000000000
--- a/.changelog/1336.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-```release-note:enhancement
-zone: Added `GetRegionalTieredCache` and `UpdateRegionalTieredCache` to allow setting Regional Tiered Cache for a zone.
-```
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 39b9094c3..c9c53ae5d 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -12,10 +12,6 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v3
-      - uses: actions/cache@v3
-        with:
-          path: ~/go/pkg/mod
-          key: ${{ runner.os }}-go${{ matrix.go-version }}-${{ hashFiles('**/go.mod') }}-${{ hashFiles('**/go.sum') }}
       - name: golangci-lint
         uses: golangci/golangci-lint-action@v3
         with:
diff --git a/.golintci.yaml b/.golintci.yaml
index c4e5df2c6..c885d96e1 100644
--- a/.golintci.yaml
+++ b/.golintci.yaml
@@ -1,8 +1,17 @@
 run:
-  timeout: 5m
+  # timeout for analysis, e.g. 30s, 5m, default is 1m
+  timeout: 1m
+
+  # exit code when at least one issue was found, default is 1
   issues-exit-code: 1
+
+  # include test files or not, default is true
   tests: true
+
+  # default is true. Enables skipping of directories:
+  #   vendor$, third_party$, testdata$, examples$, Godeps$, builtin$
   skip-dirs-use-default: true
+
   modules-download-mode: readonly
 
 linters:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e1f0900bd..009fdce14 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,23 +6,6 @@ BREAKING CHANGES:
 * pages_deployment: change DeletePagesDeploymentParams to contain all parameters ([#1264](https://github.com/cloudflare/cloudflare-go/issues/1264))
 * pages_project: change to use ResourceContainer for account ID ([#1264](https://github.com/cloudflare/cloudflare-go/issues/1264))
 * pages_project: rename PagesProject to GetPagesProject ([#1264](https://github.com/cloudflare/cloudflare-go/issues/1264))
-* rulesets: `CreateAccountRuleset` is removed in favour of `CreateRuleset` ([#1333](https://github.com/cloudflare/cloudflare-go/issues/1333))
-* rulesets: `CreateZoneRuleset` is removed in favour of `CreateRuleset` ([#1333](https://github.com/cloudflare/cloudflare-go/issues/1333))
-* rulesets: `DeleteAccountRuleset` is removed in favour of `DeleteRuleset` ([#1333](https://github.com/cloudflare/cloudflare-go/issues/1333))
-* rulesets: `DeleteZoneRuleset` is removed in favour of `DeleteRuleset` ([#1333](https://github.com/cloudflare/cloudflare-go/issues/1333))
-* rulesets: `GetAccountRulesetPhase` is removed in favour of `GetEntrypointRuleset` ([#1333](https://github.com/cloudflare/cloudflare-go/issues/1333))
-* rulesets: `GetAccountRuleset` is removed in favour of `GetRuleset` ([#1333](https://github.com/cloudflare/cloudflare-go/issues/1333))
-* rulesets: `GetZoneRulesetPhase` is removed in favour of `GetEntrypointRuleset` ([#1333](https://github.com/cloudflare/cloudflare-go/issues/1333))
-* rulesets: `GetZoneRuleset` is removed in favour of `GetRuleset` ([#1333](https://github.com/cloudflare/cloudflare-go/issues/1333))
-* rulesets: `UpdateAccountRulesetPhase` is removed in favour of `UpdateEntrypointRuleset` ([#1333](https://github.com/cloudflare/cloudflare-go/issues/1333))
-* rulesets: `UpdateAccountRuleset` is removed in favour of `UpdateRuleset` ([#1333](https://github.com/cloudflare/cloudflare-go/issues/1333))
-* rulesets: `UpdateZoneRulesetPhase` is removed in favour of `UpdateEntrypointRuleset` ([#1333](https://github.com/cloudflare/cloudflare-go/issues/1333))
-* rulesets: `UpdateZoneRuleset` is removed in favour of `UpdateRuleset` ([#1333](https://github.com/cloudflare/cloudflare-go/issues/1333))
-
-ENHANCEMENTS:
-
-* images: adds ability to upload image by url ([#1335](https://github.com/cloudflare/cloudflare-go/issues/1335))
-* zone: Added `GetRegionalTieredCache` and `UpdateRegionalTieredCache` to allow setting Regional Tiered Cache for a zone. ([#1336](https://github.com/cloudflare/cloudflare-go/issues/1336))
 
 DEPENDENCIES:
 
diff --git a/images.go b/images.go
index 59209ad1f..b93b2f46d 100644
--- a/images.go
+++ b/images.go
@@ -37,7 +37,6 @@ type Image struct {
 // UploadImageParams is the data required for an Image Upload request.
 type UploadImageParams struct {
 	File              io.ReadCloser
-	URL               string
 	Name              string
 	RequireSignedURLs bool
 	Metadata          map[string]interface{}
@@ -46,43 +45,33 @@ type UploadImageParams struct {
 // write writes the image upload data to a multipart writer, so
 // it can be used in an HTTP request.
 func (b UploadImageParams) write(mpw *multipart.Writer) error {
-	if b.File == nil && b.URL == "" {
-		return errors.New("a file or url to upload must be specified")
+	if b.File == nil {
+		return errors.New("a file to upload must be specified")
 	}
-
-	if b.File != nil {
-		name := b.Name
-		part, err := mpw.CreateFormFile("file", name)
-		if err != nil {
-			return err
-		}
-		_, err = io.Copy(part, b.File)
-		if err != nil {
-			_ = b.File.Close()
-			return err
-		}
-		_ = b.File.Close()
+	name := b.Name
+	part, err := mpw.CreateFormFile("file", name)
+	if err != nil {
+		return err
 	}
-
-	if b.URL != "" {
-		err := mpw.WriteField("url", b.URL)
-		if err != nil {
-			return err
-		}
+	_, err = io.Copy(part, b.File)
+	if err != nil {
+		_ = b.File.Close()
+		return err
 	}
+	_ = b.File.Close()
 
 	// According to the Cloudflare docs, this field defaults to false.
 	// For simplicity, we will only send it if the value is true, however
 	// if the default is changed to true, this logic will need to be updated.
 	if b.RequireSignedURLs {
-		err := mpw.WriteField("requireSignedURLs", "true")
+		err = mpw.WriteField("requireSignedURLs", "true")
 		if err != nil {
 			return err
 		}
 	}
 
 	if b.Metadata != nil {
-		part, err := mpw.CreateFormField("metadata")
+		part, err = mpw.CreateFormField("metadata")
 		if err != nil {
 			return err
 		}
@@ -162,10 +151,6 @@ func (api *API) UploadImage(ctx context.Context, rc *ResourceContainer, params U
 		return Image{}, ErrRequiredAccountLevelResourceContainer
 	}
 
-	if params.File != nil && params.URL != "" {
-		return Image{}, errors.New("file and url uploads are mutually exclusive and can only be performed individually")
-	}
-
 	uri := fmt.Sprintf("/accounts/%s/images/v1", rc.Identifier)
 
 	body := &bytes.Buffer{}
diff --git a/regional_tiered_cache.go b/regional_tiered_cache.go
deleted file mode 100644
index 31471178a..000000000
--- a/regional_tiered_cache.go
+++ /dev/null
@@ -1,84 +0,0 @@
-package cloudflare
-
-import (
-	"context"
-	"encoding/json"
-	"fmt"
-	"net/http"
-	"time"
-)
-
-// RegionalTieredCache is the structure of the API object for the regional tiered cache
-// setting.
-type RegionalTieredCache struct {
-	ID         string    `json:"id,omitempty"`
-	ModifiedOn time.Time `json:"modified_on,omitempty"`
-	Value      string    `json:"value"`
-}
-
-// RegionalTieredCacheDetailsResponse is the API response for the regional tiered cache
-// setting.
-type RegionalTieredCacheDetailsResponse struct {
-	Result RegionalTieredCache `json:"result"`
-	Response
-}
-
-type zoneRegionalTieredCacheSingleResponse struct {
-	Response
-	Result RegionalTieredCache `json:"result"`
-}
-
-type GetRegionalTieredCacheParams struct{}
-
-type UpdateRegionalTieredCacheParams struct {
-	Value string `json:"value"`
-}
-
-// GetRegionalTieredCache returns information about the current regional tiered
-// cache settings.
-//
-// API reference: https://developers.cloudflare.com/api/operations/zone-cache-settings-get-regional-tiered-cache-setting
-func (api *API) GetRegionalTieredCache(ctx context.Context, rc *ResourceContainer, params GetRegionalTieredCacheParams) (RegionalTieredCache, error) {
-	if rc.Level != ZoneRouteLevel {
-		return RegionalTieredCache{}, ErrRequiredZoneLevelResourceContainer
-	}
-
-	uri := fmt.Sprintf("/%s/%s/cache/regional_tiered_cache", rc.Level, rc.Identifier)
-
-	res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
-	if err != nil {
-		return RegionalTieredCache{}, err
-	}
-
-	var RegionalTieredCacheDetailsResponse RegionalTieredCacheDetailsResponse
-	err = json.Unmarshal(res, &RegionalTieredCacheDetailsResponse)
-	if err != nil {
-		return RegionalTieredCache{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
-	}
-	return RegionalTieredCacheDetailsResponse.Result, nil
-}
-
-// UpdateRegionalTieredCache updates the regional tiered cache setting for a
-// zone.
-//
-// API reference: https://developers.cloudflare.com/api/operations/zone-cache-settings-change-regional-tiered-cache-setting
-func (api *API) UpdateRegionalTieredCache(ctx context.Context, rc *ResourceContainer, params UpdateRegionalTieredCacheParams) (RegionalTieredCache, error) {
-	if rc.Level != ZoneRouteLevel {
-		return RegionalTieredCache{}, ErrRequiredZoneLevelResourceContainer
-	}
-
-	uri := fmt.Sprintf("/%s/%s/cache/regional_tiered_cache", rc.Level, rc.Identifier)
-
-	res, err := api.makeRequestContext(ctx, http.MethodPatch, uri, params)
-	if err != nil {
-		return RegionalTieredCache{}, err
-	}
-
-	response := &zoneRegionalTieredCacheSingleResponse{}
-	err = json.Unmarshal(res, &response)
-	if err != nil {
-		return RegionalTieredCache{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
-	}
-
-	return response.Result, nil
-}
diff --git a/rulesets.go b/rulesets.go
index 80ebed208..72a7a4f3b 100644
--- a/rulesets.go
+++ b/rulesets.go
@@ -10,10 +10,6 @@ import (
 	"time"
 )
 
-var (
-	ErrMissingRulesetPhase = errors.New("missing required phase")
-)
-
 const (
 	RulesetKindCustom  RulesetKind = "custom"
 	RulesetKindManaged RulesetKind = "managed"
@@ -715,40 +711,24 @@ type UpdateRulesetResponse struct {
 	Result Ruleset `json:"result"`
 }
 
-type ListRulesetsParams struct{}
-
-type CreateRulesetParams struct {
-	Name                     string        `json:"name,omitempty"`
-	Description              string        `json:"description,omitempty"`
-	Kind                     string        `json:"kind,omitempty"`
-	Version                  *string       `json:"version,omitempty"`
-	Phase                    string        `json:"phase,omitempty"`
-	Rules                    []RulesetRule `json:"rules"`
-	ShareableEntitlementName string        `json:"shareable_entitlement_name,omitempty"`
-}
-
-type UpdateRulesetParams struct {
-	ID          string        `json:"-"`
-	Description string        `json:"description"`
-	Rules       []RulesetRule `json:"rules"`
-}
-
-type UpdateEntrypointRulesetParams struct {
-	Name        string        `json:"name,omitempty"`
-	Description string        `json:"description,omitempty"`
-	Kind        string        `json:"kind,omitempty"`
-	Version     *string       `json:"version,omitempty"`
-	Phase       string        `json:"-"`
-	Rules       []RulesetRule `json:"rules"`
+// ListZoneRulesets fetches all rulesets for a zone.
+//
+// API reference: https://api.cloudflare.com/#zone-rulesets-list-zone-rulesets
+func (api *API) ListZoneRulesets(ctx context.Context, zoneID string) ([]Ruleset, error) {
+	return api.listRulesets(ctx, ZoneRouteRoot, zoneID)
 }
 
-// ListRulesets lists all Rulesets for a given zone or account depending on the
-// ResourceContainer type provided.
+// ListAccountRulesets fetches all rulesets for an account.
 //
-// API reference: https://api.cloudflare.com/#zone-rulesets-list-zone-rulesets
 // API reference: https://api.cloudflare.com/#account-rulesets-list-account-rulesets
-func (api *API) ListRulesets(ctx context.Context, rc *ResourceContainer, params ListRulesetsParams) ([]Ruleset, error) {
-	uri := fmt.Sprintf("/%s/%s/rulesets", rc.Level, rc.Identifier)
+func (api *API) ListAccountRulesets(ctx context.Context, accountID string) ([]Ruleset, error) {
+	return api.listRulesets(ctx, AccountRouteRoot, accountID)
+}
+
+// listRulesets lists all Rulesets for a given zone or account depending on the
+// identifier type provided.
+func (api *API) listRulesets(ctx context.Context, identifierType RouteRoot, identifier string) ([]Ruleset, error) {
+	uri := fmt.Sprintf("/%s/%s/rulesets", identifierType, identifier)
 
 	res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
 	if err != nil {
@@ -763,12 +743,24 @@ func (api *API) ListRulesets(ctx context.Context, rc *ResourceContainer, params
 	return result.Result, nil
 }
 
-// GetRuleset fetches a single ruleset.
+// GetZoneRuleset fetches a single ruleset for a zone.
 //
 // API reference: https://api.cloudflare.com/#zone-rulesets-get-a-zone-ruleset
+func (api *API) GetZoneRuleset(ctx context.Context, zoneID, rulesetID string) (Ruleset, error) {
+	return api.getRuleset(ctx, ZoneRouteRoot, zoneID, rulesetID)
+}
+
+// GetAccountRuleset fetches a single ruleset for an account.
+//
 // API reference: https://api.cloudflare.com/#account-rulesets-get-an-account-ruleset
-func (api *API) GetRuleset(ctx context.Context, rc *ResourceContainer, rulesetID string) (Ruleset, error) {
-	uri := fmt.Sprintf("/%s/%s/rulesets/%s", rc.Level, rc.Identifier, rulesetID)
+func (api *API) GetAccountRuleset(ctx context.Context, accountID, rulesetID string) (Ruleset, error) {
+	return api.getRuleset(ctx, AccountRouteRoot, accountID, rulesetID)
+}
+
+// getRuleset fetches a single ruleset based on the zone or account, the
+// identifier and the ruleset ID.
+func (api *API) getRuleset(ctx context.Context, identifierType RouteRoot, identifier, rulesetID string) (Ruleset, error) {
+	uri := fmt.Sprintf("/%s/%s/rulesets/%s", identifierType, identifier, rulesetID)
 	res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
 	if err != nil {
 		return Ruleset{}, err
@@ -782,13 +774,23 @@ func (api *API) GetRuleset(ctx context.Context, rc *ResourceContainer, rulesetID
 	return result.Result, nil
 }
 
-// CreateRuleset initialises a new ruleset.
+// CreateZoneRuleset creates a new ruleset for a zone.
 //
 // API reference: https://api.cloudflare.com/#zone-rulesets-create-zone-ruleset
+func (api *API) CreateZoneRuleset(ctx context.Context, zoneID string, ruleset Ruleset) (Ruleset, error) {
+	return api.createRuleset(ctx, ZoneRouteRoot, zoneID, ruleset)
+}
+
+// CreateAccountRuleset creates a new ruleset for an account.
+//
 // API reference: https://api.cloudflare.com/#account-rulesets-create-account-ruleset
-func (api *API) CreateRuleset(ctx context.Context, rc *ResourceContainer, params CreateRulesetParams) (Ruleset, error) {
-	uri := fmt.Sprintf("/%s/%s/rulesets", rc.Level, rc.Identifier)
-	res, err := api.makeRequestContext(ctx, http.MethodPost, uri, params)
+func (api *API) CreateAccountRuleset(ctx context.Context, accountID string, ruleset Ruleset) (Ruleset, error) {
+	return api.createRuleset(ctx, AccountRouteRoot, accountID, ruleset)
+}
+
+func (api *API) createRuleset(ctx context.Context, identifierType RouteRoot, identifier string, ruleset Ruleset) (Ruleset, error) {
+	uri := fmt.Sprintf("/%s/%s/rulesets", identifierType, identifier)
+	res, err := api.makeRequestContext(ctx, http.MethodPost, uri, ruleset)
 	if err != nil {
 		return Ruleset{}, err
 	}
@@ -801,12 +803,23 @@ func (api *API) CreateRuleset(ctx context.Context, rc *ResourceContainer, params
 	return result.Result, nil
 }
 
-// DeleteRuleset removes a ruleset based on the ruleset ID.
+// DeleteZoneRuleset deletes a single ruleset for a zone.
 //
 // API reference: https://api.cloudflare.com/#zone-rulesets-delete-zone-ruleset
+func (api *API) DeleteZoneRuleset(ctx context.Context, zoneID, rulesetID string) error {
+	return api.deleteRuleset(ctx, ZoneRouteRoot, zoneID, rulesetID)
+}
+
+// DeleteAccountRuleset deletes a single ruleset for an account.
+//
 // API reference: https://api.cloudflare.com/#account-rulesets-delete-account-ruleset
-func (api *API) DeleteRuleset(ctx context.Context, rc *ResourceContainer, rulesetID string) error {
-	uri := fmt.Sprintf("/%s/%s/rulesets/%s", rc.Level, rc.Identifier, rulesetID)
+func (api *API) DeleteAccountRuleset(ctx context.Context, accountID, rulesetID string) error {
+	return api.deleteRuleset(ctx, AccountRouteRoot, accountID, rulesetID)
+}
+
+// deleteRuleset removes a ruleset based on the ruleset ID.
+func (api *API) deleteRuleset(ctx context.Context, identifierType RouteRoot, identifier, rulesetID string) error {
+	uri := fmt.Sprintf("/%s/%s/rulesets/%s", identifierType, identifier, rulesetID)
 	res, err := api.makeRequestContext(ctx, http.MethodDelete, uri, nil)
 	if err != nil {
 		return err
@@ -822,17 +835,25 @@ func (api *API) DeleteRuleset(ctx context.Context, rc *ResourceContainer, rulese
 	return nil
 }
 
-// UpdateRuleset updates a ruleset based on the ruleset ID.
+// UpdateZoneRuleset updates a single ruleset for a zone.
 //
 // API reference: https://api.cloudflare.com/#zone-rulesets-update-a-zone-ruleset
+func (api *API) UpdateZoneRuleset(ctx context.Context, zoneID, rulesetID, description string, rules []RulesetRule) (Ruleset, error) {
+	return api.updateRuleset(ctx, ZoneRouteRoot, zoneID, rulesetID, description, rules)
+}
+
+// UpdateAccountRuleset updates a single ruleset for an account.
+//
 // API reference: https://api.cloudflare.com/#account-rulesets-update-account-ruleset
-func (api *API) UpdateRuleset(ctx context.Context, rc *ResourceContainer, params UpdateRulesetParams) (Ruleset, error) {
-	if params.ID == "" {
-		return Ruleset{}, ErrMissingResourceIdentifier
-	}
+func (api *API) UpdateAccountRuleset(ctx context.Context, accountID, rulesetID, description string, rules []RulesetRule) (Ruleset, error) {
+	return api.updateRuleset(ctx, AccountRouteRoot, accountID, rulesetID, description, rules)
+}
 
-	uri := fmt.Sprintf("/%s/%s/rulesets/%s", rc.Level, rc.Identifier, params.ID)
-	res, err := api.makeRequestContext(ctx, http.MethodPut, uri, params)
+// updateRuleset updates a ruleset based on the ruleset ID.
+func (api *API) updateRuleset(ctx context.Context, identifierType RouteRoot, identifier, rulesetID, description string, rules []RulesetRule) (Ruleset, error) {
+	uri := fmt.Sprintf("/%s/%s/rulesets/%s", identifierType, identifier, rulesetID)
+	payload := UpdateRulesetRequest{Description: description, Rules: rules}
+	res, err := api.makeRequestContext(ctx, http.MethodPut, uri, payload)
 	if err != nil {
 		return Ruleset{}, err
 	}
@@ -845,9 +866,24 @@ func (api *API) UpdateRuleset(ctx context.Context, rc *ResourceContainer, params
 	return result.Result, nil
 }
 
-// GetEntrypointRuleset returns a ruleset phase based on the resource entrypoint.
-func (api *API) GetEntrypointRuleset(ctx context.Context, rc *ResourceContainer, phase string) (Ruleset, error) {
-	uri := fmt.Sprintf("/%s/%s/rulesets/phases/%s/entrypoint", rc.Level, rc.Identifier, phase)
+// GetZoneRulesetPhase returns a ruleset phase for a zone.
+//
+// API reference: TBA.
+func (api *API) GetZoneRulesetPhase(ctx context.Context, zoneID, rulesetPhase string) (Ruleset, error) {
+	return api.getRulesetPhase(ctx, ZoneRouteRoot, zoneID, rulesetPhase)
+}
+
+// GetAccountRulesetPhase returns a ruleset phase for an account.
+//
+// API reference: TBA.
+func (api *API) GetAccountRulesetPhase(ctx context.Context, accountID, rulesetPhase string) (Ruleset, error) {
+	return api.getRulesetPhase(ctx, AccountRouteRoot, accountID, rulesetPhase)
+}
+
+// getRulesetPhase returns a ruleset phase based on the zone or account and the
+// identifier.
+func (api *API) getRulesetPhase(ctx context.Context, identifierType RouteRoot, identifier, rulesetPhase string) (Ruleset, error) {
+	uri := fmt.Sprintf("/%s/%s/rulesets/phases/%s/entrypoint", identifierType, identifier, rulesetPhase)
 	res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
 	if err != nil {
 		return Ruleset{}, err
@@ -861,14 +897,25 @@ func (api *API) GetEntrypointRuleset(ctx context.Context, rc *ResourceContainer,
 	return result.Result, nil
 }
 
-// UpdateEntrypointRuleset updates a ruleset phase based on the entrypoint.
-func (api *API) UpdateEntrypointRuleset(ctx context.Context, rc *ResourceContainer, params UpdateEntrypointRulesetParams) (Ruleset, error) {
-	if params.Phase == "" {
-		return Ruleset{}, ErrMissingRulesetPhase
-	}
+// UpdateZoneRulesetPhase updates a ruleset phase for a zone.
+//
+// API reference: TBA.
+func (api *API) UpdateZoneRulesetPhase(ctx context.Context, zoneID, rulesetPhase string, ruleset Ruleset) (Ruleset, error) {
+	return api.updateRulesetPhase(ctx, ZoneRouteRoot, zoneID, rulesetPhase, ruleset)
+}
+
+// UpdateAccountRulesetPhase updates a ruleset phase for an account.
+//
+// API reference: TBA.
+func (api *API) UpdateAccountRulesetPhase(ctx context.Context, accountID, rulesetPhase string, ruleset Ruleset) (Ruleset, error) {
+	return api.updateRulesetPhase(ctx, AccountRouteRoot, accountID, rulesetPhase, ruleset)
+}
 
-	uri := fmt.Sprintf("/%s/%s/rulesets/phases/%s/entrypoint", rc.Level, rc.Identifier, params.Phase)
-	res, err := api.makeRequestContext(ctx, http.MethodPut, uri, params)
+// updateRulesetPhase updates a ruleset phase based on the zone or account, the
+// identifier and the rules.
+func (api *API) updateRulesetPhase(ctx context.Context, identifierType RouteRoot, identifier, rulesetPhase string, ruleset Ruleset) (Ruleset, error) {
+	uri := fmt.Sprintf("/%s/%s/rulesets/phases/%s/entrypoint", identifierType, identifier, rulesetPhase)
+	res, err := api.makeRequestContext(ctx, http.MethodPut, uri, ruleset)
 	if err != nil {
 		return Ruleset{}, err
 	}
diff --git a/workers.go b/workers.go
index 53711f906..8d4297ce4 100644
--- a/workers.go
+++ b/workers.go
@@ -24,6 +24,9 @@ type CreateWorkerParams struct {
 	ScriptName string
 	Script     string
 
+	// DispatchNamespaceName uploads the worker to a WFP dispatch namespace if provided
+	DispatchNamespaceName *string
+
 	// Module changes the Content-Type header to specify the script is an
 	// ES Module syntax script.
 	Module bool
@@ -114,6 +117,7 @@ type WorkerMetaData struct {
 	LastDeployedFrom *string                `json:"last_deployed_from,omitempty"`
 	DeploymentId     *string                `json:"deployment_id,omitempty"`
 	PlacementMode    *PlacementMode         `json:"placement_mode,omitempty"`
+	PipelineHash     *string                `json:"pipeline_hash,omitempty"`
 }
 
 // WorkerListResponse wrapper struct for API response to worker script list API call.
@@ -272,6 +276,10 @@ func (api *API) UploadWorker(ctx context.Context, rc *ResourceContainer, params
 	}
 
 	uri := fmt.Sprintf("/accounts/%s/workers/scripts/%s", rc.Identifier, params.ScriptName)
+	if params.DispatchNamespaceName != nil {
+		uri = fmt.Sprintf("/accounts/%s/workers/namespaces/%s/scripts/%s", rc.Identifier, *params.DispatchNamespaceName, params.ScriptName)
+	}
+
 	headers := make(http.Header)
 	headers.Set("Content-Type", contentType)
 	res, err := api.makeRequestContextWithHeaders(ctx, http.MethodPut, uri, body, headers)
diff --git a/workers_bindings.go b/workers_bindings.go
index 11f4ba7cd..f5bcd624a 100644
--- a/workers_bindings.go
+++ b/workers_bindings.go
@@ -41,6 +41,8 @@ const (
 	WorkerAnalyticsEngineBindingType WorkerBindingType = "analytics_engine"
 	// WorkerQueueBindingType is the type for queue bindings.
 	WorkerQueueBindingType WorkerBindingType = "queue"
+	// DispatchNamespaceBindingType is the type for WFP namespace bindings.
+	DispatchNamespaceBindingType WorkerBindingType = "dispatch_namespace"
 )
 
 type ListWorkerBindingsParams struct {
@@ -339,6 +341,85 @@ func (b WorkerQueueBinding) serialize(bindingName string) (workerBindingMeta, wo
 	}, nil, nil
 }
 
+// DispatchNamespaceBinding is a binding to a Workers for Platforms namespace
+//
+// https://developers.cloudflare.com/workers/platform/bindings/#dispatch-namespace-bindings-workers-for-platforms
+type DispatchNamespaceBinding struct {
+	Binding   string
+	Namespace string
+	Outbound  *NamespaceOutboundOptions
+}
+
+type NamespaceOutboundOptions struct {
+	Worker WorkerReference
+	Params []OutboundParamSchema
+}
+
+type WorkerReference struct {
+	Service     string
+	Environment *string
+}
+
+type OutboundParamSchema struct {
+	Name string
+}
+
+// Type returns the type of the binding.
+func (b DispatchNamespaceBinding) Type() WorkerBindingType {
+	return DispatchNamespaceBindingType
+}
+
+func (b DispatchNamespaceBinding) serialize(bindingName string) (workerBindingMeta, workerBindingBodyWriter, error) {
+	if b.Binding == "" {
+		return nil, nil, fmt.Errorf(`Binding name for binding "%s" cannot be empty`, bindingName)
+	}
+	if b.Namespace == "" {
+		return nil, nil, fmt.Errorf(`Namespace name for binding "%s" cannot be empty`, bindingName)
+	}
+
+	meta := workerBindingMeta{
+		"type":      b.Type(),
+		"name":      b.Binding,
+		"namespace": b.Namespace,
+	}
+
+	if b.Outbound != nil {
+		if b.Outbound.Worker.Service == "" {
+			return nil, nil, fmt.Errorf(`Outbound options for binding "%s" must have a service name`, bindingName)
+		}
+
+		var params []map[string]interface{}
+		for _, param := range b.Outbound.Params {
+			params = append(params, map[string]interface{}{
+				"name": param.Name,
+			})
+		}
+
+		meta["outbound"] = map[string]interface{}{
+			"worker": map[string]interface{}{
+				"service":     b.Outbound.Worker.Service,
+				"environment": b.Outbound.Worker.Environment,
+			},
+			"params": params,
+		}
+	}
+
+	return meta, nil, nil
+}
+
+// UnsafeBinding is for experimental or deprecated bindings, and allows specifying any binding type or property.
+type UnsafeBinding map[string]interface{}
+
+// Type returns the type of the binding.
+func (b UnsafeBinding) Type() WorkerBindingType {
+	return ""
+}
+
+func (b UnsafeBinding) serialize(bindingName string) (workerBindingMeta, workerBindingBodyWriter, error) {
+	b["name"] = bindingName
+	return b, nil, nil
+}
+
 // Each binding that adds a part to the multipart form body will need
 // a unique part name so we just generate a random 128bit hex string.
 func getRandomPartName() string {


Test patch:
diff --git a/images_test.go b/images_test.go
index 43cd8ad34..3640d9f31 100644
--- a/images_test.go
+++ b/images_test.go
@@ -96,61 +96,6 @@ func TestUploadImage(t *testing.T) {
 	}
 }
 
-func TestUploadImageByUrl(t *testing.T) {
-	setup()
-	defer teardown()
-
-	handler := func(w http.ResponseWriter, r *http.Request) {
-		assert.Equal(t, http.MethodPost, r.Method, "Expected method 'POST', got %s", r.Method)
-
-		u, err := parseImageMultipartUpload(r)
-		if !assert.NoError(t, err) {
-			w.WriteHeader(http.StatusBadRequest)
-			return
-		}
-		assert.Equal(t, u.RequireSignedURLs, true)
-		assert.Equal(t, u.Metadata, map[string]interface{}{"meta": "metaID"})
-		assert.Equal(t, u.Url, "https://www.images-elsewhere.com/avatar.png")
-
-		w.Header().Set("content-type", "application/json")
-		fmt.Fprintf(w, `{
-			"success": true,
-			"errors": [],
-			"messages": [],
-			"result": {
-				"id": "ZxR0pLaXRldlBtaFhhO2FiZGVnaA",
-				"filename": "avatar.png",
-				"metadata": {
-					"meta": "metaID"
-				},
-				"requireSignedURLs": true,
-				"variants": [
-					"https://imagedelivery.net/MTt4OTd0b0w5aj/ZxR0pLaXRldlBtaFhhO2FiZGVnaA/hero",
-					"https://imagedelivery.net/MTt4OTd0b0w5aj/ZxR0pLaXRldlBtaFhhO2FiZGVnaA/original",
-					"https://imagedelivery.net/MTt4OTd0b0w5aj/ZxR0pLaXRldlBtaFhhO2FiZGVnaA/thumbnail"
-				],
-				"uploaded": "2014-01-02T02:20:00Z"
-			}
-		}
-		`)
-	}
-
-	mux.HandleFunc("/accounts/"+testAccountID+"/images/v1", handler)
-	want := expectedImageStruct
-
-	actual, err := client.UploadImage(context.Background(), AccountIdentifier(testAccountID), UploadImageParams{
-		URL:               "https://www.images-elsewhere.com/avatar.png",
-		RequireSignedURLs: true,
-		Metadata: map[string]interface{}{
-			"meta": "metaID",
-		},
-	})
-
-	if assert.NoError(t, err) {
-		assert.Equal(t, want, actual)
-	}
-}
-
 func TestUpdateImage(t *testing.T) {
 	setup()
 	defer teardown()
@@ -252,20 +197,6 @@ func TestCreateImageDirectUploadURL(t *testing.T) {
 	}
 }
 
-func TestCreateImageConflictingTypes(t *testing.T) {
-	setup()
-	defer teardown()
-
-	_, err := client.UploadImage(context.Background(), AccountIdentifier(testAccountID), UploadImageParams{
-		URL: "https://example.com/foo.jpg",
-		File: fakeFile{
-			Buffer: bytes.NewBufferString("this is definitely an image"),
-		},
-	})
-
-	assert.Error(t, err)
-}
-
 func TestCreateImageDirectUploadURLV2(t *testing.T) {
 	setup()
 	defer teardown()
@@ -456,7 +387,6 @@ type imageMultipartUpload struct {
 	// this is for testing, never read an entire file into memory,
 	// especially when being done on a per-http request basis.
 	File              []byte
-	Url               string
 	RequireSignedURLs bool
 	Metadata          map[string]interface{}
 }
@@ -488,27 +418,15 @@ func parseImageMultipartUpload(r *http.Request) (imageMultipartUpload, error) {
 		}
 	}
 
-	if _, ok := r.MultipartForm.Value["url"]; ok {
-		urlBytes, err := getImageFormValue(r, "url")
-		if err != nil {
-			if !strings.HasPrefix(err.Error(), "no value found for key") {
-				return u, err
-			}
-		}
-		if urlBytes != nil {
-			u.Url = string(urlBytes)
-		}
-	} else {
-		f, _, err := r.FormFile("file")
-		if err != nil {
-			return u, err
-		}
-		defer f.Close()
+	f, _, err := r.FormFile("file")
+	if err != nil {
+		return u, err
+	}
+	defer f.Close()
 
-		u.File, err = io.ReadAll(f)
-		if err != nil {
-			return u, err
-		}
+	u.File, err = io.ReadAll(f)
+	if err != nil {
+		return u, err
 	}
 
 	return u, nil
diff --git a/regional_tiered_cache_test.go b/regional_tiered_cache_test.go
deleted file mode 100644
index df74f3bfe..000000000
--- a/regional_tiered_cache_test.go
+++ /dev/null
@@ -1,90 +0,0 @@
-package cloudflare
-
-import (
-	"context"
-	"fmt"
-	"net/http"
-	"testing"
-	"time"
-
-	"github.com/stretchr/testify/assert"
-)
-
-var regionalTieredCacheTimestampString = "2019-02-20T22:37:07.107449Z"
-var regionalTieredCacheTimestamp, _ = time.Parse(time.RFC3339Nano, regionalTieredCacheTimestampString)
-
-func TestRegionalTieredCache(t *testing.T) {
-	setup()
-	defer teardown()
-
-	handler := func(w http.ResponseWriter, r *http.Request) {
-		assert.Equal(t, http.MethodGet, r.Method, "Expected method 'GET', got %s", r.Method)
-		w.Header().Set("content-type", "application/json")
-		fmt.Fprintf(w, `{
-			"success": true,
-			"errors": [],
-			"messages": [],
-			"result": {
-				"id": "regional_tiered_cache",
-				"value": "on",
-				"modified_on": "%s"
-			}
-		}
-		`, regionalTieredCacheTimestampString)
-	}
-
-	mux.HandleFunc("/zones/"+testZoneID+"/cache/regional_tiered_cache", handler)
-	want := RegionalTieredCache{
-		ID:         "regional_tiered_cache",
-		Value:      "on",
-		ModifiedOn: regionalTieredCacheTimestamp,
-	}
-
-	actual, err := client.GetRegionalTieredCache(
-		context.Background(),
-		ZoneIdentifier(testZoneID),
-		GetRegionalTieredCacheParams{},
-	)
-
-	if assert.NoError(t, err) {
-		assert.Equal(t, want, actual)
-	}
-}
-
-func TestUpdateRegionalTieredCache(t *testing.T) {
-	setup()
-	defer teardown()
-
-	handler := func(w http.ResponseWriter, r *http.Request) {
-		assert.Equal(t, http.MethodPatch, r.Method, "Expected method 'PATCH', got %s", r.Method)
-		w.Header().Set("content-type", "application/json")
-		fmt.Fprintf(w, `{
-			"success": true,
-			"errors": [],
-			"messages": [],
-			"result": {
-				"id": "regional_tiered_cache",
-				"value": "off",
-				"modified_on": "%s"
-			}
-		}
-		`, regionalTieredCacheTimestampString)
-	}
-
-	mux.HandleFunc("/zones/"+testZoneID+"/cache/regional_tiered_cache", handler)
-	want := RegionalTieredCache{
-		ID:         "regional_tiered_cache",
-		Value:      "off",
-		ModifiedOn: regionalTieredCacheTimestamp,
-	}
-
-	actual, err := client.UpdateRegionalTieredCache(
-		context.Background(),
-		ZoneIdentifier(testZoneID),
-		UpdateRegionalTieredCacheParams{Value: "off"},
-	)
-
-	if assert.NoError(t, err) {
-		assert.Equal(t, want, actual)
-	}
-}
diff --git a/rulesets_test.go b/rulesets_test.go
index d2d654a1f..52e9ed88c 100644
--- a/rulesets_test.go
+++ b/rulesets_test.go
@@ -52,12 +52,12 @@ func TestListRulesets(t *testing.T) {
 		},
 	}
 
-	zoneActual, err := client.ListRulesets(context.Background(), ZoneIdentifier(testZoneID), ListRulesetsParams{})
+	zoneActual, err := client.ListZoneRulesets(context.Background(), testZoneID)
 	if assert.NoError(t, err) {
 		assert.Equal(t, want, zoneActual)
 	}
 
-	accountActual, err := client.ListRulesets(context.Background(), AccountIdentifier(testAccountID), ListRulesetsParams{})
+	accountActual, err := client.ListAccountRulesets(context.Background(), testAccountID)
 	if assert.NoError(t, err) {
 		assert.Equal(t, want, accountActual)
 	}
@@ -101,12 +101,12 @@ func TestGetRuleset_MagicTransit(t *testing.T) {
 		Phase:       string(RulesetPhaseMagicTransit),
 	}
 
-	zoneActual, err := client.GetRuleset(context.Background(), ZoneIdentifier(testZoneID), "2c0fc9fa937b11eaa1b71c4d701ab86e")
+	zoneActual, err := client.GetZoneRuleset(context.Background(), testZoneID, "2c0fc9fa937b11eaa1b71c4d701ab86e")
 	if assert.NoError(t, err) {
 		assert.Equal(t, want, zoneActual)
 	}
 
-	accountActual, err := client.GetRuleset(context.Background(), AccountIdentifier(testAccountID), "2c0fc9fa937b11eaa1b71c4d701ab86e")
+	accountActual, err := client.GetAccountRuleset(context.Background(), testAccountID, "2c0fc9fa937b11eaa1b71c4d701ab86e")
 	if assert.NoError(t, err) {
 		assert.Equal(t, want, accountActual)
 	}
@@ -188,12 +188,12 @@ func TestGetRuleset_WAF(t *testing.T) {
 		Rules:       rules,
 	}
 
-	zoneActual, err := client.GetRuleset(context.Background(), ZoneIdentifier(testZoneID), "b232b534beea4e00a21dcbb7a8a545e9")
+	zoneActual, err := client.GetZoneRuleset(context.Background(), testZoneID, "b232b534beea4e00a21dcbb7a8a545e9")
 	if assert.NoError(t, err) {
 		assert.Equal(t, want, zoneActual)
 	}
 
-	accountActual, err := client.GetRuleset(context.Background(), AccountIdentifier(testAccountID), "b232b534beea4e00a21dcbb7a8a545e9")
+	accountActual, err := client.GetAccountRuleset(context.Background(), testAccountID, "b232b534beea4e00a21dcbb7a8a545e9")
 	if assert.NoError(t, err) {
 		assert.Equal(t, want, accountActual)
 	}
@@ -343,12 +343,12 @@ func TestGetRuleset_SetCacheSettings(t *testing.T) {
 		Rules:       rules,
 	}
 
-	zoneActual, err := client.GetRuleset(context.Background(), ZoneIdentifier(testZoneID), "b232b534beea4e00a21dcbb7a8a545e9")
+	zoneActual, err := client.GetZoneRuleset(context.Background(), testZoneID, "b232b534beea4e00a21dcbb7a8a545e9")
 	if assert.NoError(t, err) {
 		assert.Equal(t, want, zoneActual)
 	}
 
-	accountActual, err := client.GetRuleset(context.Background(), AccountIdentifier(testAccountID), "b232b534beea4e00a21dcbb7a8a545e9")
+	accountActual, err := client.GetAccountRuleset(context.Background(), testAccountID, "b232b534beea4e00a21dcbb7a8a545e9")
 	if assert.NoError(t, err) {
 		assert.Equal(t, want, accountActual)
 	}
@@ -454,12 +454,12 @@ func TestGetRuleset_SetConfig(t *testing.T) {
 		Rules:       rules,
 	}
 
-	zoneActual, err := client.GetRuleset(context.Background(), ZoneIdentifier(testZoneID), "b232b534beea4e00a21dcbb7a8a545e9")
+	zoneActual, err := client.GetZoneRuleset(context.Background(), testZoneID, "b232b534beea4e00a21dcbb7a8a545e9")
 	if assert.NoError(t, err) {
 		assert.Equal(t, want, zoneActual)
 	}
 
-	accountActual, err := client.GetRuleset(context.Background(), AccountIdentifier(testAccountID), "b232b534beea4e00a21dcbb7a8a545e9")
+	accountActual, err := client.GetAccountRuleset(context.Background(), testAccountID, "b232b534beea4e00a21dcbb7a8a545e9")
 	if assert.NoError(t, err) {
 		assert.Equal(t, want, accountActual)
 	}
@@ -543,12 +543,12 @@ func TestGetRuleset_RedirectFromValue(t *testing.T) {
 		Rules:       rules,
 	}
 
-	zoneActual, err := client.GetRuleset(context.Background(), ZoneIdentifier(testZoneID), "b232b534beea4e00a21dcbb7a8a545e9")
+	zoneActual, err := client.GetZoneRuleset(context.Background(), testZoneID, "b232b534beea4e00a21dcbb7a8a545e9")
 	if assert.NoError(t, err) {
 		assert.Equal(t, want, zoneActual)
 	}
 
-	accountActual, err := client.GetRuleset(context.Background(), AccountIdentifier(testAccountID), "b232b534beea4e00a21dcbb7a8a545e9")
+	accountActual, err := client.GetAccountRuleset(context.Background(), testAccountID, "b232b534beea4e00a21dcbb7a8a545e9")
 	if assert.NoError(t, err) {
 		assert.Equal(t, want, accountActual)
 	}
@@ -623,12 +623,12 @@ func TestGetRuleset_CompressResponse(t *testing.T) {
 		Rules:       rules,
 	}
 
-	zoneActual, err := client.GetRuleset(context.Background(), ZoneIdentifier(testZoneID), "b232b534beea4e00a21dcbb7a8a545e9")
+	zoneActual, err := client.GetZoneRuleset(context.Background(), testZoneID, "b232b534beea4e00a21dcbb7a8a545e9")
 	if assert.NoError(t, err) {
 		assert.Equal(t, want, zoneActual)
 	}
 
-	accountActual, err := client.GetRuleset(context.Background(), AccountIdentifier(testAccountID), "b232b534beea4e00a21dcbb7a8a545e9")
+	accountActual, err := client.GetAccountRuleset(context.Background(), testAccountID, "b232b534beea4e00a21dcbb7a8a545e9")
 	if assert.NoError(t, err) {
 		assert.Equal(t, want, accountActual)
 	}
@@ -691,7 +691,7 @@ func TestCreateRuleset(t *testing.T) {
 		Enabled:     BoolPtr(true),
 	}}
 
-	newRuleset := CreateRulesetParams{
+	newRuleset := Ruleset{
 		Name:        "my example ruleset",
 		Description: "Test magic transit ruleset",
 		Kind:        "root",
@@ -710,12 +710,12 @@ func TestCreateRuleset(t *testing.T) {
 		Rules:       rules,
 	}
 
-	zoneActual, err := client.CreateRuleset(context.Background(), ZoneIdentifier(testZoneID), newRuleset)
+	zoneActual, err := client.CreateZoneRuleset(context.Background(), testZoneID, newRuleset)
 	if assert.NoError(t, err) {
 		assert.Equal(t, want, zoneActual)
 	}
 
-	accountActual, err := client.CreateRuleset(context.Background(), AccountIdentifier(testAccountID), newRuleset)
+	accountActual, err := client.CreateAccountRuleset(context.Background(), testAccountID, newRuleset)
 	if assert.NoError(t, err) {
 		assert.Equal(t, want, accountActual)
 	}
@@ -734,10 +734,10 @@ func TestDeleteRuleset(t *testing.T) {
 	mux.HandleFunc("/accounts/"+testAccountID+"/rulesets/2c0fc9fa937b11eaa1b71c4d701ab86e", handler)
 	mux.HandleFunc("/zones/"+testZoneID+"/rulesets/2c0fc9fa937b11eaa1b71c4d701ab86e", handler)
 
-	zErr := client.DeleteRuleset(context.Background(), ZoneIdentifier(testZoneID), "2c0fc9fa937b11eaa1b71c4d701ab86e")
+	zErr := client.DeleteZoneRuleset(context.Background(), testZoneID, "2c0fc9fa937b11eaa1b71c4d701ab86e")
 	assert.NoError(t, zErr)
 
-	aErr := client.DeleteRuleset(context.Background(), AccountIdentifier(testAccountID), "2c0fc9fa937b11eaa1b71c4d701ab86e")
+	aErr := client.DeleteAccountRuleset(context.Background(), testAccountID, "2c0fc9fa937b11eaa1b71c4d701ab86e")
 	assert.NoError(t, aErr)
 }
 
@@ -823,12 +823,6 @@ func TestUpdateRuleset(t *testing.T) {
 		Enabled:     BoolPtr(true),
 	}}
 
-	updated := UpdateRulesetParams{
-		ID:          "2c0fc9fa937b11eaa1b71c4d701ab86e",
-		Description: "Test Firewall Ruleset Update",
-		Rules:       rules,
-	}
-
 	want := Ruleset{
 		ID:          "2c0fc9fa937b11eaa1b71c4d701ab86e",
 		Name:        "ruleset1",
@@ -840,12 +834,12 @@ func TestUpdateRuleset(t *testing.T) {
 		Rules:       rules,
 	}
 
-	zoneActual, err := client.UpdateRuleset(context.Background(), ZoneIdentifier(testZoneID), updated)
+	zoneActual, err := client.UpdateZoneRuleset(context.Background(), testZoneID, "2c0fc9fa937b11eaa1b71c4d701ab86e", "Test Firewall Ruleset Update", rules)
 	if assert.NoError(t, err) {
 		assert.Equal(t, want, zoneActual)
 	}
 
-	accountActual, err := client.UpdateRuleset(context.Background(), AccountIdentifier(testAccountID), updated)
+	accountActual, err := client.UpdateAccountRuleset(context.Background(), testAccountID, "2c0fc9fa937b11eaa1b71c4d701ab86e", "Test Firewall Ruleset Update", rules)
 	if assert.NoError(t, err) {
 		assert.Equal(t, want, accountActual)
 	}
diff --git a/workers_bindings_test.go b/workers_bindings_test.go
index f8dea25ad..b7514449b 100644
--- a/workers_bindings_test.go
+++ b/workers_bindings_test.go
@@ -1,7 +1,9 @@
 package cloudflare
 
 import (
+	"bytes"
 	"context"
+	"encoding/json"
 	"fmt"
 	"io"
 	"net/http"
@@ -95,3 +97,46 @@ func TestListWorkerBindings(t *testing.T) {
 	})
 	assert.Equal(t, WorkerAnalyticsEngineBindingType, res.BindingList[7].Binding.Type())
 }
+
+func ExampleUnsafeBinding() {
+	pretty := func(meta workerBindingMeta) string {
+		buf := bytes.NewBufferString("")
+		encoder := json.NewEncoder(buf)
+		encoder.SetIndent("", "  ")
+		if err := encoder.Encode(meta); err != nil {
+			fmt.Println("error:", err)
+		}
+		return buf.String()
+	}
+
+	binding_a := WorkerServiceBinding{
+		Service: "foo",
+	}
+	meta_a, _, _ := binding_a.serialize("my_binding")
+	meta_a_json := pretty(meta_a)
+	fmt.Println(meta_a_json)
+
+	binding_b := UnsafeBinding{
+		"type":    "service",
+		"service": "foo",
+	}
+	meta_b, _, _ := binding_b.serialize("my_binding")
+	meta_b_json := pretty(meta_b)
+	fmt.Println(meta_b_json)
+
+	fmt.Println(meta_a_json == meta_b_json)
+	// Output:
+	// {
+	//   "name": "my_binding",
+	//   "service": "foo",
+	//   "type": "service"
+	// }
+	//
+	// {
+	//   "name": "my_binding",
+	//   "service": "foo",
+	//   "type": "service"
+	// }
+	//
+	// true
+}
diff --git a/workers_test.go b/workers_test.go
index 1a48bf4b0..d116e9384 100644
--- a/workers_test.go
+++ b/workers_test.go
@@ -1054,6 +1054,64 @@ func TestUploadWorker_WithQueueBinding(t *testing.T) {
 	assert.NoError(t, err)
 }
 
+func TestUploadWorker_WithDispatchNamespaceBinding(t *testing.T) {
+	setup()
+	defer teardown()
+
+	handler := func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, http.MethodPut, r.Method, "Expected method 'PUT', got %s", r.Method)
+
+		mpUpload, err := parseMultipartUpload(r)
+		assert.NoError(t, err)
+
+		expectedBindings := map[string]workerBindingMeta{
+			"b1": {
+				"name":      "b1",
+				"type":      "dispatch_namespace",
+				"namespace": "n1",
+				"outbound": map[string]interface{}{
+					"worker": map[string]interface{}{
+						"service":     "w1",
+						"environment": "e1",
+					},
+					"params": []interface{}{
+						map[string]interface{}{"name": "param1"},
+					},
+				},
+			},
+		}
+		assert.Equal(t, workerScript, mpUpload.Script)
+		assert.Equal(t, expectedBindings, mpUpload.BindingMeta)
+
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprint(w, workersScriptResponse(t))
+	}
+	mux.HandleFunc("/accounts/"+testAccountID+"/workers/scripts/bar", handler)
+
+	environmentName := "e1"
+	_, err := client.UploadWorker(context.Background(), AccountIdentifier(testAccountID), CreateWorkerParams{
+		ScriptName: "bar",
+		Script:     workerScript,
+		Bindings: map[string]WorkerBinding{
+			"b1": DispatchNamespaceBinding{
+				Binding:   "b1",
+				Namespace: "n1",
+				Outbound: &NamespaceOutboundOptions{
+					Worker: WorkerReference{
+						Service:     "w1",
+						Environment: &environmentName,
+					},
+					Params: []OutboundParamSchema{
+						{
+							Name: "param1",
+						},
+					},
+				},
+			},
+		}})
+	assert.NoError(t, err)
+}
+
 func TestUploadWorker_WithSmartPlacementEnabled(t *testing.T) {
 	setup()
 	defer teardown()
@@ -1140,3 +1198,74 @@ func TestUploadWorker_WithTailConsumers(t *testing.T) {
 		assert.Len(t, *worker.TailConsumers, 2)
 	})
 }
+
+func TestUploadWorker_ToDispatchNamespace(t *testing.T) {
+	setup()
+	defer teardown()
+
+	namespaceName := "n1"
+
+	handler := func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, http.MethodPut, r.Method, "Expected method 'PUT', got %s", r.Method)
+
+		mpUpload, err := parseMultipartUpload(r)
+		require.NoError(t, err)
+
+		assert.Equal(t, workerScript, mpUpload.Script)
+
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprint(w, workersScriptResponse(t))
+	}
+	mux.HandleFunc(
+		fmt.Sprintf("/accounts/"+testAccountID+"/workers/namespaces/%s/scripts/bar", namespaceName),
+		handler,
+	)
+
+	_, err := client.UploadWorker(context.Background(), AccountIdentifier(testAccountID), CreateWorkerParams{
+		ScriptName:            "bar",
+		Script:                workerScript,
+		DispatchNamespaceName: &namespaceName,
+		Bindings: map[string]WorkerBinding{
+			"b1": WorkerPlainTextBinding{
+				Text: "hello",
+			},
+		},
+	})
+	assert.NoError(t, err)
+}
+
+func TestUploadWorker_UnsafeBinding(t *testing.T) {
+	setup()
+	defer teardown()
+
+	handler := func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, http.MethodPut, r.Method, "Expected method 'PUT', got %s", r.Method)
+
+		mpUpload, err := parseMultipartUpload(r)
+		require.NoError(t, err)
+
+		assert.Equal(t, workerScript, mpUpload.Script)
+
+		require.Contains(t, mpUpload.BindingMeta, "b1")
+		assert.Contains(t, mpUpload.BindingMeta["b1"], "name")
+		assert.Equal(t, "b1", mpUpload.BindingMeta["b1"]["name"])
+		assert.Contains(t, mpUpload.BindingMeta["b1"], "type")
+		assert.Equal(t, "dynamic_dispatch", mpUpload.BindingMeta["b1"]["type"])
+
+		w.Header().Set("content-type", "application/json")
+		fmt.Println(workersScriptResponse(t))
+		fmt.Fprint(w, workersScriptResponse(t))
+	}
+	mux.HandleFunc("/accounts/"+testAccountID+"/workers/scripts/bar", handler)
+
+	_, err := client.UploadWorker(context.Background(), AccountIdentifier(testAccountID), CreateWorkerParams{
+		ScriptName: "bar",
+		Script:     workerScript,
+		Bindings: map[string]WorkerBinding{
+			"b1": UnsafeBinding{
+				"type": "dynamic_dispatch",
+			},
+		},
+	})
+	assert.NoError(t, err)
+}