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
- Variables
- func CloudflareTokenPath(configPath string) string
- func CredentialsPath(configPath string) string
- func DeleteCloudflareToken(path string) (removed bool, err error)
- func LoadCloudflareToken(path string) (token string, ok bool, err error)
- func NormalizeWorkersDevSubdomainName(name string) string
- func SaveCloudflareToken(path, token string) error
- func SaveCredentials(path string, creds RelayCredentials) error
- func ValidateWorkersDevSubdomainName(name string) error
- type Client
- func (c *Client) ClaimWorkersDevSubdomain(ctx context.Context, accountID, subdomain string) error
- func (c *Client) EnableWorkersDevRoute(ctx context.Context, accountID, scriptName string) error
- func (c *Client) ResolveAccountID(ctx context.Context) (string, error)
- func (c *Client) ScriptMigrationTag(ctx context.Context, accountID, scriptName string) (tag string, found bool, err error)
- func (c *Client) SetSecret(ctx context.Context, accountID, scriptName, name, value string) error
- func (c *Client) UploadWorkerModule(ctx context.Context, accountID, scriptName, mainModule string, ...) error
- func (c *Client) VerifyToken(ctx context.Context) error
- func (c *Client) WorkersDevSubdomain(ctx context.Context, accountID string) (subdomain string, claimed bool, err error)
- type CloudflareAPIError
- type CloudflareMessage
- type Options
- type RelayCredentials
- type Result
Constants ¶
const DefaultScriptName = "moltnet-relay"
DefaultScriptName is the Cloudflare Worker script name used when `moltnet relay deploy` is run without --name.
Variables ¶
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.
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 NewClientForTesting ¶
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 ¶
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 ¶
EnableWorkersDevRoute turns on the script's *.workers.dev route.
func (*Client) ResolveAccountID ¶
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) 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 ¶
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 ¶
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 ¶
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.