relaydeploy

package
v0.1.22 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 26, 2026 License: MIT Imports: 19 Imported by: 0

Documentation

Overview

Package relaydeploy implements `moltnet relay deploy`: a minimal Cloudflare REST API client (stdlib net/http only) plus deploy orchestration for the relay Worker embedded from the sibling relay/ package.

Index

Constants

View Source
const DefaultScriptName = "moltnet-relay"

DefaultScriptName is the Cloudflare Worker script name used when `moltnet relay deploy` is run without --name.

Variables

View Source
var ErrWorkersDevSubdomainConflict = errors.New("requested workers.dev subdomain conflicts with an existing claim for this account")

ErrWorkersDevSubdomainConflict indicates opts.Subdomain named a workers.dev subdomain that does not match the name this Cloudflare account has already claimed. Claiming a workers.dev subdomain is permanent and limited to exactly one per account, ever (see Client.ClaimWorkersDevSubdomain's doc comment on Cloudflare error code 10036), so a request naming a different one can never be honored: Deploy refuses before any remote mutation rather than silently adopting the account's existing claim (ignoring what the caller actually asked for) or attempting a claim Cloudflare will only reject.

View Source
var ErrWorkersDevSubdomainUnclaimed = errors.New("workers.dev subdomain not yet claimed for this Cloudflare account")

ErrWorkersDevSubdomainUnclaimed indicates the Cloudflare account has never claimed a workers.dev subdomain. The API cannot claim one on the caller's behalf (PLAN.md review finding), so the caller must do the one-time dashboard step before a *.workers.dev URL exists to deploy to, or supply Options.Subdomain to claim it non-interactively.

Functions

func CloudflareTokenPath

func CloudflareTokenPath(configPath string) string

CloudflareTokenPath returns the .moltnet/cloudflare.json path to use for the Moltnet config at configPath, sitting next to CredentialsPath's relay.json in the same .moltnet/ runtime-state directory.

func CredentialsPath

func CredentialsPath(configPath string) string

CredentialsPath returns the .moltnet/relay.json path to use for the Moltnet config at configPath, mirroring the .moltnet/ runtime-state convention used elsewhere (e.g. register-agent's identity.json).

func DeleteCloudflareToken

func DeleteCloudflareToken(path string) (removed bool, err error)

DeleteCloudflareToken removes a stored Cloudflare API token file, for `relay deploy --forget-token`. removed is false, with a nil error, when there was nothing stored at path.

func LoadCloudflareToken

func LoadCloudflareToken(path string) (token string, ok bool, err error)

LoadCloudflareToken reads a stored Cloudflare API token. ok is false, with a nil error, when nothing has been saved yet.

If the file is readable by group or other (SaveCloudflareToken always writes 0600, so this means something outside Moltnet loosened it), a warning naming the file is printed to stderr. It is only a warning: the permissions are never silently chmod'd back, since that could surprise whatever changed them on purpose.

func NormalizeWorkersDevSubdomainName

func NormalizeWorkersDevSubdomainName(name string) string

NormalizeWorkersDevSubdomainName lowercases name so a user-typed subdomain name validates and claims the same way regardless of case. Cloudflare's account-level workers.dev subdomain is effectively case-insensitive in practice (it becomes a DNS label, and DNS labels are case-insensitive), but workersDevSubdomainNamePattern's rule is lowercase-only, matching wrangler's own lowercase-before-validate behavior, so callers must call this before ValidateWorkersDevSubdomainName and before sending the name to Client.ClaimWorkersDevSubdomain, not after.

func SaveCloudflareToken

func SaveCloudflareToken(path, token string) error

SaveCloudflareToken writes token to path atomically (temp file + rename), mode 0600, creating the parent .moltnet/ directory if needed. It never logs or echoes token itself.

func SaveCredentials

func SaveCredentials(path string, creds RelayCredentials) error

SaveCredentials writes creds to path atomically (temp file + rename), mode 0600, creating the parent .moltnet/ directory if needed.

func ValidateWorkersDevSubdomainName

func ValidateWorkersDevSubdomainName(name string) error

ValidateWorkersDevSubdomainName reports whether name satisfies workersDevSubdomainNamePattern above. It exists so the interactive claim prompt (cmd/moltnet's `relay deploy`, on ErrWorkersDevSubdomainUnclaimed) can reject an obviously invalid name locally, before ever spending a round trip on Client.ClaimWorkersDevSubdomain. Callers must normalize a user-typed name with NormalizeWorkersDevSubdomainName before calling this: an uppercase letter is rejected here, not silently accepted.

Types

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client is a minimal Cloudflare REST API client covering exactly the calls `moltnet relay deploy` needs: token verification, account resolution, module worker upload, secret management, and workers.dev route/subdomain handling. Auth is always CLOUDFLARE_API_TOKEN-style bearer auth; OAuth is deferred (see PLAN.md).

func NewClient

func NewClient(token string) *Client

NewClient returns a Cloudflare API client authenticated with token.

func NewClientForTesting

func NewClientForTesting(token, baseURL string, httpClient *http.Client) *Client

NewClientForTesting returns a Client pointed at baseURL using httpClient, instead of the real Cloudflare API host. It exists so tests in other packages (e.g. cmd/moltnet, which cannot reach Client's unexported fields directly) can exercise a real end-to-end Deploy call against a net/http/httptest fake Cloudflare server. Production code never calls this; only NewClient does. baseURL's host must be loopback (127.0.0.1/::1/localhost, as net/http/httptest.Server always is); this panics otherwise, so a baseURL pointed at a real host (production misuse, or a typo'd test) fails immediately instead of silently working.

func (*Client) ClaimWorkersDevSubdomain

func (c *Client) ClaimWorkersDevSubdomain(ctx context.Context, accountID, subdomain string) error

ClaimWorkersDevSubdomain registers subdomain as accountID's account-level workers.dev subdomain (PUT /accounts/{account_id}/workers/subdomain, Cloudflare's "Create Subdomain" operation); the one-time claim WorkersDevSubdomain above reads back afterward. It is the API call the interactive claim prompt (cmd/moltnet's `relay deploy`) makes after an operator types a name in response to ErrWorkersDevSubdomainUnclaimed; callers should normalize and validate the name against NormalizeWorkersDevSubdomainName / ValidateWorkersDevSubdomainName before calling this, to avoid spending a round trip on an obviously invalid one.

Cloudflare documents two error codes this call can return that callers should branch on rather than treat identically: 10031 ("subdomain already exists"; the name is taken by some account, this one included) is retryable with a different name; 10036 ("account already has a subdomain", HTTP 409; this account has already claimed one, and Cloudflare allows exactly one per account, ever) is not a failure at all; the caller should re-read the existing claim via WorkersDevSubdomain and continue as already claimed (cmd/moltnet's interactive claim prompt does both). Any other rejection surfaces as a *CloudflareAPIError carrying whatever message Cloudflare returned, shown verbatim.

func (*Client) EnableWorkersDevRoute

func (c *Client) EnableWorkersDevRoute(ctx context.Context, accountID, scriptName string) error

EnableWorkersDevRoute turns on the script's *.workers.dev route.

func (*Client) ResolveAccountID

func (c *Client) ResolveAccountID(ctx context.Context) (string, error)

ResolveAccountID returns the first Cloudflare account the token can see. Cloudflare tokens are typically scoped to exactly one account; if the token spans several, the first is used (matching wrangler's default behavior when only one account is configured).

func (*Client) ScriptMigrationTag

func (c *Client) ScriptMigrationTag(ctx context.Context, accountID, scriptName string) (tag string, found bool, err error)

ScriptMigrationTag returns scriptName's current Durable Object migration tag, via Cloudflare's per-script Get Service Metadata operation (GET /accounts/{account_id}/workers/services/{script_name}), reading result.default_environment.script.migration_tag; the same call and field wrangler itself reads to compute old_tag before an upload (see deploy.go's prepareUploadMetadata, which this backs). Unlike the account-wide "List Workers" endpoint this used to call, this is a single per-script lookup: O(1) in the account's script count.

found is false when no script named scriptName exists yet: Cloudflare reports that as a failed envelope carrying error code 10007, 10090, or 10092 (script/service/environment not found, wrangler's own suppressNotFoundError treats the same three as equivalent for this call), all mapped here to found=false. Any other failure surfaces as a *CloudflareAPIError. tag == "" with found true means the script exists but has never had a migration applied.

func (*Client) SetSecret

func (c *Client) SetSecret(ctx context.Context, accountID, scriptName, name, value string) error

SetSecret creates or updates a Worker secret binding.

func (*Client) UploadWorkerModule

func (c *Client) UploadWorkerModule(ctx context.Context, accountID, scriptName, mainModule string, script, metadataJSON []byte) error

UploadWorkerModule uploads script as the module worker's mainModule entry point, alongside metadataJSON (bindings, migrations, compatibility date), creating or overwriting scriptName. Uploading an existing script name is how idempotent re-deploy updates the code.

func (*Client) VerifyToken

func (c *Client) VerifyToken(ctx context.Context) error

VerifyToken confirms CLOUDFLARE_API_TOKEN is valid and usable.

func (*Client) WorkersDevSubdomain

func (c *Client) WorkersDevSubdomain(ctx context.Context, accountID string) (subdomain string, claimed bool, err error)

WorkersDevSubdomain resolves the account-level workers.dev subdomain (e.g. "acme" in "acme.workers.dev"). The account-level subdomain is a one-time claim the API cannot perform on the caller's behalf (PLAN.md review finding): an unclaimed subdomain is reported as claimed=false with a nil error, not as a failure, so callers can print the one dashboard step instead of a generic error. Only an authentication failure (401/403) is treated as a hard error here.

type CloudflareAPIError

type CloudflareAPIError struct {
	StatusCode int
	Messages   []CloudflareMessage
}

CloudflareAPIError reports a Cloudflare REST API call that returned success:false, keeping the HTTP status and the API's own messages.

func (*CloudflareAPIError) Error

func (e *CloudflareAPIError) Error() string

func (*CloudflareAPIError) Unauthorized

func (e *CloudflareAPIError) Unauthorized() bool

Unauthorized reports whether the error represents a rejected or insufficiently scoped API token, as opposed to e.g. a not-found result.

type CloudflareMessage

type CloudflareMessage struct {
	Code    int    `json:"code"`
	Message string `json:"message"`
}

CloudflareMessage is a single error or informational message from a Cloudflare API response envelope.

type Options

type Options struct {
	// ScriptName is the Cloudflare Worker script name. Empty defaults to
	// DefaultScriptName.
	ScriptName string
	// ExistingToken, when non-empty, is set as the RELAY_TOKEN secret
	// instead of generating or reusing one (the CLI --token-env path).
	ExistingToken string
	// PriorToken, used only when ExistingToken is empty, is reused instead
	// of generating a new RELAY_TOKEN, so a re-deploy stays idempotent and
	// does not rotate every pairing's shared secret out from under it.
	PriorToken string
	// Subdomain, when non-empty, is a workers.dev subdomain name to claim
	// for this Cloudflare account non-interactively (the CLI's --subdomain
	// flag) instead of erroring with ErrWorkersDevSubdomainUnclaimed. It is
	// resolved and validated before any remote mutation, see deploy's own
	// doc comment for the adopt/claim/conflict/fail decision table. Ignored
	// (no claim attempted) when the account already has a claim matching
	// this name; ErrWorkersDevSubdomainConflict when it has a different
	// one. Passing this is the caller's explicit confirmation that claiming
	// a workers.dev subdomain is permanent for the account, see
	// NormalizeWorkersDevSubdomainName / ValidateWorkersDevSubdomainName for
	// the rules it is checked against.
	Subdomain string
	// ResolveHostname resolves the deployed workers.dev hostname's DNS
	// availability. Nil defaults to the package's real DNS lookup; tests
	// inject their own stub here instead of mutating shared package state,
	// which keeps parallel tests independent of each other.
	ResolveHostname func(ctx context.Context, hostname string) bool
	// StoredTokenPath, when non-empty, names the per-network stored
	// Cloudflare API token file (CloudflareTokenPath) that supplied the
	// token this deploy is using. It carries no auth behavior of its own;
	// it only lets Deploy recognize a rejected/insufficiently-scoped token
	// (CloudflareAPIError.Unauthorized()) as coming from that stored file,
	// so the returned error can name it and suggest --forget-token or the
	// CLOUDFLARE_API_TOKEN env override instead of a generic auth failure.
	// Leave empty when the token came from CLOUDFLARE_API_TOKEN or
	// --token-env.
	StoredTokenPath string
}

Options configures a single Deploy call.

type RelayCredentials

type RelayCredentials struct {
	URL   string `json:"url"`
	Token string `json:"token"`
	// ScriptName is the Cloudflare Worker script name (deploy's --name) these
	// credentials belong to. Empty for credentials files written before this
	// field existed; MatchesScriptName falls back to a hostname-prefix match
	// for those.
	ScriptName string `json:"scriptName,omitempty"`
}

RelayCredentials is the local record `moltnet relay deploy` writes so `moltnet pair invite` can reuse an already-deployed relay without re-flagging --relay-url/--relay-token-env every time (PLAN.md "Relay credentials storage").

func LoadCredentials

func LoadCredentials(path string) (creds RelayCredentials, ok bool, err error)

LoadCredentials reads stored relay credentials. ok is false, with a nil error, when nothing has been saved yet.

func (RelayCredentials) MatchesScriptName

func (creds RelayCredentials) MatchesScriptName(scriptName string) bool

MatchesScriptName reports whether creds were saved for a deploy of scriptName, so their Token is safe to reuse as a redeploy's PriorToken. Reusing a token across different script names would seed a brand-new Worker with a different relay's shared secret instead of generating its own (the P2 cross-name RELAY_TOKEN reuse bug).

Credentials saved before ScriptName existed fall back to matching the stored URL's hostname prefix against scriptName, since Deploy always derives the hostname as "<scriptName>.<subdomain>.workers.dev".

type Result

type Result struct {
	ScriptName string
	AccountID  string
	Token      string
	// Subdomain is the account's workers.dev subdomain name (e.g. "acme" in
	// "acme.workers.dev") that this deploy resolved, whether it was
	// already claimed, adopted from an existing claim matching
	// opts.Subdomain, or freshly claimed via opts.Subdomain. Left empty on
	// both ErrWorkersDevSubdomainUnclaimed (there is, by definition, none
	// yet) and ErrWorkersDevSubdomainConflict (deploy wraps that error as
	// Result{AccountID: accountID}, deliberately not re-deriving Subdomain
	// from resolveWorkersDevSubdomainClaim's own conflict-path locals) --
	// the conflict error message itself already names both the requested
	// and the account's actual subdomain, so no caller has needed this
	// field for that case.
	//
	// Claimed is true only when this exact Deploy call is the one that
	// performed a fresh, brand-new, permanent account-level workers.dev
	// claim (opts.Subdomain set, account previously unclaimed) -- never on
	// the already-claimed/adopted paths, where Subdomain is populated but
	// nothing was newly committed. A first-ever permanent claim is
	// otherwise indistinguishable from adopting an existing one to a
	// caller, with no acknowledgement at all; see the CLI's own use of this
	// field (cmd/moltnet/relay_deploy.go) for the receipt it prints.
	Claimed          bool
	Subdomain        string
	Hostname         string
	URL              string
	HostnameResolved bool
}

Result is what a successful Deploy learned, for the CLI to print and the caller to persist via SaveCredentials. On an ErrWorkersDevSubdomainUnclaimed error specifically, Result is not fully zeroed: AccountID is still populated (account resolution always happens before any step that can return that sentinel), so a caller driving the interactive claim prompt (cmd/moltnet's attemptInteractiveWorkersDevSubdomainClaim) can reuse it instead of resolving the account a second time.

func Deploy

func Deploy(ctx context.Context, client *Client, opts Options) (Result, error)

Deploy uploads the embedded relay Worker via client, sets its RELAY_TOKEN secret, enables its workers.dev route, and resolves the resulting wss:// URL. It is safe to call repeatedly: re-uploading an existing script name updates the code, and the RELAY_TOKEN secret is only regenerated when neither opts.ExistingToken nor opts.PriorToken is set.

A rejected/insufficiently-scoped Cloudflare API token error is enriched with opts.StoredTokenPath guidance, when set, via wrapStoredTokenError. See Result's doc comment for what is still populated on an ErrWorkersDevSubdomainUnclaimed error specifically.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL