feat(calcom): route Cal.com egress through host OutboundHTTP (ADR 0023)
Under wasm the guest has no network, so every Cal.com v2 call was dying at "dial tcp: lookup api.cal.com". Thread deps.OutboundHTTP (http.RoundTripper) from NewCalcomRouter onto CalcomHandler.rt and into NewCalcomClient, building the client with Transport: rt. A nil rt (native/DESCRIBE/test) falls back to the default transport, so bundled/httptest paths are unchanged. Declare allowed_hosts = ["api.cal.com"] (deny-by-default; admin-granted at install per the Phase 3 consent gate) and bump pluginsdk v0.2.4 -> v0.2.5 for CoreServices.OutboundHTTP. Bump 2.0.5 -> 2.0.6. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
f597fa8b52
commit
0d56f6adf1
10
client.go
10
client.go
@ -38,10 +38,14 @@ type CalcomClient struct {
|
||||
baseURL string
|
||||
}
|
||||
|
||||
// NewCalcomClient builds a client with the standard 30s HTTP timeout.
|
||||
func NewCalcomClient(apiKey string) *CalcomClient {
|
||||
// NewCalcomClient builds a client with the standard 30s HTTP timeout. rt is the
|
||||
// host-mediated egress transport (deps.OutboundHTTP, ADR 0023) — under wasm the
|
||||
// guest has no network, so every Cal.com request must ride it. A nil rt falls
|
||||
// back to http.DefaultTransport, keeping native/DESCRIBE and httptest paths
|
||||
// working (tests reach their fake server by overriding calcomBaseURL).
|
||||
func NewCalcomClient(apiKey string, rt http.RoundTripper) *CalcomClient {
|
||||
return &CalcomClient{
|
||||
http: &http.Client{Timeout: 30 * time.Second},
|
||||
http: &http.Client{Transport: rt, Timeout: 30 * time.Second},
|
||||
apiKey: apiKey,
|
||||
baseURL: calcomBaseURL,
|
||||
}
|
||||
|
||||
5
go.mod
5
go.mod
@ -3,7 +3,7 @@ module git.dev.alexdunmow.com/block/calcomblock
|
||||
go 1.26.4
|
||||
|
||||
require (
|
||||
git.dev.alexdunmow.com/block/pluginsdk v0.2.4
|
||||
git.dev.alexdunmow.com/block/pluginsdk v0.2.5
|
||||
github.com/a-h/templ v0.3.1020
|
||||
github.com/go-chi/chi/v5 v5.3.0
|
||||
github.com/google/uuid v1.6.0
|
||||
@ -17,6 +17,7 @@ require (
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
golang.org/x/mod v0.37.0 // indirect
|
||||
golang.org/x/text v0.36.0 // indirect
|
||||
golang.org/x/net v0.56.0 // indirect
|
||||
golang.org/x/text v0.38.0 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
)
|
||||
|
||||
20
go.sum
20
go.sum
@ -1,9 +1,7 @@
|
||||
connectrpc.com/connect v1.20.0 h1:6TNDAB+WeNd2uolWNlYczB5E0KNNaVMNUEx8JEUsPmQ=
|
||||
connectrpc.com/connect v1.20.0/go.mod h1:A2ygJrukXwWy32vkCAAHNVguZrqZ+jeZ9rGRnGR4dN4=
|
||||
git.dev.alexdunmow.com/block/pluginsdk v0.2.2 h1:jXq6ZAIVmsKtjAGdpYQvey9Uwl+poero/V2X1MHH80c=
|
||||
git.dev.alexdunmow.com/block/pluginsdk v0.2.2/go.mod h1:uz6oRurbuHdTcUxg47ymVgBqRARKlpXp1JIziXevzms=
|
||||
git.dev.alexdunmow.com/block/pluginsdk v0.2.4 h1:JC+VDHOE/SzXzR3znwAJyrN8c9gKhB91cfxUQBX+jAs=
|
||||
git.dev.alexdunmow.com/block/pluginsdk v0.2.4/go.mod h1:uz6oRurbuHdTcUxg47ymVgBqRARKlpXp1JIziXevzms=
|
||||
git.dev.alexdunmow.com/block/pluginsdk v0.2.5 h1:01d+5stAywSENScafnNKyMc2ZM5GGFt+hCBKuKTFJW4=
|
||||
git.dev.alexdunmow.com/block/pluginsdk v0.2.5/go.mod h1:Z+eG+WZxAP0jfreLqlGcc0kkWKt8RWevzWyWn8d+dhM=
|
||||
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
|
||||
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/a-h/templ v0.3.1020 h1:ypAT/L5ySWEnZ6Zft/5yfoWXYYkhFNvEFOeeqecg4tw=
|
||||
@ -38,12 +36,14 @@ github.com/tetratelabs/wazero v1.12.0 h1:DuWcpNu/FzgEXgGBDp8J1Spc+CWOvvtvVyjKlaZ
|
||||
github.com/tetratelabs/wazero v1.12.0/go.mod h1:LvKtzl2RqO4gyF27BiXU+nKAjcV8f38U+kP/q2vgxh0=
|
||||
golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
|
||||
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
|
||||
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
|
||||
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
|
||||
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
|
||||
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
|
||||
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
|
||||
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
|
||||
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
|
||||
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
||||
30
handler.go
30
handler.go
@ -251,6 +251,10 @@ type CalcomHandler struct {
|
||||
// trusting the POST body. nil in editor-preview / direct-handler unit tests,
|
||||
// where blockCaptchaEnabled reports false.
|
||||
blockConfig blockContentResolver
|
||||
// rt is the host-mediated egress transport (deps.OutboundHTTP, ADR 0023)
|
||||
// every Cal.com client is built with. nil in native/DESCRIBE/test builds,
|
||||
// where NewCalcomClient falls back to the default transport.
|
||||
rt http.RoundTripper
|
||||
}
|
||||
|
||||
// NewCalcomHandler creates a new Cal.com handler. appURL is the externally
|
||||
@ -282,6 +286,10 @@ func NewCalcomRouter(deps plugin.CoreServices) http.Handler {
|
||||
limiter := NewRateLimiter(bookingsPerHourPerIP)
|
||||
helpers.StartCleanupLoop(context.Background(), 15*time.Minute, limiter.SweepStale)
|
||||
h := NewCalcomHandler(settings, limiter, deps.AppURL)
|
||||
// Route every Cal.com call through the host-mediated egress transport. nil
|
||||
// under native/DESCRIBE builds (deps has no host); the client then uses the
|
||||
// default transport, which only the non-wasm paths can reach.
|
||||
h.rt = deps.OutboundHTTP
|
||||
// blockConfig resolves the server-authoritative captcha requirement from
|
||||
// published block content via the content.published_block_configs
|
||||
// capability (pluginsdk >= v0.2.4) — the sandboxed plugin role cannot read
|
||||
@ -375,8 +383,8 @@ func resetEventTZCache() {
|
||||
// type's pinned availability schedule when one is set, else the account's
|
||||
// default schedule. Returns "" when any hop fails — callers then fall down
|
||||
// the site-setting ladder rather than guessing.
|
||||
func fetchEventTimezone(ctx context.Context, apiKey, username, eventType string) string {
|
||||
client := NewCalcomClient(apiKey)
|
||||
func fetchEventTimezone(ctx context.Context, rt http.RoundTripper, apiKey, username, eventType string) string {
|
||||
client := NewCalcomClient(apiKey, rt)
|
||||
schedules, err := client.ListSchedules(ctx)
|
||||
if err != nil || len(schedules) == 0 {
|
||||
log.Printf("calcom: list schedules for %s/%s: %v", username, eventType, err)
|
||||
@ -411,7 +419,7 @@ func fetchEventTimezone(ctx context.Context, apiKey, username, eventType string)
|
||||
func (h *CalcomHandler) eventLocation(ctx context.Context, apiKey, username, eventType string) (*time.Location, string) {
|
||||
tz := peekEventTZ(username, eventType)
|
||||
if tz == "" && apiKey != "" && username != "" && eventType != "" {
|
||||
if tz = fetchEventTimezone(ctx, apiKey, username, eventType); tz != "" {
|
||||
if tz = fetchEventTimezone(ctx, h.rt, apiKey, username, eventType); tz != "" {
|
||||
storeEventTZ(username, eventType, tz)
|
||||
}
|
||||
}
|
||||
@ -458,7 +466,7 @@ func (h *CalcomHandler) HandleGetSlots(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
dayEnd := dayStart.AddDate(0, 0, 1)
|
||||
|
||||
client := NewCalcomClient(apiKey)
|
||||
client := NewCalcomClient(apiKey, h.rt)
|
||||
slotsResp, err := client.GetSlots(
|
||||
r.Context(),
|
||||
username,
|
||||
@ -554,7 +562,7 @@ func (h *CalcomHandler) HandleGetForm(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
var bookingFields []BookingField
|
||||
if apiKeyErr == nil && apiKey != "" && username != "" && eventType != "" {
|
||||
client := NewCalcomClient(apiKey)
|
||||
client := NewCalcomClient(apiKey, h.rt)
|
||||
et, etErr := client.GetEventType(r.Context(), username, eventType)
|
||||
if etErr != nil {
|
||||
// Fall back to default fields so a transient Cal.com error doesn't
|
||||
@ -719,7 +727,7 @@ func (h *CalcomHandler) HandleCreateBooking(w http.ResponseWriter, r *http.Reque
|
||||
}
|
||||
payload := routeBookingForm(r.Form, region)
|
||||
|
||||
client := NewCalcomClient(apiKey)
|
||||
client := NewCalcomClient(apiKey, h.rt)
|
||||
br := BookingRequest{
|
||||
EventTypeSlug: eventType,
|
||||
Username: username,
|
||||
@ -832,7 +840,7 @@ func (h *CalcomHandler) HandleCancelBooking(w http.ResponseWriter, r *http.Reque
|
||||
return
|
||||
}
|
||||
|
||||
client := NewCalcomClient(apiKey)
|
||||
client := NewCalcomClient(apiKey, h.rt)
|
||||
if err := client.CancelBooking(r.Context(), uid, "Cancelled by attendee"); err != nil {
|
||||
// Full technical cause stays server-side; the visitor sees curated copy.
|
||||
log.Printf("calcom: cancel booking %q: %v", uid, err)
|
||||
@ -948,7 +956,7 @@ func (h *CalcomHandler) HandleGetSettings(w http.ResponseWriter, r *http.Request
|
||||
// have no cached username. Fire /me once on the first settings load and
|
||||
// persist the result so subsequent loads are cache-hits.
|
||||
if username == "" && apiKey != "" {
|
||||
client := NewCalcomClient(apiKey)
|
||||
client := NewCalcomClient(apiKey, h.rt)
|
||||
if me, err := client.GetMe(ctx); err != nil {
|
||||
log.Printf("calcom: backfill /me in HandleGetSettings: %v", err)
|
||||
} else {
|
||||
@ -1010,7 +1018,7 @@ func (h *CalcomHandler) HandleSaveSettings(w http.ResponseWriter, r *http.Reques
|
||||
// ask the admin to type their own username. Best-effort: if /me is
|
||||
// down or rate-limited, the save still succeeds and the admin can
|
||||
// refresh later via Test Connection.
|
||||
client := NewCalcomClient(req.APIKey)
|
||||
client := NewCalcomClient(req.APIKey, h.rt)
|
||||
if me, err := client.GetMe(ctx); err != nil {
|
||||
log.Printf("calcom: /me lookup after save: %v", err)
|
||||
} else if err := h.settings.SetUsername(ctx, me.Username); err != nil {
|
||||
@ -1074,7 +1082,7 @@ func (h *CalcomHandler) HandleListEventTypes(w http.ResponseWriter, r *http.Requ
|
||||
return
|
||||
}
|
||||
username := r.URL.Query().Get("username")
|
||||
client := NewCalcomClient(apiKey)
|
||||
client := NewCalcomClient(apiKey, h.rt)
|
||||
ets, err := client.ListEventTypes(r.Context(), username)
|
||||
if err != nil {
|
||||
log.Printf("calcom: list event types: %v", err)
|
||||
@ -1097,7 +1105,7 @@ func (h *CalcomHandler) HandleTestConnection(w http.ResponseWriter, r *http.Requ
|
||||
return
|
||||
}
|
||||
|
||||
client := NewCalcomClient(apiKey)
|
||||
client := NewCalcomClient(apiKey, h.rt)
|
||||
ets, err := client.ListEventTypes(r.Context(), "")
|
||||
if err != nil {
|
||||
log.Printf("calcom: test connection: %v", err)
|
||||
|
||||
@ -2,8 +2,9 @@
|
||||
name = "calcomblock"
|
||||
display_name = "Cal.com Booking"
|
||||
scope = "@ninja"
|
||||
version = "2.0.5"
|
||||
version = "2.0.6"
|
||||
description = "Embeddable Cal.com booking calendar block with custom styling, timezone-aware slot windowing, honeypot + captcha + rate-limited public booking endpoints, and webhook receiver."
|
||||
kind = "plugin"
|
||||
categories = ["forms"]
|
||||
tags = ["calcom", "booking", "calendar", "scheduling", "appointments"]
|
||||
allowed_hosts = ["api.cal.com"]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user