Compare commits

...

10 Commits

Author SHA1 Message Date
0d56f6adf1 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>
2026-07-08 08:57:10 +08:00
f597fa8b52 chore(web): rebuild dist bundle for v2.0.5 captchaEnabled editor
The v2.0.5 captchaEnabled editor toggle (fe5d117) changed web/src but never
rebuilt the committed web/dist. Rebuild so the shipped bundle matches source.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 08:57:02 +08:00
fe5d11754a feat(editor): expose the captchaEnabled toggle — v2.0.5
The schema declares captchaEnabled (x-editor checkbox) but the custom
MF editor replaces schema-driven rendering and never drew a control for
it, so the captcha gate could not be enabled through the visual editor
at all. Add the switch beside Show Timezone. pnpm build-script approval
for esbuild (package.json onlyBuiltDependencies) so the editor bundle
rebuilds non-interactively.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 00:15:30 +08:00
3967fa955a fix: rebuild admin claims from trusted headers — v2.0.4
Context does not cross the wasm ABI, so requireAdmin's
auth.GetUserFromContext only sees claims if the guest rebuilds them from
the host-stamped X-Bn-Verified-* headers. 2.0.3 never mounted
auth.TrustedHeaderMiddleware, so every admin endpoint (settings save,
event-types, test, rotate) returned 401 for real admins — the settings
panel could not store the Cal.com API key. Mount the middleware on the
router and pin the contract with a header-driven test (viewer headers
must reach requireAdmin and 403, never 401): the existing admin tests
injected claims straight into context, which is exactly the path wasm
does not have.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 00:03:28 +08:00
7c7bceb9d7 fix!: absolute routes + live captcha requirement — v2.0.3
Two wasm-era regressions fixed:

Routes: the wasm host forwards the FULL request path with no prefix
strip, but every route was registered relative — the plugin's entire
HTTP surface (slots/book/cancel/date-grid/admin) 404'd at runtime on
every instance since 2.0.0. All routes now register under
/api/plugins/calcomblock (testplugin's httpBase convention), pinned by
a route-matching test.

Captcha requirement: blockConfig was hardcoded nil (no capability
exposed published block content), so bookingRequiresCaptcha always
reported false — the widget never rendered and the gate never ran.
publishedBlockResolver now answers it via pluginsdk v0.2.4's
content.published_block_configs: any currently-published calcom:booking
block matching the posted username+eventTypeSlug with
captchaEnabled=true demands the host-stamped X-Bn-Verified-Captcha
header; an empty config username applies to any posted username.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 23:47:59 +08:00
c1f8fe8a2c chore: v2.0.2 — host-verified captcha via X-Bn-Verified-Captcha
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 23:18:09 +08:00
7e9265c6a7 feat!: captcha via host-stamped X-Bn-Verified-Captcha; drop block/core entirely
SetCaptchaServer was .so-era injection — nothing wasm-side ever called
it, so the verifier was permanently nil and captcha-enabled booking
blocks failed closed on every submission. A guest can't hold the host's
stateful captcha server; the host now verifies+consumes the cap-token
before dispatch and stamps the unforgeable X-Bn-Verified-Captcha
trusted header (pluginsdk v0.2.2 auth.CaptchaVerified). Fail-closed
semantics preserved: no stamp = reject.

Deletes the last block/core import (captcha) and the test-side PoW
solver; go.mod no longer requires core.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 23:00:10 +08:00
28b50128d9 chore: bump pluginsdk to v0.2.1
Picks up the render package move-in (BlockNote renderer now reads
embed-resolver/human-proof context keys from pluginsdk/blocks) and
keeps the check-safety 2c fleet version anchor satisfied.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 20:34:04 +08:00
d5b158e0bf chore: pluginsdk v0.2.0 (P3 host-composed chrome)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 14:56:28 +08:00
324a2e6fe0 chore: migrate from block/core to block/pluginsdk (keep core for captcha)
Rewrite imports for plugin, blocks, templates, auth, settings, crypto, and
rbac packages from git.dev.alexdunmow.com/block/core/* to
git.dev.alexdunmow.com/block/pluginsdk/* across all Go files. The captcha
package stays on block/core, so the repo keeps both requires.

go.mod: add pluginsdk v0.1.0; bump core v0.20.2 -> v0.20.3 (satisfies the
check-safety core-version gate); transitive churn pgx v5.9.2 -> v5.10.0 and
x/mod v0.34.0 -> v0.37.0 from go mod tidy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 11:03:57 +08:00
40 changed files with 132008 additions and 143009 deletions

View File

@ -5,8 +5,8 @@ import (
"context"
"time"
"git.dev.alexdunmow.com/block/core/blocks"
"git.dev.alexdunmow.com/block/core/settings"
"git.dev.alexdunmow.com/block/pluginsdk/blocks"
"git.dev.alexdunmow.com/block/pluginsdk/settings"
)
// renderSettings is the SettingsManager available to CalcomBookingFunc, which

64
block_resolver.go Normal file
View File

@ -0,0 +1,64 @@
package main
import (
"context"
"strings"
)
// httpBase is the prefix the host mounts the plugin HTTP handler at; the wasm
// host forwards full paths, so every route registers under it.
const httpBase = "/api/plugins/calcomblock"
// publishedConfigsSource is the slice of content.Content the resolver needs
// (satisfied by deps.Content).
type publishedConfigsSource interface {
PublishedBlockConfigs(ctx context.Context, blockKey string) ([]map[string]any, error)
}
// publishedBlockResolver answers CalcomBookingRequiresCaptcha from published
// block content served by the host (content.published_block_configs) — the
// server-authoritative source, never the POST body/blockId. A config with an
// empty username applies to any posted username (it falls back to the
// plugin's default account), erring toward requiring captcha.
type publishedBlockResolver struct {
source publishedConfigsSource
}
func (r publishedBlockResolver) CalcomBookingRequiresCaptcha(ctx context.Context, username, eventTypeSlug string) (bool, error) {
configs, err := r.source.PublishedBlockConfigs(ctx, "calcom:booking")
if err != nil {
return false, err
}
for _, cfg := range configs {
if !configBool(cfg["captchaEnabled"]) {
continue
}
if !configFieldMatches(cfg["eventTypeSlug"], eventTypeSlug, false) {
continue
}
if configFieldMatches(cfg["username"], username, true) {
return true, nil
}
}
return false, nil
}
func configBool(v any) bool {
switch b := v.(type) {
case bool:
return b
case string:
return strings.EqualFold(strings.TrimSpace(b), "true")
default:
return false
}
}
func configFieldMatches(cfgValue any, posted string, emptyMatchesAny bool) bool {
s, _ := cfgValue.(string)
s = strings.TrimSpace(s)
if s == "" {
return emptyMatchesAny
}
return strings.EqualFold(s, strings.TrimSpace(posted))
}

69
block_resolver_test.go Normal file
View File

@ -0,0 +1,69 @@
package main
import (
"context"
"errors"
"testing"
)
type fakeConfigsSource struct {
configs []map[string]any
err error
gotKey string
}
func (f *fakeConfigsSource) PublishedBlockConfigs(_ context.Context, blockKey string) ([]map[string]any, error) {
f.gotKey = blockKey
return f.configs, f.err
}
func TestPublishedBlockResolverRequiresCaptchaOnMatch(t *testing.T) {
src := &fakeConfigsSource{configs: []map[string]any{
{"username": "other", "eventTypeSlug": "30min", "captchaEnabled": true},
{"username": "Alice", "eventTypeSlug": "30min", "captchaEnabled": true},
}}
r := publishedBlockResolver{source: src}
got, err := r.CalcomBookingRequiresCaptcha(context.Background(), "alice", "30min")
if err != nil || !got {
t.Fatalf("RequiresCaptcha = %v, %v; want true (case-insensitive username match)", got, err)
}
if src.gotKey != "calcom:booking" {
t.Fatalf("queried block key %q, want calcom:booking", src.gotKey)
}
}
func TestPublishedBlockResolverNoRequirementWhenDisabledOrUnmatched(t *testing.T) {
src := &fakeConfigsSource{configs: []map[string]any{
{"username": "alice", "eventTypeSlug": "30min", "captchaEnabled": false},
{"username": "alice", "eventTypeSlug": "60min", "captchaEnabled": true},
}}
r := publishedBlockResolver{source: src}
if got, _ := r.CalcomBookingRequiresCaptcha(context.Background(), "alice", "30min"); got {
t.Fatal("RequiresCaptcha = true; want false (disabled for 30min, 60min doesn't match)")
}
}
func TestPublishedBlockResolverEmptyConfigUsernameMatchesAny(t *testing.T) {
// A block may omit username (falls back to the plugin's default account);
// the requirement then applies to the event type for any posted username —
// erring toward requiring captcha, never stripping it.
src := &fakeConfigsSource{configs: []map[string]any{
{"eventTypeSlug": "30min", "captchaEnabled": true},
}}
r := publishedBlockResolver{source: src}
if got, _ := r.CalcomBookingRequiresCaptcha(context.Background(), "whoever", "30min"); !got {
t.Fatal("RequiresCaptcha = false; want true (config without username applies to any)")
}
}
func TestPublishedBlockResolverPropagatesError(t *testing.T) {
src := &fakeConfigsSource{err: errors.New("hostcall failed")}
r := publishedBlockResolver{source: src}
if _, err := r.CalcomBookingRequiresCaptcha(context.Background(), "alice", "30min"); err == nil {
t.Fatal("expected error to propagate (caller decides fail-open policy)")
}
}

View File

@ -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,
}

9
go.mod
View File

@ -3,11 +3,11 @@ module git.dev.alexdunmow.com/block/calcomblock
go 1.26.4
require (
git.dev.alexdunmow.com/block/core v0.20.2
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
github.com/jackc/pgx/v5 v5.9.2
github.com/jackc/pgx/v5 v5.10.0
github.com/nyaruka/phonenumbers v1.8.0
)
@ -16,7 +16,8 @@ require (
github.com/BurntSushi/toml v1.6.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
golang.org/x/mod v0.34.0 // indirect
golang.org/x/text v0.36.0 // indirect
golang.org/x/mod v0.37.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
)

26
go.sum
View File

@ -1,7 +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/core v0.20.2 h1:OVh5J7sYsjMDq6UBUpm/6FoUcfeuTr+YUPp0rQiYDuo=
git.dev.alexdunmow.com/block/core v0.20.2/go.mod h1:n/0y+8g/zabjwRNseNjme3w5J8thKqdjkIE9YJCzGow=
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=
@ -19,8 +19,8 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw=
github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
github.com/jackc/pgx/v5 v5.10.0 h1:VhSvgU2jSli8o3AqIEOTJr7rZwAEUVo4E4XhR94Zfr0=
github.com/jackc/pgx/v5 v5.10.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/nyaruka/phonenumbers v1.8.0 h1:TrXNJmbwcAHajzDqin3mLWw57vqLUA6ZjVdeNds0heQ=
@ -34,14 +34,16 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tetratelabs/wazero v1.12.0 h1:DuWcpNu/FzgEXgGBDp8J1Spc+CWOvvtvVyjKlaZopYU=
github.com/tetratelabs/wazero v1.12.0/go.mod h1:LvKtzl2RqO4gyF27BiXU+nKAjcV8f38U+kP/q2vgxh0=
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
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/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
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=

View File

@ -15,30 +15,14 @@ import (
"sync"
"time"
"git.dev.alexdunmow.com/block/core/auth"
"git.dev.alexdunmow.com/block/core/captcha"
"git.dev.alexdunmow.com/block/core/plugin"
"git.dev.alexdunmow.com/block/core/rbac"
"git.dev.alexdunmow.com/block/pluginsdk/auth"
"git.dev.alexdunmow.com/block/pluginsdk/plugin"
"git.dev.alexdunmow.com/block/pluginsdk/rbac"
"git.dev.alexdunmow.com/block/calcomblock/internal/helpers"
"github.com/go-chi/chi/v5"
"github.com/nyaruka/phonenumbers"
)
// captchaVerifier is the shared Cap (trycap.dev) proof-of-work verifier, built
// once from the per-instance secret and injected at server startup via
// SetCaptchaServer — mirroring SetNotifyFn. It is nil when the instance captcha
// secret failed to load: an enabled booking block then FAILS CLOSED (rejects
// rather than allowing an unverified booking through). Read directly by
// HandleCreateBooking; the mailing-list / block-form handlers hold the same
// server on their struct, but the plugin HTTP mount is generic so a package
// seam is the established injection point here.
var captchaVerifier *captcha.Server
// SetCaptchaServer wires the CMS's shared captcha server into the Cal.com
// booking handler. Called once at server startup from server.go. Safe to pass
// nil (captcha then unavailable → enabled blocks fail closed).
func SetCaptchaServer(s *captcha.Server) { captchaVerifier = s }
// blockContentResolver answers the server-authoritative question "does a
// booking for this Cal.com username + event-type slug require a captcha token?"
// by scanning currently-published block content — NEVER a client-supplied
@ -267,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
@ -281,6 +269,11 @@ func NewCalcomHandler(settings *SettingsManager, limiter *RateLimiter, appURL st
// and encrypt the Cal.com API key + webhook secret.
func NewCalcomRouter(deps plugin.CoreServices) http.Handler {
r := chi.NewRouter()
// Context does not cross the wasm ABI: requireAdmin's claims exist only if
// the guest rebuilds them from the host-stamped X-Bn-Verified-* headers.
// Without this middleware every admin endpoint 401s for real admins (the
// settings panel could not save the API key — 2.0.3 regression).
r.Use(auth.TrustedHeaderMiddleware)
// Settings persistence goes through the SDK settings capabilities: under the
// wasm sandbox the plugin's Postgres role cannot touch the CMS `settings`
// table (public schema) directly, so the host performs the DB work.
@ -293,38 +286,48 @@ 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)
// blockConfig scans published block content for the server-authoritative
// captcha requirement (CalcomBookingRequiresCaptcha over page_block_snapshots
// in the public schema). No wasm-ABI capability exposes that today, and the
// sandboxed plugin role cannot read it via SQL, so it is left nil:
// bookingRequiresCaptcha then reports false and the baseline honeypot +
// per-IP rate limit still apply. Restoring server-authoritative captcha
// enforcement needs a future core capability that exposes published-block
// content across the wasm ABI (tracked as a follow-up).
h.blockConfig = nil
// 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
// page_block_snapshots itself. deps.Content is nil in native/DESCRIBE
// builds; blockConfig then stays nil and bookingRequiresCaptcha reports
// false (honeypot + per-IP rate limit remain the baseline).
if deps.Content != nil {
h.blockConfig = publishedBlockResolver{source: deps.Content}
}
// Public booking endpoints — reachable by anonymous visitors.
r.Get("/slots", h.HandleGetSlots)
r.Get("/form", h.HandleGetForm)
r.Post("/book", h.HandleCreateBooking)
r.Post("/cancel", h.HandleCancelBooking)
r.Get("/reset", h.HandleReset)
r.Get("/date-grid", h.HandleGetDateGrid)
// Routes are registered ABSOLUTE: the wasm host forwards the full request
// path (no prefix strip), so relative registrations never match — the
// 2.0.02.0.2 regression that 404'd the plugin's entire HTTP surface.
// Mirrors testplugin's httpBase convention.
r.Route(httpBase, func(r chi.Router) {
// Public booking endpoints — reachable by anonymous visitors.
r.Get("/slots", h.HandleGetSlots)
r.Get("/form", h.HandleGetForm)
r.Post("/book", h.HandleCreateBooking)
r.Post("/cancel", h.HandleCancelBooking)
r.Get("/reset", h.HandleReset)
r.Get("/date-grid", h.HandleGetDateGrid)
// Webhook receiver — HMAC-verified inside the handler, so no admin gate.
wh := NewWebhookHandler(settings, slog.Default())
r.Post("/webhook", wh.Handle)
// Webhook receiver — HMAC-verified inside the handler, so no admin gate.
wh := NewWebhookHandler(settings, slog.Default())
r.Post("/webhook", wh.Handle)
// Admin endpoints — gated by requireAdmin. The plugin HTTP mount has no
// outer admin middleware, so this is the only line keeping the API key
// + webhook secret + Cal.com proxy out of attackers' hands.
r.Group(func(admin chi.Router) {
admin.Use(requireAdmin)
admin.Get("/settings", h.HandleGetSettings)
admin.Post("/settings", h.HandleSaveSettings)
admin.Post("/settings/rotate-webhook-secret", h.HandleRotateWebhookSecret)
admin.Post("/test", h.HandleTestConnection)
admin.Get("/event-types", h.HandleListEventTypes)
// Admin endpoints — gated by requireAdmin. The plugin HTTP mount has no
// outer admin middleware, so this is the only line keeping the API key
// + webhook secret + Cal.com proxy out of attackers' hands.
r.Group(func(admin chi.Router) {
admin.Use(requireAdmin)
admin.Get("/settings", h.HandleGetSettings)
admin.Post("/settings", h.HandleSaveSettings)
admin.Post("/settings/rotate-webhook-secret", h.HandleRotateWebhookSecret)
admin.Post("/test", h.HandleTestConnection)
admin.Get("/event-types", h.HandleListEventTypes)
})
})
return r
@ -380,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)
@ -416,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)
}
}
@ -463,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,
@ -559,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
@ -697,13 +700,16 @@ func (h *CalcomHandler) HandleCreateBooking(w http.ResponseWriter, r *http.Reque
// authoritative source is whether any currently-published calcom:booking
// block for this username+eventType has captchaEnabled=true — resolved from
// published content, never the POST body/blockId — so a bot cannot strip the
// requirement by omitting or forging blockId. When captcha IS required we
// fail CLOSED: a nil shared captcha server (instance secret failed at
// startup) rejects rather than allows. On failure we render the same error
// partial as other validation failures (not a 500), so the visitor can
// re-solve and retry.
// requirement by omitting or forging blockId. Verification is HOST-side: a
// guest cannot hold the host's stateful captcha server, so the host consumes
// the cap-token and stamps the unforgeable X-Bn-Verified-Captcha trusted
// header (pluginsdk/auth). When captcha IS required we fail CLOSED: no
// stamp (invalid token, or the instance captcha server failed at startup)
// rejects rather than allows. On failure we render the same error partial
// as other validation failures (not a 500), so the visitor can re-solve
// and retry.
if h.bookingRequiresCaptcha(r.Context(), username, eventType) {
if captchaVerifier == nil || !captchaVerifier.VerifyRequest(r) {
if !auth.CaptchaVerified(r.Header) {
h.renderError(w, "Captcha check failed, please try again.", blockID, false)
return
}
@ -721,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,
@ -834,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)
@ -950,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 {
@ -1012,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 {
@ -1076,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)
@ -1099,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)

View File

@ -2,22 +2,18 @@ package main
import (
"context"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"io"
"net/http"
"net/http/httptest"
"net/url"
"strconv"
"strings"
"sync/atomic"
"testing"
"github.com/google/uuid"
"git.dev.alexdunmow.com/block/core/captcha"
"git.dev.alexdunmow.com/block/pluginsdk/auth"
)
// fakeBlockResolver is an in-memory blockContentResolver so the booking handler
@ -46,7 +42,7 @@ func (f fakeBlockResolver) CalcomBookingRequiresCaptcha(_ context.Context, usern
// block for username "alice" / eventType "30min" carries the given
// captchaEnabled flag, plus a persisted API key so the booking flow reaches the
// Cal.com call. The requirement is keyed on username+eventType, NOT a blockId —
// so the booking form may post any blockId (or none) without affecting it.
// see fakeBlockResolver.
func newCalcomCaptchaHandler(t *testing.T, captchaEnabled bool) *CalcomHandler {
t.Helper()
h, _ := newTestHandler()
@ -59,12 +55,6 @@ func newCalcomCaptchaHandler(t *testing.T, captchaEnabled bool) *CalcomHandler {
return h
}
// newCalcomTestCaptchaServer builds a captcha.Server with a low PoW difficulty
// so the challenge solves quickly in tests.
func newCalcomTestCaptchaServer() *captcha.Server {
return captcha.New([]byte("calcom-captcha-test-secret"), captcha.WithChallenge(4, 8, 2))
}
// captchaBookingForm builds a booking POST body that passes every cheap
// validation (name+email present, valid email) so the only thing standing
// between the request and the Cal.com call is the captcha check. blockID is
@ -84,10 +74,18 @@ func captchaBookingForm(blockID string) url.Values {
return v
}
func postCaptchaBooking(h *CalcomHandler, form url.Values) *httptest.ResponseRecorder {
// postCaptchaBooking submits the form. captchaHeader is the raw value of the
// host-stamped X-Bn-Verified-Captcha trusted header ("" = host stamped
// nothing: no valid cap-token crossed the host, or the instance captcha
// server was unavailable). Guests never see raw tokens — the host verifies
// and consumes them before dispatch (pluginsdk/auth/trustedheaders.go).
func postCaptchaBooking(h *CalcomHandler, form url.Values, captchaHeader string) *httptest.ResponseRecorder {
req := httptest.NewRequest(http.MethodPost, "/book", strings.NewReader(form.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("X-Forwarded-For", "198.51.100.7") // TEST-NET-2, isolated bucket
if captchaHeader != "" {
req.Header.Set(auth.HeaderVerifiedCaptcha, captchaHeader)
}
rec := httptest.NewRecorder()
h.HandleCreateBooking(rec, req)
return rec
@ -108,60 +106,55 @@ func fakeCalcomServer(t *testing.T) (*httptest.Server, *atomic.Int32) {
return srv, &bookings
}
func TestCalcomBooking_CaptchaEnabled_RejectsWithoutToken(t *testing.T) {
func TestCalcomBooking_CaptchaEnabled_RejectsWithoutVerifiedHeader(t *testing.T) {
fake, bookings := fakeCalcomServer(t)
withCalcomBaseURL(t, fake.URL)
h := newCalcomCaptchaHandler(t, true)
SetCaptchaServer(newCalcomTestCaptchaServer())
t.Cleanup(func() { SetCaptchaServer(nil) })
rec := postCaptchaBooking(h, captchaBookingForm(uuid.NewString())) // no cap-token
rec := postCaptchaBooking(h, captchaBookingForm(uuid.NewString()), "") // host stamped nothing
if got := bookings.Load(); got != 0 {
t.Fatalf("Cal.com booking created %d times; want 0 (missing captcha token must be rejected)", got)
t.Fatalf("Cal.com booking created %d times; want 0 (missing verified-captcha header must be rejected)", got)
}
if !strings.Contains(rec.Body.String(), "aptcha") {
t.Fatalf("expected captcha error fragment, got: %s", rec.Body.String())
}
}
func TestCalcomBooking_CaptchaEnabled_RejectsInvalidToken(t *testing.T) {
func TestCalcomBooking_CaptchaEnabled_RejectsNonCanonicalHeaderValue(t *testing.T) {
fake, bookings := fakeCalcomServer(t)
withCalcomBaseURL(t, fake.URL)
h := newCalcomCaptchaHandler(t, true)
SetCaptchaServer(newCalcomTestCaptchaServer())
t.Cleanup(func() { SetCaptchaServer(nil) })
form := captchaBookingForm(uuid.NewString())
form.Set("cap-token", "not-a-real-token")
rec := postCaptchaBooking(h, form)
// Only the exact host-stamped value "1" counts; anything else reads as
// unverified (auth.CaptchaVerified).
rec := postCaptchaBooking(h, captchaBookingForm(uuid.NewString()), "true")
if got := bookings.Load(); got != 0 {
t.Fatalf("Cal.com booking created %d times; want 0 (invalid token rejected)", got)
t.Fatalf("Cal.com booking created %d times; want 0 (non-canonical header value rejected)", got)
}
if !strings.Contains(rec.Body.String(), "aptcha") {
t.Fatalf("expected captcha error fragment, got: %s", rec.Body.String())
}
}
func TestCalcomBooking_CaptchaEnabled_ProceedsWithValidToken(t *testing.T) {
func TestCalcomBooking_CaptchaEnabled_ProceedsWithVerifiedHeader(t *testing.T) {
fake, bookings := fakeCalcomServer(t)
withCalcomBaseURL(t, fake.URL)
srv := newCalcomTestCaptchaServer()
h := newCalcomCaptchaHandler(t, true)
SetCaptchaServer(srv)
t.Cleanup(func() { SetCaptchaServer(nil) })
token := mintCalcomCaptchaToken(t, srv)
// A real widget still posts the cap-token field; the HOST consumes it and
// stamps the header. The guest must trust the header and never forward the
// raw token to Cal.com.
form := captchaBookingForm(uuid.NewString())
form.Set("cap-token", token)
rec := postCaptchaBooking(h, form)
form.Set("cap-token", "host-already-consumed-this")
rec := postCaptchaBooking(h, form, "1")
if got := bookings.Load(); got != 1 {
t.Fatalf("Cal.com booking created %d times; want 1 (valid token must pass captcha)", got)
t.Fatalf("Cal.com booking created %d times; want 1 (host-verified captcha must pass)", got)
}
if rec.Code != http.StatusOK {
t.Fatalf("status = %d; want 200 (body=%s)", rec.Code, rec.Body.String())
@ -172,16 +165,13 @@ func TestCalcomBooking_CaptchaEnabled_ProceedsWithValidToken(t *testing.T) {
}
}
func TestCalcomBooking_CaptchaDisabled_TokenNotRequired(t *testing.T) {
func TestCalcomBooking_CaptchaDisabled_HeaderNotRequired(t *testing.T) {
fake, bookings := fakeCalcomServer(t)
withCalcomBaseURL(t, fake.URL)
h := newCalcomCaptchaHandler(t, false)
// A non-nil server would still reject if enforcement wrongly kicked in; nil
// proves the disabled path never touches the verifier.
SetCaptchaServer(nil)
rec := postCaptchaBooking(h, captchaBookingForm(uuid.NewString())) // no cap-token
rec := postCaptchaBooking(h, captchaBookingForm(uuid.NewString()), "") // no header
if got := bookings.Load(); got != 1 {
t.Fatalf("Cal.com booking created %d times; want 1 (captcha disabled)", got)
@ -194,21 +184,22 @@ func TestCalcomBooking_CaptchaDisabled_TokenNotRequired(t *testing.T) {
}
}
func TestCalcomBooking_CaptchaEnabled_FailsClosedWhenServerNil(t *testing.T) {
// TestCalcomBooking_CaptchaEnabled_FailsClosedWithoutHostStamp pins the
// fail-closed contract: a request that carries a cap-token but NO host stamp
// (instance captcha server down, or the token was invalid/replayed host-side)
// must be rejected — the guest never verifies tokens itself.
func TestCalcomBooking_CaptchaEnabled_FailsClosedWithoutHostStamp(t *testing.T) {
fake, bookings := fakeCalcomServer(t)
withCalcomBaseURL(t, fake.URL)
h := newCalcomCaptchaHandler(t, true)
// nil captcha server: the instance secret failed at startup. An enabled
// block must reject rather than allow the booking through unverified.
SetCaptchaServer(nil)
form := captchaBookingForm(uuid.NewString())
form.Set("cap-token", "anything")
rec := postCaptchaBooking(h, form)
rec := postCaptchaBooking(h, form, "")
if got := bookings.Load(); got != 0 {
t.Fatalf("Cal.com booking created %d times; want 0 (fail closed on nil server)", got)
t.Fatalf("Cal.com booking created %d times; want 0 (fail closed without host stamp)", got)
}
if !strings.Contains(rec.Body.String(), "aptcha") {
t.Fatalf("expected captcha error fragment, got: %s", rec.Body.String())
@ -232,11 +223,9 @@ func TestCalcomBooking_CaptchaBypass_BlockIdCannotStripRequirement(t *testing.T)
withCalcomBaseURL(t, fake.URL)
h := newCalcomCaptchaHandler(t, true) // published block requires captcha
SetCaptchaServer(newCalcomTestCaptchaServer())
t.Cleanup(func() { SetCaptchaServer(nil) })
// No valid cap-token; the requirement must not depend on blockId.
rec := postCaptchaBooking(h, captchaBookingForm(tc.blockID))
// No host stamp; the requirement must not depend on blockId.
rec := postCaptchaBooking(h, captchaBookingForm(tc.blockID), "")
if got := bookings.Load(); got != 0 {
t.Fatalf("Cal.com booking created %d times; want 0 (captcha requirement must not depend on blockId)", got)
@ -252,7 +241,7 @@ func TestCalcomBooking_CaptchaBypass_BlockIdCannotStripRequirement(t *testing.T)
// deliberate choice for the requirement lookup: a resolver (DB) error is logged
// and treated as "not required", so a transient DB blip cannot block every
// booking site-wide. The honeypot + per-IP rate limit remain the baseline
// protection. (This is distinct from the nil-captcha-server path, which fails
// protection. (This is distinct from the missing-host-stamp path, which fails
// CLOSED when a block IS known to require captcha.)
func TestCalcomBooking_CaptchaRequirement_ResolverErrorFailsOpen(t *testing.T) {
fake, bookings := fakeCalcomServer(t)
@ -263,10 +252,8 @@ func TestCalcomBooking_CaptchaRequirement_ResolverErrorFailsOpen(t *testing.T) {
t.Fatalf("SetAPIKey: %v", err)
}
h.blockConfig = fakeBlockResolver{err: errors.New("db unavailable")}
SetCaptchaServer(newCalcomTestCaptchaServer())
t.Cleanup(func() { SetCaptchaServer(nil) })
rec := postCaptchaBooking(h, captchaBookingForm("")) // no cap-token
rec := postCaptchaBooking(h, captchaBookingForm(""), "") // no host stamp
if got := bookings.Load(); got != 1 {
t.Fatalf("Cal.com booking created %d times; want 1 (resolver error → captcha not enforced; honeypot + rate limit still apply)", got)
}
@ -274,67 +261,3 @@ func TestCalcomBooking_CaptchaRequirement_ResolverErrorFailsOpen(t *testing.T) {
t.Fatalf("status = %d; want 200 (body=%s)", rec.Code, rec.Body.String())
}
}
// ---------------------------------------------------------------------------
// captcha PoW helpers — local copies of the (unexported) core/captcha PRNG so
// the test can solve a challenge and mint a real verification token. Mirrors
// the helpers in internal/handlers/captcha_challenge_test.go.
// ---------------------------------------------------------------------------
func mintCalcomCaptchaToken(t *testing.T, srv *captcha.Server) string {
t.Helper()
ch, err := srv.CreateChallenge()
if err != nil {
t.Fatalf("CreateChallenge: %v", err)
}
intSols := solveCalcomChallenge(ch.Token, ch.Challenge.C, ch.Challenge.S, ch.Challenge.D)
sols := make([]string, len(intSols))
for i, n := range intSols {
sols[i] = strconv.Itoa(n)
}
rd := srv.Redeem(ch.Token, sols)
if !rd.Success || rd.Token == "" {
t.Fatalf("Redeem failed: %+v", rd)
}
return rd.Token
}
func solveCalcomChallenge(token string, c, s, d int) []int {
sols := make([]int, c)
for i := 1; i <= c; i++ {
salt := calcomPRNG(token+strconv.Itoa(i), s)
target := calcomPRNG(token+strconv.Itoa(i)+"d", d)
for n := 0; ; n++ {
sum := sha256.Sum256([]byte(salt + strconv.Itoa(n)))
h := hex.EncodeToString(sum[:])
if len(h) >= len(target) && h[:len(target)] == target {
sols[i-1] = n
break
}
}
}
return sols
}
func calcomPRNG(seed string, length int) string {
const mask = 0xFFFFFFFF
state := calcomFNV1a(seed)
var b strings.Builder
for b.Len() < length {
state ^= (state << 13) & mask
state ^= state >> 17
state ^= (state << 5) & mask
state &= mask
fmt.Fprintf(&b, "%08x", state)
}
return b.String()[:length]
}
func calcomFNV1a(s string) uint32 {
var h uint32 = 2166136261
for _, ch := range s {
h ^= uint32(ch)
h = (h + (h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24)) & 0xFFFFFFFF
}
return h
}

View File

@ -15,7 +15,7 @@ import (
"testing"
"time"
"git.dev.alexdunmow.com/block/core/auth"
"git.dev.alexdunmow.com/block/pluginsdk/auth"
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
)

View File

@ -6,7 +6,7 @@
// init (hence wasmguest.Serve) before any ABI hook call. main is never called.
package main
import "git.dev.alexdunmow.com/block/core/plugin/wasmguest"
import "git.dev.alexdunmow.com/block/pluginsdk/plugin/wasmguest"
func init() { wasmguest.Serve(Registration) }

View File

@ -2,8 +2,9 @@
name = "calcomblock"
display_name = "Cal.com Booking"
scope = "@ninja"
version = "2.0.1"
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"]

View File

@ -5,9 +5,9 @@ import (
"io/fs"
"net/http"
"git.dev.alexdunmow.com/block/core/blocks"
"git.dev.alexdunmow.com/block/core/plugin"
"git.dev.alexdunmow.com/block/core/templates"
"git.dev.alexdunmow.com/block/pluginsdk/blocks"
"git.dev.alexdunmow.com/block/pluginsdk/plugin"
"git.dev.alexdunmow.com/block/pluginsdk/templates"
)
//go:embed all:web/dist/assets

View File

@ -4,9 +4,9 @@ import (
"io/fs"
"net/http"
"git.dev.alexdunmow.com/block/core/blocks"
"git.dev.alexdunmow.com/block/core/plugin"
"git.dev.alexdunmow.com/block/core/templates"
"git.dev.alexdunmow.com/block/pluginsdk/blocks"
"git.dev.alexdunmow.com/block/pluginsdk/plugin"
"git.dev.alexdunmow.com/block/pluginsdk/templates"
)
// Registration is the compile-time plugin registration for the Cal.com Booking block.

63
router_paths_test.go Normal file
View File

@ -0,0 +1,63 @@
package main
import (
"net/http"
"net/http/httptest"
"testing"
"git.dev.alexdunmow.com/block/pluginsdk/auth"
"git.dev.alexdunmow.com/block/pluginsdk/plugin"
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
)
// TestRouterMatchesAbsolutePluginPaths pins the wasm routing contract: the
// host forwards the FULL request path (/api/plugins/calcomblock/...) to the
// guest with no prefix strip, so every route must be registered absolute.
// Relative registrations 404 the plugin's entire HTTP surface at runtime —
// exactly the regression that shipped in 2.0.02.0.2.
func TestRouterMatchesAbsolutePluginPaths(t *testing.T) {
router, ok := NewCalcomRouter(plugin.CoreServices{}).(chi.Routes)
if !ok {
t.Fatal("NewCalcomRouter no longer returns a chi router")
}
for _, tc := range []struct{ method, path string }{
{http.MethodGet, "/api/plugins/calcomblock/reset"},
{http.MethodGet, "/api/plugins/calcomblock/slots"},
{http.MethodGet, "/api/plugins/calcomblock/date-grid"},
{http.MethodPost, "/api/plugins/calcomblock/book"},
{http.MethodPost, "/api/plugins/calcomblock/cancel"},
{http.MethodGet, "/api/plugins/calcomblock/event-types"},
} {
rctx := chi.NewRouteContext()
if !router.Match(rctx, tc.method, tc.path) {
t.Errorf("%s %s does not match any route — must be registered at the absolute plugin path", tc.method, tc.path)
}
}
}
// TestRouterRebuildsClaimsFromTrustedHeaders pins the wasm identity contract:
// context does not cross the ABI, so the ONLY way requireAdmin can see the
// caller is for the router to rebuild claims from the host-stamped
// X-Bn-Verified-* headers (auth.TrustedHeaderMiddleware). 2.0.3 shipped
// without it — every admin endpoint 401'd for real admins. A viewer-role
// header must reach requireAdmin and be rejected 403 (claims seen), never
// 401 (claims lost); no handler runs, so empty CoreServices is safe.
func TestRouterRebuildsClaimsFromTrustedHeaders(t *testing.T) {
router := NewCalcomRouter(plugin.CoreServices{})
req := httptest.NewRequest(http.MethodGet, "/api/plugins/calcomblock/settings", nil)
req.Header.Set(auth.HeaderVerifiedUserID, uuid.NewString())
req.Header.Set(auth.HeaderVerifiedEmail, "viewer@example.test")
req.Header.Set(auth.HeaderVerifiedRole, "viewer")
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
if rec.Code == http.StatusUnauthorized {
t.Fatalf("viewer trusted headers yielded 401 — TrustedHeaderMiddleware is not mounted, claims never reach requireAdmin")
}
if rec.Code != http.StatusForbidden {
t.Errorf("viewer trusted headers: expected 403 from requireAdmin, got %d (body %q)", rec.Code, rec.Body.String())
}
}

View File

@ -8,8 +8,8 @@ import (
"strings"
"time"
"git.dev.alexdunmow.com/block/core/crypto"
"git.dev.alexdunmow.com/block/core/settings"
"git.dev.alexdunmow.com/block/pluginsdk/crypto"
"git.dev.alexdunmow.com/block/pluginsdk/settings"
"git.dev.alexdunmow.com/block/calcomblock/internal/db"
"git.dev.alexdunmow.com/block/calcomblock/internal/helpers"

View File

@ -1,550 +0,0 @@
import { importShared } from "./__federation_fn_import-hlt2XzeI.js";
import { j as jsxRuntimeExports } from "./jsx-runtime-CvJTHeKY.js";
const { useCallback, useEffect, useState } = await importShared("react");
const {
Alert,
AlertDescription,
Button,
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
Tabs,
TabsContent,
TabsList,
TabsTrigger,
Input,
Label,
Textarea,
Switch,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
Settings,
Edit3,
CheckCircle2,
XCircle,
Loader2,
Key,
AlertCircle,
ExternalLink,
RefreshCw,
t,
} = await importShared("@block-ninja/ui");
function renderConnectedAccount(apiKeyConfigured, calcomUsername) {
if (apiKeyConfigured === false) {
return /* @__PURE__ */ jsxRuntimeExports.jsx("p", {
className: "text-xs text-muted-foreground",
children: t("calcom.editor.connectedAs.noKey", "Configure the API key in plugin settings to connect an account."),
});
}
if (calcomUsername) {
return /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-sm font-mono", children: calcomUsername });
}
return /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-muted-foreground", children: t("calcom.editor.connectedAs.resolving", "Resolving account from API key…") });
}
function CalcomBlockEditor({ content, onChange }) {
const [activeTab, setActiveTab] = useState("config");
const [apiKeyConfigured, setApiKeyConfigured] = useState(null);
const [calcomUsername, setCalcomUsername] = useState("");
const [eventTypes, setEventTypes] = useState([]);
const [loadingEventTypes, setLoadingEventTypes] = useState(false);
const [fetchFailed, setFetchFailed] = useState(false);
const [retryCounter, setRetryCounter] = useState(0);
const handleFieldChange = useCallback(
(field, value) => {
onChange({ ...content, [field]: value });
},
[content, onChange],
);
const getString = (field, defaultValue = "") => {
return content[field] || defaultValue;
};
const getNumber = (field, defaultValue = 0) => {
const val = content[field];
if (typeof val === "number") return val;
if (typeof val === "string") return parseInt(val, 10) || defaultValue;
return defaultValue;
};
const getBool = (field, defaultValue = false) => {
const val = content[field];
if (typeof val === "boolean") return val;
return defaultValue;
};
const eventTypeSlug = getString("eventTypeSlug");
useEffect(() => {
fetch(`/api/plugins/calcomblock/settings`)
.then((r) => r.json())
.then((d) => {
setApiKeyConfigured(!!d.api_key_configured);
setCalcomUsername(d.username ?? "");
})
.catch(() => setApiKeyConfigured(false));
}, []);
useEffect(() => {
if (calcomUsername && content.username !== calcomUsername) {
onChange({ ...content, username: calcomUsername });
}
}, [calcomUsername, content, onChange]);
useEffect(() => {
if (!apiKeyConfigured) {
setEventTypes([]);
setFetchFailed(false);
return;
}
const timer = setTimeout(() => {
setLoadingEventTypes(true);
setFetchFailed(false);
fetch(`/api/plugins/calcomblock/event-types?username=${encodeURIComponent(calcomUsername)}`)
.then((r) => r.json())
.then((d) => {
if (d.success) {
setEventTypes(d.event_types ?? []);
} else {
setEventTypes([]);
setFetchFailed(true);
}
})
.catch(() => {
setEventTypes([]);
setFetchFailed(true);
})
.finally(() => setLoadingEventTypes(false));
}, 300);
return () => clearTimeout(timer);
}, [apiKeyConfigured, calcomUsername, retryCounter]);
const handleRetry = useCallback(() => {
setRetryCounter((n) => n + 1);
}, []);
const selectPlaceholder = (() => {
if (apiKeyConfigured === false) {
return /* @__PURE__ */ jsxRuntimeExports.jsxs("span", {
className: "inline-flex items-center gap-1.5 text-muted-foreground",
children: [/* @__PURE__ */ jsxRuntimeExports.jsx(Key, { className: "h-3 w-3" }), t("calcom.editor.eventType.placeholder.noApiKey", "Configure API key first")],
});
}
if (loadingEventTypes) {
return /* @__PURE__ */ jsxRuntimeExports.jsxs("span", {
className: "inline-flex items-center gap-1.5 text-muted-foreground",
children: [/* @__PURE__ */ jsxRuntimeExports.jsx(Loader2, { className: "h-3 w-3 animate-spin" }), t("calcom.editor.eventType.placeholder.loading", "Loading…")],
});
}
if (!calcomUsername) {
return /* @__PURE__ */ jsxRuntimeExports.jsxs("span", {
className: "inline-flex items-center gap-1.5 text-muted-foreground",
children: [/* @__PURE__ */ jsxRuntimeExports.jsx(Loader2, { className: "h-3 w-3 animate-spin" }), t("calcom.editor.eventType.placeholder.resolvingUser", "Resolving account…")],
});
}
if (eventTypes.length === 0) {
return /* @__PURE__ */ jsxRuntimeExports.jsxs("span", {
className: "inline-flex items-center gap-1.5 text-muted-foreground",
children: [/* @__PURE__ */ jsxRuntimeExports.jsx(XCircle, { className: "h-3 w-3" }), t("calcom.editor.eventType.placeholder.noEventTypes", "No event types found")],
});
}
return t("calcom.editor.eventType.placeholder.select", "Select event type");
})();
const apiKeyReady = apiKeyConfigured === true;
const usernameReady = calcomUsername.length > 0;
const eventTypeReady = eventTypeSlug.length > 0;
const canOpenInCalcom = usernameReady && eventTypeReady;
const renderStatusRow = (ok, label, action) =>
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", {
className: "flex items-center gap-2 text-sm",
children: [
ok ?
/* @__PURE__ */ jsxRuntimeExports.jsx(CheckCircle2, { "className": "h-4 w-4 text-primary", "aria-hidden": "true" })
: /* @__PURE__ */ jsxRuntimeExports.jsx(XCircle, { "className": "h-4 w-4 text-destructive", "aria-hidden": "true" }),
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: ok ? "text-foreground" : "text-muted-foreground", children: label }),
action ? /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "ml-auto", children: action }) : null,
],
});
return /* @__PURE__ */ jsxRuntimeExports.jsx("div", {
className: "space-y-4",
children: /* @__PURE__ */ jsxRuntimeExports.jsxs(Tabs, {
value: activeTab,
onValueChange: setActiveTab,
children: [
/* @__PURE__ */ jsxRuntimeExports.jsxs(TabsList, {
className: "grid w-full grid-cols-2",
children: [
/* @__PURE__ */ jsxRuntimeExports.jsxs(TabsTrigger, {
value: "config",
className: "gap-2",
children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(Settings, { className: "h-4 w-4" }),
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: t("calcom.editor.tab.configuration", "Configuration") }),
],
}),
/* @__PURE__ */ jsxRuntimeExports.jsxs(TabsTrigger, {
value: "content",
className: "gap-2",
children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(Edit3, { className: "h-4 w-4" }),
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: t("calcom.editor.tab.content", "Content") }),
],
}),
],
}),
/* @__PURE__ */ jsxRuntimeExports.jsxs(TabsContent, {
value: "config",
className: "space-y-4 mt-4",
children: [
apiKeyConfigured === false &&
/* @__PURE__ */ jsxRuntimeExports.jsx(Alert, {
children: /* @__PURE__ */ jsxRuntimeExports.jsxs(AlertDescription, {
children: [
t("calcom.editor.alert.apiKeyMissing.prefix", "Configure your Cal.com API key in"),
" ",
/* @__PURE__ */ jsxRuntimeExports.jsx("a", {
href: "/admin/plugins?plugin=calcomblock",
className: "text-primary hover:underline",
children: t("calcom.editor.alert.apiKeyMissing.linkLabel", "plugin settings"),
}),
" ",
t("calcom.editor.alert.apiKeyMissing.suffix", "to enable the event-type dropdown."),
],
}),
}),
/* @__PURE__ */ jsxRuntimeExports.jsxs(Card, {
children: [
/* @__PURE__ */ jsxRuntimeExports.jsxs(CardHeader, {
children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(CardTitle, { className: "text-base", children: t("calcom.editor.section.config.title", "Cal.com Settings") }),
/* @__PURE__ */ jsxRuntimeExports.jsx(CardDescription, {
children: t("calcom.editor.section.config.description", "Connect to your Cal.com account and select an event type"),
}),
],
}),
/* @__PURE__ */ jsxRuntimeExports.jsxs(CardContent, {
className: "space-y-4",
children: [
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", {
className: "space-y-2",
children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(Label, { children: t("calcom.editor.connectedAs.label", "Connected Account") }),
renderConnectedAccount(apiKeyConfigured, calcomUsername),
],
}),
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", {
className: "space-y-2",
children: [
/* @__PURE__ */ jsxRuntimeExports.jsxs(Label, {
htmlFor: "eventTypeSlug",
children: [
t("calcom.editor.eventType.label", "Event Type"),
" ",
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-destructive", children: "*" }),
],
}),
fetchFailed ?
/* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, {
children: [
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", {
className: "flex items-center gap-2",
children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(Input, {
id: "eventTypeSlug",
value: eventTypeSlug,
onChange: (e) => handleFieldChange("eventTypeSlug", e.target.value),
placeholder: t("calcom.editor.eventType.fallbackPlaceholder", "30min"),
className: "flex-1",
}),
/* @__PURE__ */ jsxRuntimeExports.jsxs(Button, {
"type": "button",
"size": "sm",
"variant": "outline",
"action": "retry",
"entity": "event-types",
"onClick": handleRetry,
"disabled": loadingEventTypes,
"aria-label": t("calcom.editor.eventType.retryAria", "Retry loading event types"),
"children": [
/* @__PURE__ */ jsxRuntimeExports.jsx(RefreshCw, {
"className": `h-4 w-4 ${loadingEventTypes ? "animate-spin" : ""}`,
"aria-hidden": "true",
}),
/* @__PURE__ */ jsxRuntimeExports.jsx("span", {
className: "ml-1",
children: t("calcom.editor.eventType.retryLabel", "Retry"),
}),
],
}),
],
}),
/* @__PURE__ */ jsxRuntimeExports.jsxs(Alert, {
variant: "destructive",
children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(AlertCircle, { "className": "h-4 w-4", "aria-hidden": "true" }),
/* @__PURE__ */ jsxRuntimeExports.jsx(AlertDescription, {
children: t("calcom.editor.eventType.fetchFailed", "Couldn't load event types from Cal.com. Enter the slug manually or retry."),
}),
],
}),
],
})
: /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, {
children: [
/* @__PURE__ */ jsxRuntimeExports.jsxs(Select, {
value: eventTypeSlug,
onValueChange: (v) => handleFieldChange("eventTypeSlug", v),
disabled: loadingEventTypes || !apiKeyConfigured || eventTypes.length === 0,
children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(SelectTrigger, {
"aria-label": t("calcom.editor.eventType.label", "Event Type"),
"children": /* @__PURE__ */ jsxRuntimeExports.jsx(SelectValue, { placeholder: selectPlaceholder }),
}),
/* @__PURE__ */ jsxRuntimeExports.jsx(SelectContent, {
children: eventTypes.map((et) =>
/* @__PURE__ */ jsxRuntimeExports.jsxs(
SelectItem,
{
value: et.slug,
children: [
et.title,
/* @__PURE__ */ jsxRuntimeExports.jsxs("span", {
className: "text-muted-foreground",
children: [" ", "(", et.slug, " · ", et.lengthInMinutes, "m)"],
}),
],
},
et.id,
),
),
}),
],
}),
/* @__PURE__ */ jsxRuntimeExports.jsx("p", {
className: "text-xs text-muted-foreground",
children: t("calcom.editor.eventType.help", "Fetched from your Cal.com account"),
}),
],
}),
],
}),
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", {
className: "space-y-2",
children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(Label, { htmlFor: "weeksToShow", children: t("calcom.editor.weeksToShow.label", "Weeks to Display") }),
/* @__PURE__ */ jsxRuntimeExports.jsxs(Select, {
value: String(getNumber("weeksToShow", 2)),
onValueChange: (v) => handleFieldChange("weeksToShow", parseInt(v, 10)),
children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(SelectTrigger, {
"aria-label": t("calcom.editor.weeksToShow.label", "Weeks to Display"),
"children": /* @__PURE__ */ jsxRuntimeExports.jsx(SelectValue, { placeholder: t("calcom.editor.weeksToShow.placeholder", "Select weeks") }),
}),
/* @__PURE__ */ jsxRuntimeExports.jsx(SelectContent, {
children: [1, 2, 3, 4, 5, 6, 7, 8].map((n) =>
/* @__PURE__ */ jsxRuntimeExports.jsxs(
SelectItem,
{
value: String(n),
children: [
n,
" ",
n > 1 ? t("calcom.editor.weeksToShow.unitPlural", "weeks") : t("calcom.editor.weeksToShow.unitSingular", "week"),
],
},
n,
),
),
}),
],
}),
/* @__PURE__ */ jsxRuntimeExports.jsx("p", {
className: "text-xs text-muted-foreground",
children: t("calcom.editor.weeksToShow.help", "How many weeks of dates to show in the calendar"),
}),
],
}),
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", {
className: "flex items-center justify-between rounded-lg border p-3",
children: [
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", {
className: "space-y-0.5",
children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(Label, { children: t("calcom.editor.showTimezone.label", "Show Timezone Selector") }),
/* @__PURE__ */ jsxRuntimeExports.jsx("p", {
className: "text-xs text-muted-foreground",
children: t("calcom.editor.showTimezone.help", "Show a label above the date grid indicating which timezone times are displayed in."),
}),
],
}),
/* @__PURE__ */ jsxRuntimeExports.jsx(Switch, {
"checked": getBool("showTimezone", true),
"onCheckedChange": (v) => handleFieldChange("showTimezone", v),
"aria-label": t("calcom.editor.showTimezone.label", "Show Timezone Selector"),
}),
],
}),
],
}),
],
}),
/* @__PURE__ */ jsxRuntimeExports.jsxs(Card, {
children: [
/* @__PURE__ */ jsxRuntimeExports.jsxs(CardHeader, {
children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(CardTitle, { className: "text-base", children: t("calcom.editor.status.title", "Configuration status") }),
/* @__PURE__ */ jsxRuntimeExports.jsx(CardDescription, {
children: t("calcom.editor.status.description", "Quick check of the values needed before this block renders bookings."),
}),
],
}),
/* @__PURE__ */ jsxRuntimeExports.jsxs(CardContent, {
className: "space-y-2",
children: [
renderStatusRow(
apiKeyReady,
t("calcom.editor.status.apiKey", "API key configured"),
!apiKeyReady ?
/* @__PURE__ */ jsxRuntimeExports.jsx("a", {
href: "/admin/plugins?plugin=calcomblock",
className: "text-xs text-primary hover:underline",
children: t("calcom.editor.status.apiKey.configureLink", "Configure"),
})
: null,
),
renderStatusRow(usernameReady, t("calcom.editor.status.username", "Account resolved")),
renderStatusRow(eventTypeReady, t("calcom.editor.status.eventType", "Event type selected")),
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", {
className: "flex items-center gap-2 text-sm",
children: [
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "inline-flex h-4 w-4 items-center justify-center text-muted-foreground", children: "·" }),
/* @__PURE__ */ jsxRuntimeExports.jsx("span", {
className: "text-muted-foreground",
children: t("calcom.editor.status.eventTypesAvailable", "{count} event types available").replace("{count}", String(eventTypes.length)),
}),
],
}),
/* @__PURE__ */ jsxRuntimeExports.jsx("div", {
className: "pt-2",
children: /* @__PURE__ */ jsxRuntimeExports.jsx(Button, {
"type": "button",
"size": "sm",
"variant": "outline",
"action": "open",
"entity": "calcom-page",
"asChild": canOpenInCalcom,
"disabled": !canOpenInCalcom,
"aria-label": t("calcom.editor.status.openExternalAria", "Open Cal.com booking page in a new tab"),
"children":
canOpenInCalcom ?
/* @__PURE__ */ jsxRuntimeExports.jsxs("a", {
href: `https://cal.com/${calcomUsername}/${eventTypeSlug}`,
target: "_blank",
rel: "noopener noreferrer",
children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(ExternalLink, { "className": "h-4 w-4", "aria-hidden": "true" }),
/* @__PURE__ */ jsxRuntimeExports.jsx("span", {
className: "ml-1",
children: t("calcom.editor.status.openExternal", "Open in Cal.com"),
}),
],
})
: /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, {
children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(ExternalLink, { "className": "h-4 w-4", "aria-hidden": "true" }),
/* @__PURE__ */ jsxRuntimeExports.jsx("span", {
className: "ml-1",
children: t("calcom.editor.status.openExternal", "Open in Cal.com"),
}),
],
}),
}),
}),
],
}),
],
}),
],
}),
/* @__PURE__ */ jsxRuntimeExports.jsx(TabsContent, {
value: "content",
className: "space-y-4 mt-4",
children: /* @__PURE__ */ jsxRuntimeExports.jsxs(Card, {
children: [
/* @__PURE__ */ jsxRuntimeExports.jsxs(CardHeader, {
children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(CardTitle, { className: "text-base", children: t("calcom.editor.section.content.title", "Widget Content") }),
/* @__PURE__ */ jsxRuntimeExports.jsx(CardDescription, {
children: t("calcom.editor.section.content.description", "Customize the text shown in the booking widget"),
}),
],
}),
/* @__PURE__ */ jsxRuntimeExports.jsxs(CardContent, {
className: "space-y-4",
children: [
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", {
className: "space-y-2",
children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(Label, { htmlFor: "title", children: t("calcom.editor.title.label", "Title") }),
/* @__PURE__ */ jsxRuntimeExports.jsx(Input, {
id: "title",
value: getString("title"),
onChange: (e) => handleFieldChange("title", e.target.value),
placeholder: t("calcom.editor.title.placeholder", "Book a Meeting"),
}),
/* @__PURE__ */ jsxRuntimeExports.jsx("p", {
className: "text-xs text-muted-foreground",
children: t("calcom.editor.title.help", "Main heading shown above the calendar"),
}),
],
}),
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", {
className: "space-y-2",
children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(Label, { htmlFor: "description", children: t("calcom.editor.description.label", "Description") }),
/* @__PURE__ */ jsxRuntimeExports.jsx(Textarea, {
id: "description",
value: getString("description"),
onChange: (e) => handleFieldChange("description", e.target.value),
placeholder: t("calcom.editor.description.placeholder", "Choose a convenient time for your consultation..."),
rows: 3,
}),
/* @__PURE__ */ jsxRuntimeExports.jsx("p", {
className: "text-xs text-muted-foreground",
children: t("calcom.editor.description.help", "Optional text shown below the title"),
}),
],
}),
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", {
className: "space-y-2",
children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(Label, {
htmlFor: "unavailableMessage",
children: t("calcom.editor.unavailableMessage.label", "Booking unavailable message"),
}),
/* @__PURE__ */ jsxRuntimeExports.jsx(Textarea, {
id: "unavailableMessage",
value: getString("unavailableMessage"),
onChange: (e) => handleFieldChange("unavailableMessage", e.target.value),
placeholder: t(
"calcom.editor.unavailableMessage.placeholder",
"Sorry — we couldn't complete your booking online. Please use our contact form and we'll get you booked in.",
),
rows: 3,
}),
/* @__PURE__ */ jsxRuntimeExports.jsx("p", {
className: "text-xs text-muted-foreground",
children: t("calcom.editor.unavailableMessage.help", "Shown if an online booking can't be completed. Leave blank to use the default."),
}),
],
}),
],
}),
],
}),
}),
],
}),
});
}
export { CalcomBlockEditor as default };

View File

@ -0,0 +1,351 @@
import { importShared } from './__federation_fn_import-hlt2XzeI.js';
import { j as jsxRuntimeExports } from './jsx-runtime-CvJTHeKY.js';
const {useCallback,useEffect,useState} = await importShared('react');
const {Alert,AlertDescription,Button,Card,CardContent,CardDescription,CardHeader,CardTitle,Tabs,TabsContent,TabsList,TabsTrigger,Input,Label,Textarea,Switch,Select,SelectContent,SelectItem,SelectTrigger,SelectValue,Settings,Edit3,CheckCircle2,XCircle,Loader2,Key,AlertCircle,ExternalLink,RefreshCw,t} = await importShared('@block-ninja/ui');
function renderConnectedAccount(apiKeyConfigured, calcomUsername) {
if (apiKeyConfigured === false) {
return /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-muted-foreground", children: t("calcom.editor.connectedAs.noKey", "Configure the API key in plugin settings to connect an account.") });
}
if (calcomUsername) {
return /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-sm font-mono", children: calcomUsername });
}
return /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-muted-foreground", children: t("calcom.editor.connectedAs.resolving", "Resolving account from API key…") });
}
function CalcomBlockEditor({ content, onChange }) {
const [activeTab, setActiveTab] = useState("config");
const [apiKeyConfigured, setApiKeyConfigured] = useState(null);
const [calcomUsername, setCalcomUsername] = useState("");
const [eventTypes, setEventTypes] = useState([]);
const [loadingEventTypes, setLoadingEventTypes] = useState(false);
const [fetchFailed, setFetchFailed] = useState(false);
const [retryCounter, setRetryCounter] = useState(0);
const handleFieldChange = useCallback(
(field, value) => {
onChange({ ...content, [field]: value });
},
[content, onChange]
);
const getString = (field, defaultValue = "") => {
return content[field] || defaultValue;
};
const getNumber = (field, defaultValue = 0) => {
const val = content[field];
if (typeof val === "number") return val;
if (typeof val === "string") return parseInt(val, 10) || defaultValue;
return defaultValue;
};
const getBool = (field, defaultValue = false) => {
const val = content[field];
if (typeof val === "boolean") return val;
return defaultValue;
};
const eventTypeSlug = getString("eventTypeSlug");
useEffect(() => {
fetch(`/api/plugins/calcomblock/settings`).then((r) => r.json()).then((d) => {
setApiKeyConfigured(!!d.api_key_configured);
setCalcomUsername(d.username ?? "");
}).catch(() => setApiKeyConfigured(false));
}, []);
useEffect(() => {
if (calcomUsername && content.username !== calcomUsername) {
onChange({ ...content, username: calcomUsername });
}
}, [calcomUsername, content, onChange]);
useEffect(() => {
if (!apiKeyConfigured) {
setEventTypes([]);
setFetchFailed(false);
return;
}
const timer = setTimeout(() => {
setLoadingEventTypes(true);
setFetchFailed(false);
fetch(`/api/plugins/calcomblock/event-types?username=${encodeURIComponent(calcomUsername)}`).then((r) => r.json()).then((d) => {
if (d.success) {
setEventTypes(d.event_types ?? []);
} else {
setEventTypes([]);
setFetchFailed(true);
}
}).catch(() => {
setEventTypes([]);
setFetchFailed(true);
}).finally(() => setLoadingEventTypes(false));
}, 300);
return () => clearTimeout(timer);
}, [apiKeyConfigured, calcomUsername, retryCounter]);
const handleRetry = useCallback(() => {
setRetryCounter((n) => n + 1);
}, []);
const selectPlaceholder = (() => {
if (apiKeyConfigured === false) {
return /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "inline-flex items-center gap-1.5 text-muted-foreground", children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(Key, { className: "h-3 w-3" }),
t("calcom.editor.eventType.placeholder.noApiKey", "Configure API key first")
] });
}
if (loadingEventTypes) {
return /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "inline-flex items-center gap-1.5 text-muted-foreground", children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(Loader2, { className: "h-3 w-3 animate-spin" }),
t("calcom.editor.eventType.placeholder.loading", "Loading…")
] });
}
if (!calcomUsername) {
return /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "inline-flex items-center gap-1.5 text-muted-foreground", children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(Loader2, { className: "h-3 w-3 animate-spin" }),
t("calcom.editor.eventType.placeholder.resolvingUser", "Resolving account…")
] });
}
if (eventTypes.length === 0) {
return /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "inline-flex items-center gap-1.5 text-muted-foreground", children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(XCircle, { className: "h-3 w-3" }),
t("calcom.editor.eventType.placeholder.noEventTypes", "No event types found")
] });
}
return t("calcom.editor.eventType.placeholder.select", "Select event type");
})();
const apiKeyReady = apiKeyConfigured === true;
const usernameReady = calcomUsername.length > 0;
const eventTypeReady = eventTypeSlug.length > 0;
const canOpenInCalcom = usernameReady && eventTypeReady;
const renderStatusRow = (ok, label, action) => /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 text-sm", children: [
ok ? /* @__PURE__ */ jsxRuntimeExports.jsx(CheckCircle2, { className: "h-4 w-4 text-primary", "aria-hidden": "true" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(XCircle, { className: "h-4 w-4 text-destructive", "aria-hidden": "true" }),
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: ok ? "text-foreground" : "text-muted-foreground", children: label }),
action ? /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "ml-auto", children: action }) : null
] });
return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "space-y-4", children: /* @__PURE__ */ jsxRuntimeExports.jsxs(Tabs, { value: activeTab, onValueChange: setActiveTab, children: [
/* @__PURE__ */ jsxRuntimeExports.jsxs(TabsList, { className: "grid w-full grid-cols-2", children: [
/* @__PURE__ */ jsxRuntimeExports.jsxs(TabsTrigger, { value: "config", className: "gap-2", children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(Settings, { className: "h-4 w-4" }),
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: t("calcom.editor.tab.configuration", "Configuration") })
] }),
/* @__PURE__ */ jsxRuntimeExports.jsxs(TabsTrigger, { value: "content", className: "gap-2", children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(Edit3, { className: "h-4 w-4" }),
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: t("calcom.editor.tab.content", "Content") })
] })
] }),
/* @__PURE__ */ jsxRuntimeExports.jsxs(TabsContent, { value: "config", className: "space-y-4 mt-4", children: [
apiKeyConfigured === false && /* @__PURE__ */ jsxRuntimeExports.jsx(Alert, { children: /* @__PURE__ */ jsxRuntimeExports.jsxs(AlertDescription, { children: [
t("calcom.editor.alert.apiKeyMissing.prefix", "Configure your Cal.com API key in"),
" ",
/* @__PURE__ */ jsxRuntimeExports.jsx("a", { href: "/admin/plugins?plugin=calcomblock", className: "text-primary hover:underline", children: t("calcom.editor.alert.apiKeyMissing.linkLabel", "plugin settings") }),
" ",
t("calcom.editor.alert.apiKeyMissing.suffix", "to enable the event-type dropdown.")
] }) }),
/* @__PURE__ */ jsxRuntimeExports.jsxs(Card, { children: [
/* @__PURE__ */ jsxRuntimeExports.jsxs(CardHeader, { children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(CardTitle, { className: "text-base", children: t("calcom.editor.section.config.title", "Cal.com Settings") }),
/* @__PURE__ */ jsxRuntimeExports.jsx(CardDescription, { children: t("calcom.editor.section.config.description", "Connect to your Cal.com account and select an event type") })
] }),
/* @__PURE__ */ jsxRuntimeExports.jsxs(CardContent, { className: "space-y-4", children: [
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-2", children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(Label, { children: t("calcom.editor.connectedAs.label", "Connected Account") }),
renderConnectedAccount(apiKeyConfigured, calcomUsername)
] }),
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-2", children: [
/* @__PURE__ */ jsxRuntimeExports.jsxs(Label, { htmlFor: "eventTypeSlug", children: [
t("calcom.editor.eventType.label", "Event Type"),
" ",
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-destructive", children: "*" })
] }),
fetchFailed ? /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(
Input,
{
id: "eventTypeSlug",
value: eventTypeSlug,
onChange: (e) => handleFieldChange("eventTypeSlug", e.target.value),
placeholder: t("calcom.editor.eventType.fallbackPlaceholder", "30min"),
className: "flex-1"
}
),
/* @__PURE__ */ jsxRuntimeExports.jsxs(
Button,
{
type: "button",
size: "sm",
variant: "outline",
action: "retry",
entity: "event-types",
onClick: handleRetry,
disabled: loadingEventTypes,
"aria-label": t("calcom.editor.eventType.retryAria", "Retry loading event types"),
children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(RefreshCw, { className: `h-4 w-4 ${loadingEventTypes ? "animate-spin" : ""}`, "aria-hidden": "true" }),
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "ml-1", children: t("calcom.editor.eventType.retryLabel", "Retry") })
]
}
)
] }),
/* @__PURE__ */ jsxRuntimeExports.jsxs(Alert, { variant: "destructive", children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(AlertCircle, { className: "h-4 w-4", "aria-hidden": "true" }),
/* @__PURE__ */ jsxRuntimeExports.jsx(AlertDescription, { children: t("calcom.editor.eventType.fetchFailed", "Couldn't load event types from Cal.com. Enter the slug manually or retry.") })
] })
] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
/* @__PURE__ */ jsxRuntimeExports.jsxs(
Select,
{
value: eventTypeSlug,
onValueChange: (v) => handleFieldChange("eventTypeSlug", v),
disabled: loadingEventTypes || !apiKeyConfigured || eventTypes.length === 0,
children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(SelectTrigger, { "aria-label": t("calcom.editor.eventType.label", "Event Type"), children: /* @__PURE__ */ jsxRuntimeExports.jsx(SelectValue, { placeholder: selectPlaceholder }) }),
/* @__PURE__ */ jsxRuntimeExports.jsx(SelectContent, { children: eventTypes.map((et) => /* @__PURE__ */ jsxRuntimeExports.jsxs(SelectItem, { value: et.slug, children: [
et.title,
/* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-muted-foreground", children: [
" ",
"(",
et.slug,
" · ",
et.lengthInMinutes,
"m)"
] })
] }, et.id)) })
]
}
),
/* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-muted-foreground", children: t("calcom.editor.eventType.help", "Fetched from your Cal.com account") })
] })
] }),
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-2", children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(Label, { htmlFor: "weeksToShow", children: t("calcom.editor.weeksToShow.label", "Weeks to Display") }),
/* @__PURE__ */ jsxRuntimeExports.jsxs(Select, { value: String(getNumber("weeksToShow", 2)), onValueChange: (v) => handleFieldChange("weeksToShow", parseInt(v, 10)), children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(SelectTrigger, { "aria-label": t("calcom.editor.weeksToShow.label", "Weeks to Display"), children: /* @__PURE__ */ jsxRuntimeExports.jsx(SelectValue, { placeholder: t("calcom.editor.weeksToShow.placeholder", "Select weeks") }) }),
/* @__PURE__ */ jsxRuntimeExports.jsx(SelectContent, { children: [1, 2, 3, 4, 5, 6, 7, 8].map((n) => /* @__PURE__ */ jsxRuntimeExports.jsxs(SelectItem, { value: String(n), children: [
n,
" ",
n > 1 ? t("calcom.editor.weeksToShow.unitPlural", "weeks") : t("calcom.editor.weeksToShow.unitSingular", "week")
] }, n)) })
] }),
/* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-muted-foreground", children: t("calcom.editor.weeksToShow.help", "How many weeks of dates to show in the calendar") })
] }),
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between rounded-lg border p-3", children: [
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-0.5", children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(Label, { children: t("calcom.editor.showTimezone.label", "Show Timezone Selector") }),
/* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-muted-foreground", children: t("calcom.editor.showTimezone.help", "Show a label above the date grid indicating which timezone times are displayed in.") })
] }),
/* @__PURE__ */ jsxRuntimeExports.jsx(
Switch,
{
checked: getBool("showTimezone", true),
onCheckedChange: (v) => handleFieldChange("showTimezone", v),
"aria-label": t("calcom.editor.showTimezone.label", "Show Timezone Selector")
}
)
] }),
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between rounded-lg border p-3", children: [
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-0.5", children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(Label, { children: t("calcom.editor.captchaEnabled.label", "Enable captcha") }),
/* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-muted-foreground", children: t("calcom.editor.captchaEnabled.help", "Require visitors to solve a privacy-friendly proof-of-work captcha before booking (spam protection).") })
] }),
/* @__PURE__ */ jsxRuntimeExports.jsx(
Switch,
{
checked: getBool("captchaEnabled", false),
onCheckedChange: (v) => handleFieldChange("captchaEnabled", v),
"aria-label": t("calcom.editor.captchaEnabled.label", "Enable captcha")
}
)
] })
] })
] }),
/* @__PURE__ */ jsxRuntimeExports.jsxs(Card, { children: [
/* @__PURE__ */ jsxRuntimeExports.jsxs(CardHeader, { children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(CardTitle, { className: "text-base", children: t("calcom.editor.status.title", "Configuration status") }),
/* @__PURE__ */ jsxRuntimeExports.jsx(CardDescription, { children: t("calcom.editor.status.description", "Quick check of the values needed before this block renders bookings.") })
] }),
/* @__PURE__ */ jsxRuntimeExports.jsxs(CardContent, { className: "space-y-2", children: [
renderStatusRow(
apiKeyReady,
t("calcom.editor.status.apiKey", "API key configured"),
!apiKeyReady ? /* @__PURE__ */ jsxRuntimeExports.jsx("a", { href: "/admin/plugins?plugin=calcomblock", className: "text-xs text-primary hover:underline", children: t("calcom.editor.status.apiKey.configureLink", "Configure") }) : null
),
renderStatusRow(usernameReady, t("calcom.editor.status.username", "Account resolved")),
renderStatusRow(eventTypeReady, t("calcom.editor.status.eventType", "Event type selected")),
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 text-sm", children: [
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "inline-flex h-4 w-4 items-center justify-center text-muted-foreground", children: "·" }),
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-muted-foreground", children: t("calcom.editor.status.eventTypesAvailable", "{count} event types available").replace("{count}", String(eventTypes.length)) })
] }),
/* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "pt-2", children: /* @__PURE__ */ jsxRuntimeExports.jsx(
Button,
{
type: "button",
size: "sm",
variant: "outline",
action: "open",
entity: "calcom-page",
asChild: canOpenInCalcom,
disabled: !canOpenInCalcom,
"aria-label": t("calcom.editor.status.openExternalAria", "Open Cal.com booking page in a new tab"),
children: canOpenInCalcom ? /* @__PURE__ */ jsxRuntimeExports.jsxs("a", { href: `https://cal.com/${calcomUsername}/${eventTypeSlug}`, target: "_blank", rel: "noopener noreferrer", children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(ExternalLink, { className: "h-4 w-4", "aria-hidden": "true" }),
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "ml-1", children: t("calcom.editor.status.openExternal", "Open in Cal.com") })
] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(ExternalLink, { className: "h-4 w-4", "aria-hidden": "true" }),
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "ml-1", children: t("calcom.editor.status.openExternal", "Open in Cal.com") })
] })
}
) })
] })
] })
] }),
/* @__PURE__ */ jsxRuntimeExports.jsx(TabsContent, { value: "content", className: "space-y-4 mt-4", children: /* @__PURE__ */ jsxRuntimeExports.jsxs(Card, { children: [
/* @__PURE__ */ jsxRuntimeExports.jsxs(CardHeader, { children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(CardTitle, { className: "text-base", children: t("calcom.editor.section.content.title", "Widget Content") }),
/* @__PURE__ */ jsxRuntimeExports.jsx(CardDescription, { children: t("calcom.editor.section.content.description", "Customize the text shown in the booking widget") })
] }),
/* @__PURE__ */ jsxRuntimeExports.jsxs(CardContent, { className: "space-y-4", children: [
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-2", children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(Label, { htmlFor: "title", children: t("calcom.editor.title.label", "Title") }),
/* @__PURE__ */ jsxRuntimeExports.jsx(
Input,
{
id: "title",
value: getString("title"),
onChange: (e) => handleFieldChange("title", e.target.value),
placeholder: t("calcom.editor.title.placeholder", "Book a Meeting")
}
),
/* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-muted-foreground", children: t("calcom.editor.title.help", "Main heading shown above the calendar") })
] }),
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-2", children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(Label, { htmlFor: "description", children: t("calcom.editor.description.label", "Description") }),
/* @__PURE__ */ jsxRuntimeExports.jsx(
Textarea,
{
id: "description",
value: getString("description"),
onChange: (e) => handleFieldChange("description", e.target.value),
placeholder: t("calcom.editor.description.placeholder", "Choose a convenient time for your consultation..."),
rows: 3
}
),
/* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-muted-foreground", children: t("calcom.editor.description.help", "Optional text shown below the title") })
] }),
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-2", children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(Label, { htmlFor: "unavailableMessage", children: t("calcom.editor.unavailableMessage.label", "Booking unavailable message") }),
/* @__PURE__ */ jsxRuntimeExports.jsx(
Textarea,
{
id: "unavailableMessage",
value: getString("unavailableMessage"),
onChange: (e) => handleFieldChange("unavailableMessage", e.target.value),
placeholder: t(
"calcom.editor.unavailableMessage.placeholder",
"Sorry — we couldn't complete your booking online. Please use our contact form and we'll get you booked in."
),
rows: 3
}
),
/* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-muted-foreground", children: t("calcom.editor.unavailableMessage.help", "Shown if an online booking can't be completed. Leave blank to use the default.") })
] })
] })
] }) })
] }) });
}
export { CalcomBlockEditor as default };

View File

@ -1,619 +1,384 @@
import { importShared } from "./__federation_fn_import-hlt2XzeI.js";
import { j as jsxRuntimeExports } from "./jsx-runtime-CvJTHeKY.js";
import { importShared } from './__federation_fn_import-hlt2XzeI.js';
import { j as jsxRuntimeExports } from './jsx-runtime-CvJTHeKY.js';
const { useState, useCallback, useEffect } = await importShared("react");
const {useState,useCallback,useEffect} = await importShared('react');
const {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
Input,
Label,
Button,
Alert,
AlertDescription,
Badge,
ScrollArea,
Loader2,
CheckCircle2,
XCircle,
AlertCircle,
Eye,
EyeOff,
Key,
Zap,
Settings,
Send,
Copy,
RefreshCw,
toast,
t,
useAlertDialog,
} = await importShared("@block-ninja/ui");
const {Card,CardContent,CardDescription,CardHeader,CardTitle,Input,Label,Button,Alert,AlertDescription,Badge,ScrollArea,Loader2,CheckCircle2,XCircle,AlertCircle,Eye,EyeOff,Key,Zap,Settings,Send,Copy,RefreshCw,toast,t,useAlertDialog} = await importShared('@block-ninja/ui');
const copyToClipboard = async (text) => {
if (!text) return;
try {
await navigator.clipboard.writeText(text);
toast.success(t("calcom.settings.copied", "Copied to clipboard"));
} catch (_error) {
toast.error(t("calcom.settings.copyFailed", "Could not copy to clipboard"));
}
if (!text) return;
try {
await navigator.clipboard.writeText(text);
toast.success(t("calcom.settings.copied", "Copied to clipboard"));
} catch (_error) {
toast.error(t("calcom.settings.copyFailed", "Could not copy to clipboard"));
}
};
const formatRelativeTime = (iso) => {
const parsed = Date.parse(iso);
if (Number.isNaN(parsed)) return iso;
const diffMs = Date.now() - parsed;
const minutes = Math.round(diffMs / 6e4);
if (minutes < 1) return t("calcom.settings.relative.justNow", "just now");
if (minutes < 60) {
return minutes === 1 ? t("calcom.settings.relative.minute", "1 minute ago") : t("calcom.settings.relative.minutes", "{count} minutes ago").replace("{count}", String(minutes));
}
const hours = Math.round(minutes / 60);
if (hours < 24) {
return hours === 1 ? t("calcom.settings.relative.hour", "1 hour ago") : t("calcom.settings.relative.hours", "{count} hours ago").replace("{count}", String(hours));
}
const days = Math.round(hours / 24);
return days === 1 ? t("calcom.settings.relative.day", "1 day ago") : t("calcom.settings.relative.days", "{count} days ago").replace("{count}", String(days));
const parsed = Date.parse(iso);
if (Number.isNaN(parsed)) return iso;
const diffMs = Date.now() - parsed;
const minutes = Math.round(diffMs / 6e4);
if (minutes < 1) return t("calcom.settings.relative.justNow", "just now");
if (minutes < 60) {
return minutes === 1 ? t("calcom.settings.relative.minute", "1 minute ago") : t("calcom.settings.relative.minutes", "{count} minutes ago").replace("{count}", String(minutes));
}
const hours = Math.round(minutes / 60);
if (hours < 24) {
return hours === 1 ? t("calcom.settings.relative.hour", "1 hour ago") : t("calcom.settings.relative.hours", "{count} hours ago").replace("{count}", String(hours));
}
const days = Math.round(hours / 24);
return days === 1 ? t("calcom.settings.relative.day", "1 day ago") : t("calcom.settings.relative.days", "{count} days ago").replace("{count}", String(days));
};
function CalcomSettings({ pluginName }) {
const { confirm } = useAlertDialog();
const [apiKey, setApiKey] = useState("");
const [showApiKey, setShowApiKey] = useState(false);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [testing, setTesting] = useState(false);
const [rotating, setRotating] = useState(false);
const [showSecret, setShowSecret] = useState(false);
const [settings, setSettings] = useState(null);
const [testResult, setTestResult] = useState(null);
const [saveMessage, setSaveMessage] = useState(null);
const baseUrl = `/api/plugins/${pluginName}`;
const loadSettings = useCallback(async () => {
try {
const res = await fetch(`${baseUrl}/settings`);
if (res.ok) {
const data = await res.json();
setSettings(data);
}
} catch (_error) {
console.error("Failed to load settings");
} finally {
setLoading(false);
}
}, [baseUrl]);
useEffect(() => {
loadSettings();
}, [loadSettings]);
const handleSave = async () => {
if (!apiKey.trim()) {
setSaveMessage({ type: "error", text: t("calcom.settings.apiKey.errorEmpty", "Please enter an API key") });
return;
}
setSaving(true);
setSaveMessage(null);
setTestResult(null);
try {
const res = await fetch(`${baseUrl}/settings`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ api_key: apiKey }),
});
if (res.ok) {
await loadSettings();
setApiKey("");
setSaveMessage({ type: "success", text: t("calcom.settings.apiKey.saved", "API key saved successfully") });
} else {
const text = await res.text();
setSaveMessage({ type: "error", text: text || t("calcom.settings.apiKey.saveFailed", "Failed to save API key") });
}
} catch (_error) {
setSaveMessage({ type: "error", text: t("calcom.settings.serverError", "Failed to connect to server") });
} finally {
setSaving(false);
}
};
const handleTest = async () => {
setTesting(true);
setTestResult(null);
try {
const res = await fetch(`${baseUrl}/test`, { method: "POST" });
const data = await res.json();
setTestResult(data);
} catch (_error) {
setTestResult({ success: false, error: t("calcom.settings.serverError", "Failed to connect to server") });
} finally {
setTesting(false);
}
};
const handleClear = async () => {
setSaving(true);
setSaveMessage(null);
try {
const res = await fetch(`${baseUrl}/settings`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ api_key: "" }),
});
if (res.ok) {
await loadSettings();
setTestResult(null);
setSaveMessage({ type: "success", text: t("calcom.settings.apiKey.removed", "API key removed") });
}
} catch (_error) {
setSaveMessage({ type: "error", text: t("calcom.settings.apiKey.clearFailed", "Failed to clear API key") });
} finally {
setSaving(false);
}
};
const handleRotate = async () => {
const confirmed = await confirm({
title: t("calcom.settings.webhook.rotateConfirmTitle", "Rotate webhook secret?"),
message: t("calcom.settings.webhook.rotateConfirmMessage", "This invalidates the existing secret immediately. You'll need to update Cal.com to keep webhooks flowing."),
confirmLabel: t("calcom.settings.webhook.rotateConfirmLabel", "Rotate"),
variant: "destructive",
});
if (!confirmed) return;
setRotating(true);
setSaveMessage(null);
try {
const res = await fetch(`${baseUrl}/settings/rotate-webhook-secret`, { method: "POST" });
if (res.ok) {
const data = await res.json();
setSettings((prev) => (prev ? { ...prev, webhook_secret_masked: data.webhook_secret_masked, webhook_secret_configured: true } : prev));
const successMsg = t("calcom.settings.webhook.rotated", "Webhook secret rotated. Update it in Cal.com.");
setSaveMessage({ type: "success", text: successMsg });
toast.success(successMsg);
} else {
setSaveMessage({ type: "error", text: t("calcom.settings.webhook.rotateFailed", "Failed to rotate webhook secret") });
}
} catch (_error) {
setSaveMessage({ type: "error", text: t("calcom.settings.serverError", "Failed to connect to server") });
} finally {
setRotating(false);
}
};
if (loading) {
return /* @__PURE__ */ jsxRuntimeExports.jsx("div", {
className: "flex items-center justify-center py-8",
children: /* @__PURE__ */ jsxRuntimeExports.jsx(Loader2, { className: "h-6 w-6 animate-spin text-muted-foreground" }),
});
}
const apiKeyVisibilityLabel = showApiKey ? t("calcom.settings.toggleVisibility.hide", "Hide") : t("calcom.settings.toggleVisibility.show", "Show");
const secretVisibilityLabel = showSecret ? t("calcom.settings.toggleVisibility.hide", "Hide") : t("calcom.settings.toggleVisibility.show", "Show");
return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", {
className: "space-y-6",
children: [
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", {
children: [
/* @__PURE__ */ jsxRuntimeExports.jsxs("h2", {
className: "text-xl font-semibold flex items-center gap-2",
children: [/* @__PURE__ */ jsxRuntimeExports.jsx(Settings, { className: "h-5 w-5" }), t("calcom.settings.heading", "Cal.com Integration")],
}),
/* @__PURE__ */ jsxRuntimeExports.jsx("p", {
className: "text-sm text-muted-foreground",
children: t("calcom.settings.headingDescription", "Configure your Cal.com API key to enable booking functionality"),
}),
],
}),
/* @__PURE__ */ jsxRuntimeExports.jsxs(Card, {
children: [
/* @__PURE__ */ jsxRuntimeExports.jsxs(CardHeader, {
children: [
/* @__PURE__ */ jsxRuntimeExports.jsxs(CardTitle, {
className: "text-base flex items-center gap-2",
children: [/* @__PURE__ */ jsxRuntimeExports.jsx(Key, { className: "h-4 w-4" }), t("calcom.settings.apiKey.title", "API Key Status")],
}),
/* @__PURE__ */ jsxRuntimeExports.jsx(CardDescription, {
children: t("calcom.settings.apiKey.description", "Your Cal.com API key is required for the booking widget to function"),
}),
],
}),
/* @__PURE__ */ jsxRuntimeExports.jsxs(CardContent, {
className: "space-y-4",
children: [
settings?.api_key_configured ?
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", {
className: "flex items-center justify-between rounded-lg border bg-muted/30 p-3",
children: [
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", {
className: "flex items-center gap-3",
children: [
/* @__PURE__ */ jsxRuntimeExports.jsxs(Badge, {
variant: "default",
className: "gap-1.5",
children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(CheckCircle2, { className: "h-3.5 w-3.5" }),
t("calcom.settings.apiKey.configuredLabel", "Configured"),
],
}),
/* @__PURE__ */ jsxRuntimeExports.jsx("p", {
className: "text-xs text-muted-foreground font-mono",
children: settings.api_key_masked || t("calcom.settings.apiKey.maskedFallback", "***configured***"),
}),
],
}),
/* @__PURE__ */ jsxRuntimeExports.jsx(Button, {
action: "delete",
entity: "plugin-setting",
variant: "ghost",
size: "sm",
onClick: handleClear,
disabled: saving,
children: t("calcom.settings.apiKey.remove", "Remove"),
}),
],
})
: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", {
className: "flex items-center gap-3 rounded-lg border bg-muted/30 p-3",
children: [
/* @__PURE__ */ jsxRuntimeExports.jsxs(Badge, {
variant: "outline",
className: "gap-1.5",
children: [/* @__PURE__ */ jsxRuntimeExports.jsx(XCircle, { className: "h-3.5 w-3.5" }), t("calcom.settings.apiKey.notConfiguredLabel", "Not configured")],
}),
/* @__PURE__ */ jsxRuntimeExports.jsx("p", {
className: "text-xs text-muted-foreground",
children: t("calcom.settings.apiKey.notConfiguredHelp", "Enter your Cal.com API key below to enable booking"),
}),
],
}),
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", {
className: "space-y-2",
children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(Label, {
htmlFor: "apiKey",
children: settings?.api_key_configured ? t("calcom.settings.apiKey.updateLabel", "Update API Key") : t("calcom.settings.apiKey.label", "API Key"),
}),
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", {
className: "flex gap-2",
children: [
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", {
className: "relative flex-1",
children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(Input, {
id: "apiKey",
type: showApiKey ? "text" : "password",
value: apiKey,
onChange: (e) => setApiKey(e.target.value),
placeholder: t("calcom.settings.apiKey.placeholder", "cal_live_..."),
className: "pr-10",
}),
/* @__PURE__ */ jsxRuntimeExports.jsx(Button, {
"type": "button",
"variant": "ghost",
"action": "toggle",
"entity": "api-key-visibility",
"onClick": () => setShowApiKey(!showApiKey),
"aria-label": apiKeyVisibilityLabel,
"className": "absolute right-3 top-1/2 -translate-y-1/2 h-auto p-0 text-muted-foreground hover:bg-transparent hover:text-foreground",
"children":
showApiKey ?
/* @__PURE__ */ jsxRuntimeExports.jsx(EyeOff, { className: "h-4 w-4" })
: /* @__PURE__ */ jsxRuntimeExports.jsx(Eye, { className: "h-4 w-4" }),
}),
],
}),
/* @__PURE__ */ jsxRuntimeExports.jsx(Button, {
action: "save",
entity: "plugin-setting",
onClick: handleSave,
disabled: saving || !apiKey.trim(),
children: saving ? /* @__PURE__ */ jsxRuntimeExports.jsx(Loader2, { className: "h-4 w-4 animate-spin" }) : t("calcom.settings.apiKey.save", "Save"),
}),
],
}),
/* @__PURE__ */ jsxRuntimeExports.jsxs("p", {
className: "text-xs text-muted-foreground",
children: [
t("calcom.settings.apiKey.helpPrefix", "Get your API key from"),
" ",
/* @__PURE__ */ jsxRuntimeExports.jsx("a", {
href: "https://app.cal.com/settings/developer/api-keys",
target: "_blank",
rel: "noopener noreferrer",
className: "text-primary hover:underline",
children: t("calcom.settings.apiKey.helpLink", "Cal.com API Settings"),
}),
],
}),
],
}),
saveMessage &&
/* @__PURE__ */ jsxRuntimeExports.jsx(Alert, {
variant: saveMessage.type === "error" ? "destructive" : "default",
children: /* @__PURE__ */ jsxRuntimeExports.jsx(AlertDescription, { children: saveMessage.text }),
}),
],
}),
],
}),
settings?.api_key_configured &&
/* @__PURE__ */ jsxRuntimeExports.jsxs(Card, {
children: [
/* @__PURE__ */ jsxRuntimeExports.jsxs(CardHeader, {
children: [
/* @__PURE__ */ jsxRuntimeExports.jsxs(CardTitle, {
className: "text-base flex items-center gap-2",
children: [/* @__PURE__ */ jsxRuntimeExports.jsx(Zap, { className: "h-4 w-4" }), t("calcom.settings.test.title", "Test Connection")],
}),
/* @__PURE__ */ jsxRuntimeExports.jsx(CardDescription, { children: t("calcom.settings.test.description", "Verify your API key works and see available event types") }),
],
}),
/* @__PURE__ */ jsxRuntimeExports.jsxs(CardContent, {
className: "space-y-4",
children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(Button, {
action: "settings",
entity: "plugin-connection",
onClick: handleTest,
disabled: testing,
children:
testing ?
/* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, {
children: [/* @__PURE__ */ jsxRuntimeExports.jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }), t("calcom.settings.test.testing", "Testing...")],
})
: t("calcom.settings.test.button", "Test Connection"),
}),
testResult &&
/* @__PURE__ */ jsxRuntimeExports.jsx("div", {
className: "space-y-3",
children:
testResult.success ?
/* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, {
children: [
/* @__PURE__ */ jsxRuntimeExports.jsxs(Alert, {
children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(CheckCircle2, { className: "h-4 w-4 text-success" }),
/* @__PURE__ */ jsxRuntimeExports.jsxs(AlertDescription, {
className: "ml-2",
children: [
t("calcom.settings.test.successPrefix", "Connection successful! Found"),
" ",
testResult.event_types?.length || 0,
" ",
t("calcom.settings.test.successSuffix", "event types."),
],
}),
],
}),
testResult.event_types &&
testResult.event_types.length > 0 &&
/* @__PURE__ */ jsxRuntimeExports.jsx(ScrollArea, {
className: "max-h-96 rounded-lg border",
children: /* @__PURE__ */ jsxRuntimeExports.jsxs("table", {
className: "w-full text-sm",
children: [
/* @__PURE__ */ jsxRuntimeExports.jsx("thead", {
className: "bg-muted",
children: /* @__PURE__ */ jsxRuntimeExports.jsxs("tr", {
children: [
/* @__PURE__ */ jsxRuntimeExports.jsx("th", {
className: "text-left px-3 py-2 font-medium",
children: t("calcom.settings.test.tableEventType", "Event Type"),
}),
/* @__PURE__ */ jsxRuntimeExports.jsx("th", {
className: "text-left px-3 py-2 font-medium",
children: t("calcom.settings.test.tableSlug", "Slug"),
}),
],
}),
}),
/* @__PURE__ */ jsxRuntimeExports.jsx("tbody", {
children: testResult.event_types.map((et) =>
/* @__PURE__ */ jsxRuntimeExports.jsxs(
"tr",
{
className: "border-t",
children: [
/* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-3 py-2", children: et.title }),
/* @__PURE__ */ jsxRuntimeExports.jsx("td", {
className: "px-3 py-2 font-mono text-xs",
children: et.slug,
}),
],
},
et.id,
),
),
}),
],
}),
}),
],
})
: /* @__PURE__ */ jsxRuntimeExports.jsxs(Alert, {
variant: "destructive",
children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(XCircle, { className: "h-4 w-4" }),
/* @__PURE__ */ jsxRuntimeExports.jsx(AlertDescription, {
className: "ml-2",
children: testResult.error || t("calcom.settings.test.failure", "Connection failed"),
}),
],
}),
}),
],
}),
],
}),
settings?.api_key_configured &&
/* @__PURE__ */ jsxRuntimeExports.jsxs(Card, {
children: [
/* @__PURE__ */ jsxRuntimeExports.jsxs(CardHeader, {
children: [
/* @__PURE__ */ jsxRuntimeExports.jsxs(CardTitle, {
className: "text-base flex items-center gap-2",
children: [/* @__PURE__ */ jsxRuntimeExports.jsx(Send, { className: "h-4 w-4" }), t("calcom.settings.webhook.title", "Webhook")],
}),
/* @__PURE__ */ jsxRuntimeExports.jsx(CardDescription, {
children: t("calcom.settings.webhook.description", "Configure this URL in Cal.com → Settings → Developer → Webhooks to receive booking events."),
}),
],
}),
/* @__PURE__ */ jsxRuntimeExports.jsxs(CardContent, {
className: "space-y-4",
children: [
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", {
className: "space-y-2",
children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(Label, { htmlFor: "webhookUrl", children: t("calcom.settings.webhook.urlLabel", "Webhook URL") }),
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", {
className: "flex gap-2",
children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(Input, {
id: "webhookUrl",
readOnly: true,
value: settings?.webhook_url || "",
className: "flex-1 font-mono text-xs",
}),
/* @__PURE__ */ jsxRuntimeExports.jsx(Button, {
"action": "copy",
"entity": "webhook-url",
"variant": "outline",
"size": "sm",
"aria-label": t("calcom.settings.webhook.copyUrl", "Copy webhook URL"),
"onClick": () => copyToClipboard(settings?.webhook_url || ""),
"children": /* @__PURE__ */ jsxRuntimeExports.jsx(Copy, { className: "h-4 w-4" }),
}),
],
}),
],
}),
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", {
className: "space-y-2",
children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(Label, { htmlFor: "webhookSecret", children: t("calcom.settings.webhook.secretLabel", "Webhook Secret") }),
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", {
className: "flex gap-2",
children: [
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", {
className: "relative flex-1",
children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(Input, {
id: "webhookSecret",
type: showSecret ? "text" : "password",
readOnly: true,
value: settings?.webhook_secret_masked || t("calcom.settings.webhook.secretNotConfigured", "Not configured"),
className: "pr-10 font-mono text-xs",
}),
/* @__PURE__ */ jsxRuntimeExports.jsx(Button, {
"type": "button",
"variant": "ghost",
"action": "toggle",
"entity": "webhook-secret-visibility",
"onClick": () => setShowSecret(!showSecret),
"aria-label": secretVisibilityLabel,
"className": "absolute right-3 top-1/2 -translate-y-1/2 h-auto p-0 text-muted-foreground hover:bg-transparent hover:text-foreground",
"children":
showSecret ?
/* @__PURE__ */ jsxRuntimeExports.jsx(EyeOff, { className: "h-4 w-4" })
: /* @__PURE__ */ jsxRuntimeExports.jsx(Eye, { className: "h-4 w-4" }),
}),
],
}),
/* @__PURE__ */ jsxRuntimeExports.jsx(Button, {
"action": "copy",
"entity": "webhook-secret",
"variant": "outline",
"size": "sm",
"aria-label": t("calcom.settings.webhook.copySecret", "Copy webhook secret"),
"onClick": () => copyToClipboard(settings?.webhook_secret_masked || ""),
"children": /* @__PURE__ */ jsxRuntimeExports.jsx(Copy, { className: "h-4 w-4" }),
}),
/* @__PURE__ */ jsxRuntimeExports.jsx(Button, {
"action": "rotate",
"entity": "webhook-secret",
"variant": "outline",
"size": "sm",
"aria-label": t("calcom.settings.webhook.rotate", "Rotate webhook secret"),
"onClick": handleRotate,
"disabled": rotating,
"children":
rotating ?
/* @__PURE__ */ jsxRuntimeExports.jsx(Loader2, { className: "h-4 w-4 animate-spin" })
: /* @__PURE__ */ jsxRuntimeExports.jsx(RefreshCw, { className: "h-4 w-4" }),
}),
],
}),
/* @__PURE__ */ jsxRuntimeExports.jsx("p", {
className: "text-xs text-muted-foreground",
children: t(
"calcom.settings.webhook.secretHelp",
"Cal.com displays the secret only once. Regenerating invalidates the previous value immediately — update it in Cal.com after rotating.",
),
}),
],
}),
/* @__PURE__ */ jsxRuntimeExports.jsx("div", {
className: "text-sm",
children:
settings?.last_event_at ?
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", {
className: "flex items-center gap-2 text-foreground",
children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(CheckCircle2, { className: "h-4 w-4 text-success" }),
/* @__PURE__ */ jsxRuntimeExports.jsxs("span", {
children: [
t("calcom.settings.webhook.lastEventPrefix", "Last event"),
" ",
formatRelativeTime(settings.last_event_at),
settings.last_event_type &&
/* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, {
children: [
" — ",
/* @__PURE__ */ jsxRuntimeExports.jsx("code", { className: "font-mono text-xs", children: settings.last_event_type }),
],
}),
],
}),
],
})
: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", {
className: "flex items-center gap-2 text-muted-foreground",
children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(AlertCircle, { className: "h-4 w-4" }),
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: t("calcom.settings.webhook.waitingForEvent", "Status: Waiting for first event") }),
],
}),
}),
],
}),
],
}),
/* @__PURE__ */ jsxRuntimeExports.jsxs(Card, {
children: [
/* @__PURE__ */ jsxRuntimeExports.jsxs(CardHeader, {
children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(CardTitle, { className: "text-base", children: t("calcom.settings.docs.title", "Documentation") }),
/* @__PURE__ */ jsxRuntimeExports.jsx(CardDescription, { children: t("calcom.settings.docs.description", "Reference material for working with the Cal.com plugin.") }),
],
}),
/* @__PURE__ */ jsxRuntimeExports.jsxs(CardContent, {
className: "space-y-2 text-sm",
children: [
/* @__PURE__ */ jsxRuntimeExports.jsx("a", {
href: "https://cal.com/docs/api-reference/v2",
target: "_blank",
rel: "noopener noreferrer",
className: "text-primary hover:underline block",
children: t("calcom.settings.docs.calcomApi", "Cal.com API documentation"),
}),
/* @__PURE__ */ jsxRuntimeExports.jsx("a", {
href: "/admin/docs/calcom-plugin",
target: "_blank",
rel: "noopener noreferrer",
className: "text-primary hover:underline block",
children: t("calcom.settings.docs.pluginGuide", "BlockNinja plugin guide"),
}),
],
}),
],
}),
],
});
const { confirm } = useAlertDialog();
const [apiKey, setApiKey] = useState("");
const [showApiKey, setShowApiKey] = useState(false);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [testing, setTesting] = useState(false);
const [rotating, setRotating] = useState(false);
const [showSecret, setShowSecret] = useState(false);
const [settings, setSettings] = useState(null);
const [testResult, setTestResult] = useState(null);
const [saveMessage, setSaveMessage] = useState(null);
const baseUrl = `/api/plugins/${pluginName}`;
const loadSettings = useCallback(async () => {
try {
const res = await fetch(`${baseUrl}/settings`);
if (res.ok) {
const data = await res.json();
setSettings(data);
}
} catch (_error) {
console.error("Failed to load settings");
} finally {
setLoading(false);
}
}, [baseUrl]);
useEffect(() => {
loadSettings();
}, [loadSettings]);
const handleSave = async () => {
if (!apiKey.trim()) {
setSaveMessage({ type: "error", text: t("calcom.settings.apiKey.errorEmpty", "Please enter an API key") });
return;
}
setSaving(true);
setSaveMessage(null);
setTestResult(null);
try {
const res = await fetch(`${baseUrl}/settings`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ api_key: apiKey })
});
if (res.ok) {
await loadSettings();
setApiKey("");
setSaveMessage({ type: "success", text: t("calcom.settings.apiKey.saved", "API key saved successfully") });
} else {
const text = await res.text();
setSaveMessage({ type: "error", text: text || t("calcom.settings.apiKey.saveFailed", "Failed to save API key") });
}
} catch (_error) {
setSaveMessage({ type: "error", text: t("calcom.settings.serverError", "Failed to connect to server") });
} finally {
setSaving(false);
}
};
const handleTest = async () => {
setTesting(true);
setTestResult(null);
try {
const res = await fetch(`${baseUrl}/test`, { method: "POST" });
const data = await res.json();
setTestResult(data);
} catch (_error) {
setTestResult({ success: false, error: t("calcom.settings.serverError", "Failed to connect to server") });
} finally {
setTesting(false);
}
};
const handleClear = async () => {
setSaving(true);
setSaveMessage(null);
try {
const res = await fetch(`${baseUrl}/settings`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ api_key: "" })
});
if (res.ok) {
await loadSettings();
setTestResult(null);
setSaveMessage({ type: "success", text: t("calcom.settings.apiKey.removed", "API key removed") });
}
} catch (_error) {
setSaveMessage({ type: "error", text: t("calcom.settings.apiKey.clearFailed", "Failed to clear API key") });
} finally {
setSaving(false);
}
};
const handleRotate = async () => {
const confirmed = await confirm({
title: t("calcom.settings.webhook.rotateConfirmTitle", "Rotate webhook secret?"),
message: t("calcom.settings.webhook.rotateConfirmMessage", "This invalidates the existing secret immediately. You'll need to update Cal.com to keep webhooks flowing."),
confirmLabel: t("calcom.settings.webhook.rotateConfirmLabel", "Rotate"),
variant: "destructive"
});
if (!confirmed) return;
setRotating(true);
setSaveMessage(null);
try {
const res = await fetch(`${baseUrl}/settings/rotate-webhook-secret`, { method: "POST" });
if (res.ok) {
const data = await res.json();
setSettings((prev) => prev ? { ...prev, webhook_secret_masked: data.webhook_secret_masked, webhook_secret_configured: true } : prev);
const successMsg = t("calcom.settings.webhook.rotated", "Webhook secret rotated. Update it in Cal.com.");
setSaveMessage({ type: "success", text: successMsg });
toast.success(successMsg);
} else {
setSaveMessage({ type: "error", text: t("calcom.settings.webhook.rotateFailed", "Failed to rotate webhook secret") });
}
} catch (_error) {
setSaveMessage({ type: "error", text: t("calcom.settings.serverError", "Failed to connect to server") });
} finally {
setRotating(false);
}
};
if (loading) {
return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex items-center justify-center py-8", children: /* @__PURE__ */ jsxRuntimeExports.jsx(Loader2, { className: "h-6 w-6 animate-spin text-muted-foreground" }) });
}
const apiKeyVisibilityLabel = showApiKey ? t("calcom.settings.toggleVisibility.hide", "Hide") : t("calcom.settings.toggleVisibility.show", "Show");
const secretVisibilityLabel = showSecret ? t("calcom.settings.toggleVisibility.hide", "Hide") : t("calcom.settings.toggleVisibility.show", "Show");
return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-6", children: [
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
/* @__PURE__ */ jsxRuntimeExports.jsxs("h2", { className: "text-xl font-semibold flex items-center gap-2", children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(Settings, { className: "h-5 w-5" }),
t("calcom.settings.heading", "Cal.com Integration")
] }),
/* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-sm text-muted-foreground", children: t("calcom.settings.headingDescription", "Configure your Cal.com API key to enable booking functionality") })
] }),
/* @__PURE__ */ jsxRuntimeExports.jsxs(Card, { children: [
/* @__PURE__ */ jsxRuntimeExports.jsxs(CardHeader, { children: [
/* @__PURE__ */ jsxRuntimeExports.jsxs(CardTitle, { className: "text-base flex items-center gap-2", children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(Key, { className: "h-4 w-4" }),
t("calcom.settings.apiKey.title", "API Key Status")
] }),
/* @__PURE__ */ jsxRuntimeExports.jsx(CardDescription, { children: t("calcom.settings.apiKey.description", "Your Cal.com API key is required for the booking widget to function") })
] }),
/* @__PURE__ */ jsxRuntimeExports.jsxs(CardContent, { className: "space-y-4", children: [
settings?.api_key_configured ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between rounded-lg border bg-muted/30 p-3", children: [
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-3", children: [
/* @__PURE__ */ jsxRuntimeExports.jsxs(Badge, { variant: "default", className: "gap-1.5", children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(CheckCircle2, { className: "h-3.5 w-3.5" }),
t("calcom.settings.apiKey.configuredLabel", "Configured")
] }),
/* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-muted-foreground font-mono", children: settings.api_key_masked || t("calcom.settings.apiKey.maskedFallback", "***configured***") })
] }),
/* @__PURE__ */ jsxRuntimeExports.jsx(Button, { action: "delete", entity: "plugin-setting", variant: "ghost", size: "sm", onClick: handleClear, disabled: saving, children: t("calcom.settings.apiKey.remove", "Remove") })
] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-3 rounded-lg border bg-muted/30 p-3", children: [
/* @__PURE__ */ jsxRuntimeExports.jsxs(Badge, { variant: "outline", className: "gap-1.5", children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(XCircle, { className: "h-3.5 w-3.5" }),
t("calcom.settings.apiKey.notConfiguredLabel", "Not configured")
] }),
/* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-muted-foreground", children: t("calcom.settings.apiKey.notConfiguredHelp", "Enter your Cal.com API key below to enable booking") })
] }),
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-2", children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(Label, { htmlFor: "apiKey", children: settings?.api_key_configured ? t("calcom.settings.apiKey.updateLabel", "Update API Key") : t("calcom.settings.apiKey.label", "API Key") }),
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-2", children: [
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative flex-1", children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(
Input,
{
id: "apiKey",
type: showApiKey ? "text" : "password",
value: apiKey,
onChange: (e) => setApiKey(e.target.value),
placeholder: t("calcom.settings.apiKey.placeholder", "cal_live_..."),
className: "pr-10"
}
),
/* @__PURE__ */ jsxRuntimeExports.jsx(
Button,
{
type: "button",
variant: "ghost",
action: "toggle",
entity: "api-key-visibility",
onClick: () => setShowApiKey(!showApiKey),
"aria-label": apiKeyVisibilityLabel,
className: "absolute right-3 top-1/2 -translate-y-1/2 h-auto p-0 text-muted-foreground hover:bg-transparent hover:text-foreground",
children: showApiKey ? /* @__PURE__ */ jsxRuntimeExports.jsx(EyeOff, { className: "h-4 w-4" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Eye, { className: "h-4 w-4" })
}
)
] }),
/* @__PURE__ */ jsxRuntimeExports.jsx(Button, { action: "save", entity: "plugin-setting", onClick: handleSave, disabled: saving || !apiKey.trim(), children: saving ? /* @__PURE__ */ jsxRuntimeExports.jsx(Loader2, { className: "h-4 w-4 animate-spin" }) : t("calcom.settings.apiKey.save", "Save") })
] }),
/* @__PURE__ */ jsxRuntimeExports.jsxs("p", { className: "text-xs text-muted-foreground", children: [
t("calcom.settings.apiKey.helpPrefix", "Get your API key from"),
" ",
/* @__PURE__ */ jsxRuntimeExports.jsx("a", { href: "https://app.cal.com/settings/developer/api-keys", target: "_blank", rel: "noopener noreferrer", className: "text-primary hover:underline", children: t("calcom.settings.apiKey.helpLink", "Cal.com API Settings") })
] })
] }),
saveMessage && /* @__PURE__ */ jsxRuntimeExports.jsx(Alert, { variant: saveMessage.type === "error" ? "destructive" : "default", children: /* @__PURE__ */ jsxRuntimeExports.jsx(AlertDescription, { children: saveMessage.text }) })
] })
] }),
settings?.api_key_configured && /* @__PURE__ */ jsxRuntimeExports.jsxs(Card, { children: [
/* @__PURE__ */ jsxRuntimeExports.jsxs(CardHeader, { children: [
/* @__PURE__ */ jsxRuntimeExports.jsxs(CardTitle, { className: "text-base flex items-center gap-2", children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(Zap, { className: "h-4 w-4" }),
t("calcom.settings.test.title", "Test Connection")
] }),
/* @__PURE__ */ jsxRuntimeExports.jsx(CardDescription, { children: t("calcom.settings.test.description", "Verify your API key works and see available event types") })
] }),
/* @__PURE__ */ jsxRuntimeExports.jsxs(CardContent, { className: "space-y-4", children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(Button, { action: "settings", entity: "plugin-connection", onClick: handleTest, disabled: testing, children: testing ? /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }),
t("calcom.settings.test.testing", "Testing...")
] }) : t("calcom.settings.test.button", "Test Connection") }),
testResult && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "space-y-3", children: testResult.success ? /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
/* @__PURE__ */ jsxRuntimeExports.jsxs(Alert, { children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(CheckCircle2, { className: "h-4 w-4 text-success" }),
/* @__PURE__ */ jsxRuntimeExports.jsxs(AlertDescription, { className: "ml-2", children: [
t("calcom.settings.test.successPrefix", "Connection successful! Found"),
" ",
testResult.event_types?.length || 0,
" ",
t("calcom.settings.test.successSuffix", "event types.")
] })
] }),
testResult.event_types && testResult.event_types.length > 0 && /* @__PURE__ */ jsxRuntimeExports.jsx(ScrollArea, { className: "max-h-96 rounded-lg border", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("table", { className: "w-full text-sm", children: [
/* @__PURE__ */ jsxRuntimeExports.jsx("thead", { className: "bg-muted", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("tr", { children: [
/* @__PURE__ */ jsxRuntimeExports.jsx("th", { className: "text-left px-3 py-2 font-medium", children: t("calcom.settings.test.tableEventType", "Event Type") }),
/* @__PURE__ */ jsxRuntimeExports.jsx("th", { className: "text-left px-3 py-2 font-medium", children: t("calcom.settings.test.tableSlug", "Slug") })
] }) }),
/* @__PURE__ */ jsxRuntimeExports.jsx("tbody", { children: testResult.event_types.map((et) => /* @__PURE__ */ jsxRuntimeExports.jsxs("tr", { className: "border-t", children: [
/* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-3 py-2", children: et.title }),
/* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-3 py-2 font-mono text-xs", children: et.slug })
] }, et.id)) })
] }) })
] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs(Alert, { variant: "destructive", children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(XCircle, { className: "h-4 w-4" }),
/* @__PURE__ */ jsxRuntimeExports.jsx(AlertDescription, { className: "ml-2", children: testResult.error || t("calcom.settings.test.failure", "Connection failed") })
] }) })
] })
] }),
settings?.api_key_configured && /* @__PURE__ */ jsxRuntimeExports.jsxs(Card, { children: [
/* @__PURE__ */ jsxRuntimeExports.jsxs(CardHeader, { children: [
/* @__PURE__ */ jsxRuntimeExports.jsxs(CardTitle, { className: "text-base flex items-center gap-2", children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(Send, { className: "h-4 w-4" }),
t("calcom.settings.webhook.title", "Webhook")
] }),
/* @__PURE__ */ jsxRuntimeExports.jsx(CardDescription, { children: t("calcom.settings.webhook.description", "Configure this URL in Cal.com → Settings → Developer → Webhooks to receive booking events.") })
] }),
/* @__PURE__ */ jsxRuntimeExports.jsxs(CardContent, { className: "space-y-4", children: [
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-2", children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(Label, { htmlFor: "webhookUrl", children: t("calcom.settings.webhook.urlLabel", "Webhook URL") }),
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-2", children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(Input, { id: "webhookUrl", readOnly: true, value: settings?.webhook_url || "", className: "flex-1 font-mono text-xs" }),
/* @__PURE__ */ jsxRuntimeExports.jsx(
Button,
{
action: "copy",
entity: "webhook-url",
variant: "outline",
size: "sm",
"aria-label": t("calcom.settings.webhook.copyUrl", "Copy webhook URL"),
onClick: () => copyToClipboard(settings?.webhook_url || ""),
children: /* @__PURE__ */ jsxRuntimeExports.jsx(Copy, { className: "h-4 w-4" })
}
)
] })
] }),
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-2", children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(Label, { htmlFor: "webhookSecret", children: t("calcom.settings.webhook.secretLabel", "Webhook Secret") }),
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-2", children: [
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative flex-1", children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(
Input,
{
id: "webhookSecret",
type: showSecret ? "text" : "password",
readOnly: true,
value: settings?.webhook_secret_masked || t("calcom.settings.webhook.secretNotConfigured", "Not configured"),
className: "pr-10 font-mono text-xs"
}
),
/* @__PURE__ */ jsxRuntimeExports.jsx(
Button,
{
type: "button",
variant: "ghost",
action: "toggle",
entity: "webhook-secret-visibility",
onClick: () => setShowSecret(!showSecret),
"aria-label": secretVisibilityLabel,
className: "absolute right-3 top-1/2 -translate-y-1/2 h-auto p-0 text-muted-foreground hover:bg-transparent hover:text-foreground",
children: showSecret ? /* @__PURE__ */ jsxRuntimeExports.jsx(EyeOff, { className: "h-4 w-4" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Eye, { className: "h-4 w-4" })
}
)
] }),
/* @__PURE__ */ jsxRuntimeExports.jsx(
Button,
{
action: "copy",
entity: "webhook-secret",
variant: "outline",
size: "sm",
"aria-label": t("calcom.settings.webhook.copySecret", "Copy webhook secret"),
onClick: () => copyToClipboard(settings?.webhook_secret_masked || ""),
children: /* @__PURE__ */ jsxRuntimeExports.jsx(Copy, { className: "h-4 w-4" })
}
),
/* @__PURE__ */ jsxRuntimeExports.jsx(
Button,
{
action: "rotate",
entity: "webhook-secret",
variant: "outline",
size: "sm",
"aria-label": t("calcom.settings.webhook.rotate", "Rotate webhook secret"),
onClick: handleRotate,
disabled: rotating,
children: rotating ? /* @__PURE__ */ jsxRuntimeExports.jsx(Loader2, { className: "h-4 w-4 animate-spin" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(RefreshCw, { className: "h-4 w-4" })
}
)
] }),
/* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-muted-foreground", children: t(
"calcom.settings.webhook.secretHelp",
"Cal.com displays the secret only once. Regenerating invalidates the previous value immediately — update it in Cal.com after rotating."
) })
] }),
/* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-sm", children: settings?.last_event_at ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 text-foreground", children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(CheckCircle2, { className: "h-4 w-4 text-success" }),
/* @__PURE__ */ jsxRuntimeExports.jsxs("span", { children: [
t("calcom.settings.webhook.lastEventPrefix", "Last event"),
" ",
formatRelativeTime(settings.last_event_at),
settings.last_event_type && /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
" — ",
/* @__PURE__ */ jsxRuntimeExports.jsx("code", { className: "font-mono text-xs", children: settings.last_event_type })
] })
] })
] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 text-muted-foreground", children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(AlertCircle, { className: "h-4 w-4" }),
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: t("calcom.settings.webhook.waitingForEvent", "Status: Waiting for first event") })
] }) })
] })
] }),
/* @__PURE__ */ jsxRuntimeExports.jsxs(Card, { children: [
/* @__PURE__ */ jsxRuntimeExports.jsxs(CardHeader, { children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(CardTitle, { className: "text-base", children: t("calcom.settings.docs.title", "Documentation") }),
/* @__PURE__ */ jsxRuntimeExports.jsx(CardDescription, { children: t("calcom.settings.docs.description", "Reference material for working with the Cal.com plugin.") })
] }),
/* @__PURE__ */ jsxRuntimeExports.jsxs(CardContent, { className: "space-y-2 text-sm", children: [
/* @__PURE__ */ jsxRuntimeExports.jsx("a", { href: "https://cal.com/docs/api-reference/v2", target: "_blank", rel: "noopener noreferrer", className: "text-primary hover:underline block", children: t("calcom.settings.docs.calcomApi", "Cal.com API documentation") }),
/* @__PURE__ */ jsxRuntimeExports.jsx("a", { href: "/admin/docs/calcom-plugin", target: "_blank", rel: "noopener noreferrer", className: "text-primary hover:underline block", children: t("calcom.settings.docs.pluginGuide", "BlockNinja plugin guide") })
] })
] })
] });
}
export { CalcomSettings as default };

View File

@ -27,366 +27,399 @@ const xRange = `^${gtlt}\\s*${xRangePlain}$`;
const comparator = `^${gtlt}\\s*(${fullPlain})$|^$`;
const gte0 = "^\\s*>=\\s*0.0.0\\s*$";
function parseRegex(source) {
return new RegExp(source);
return new RegExp(source);
}
function isXVersion(version) {
return !version || version.toLowerCase() === "x" || version === "*";
return !version || version.toLowerCase() === "x" || version === "*";
}
function pipe(...fns) {
return (x) => {
return fns.reduce((v, f) => f(v), x);
};
return (x) => {
return fns.reduce((v, f) => f(v), x);
};
}
function extractComparator(comparatorString) {
return comparatorString.match(parseRegex(comparator));
return comparatorString.match(parseRegex(comparator));
}
function combineVersion(major, minor, patch, preRelease2) {
const mainVersion2 = `${major}.${minor}.${patch}`;
if (preRelease2) {
return `${mainVersion2}-${preRelease2}`;
}
return mainVersion2;
const mainVersion2 = `${major}.${minor}.${patch}`;
if (preRelease2) {
return `${mainVersion2}-${preRelease2}`;
}
return mainVersion2;
}
function parseHyphen(range) {
return range.replace(parseRegex(hyphenRange), (_range, from, fromMajor, fromMinor, fromPatch, _fromPreRelease, _fromBuild, to, toMajor, toMinor, toPatch, toPreRelease) => {
if (isXVersion(fromMajor)) {
from = "";
} else if (isXVersion(fromMinor)) {
from = `>=${fromMajor}.0.0`;
} else if (isXVersion(fromPatch)) {
from = `>=${fromMajor}.${fromMinor}.0`;
} else {
from = `>=${from}`;
}
if (isXVersion(toMajor)) {
to = "";
} else if (isXVersion(toMinor)) {
to = `<${+toMajor + 1}.0.0-0`;
} else if (isXVersion(toPatch)) {
to = `<${toMajor}.${+toMinor + 1}.0-0`;
} else if (toPreRelease) {
to = `<=${toMajor}.${toMinor}.${toPatch}-${toPreRelease}`;
} else {
to = `<=${to}`;
}
return `${from} ${to}`.trim();
});
return range.replace(
parseRegex(hyphenRange),
(_range, from, fromMajor, fromMinor, fromPatch, _fromPreRelease, _fromBuild, to, toMajor, toMinor, toPatch, toPreRelease) => {
if (isXVersion(fromMajor)) {
from = "";
} else if (isXVersion(fromMinor)) {
from = `>=${fromMajor}.0.0`;
} else if (isXVersion(fromPatch)) {
from = `>=${fromMajor}.${fromMinor}.0`;
} else {
from = `>=${from}`;
}
if (isXVersion(toMajor)) {
to = "";
} else if (isXVersion(toMinor)) {
to = `<${+toMajor + 1}.0.0-0`;
} else if (isXVersion(toPatch)) {
to = `<${toMajor}.${+toMinor + 1}.0-0`;
} else if (toPreRelease) {
to = `<=${toMajor}.${toMinor}.${toPatch}-${toPreRelease}`;
} else {
to = `<=${to}`;
}
return `${from} ${to}`.trim();
}
);
}
function parseComparatorTrim(range) {
return range.replace(parseRegex(comparatorTrim), "$1$2$3");
return range.replace(parseRegex(comparatorTrim), "$1$2$3");
}
function parseTildeTrim(range) {
return range.replace(parseRegex(tildeTrim), "$1~");
return range.replace(parseRegex(tildeTrim), "$1~");
}
function parseCaretTrim(range) {
return range.replace(parseRegex(caretTrim), "$1^");
return range.replace(parseRegex(caretTrim), "$1^");
}
function parseCarets(range) {
return range
.trim()
.split(/\s+/)
.map((rangeVersion) => {
return rangeVersion.replace(parseRegex(caret), (_, major, minor, patch, preRelease2) => {
if (isXVersion(major)) {
return "";
} else if (isXVersion(minor)) {
return `>=${major}.0.0 <${+major + 1}.0.0-0`;
} else if (isXVersion(patch)) {
if (major === "0") {
return `>=${major}.${minor}.0 <${major}.${+minor + 1}.0-0`;
} else {
return `>=${major}.${minor}.0 <${+major + 1}.0.0-0`;
}
} else if (preRelease2) {
if (major === "0") {
if (minor === "0") {
return `>=${major}.${minor}.${patch}-${preRelease2} <${major}.${minor}.${+patch + 1}-0`;
} else {
return `>=${major}.${minor}.${patch}-${preRelease2} <${major}.${+minor + 1}.0-0`;
}
} else {
return `>=${major}.${minor}.${patch}-${preRelease2} <${+major + 1}.0.0-0`;
}
} else {
if (major === "0") {
if (minor === "0") {
return `>=${major}.${minor}.${patch} <${major}.${minor}.${+patch + 1}-0`;
} else {
return `>=${major}.${minor}.${patch} <${major}.${+minor + 1}.0-0`;
}
}
return `>=${major}.${minor}.${patch} <${+major + 1}.0.0-0`;
}
});
})
.join(" ");
return range.trim().split(/\s+/).map((rangeVersion) => {
return rangeVersion.replace(
parseRegex(caret),
(_, major, minor, patch, preRelease2) => {
if (isXVersion(major)) {
return "";
} else if (isXVersion(minor)) {
return `>=${major}.0.0 <${+major + 1}.0.0-0`;
} else if (isXVersion(patch)) {
if (major === "0") {
return `>=${major}.${minor}.0 <${major}.${+minor + 1}.0-0`;
} else {
return `>=${major}.${minor}.0 <${+major + 1}.0.0-0`;
}
} else if (preRelease2) {
if (major === "0") {
if (minor === "0") {
return `>=${major}.${minor}.${patch}-${preRelease2} <${major}.${minor}.${+patch + 1}-0`;
} else {
return `>=${major}.${minor}.${patch}-${preRelease2} <${major}.${+minor + 1}.0-0`;
}
} else {
return `>=${major}.${minor}.${patch}-${preRelease2} <${+major + 1}.0.0-0`;
}
} else {
if (major === "0") {
if (minor === "0") {
return `>=${major}.${minor}.${patch} <${major}.${minor}.${+patch + 1}-0`;
} else {
return `>=${major}.${minor}.${patch} <${major}.${+minor + 1}.0-0`;
}
}
return `>=${major}.${minor}.${patch} <${+major + 1}.0.0-0`;
}
}
);
}).join(" ");
}
function parseTildes(range) {
return range
.trim()
.split(/\s+/)
.map((rangeVersion) => {
return rangeVersion.replace(parseRegex(tilde), (_, major, minor, patch, preRelease2) => {
if (isXVersion(major)) {
return "";
} else if (isXVersion(minor)) {
return `>=${major}.0.0 <${+major + 1}.0.0-0`;
} else if (isXVersion(patch)) {
return `>=${major}.${minor}.0 <${major}.${+minor + 1}.0-0`;
} else if (preRelease2) {
return `>=${major}.${minor}.${patch}-${preRelease2} <${major}.${+minor + 1}.0-0`;
}
return `>=${major}.${minor}.${patch} <${major}.${+minor + 1}.0-0`;
});
})
.join(" ");
return range.trim().split(/\s+/).map((rangeVersion) => {
return rangeVersion.replace(
parseRegex(tilde),
(_, major, minor, patch, preRelease2) => {
if (isXVersion(major)) {
return "";
} else if (isXVersion(minor)) {
return `>=${major}.0.0 <${+major + 1}.0.0-0`;
} else if (isXVersion(patch)) {
return `>=${major}.${minor}.0 <${major}.${+minor + 1}.0-0`;
} else if (preRelease2) {
return `>=${major}.${minor}.${patch}-${preRelease2} <${major}.${+minor + 1}.0-0`;
}
return `>=${major}.${minor}.${patch} <${major}.${+minor + 1}.0-0`;
}
);
}).join(" ");
}
function parseXRanges(range) {
return range
.split(/\s+/)
.map((rangeVersion) => {
return rangeVersion.trim().replace(parseRegex(xRange), (ret, gtlt2, major, minor, patch, preRelease2) => {
const isXMajor = isXVersion(major);
const isXMinor = isXMajor || isXVersion(minor);
const isXPatch = isXMinor || isXVersion(patch);
if (gtlt2 === "=" && isXPatch) {
gtlt2 = "";
}
preRelease2 = "";
if (isXMajor) {
if (gtlt2 === ">" || gtlt2 === "<") {
return "<0.0.0-0";
} else {
return "*";
}
} else if (gtlt2 && isXPatch) {
if (isXMinor) {
minor = 0;
}
patch = 0;
if (gtlt2 === ">") {
gtlt2 = ">=";
if (isXMinor) {
major = +major + 1;
minor = 0;
patch = 0;
} else {
minor = +minor + 1;
patch = 0;
}
} else if (gtlt2 === "<=") {
gtlt2 = "<";
if (isXMinor) {
major = +major + 1;
} else {
minor = +minor + 1;
}
}
if (gtlt2 === "<") {
preRelease2 = "-0";
}
return `${gtlt2 + major}.${minor}.${patch}${preRelease2}`;
} else if (isXMinor) {
return `>=${major}.0.0${preRelease2} <${+major + 1}.0.0-0`;
} else if (isXPatch) {
return `>=${major}.${minor}.0${preRelease2} <${major}.${+minor + 1}.0-0`;
}
return ret;
});
})
.join(" ");
return range.split(/\s+/).map((rangeVersion) => {
return rangeVersion.trim().replace(
parseRegex(xRange),
(ret, gtlt2, major, minor, patch, preRelease2) => {
const isXMajor = isXVersion(major);
const isXMinor = isXMajor || isXVersion(minor);
const isXPatch = isXMinor || isXVersion(patch);
if (gtlt2 === "=" && isXPatch) {
gtlt2 = "";
}
preRelease2 = "";
if (isXMajor) {
if (gtlt2 === ">" || gtlt2 === "<") {
return "<0.0.0-0";
} else {
return "*";
}
} else if (gtlt2 && isXPatch) {
if (isXMinor) {
minor = 0;
}
patch = 0;
if (gtlt2 === ">") {
gtlt2 = ">=";
if (isXMinor) {
major = +major + 1;
minor = 0;
patch = 0;
} else {
minor = +minor + 1;
patch = 0;
}
} else if (gtlt2 === "<=") {
gtlt2 = "<";
if (isXMinor) {
major = +major + 1;
} else {
minor = +minor + 1;
}
}
if (gtlt2 === "<") {
preRelease2 = "-0";
}
return `${gtlt2 + major}.${minor}.${patch}${preRelease2}`;
} else if (isXMinor) {
return `>=${major}.0.0${preRelease2} <${+major + 1}.0.0-0`;
} else if (isXPatch) {
return `>=${major}.${minor}.0${preRelease2} <${major}.${+minor + 1}.0-0`;
}
return ret;
}
);
}).join(" ");
}
function parseStar(range) {
return range.trim().replace(parseRegex(star), "");
return range.trim().replace(parseRegex(star), "");
}
function parseGTE0(comparatorString) {
return comparatorString.trim().replace(parseRegex(gte0), "");
return comparatorString.trim().replace(parseRegex(gte0), "");
}
function compareAtom(rangeAtom, versionAtom) {
rangeAtom = +rangeAtom || rangeAtom;
versionAtom = +versionAtom || versionAtom;
if (rangeAtom > versionAtom) {
return 1;
}
if (rangeAtom === versionAtom) {
return 0;
}
return -1;
rangeAtom = +rangeAtom || rangeAtom;
versionAtom = +versionAtom || versionAtom;
if (rangeAtom > versionAtom) {
return 1;
}
if (rangeAtom === versionAtom) {
return 0;
}
return -1;
}
function comparePreRelease(rangeAtom, versionAtom) {
const { preRelease: rangePreRelease } = rangeAtom;
const { preRelease: versionPreRelease } = versionAtom;
if (rangePreRelease === void 0 && !!versionPreRelease) {
return 1;
}
if (!!rangePreRelease && versionPreRelease === void 0) {
return -1;
}
if (rangePreRelease === void 0 && versionPreRelease === void 0) {
return 0;
}
for (let i = 0, n = rangePreRelease.length; i <= n; i++) {
const rangeElement = rangePreRelease[i];
const versionElement = versionPreRelease[i];
if (rangeElement === versionElement) {
continue;
}
if (rangeElement === void 0 && versionElement === void 0) {
return 0;
}
if (!rangeElement) {
return 1;
}
if (!versionElement) {
return -1;
}
return compareAtom(rangeElement, versionElement);
}
return 0;
const { preRelease: rangePreRelease } = rangeAtom;
const { preRelease: versionPreRelease } = versionAtom;
if (rangePreRelease === void 0 && !!versionPreRelease) {
return 1;
}
if (!!rangePreRelease && versionPreRelease === void 0) {
return -1;
}
if (rangePreRelease === void 0 && versionPreRelease === void 0) {
return 0;
}
for (let i = 0, n = rangePreRelease.length; i <= n; i++) {
const rangeElement = rangePreRelease[i];
const versionElement = versionPreRelease[i];
if (rangeElement === versionElement) {
continue;
}
if (rangeElement === void 0 && versionElement === void 0) {
return 0;
}
if (!rangeElement) {
return 1;
}
if (!versionElement) {
return -1;
}
return compareAtom(rangeElement, versionElement);
}
return 0;
}
function compareVersion(rangeAtom, versionAtom) {
return (
compareAtom(rangeAtom.major, versionAtom.major) ||
compareAtom(rangeAtom.minor, versionAtom.minor) ||
compareAtom(rangeAtom.patch, versionAtom.patch) ||
comparePreRelease(rangeAtom, versionAtom)
);
return compareAtom(rangeAtom.major, versionAtom.major) || compareAtom(rangeAtom.minor, versionAtom.minor) || compareAtom(rangeAtom.patch, versionAtom.patch) || comparePreRelease(rangeAtom, versionAtom);
}
function eq(rangeAtom, versionAtom) {
return rangeAtom.version === versionAtom.version;
return rangeAtom.version === versionAtom.version;
}
function compare(rangeAtom, versionAtom) {
switch (rangeAtom.operator) {
case "":
case "=":
return eq(rangeAtom, versionAtom);
case ">":
return compareVersion(rangeAtom, versionAtom) < 0;
case ">=":
return eq(rangeAtom, versionAtom) || compareVersion(rangeAtom, versionAtom) < 0;
case "<":
return compareVersion(rangeAtom, versionAtom) > 0;
case "<=":
return eq(rangeAtom, versionAtom) || compareVersion(rangeAtom, versionAtom) > 0;
case void 0: {
return true;
}
default:
return false;
}
switch (rangeAtom.operator) {
case "":
case "=":
return eq(rangeAtom, versionAtom);
case ">":
return compareVersion(rangeAtom, versionAtom) < 0;
case ">=":
return eq(rangeAtom, versionAtom) || compareVersion(rangeAtom, versionAtom) < 0;
case "<":
return compareVersion(rangeAtom, versionAtom) > 0;
case "<=":
return eq(rangeAtom, versionAtom) || compareVersion(rangeAtom, versionAtom) > 0;
case void 0: {
return true;
}
default:
return false;
}
}
function parseComparatorString(range) {
return pipe(parseCarets, parseTildes, parseXRanges, parseStar)(range);
return pipe(
parseCarets,
parseTildes,
parseXRanges,
parseStar
)(range);
}
function parseRange(range) {
return pipe(parseHyphen, parseComparatorTrim, parseTildeTrim, parseCaretTrim)(range.trim()).split(/\s+/).join(" ");
return pipe(
parseHyphen,
parseComparatorTrim,
parseTildeTrim,
parseCaretTrim
)(range.trim()).split(/\s+/).join(" ");
}
function satisfy(version, range) {
if (!version) {
return false;
}
const parsedRange = parseRange(range);
const parsedComparator = parsedRange
.split(" ")
.map((rangeVersion) => parseComparatorString(rangeVersion))
.join(" ");
const comparators = parsedComparator.split(/\s+/).map((comparator2) => parseGTE0(comparator2));
const extractedVersion = extractComparator(version);
if (!extractedVersion) {
return false;
}
const [, versionOperator, , versionMajor, versionMinor, versionPatch, versionPreRelease] = extractedVersion;
const versionAtom = {
version: combineVersion(versionMajor, versionMinor, versionPatch, versionPreRelease),
major: versionMajor,
minor: versionMinor,
patch: versionPatch,
preRelease: versionPreRelease == null ? void 0 : versionPreRelease.split("."),
};
for (const comparator2 of comparators) {
const extractedComparator = extractComparator(comparator2);
if (!extractedComparator) {
return false;
}
const [, rangeOperator, , rangeMajor, rangeMinor, rangePatch, rangePreRelease] = extractedComparator;
const rangeAtom = {
operator: rangeOperator,
version: combineVersion(rangeMajor, rangeMinor, rangePatch, rangePreRelease),
major: rangeMajor,
minor: rangeMinor,
patch: rangePatch,
preRelease: rangePreRelease == null ? void 0 : rangePreRelease.split("."),
};
if (!compare(rangeAtom, versionAtom)) {
return false;
}
}
return true;
if (!version) {
return false;
}
const parsedRange = parseRange(range);
const parsedComparator = parsedRange.split(" ").map((rangeVersion) => parseComparatorString(rangeVersion)).join(" ");
const comparators = parsedComparator.split(/\s+/).map((comparator2) => parseGTE0(comparator2));
const extractedVersion = extractComparator(version);
if (!extractedVersion) {
return false;
}
const [
,
versionOperator,
,
versionMajor,
versionMinor,
versionPatch,
versionPreRelease
] = extractedVersion;
const versionAtom = {
version: combineVersion(
versionMajor,
versionMinor,
versionPatch,
versionPreRelease
),
major: versionMajor,
minor: versionMinor,
patch: versionPatch,
preRelease: versionPreRelease == null ? void 0 : versionPreRelease.split(".")
};
for (const comparator2 of comparators) {
const extractedComparator = extractComparator(comparator2);
if (!extractedComparator) {
return false;
}
const [
,
rangeOperator,
,
rangeMajor,
rangeMinor,
rangePatch,
rangePreRelease
] = extractedComparator;
const rangeAtom = {
operator: rangeOperator,
version: combineVersion(
rangeMajor,
rangeMinor,
rangePatch,
rangePreRelease
),
major: rangeMajor,
minor: rangeMinor,
patch: rangePatch,
preRelease: rangePreRelease == null ? void 0 : rangePreRelease.split(".")
};
if (!compare(rangeAtom, versionAtom)) {
return false;
}
}
return true;
}
const currentImports = {};
// eslint-disable-next-line no-undef
const moduleMap = {
"react": { get: () => () => __federation_import(new URL("__federation_shared_react-DoKb58Ht.js", import.meta.url).href), import: true },
"react-dom": { get: () => () => __federation_import(new URL("__federation_shared_react-dom-DU2-P0kt.js", import.meta.url).href), import: true },
"@block-ninja/ui": { get: () => () => __federation_import(new URL("__federation_shared_@block-ninja/ui-C3CAr7wz.js", import.meta.url).href), import: true },
};
const moduleMap = {'react':{get:()=>()=>__federation_import(new URL('__federation_shared_react-DoKb58Ht.js', import.meta.url).href),import:true},'react-dom':{get:()=>()=>__federation_import(new URL('__federation_shared_react-dom-DU2-P0kt.js', import.meta.url).href),import:true},'@block-ninja/ui':{get:()=>()=>__federation_import(new URL('__federation_shared_@block-ninja/ui-C3CAr7wz.js', import.meta.url).href),import:true}};
const moduleCache = Object.create(null);
async function importShared(name, shareScope = "default") {
return moduleCache[name] ? new Promise((r) => r(moduleCache[name])) : (await getSharedFromRuntime(name, shareScope)) || getSharedFromLocal(name);
async function importShared(name, shareScope = 'default') {
return moduleCache[name]
? new Promise((r) => r(moduleCache[name]))
: (await getSharedFromRuntime(name, shareScope)) || getSharedFromLocal(name)
}
// eslint-disable-next-line
async function __federation_import(name) {
currentImports[name] ??= import(name);
return currentImports[name];
currentImports[name] ??= import(name);
return currentImports[name]
}
async function getSharedFromRuntime(name, shareScope) {
let module = null;
if (globalThis?.__federation_shared__?.[shareScope]?.[name]) {
const versionObj = globalThis.__federation_shared__[shareScope][name];
const requiredVersion = moduleMap[name]?.requiredVersion;
const hasRequiredVersion = !!requiredVersion;
if (hasRequiredVersion) {
const versionKey = Object.keys(versionObj).find((version) => satisfy(version, requiredVersion));
if (versionKey) {
const versionValue = versionObj[versionKey];
module = await (await versionValue.get())();
} else {
console.log(`provider support ${name}(${versionKey}) is not satisfied requiredVersion(\${moduleMap[name].requiredVersion})`);
}
} else {
const versionKey = Object.keys(versionObj)[0];
const versionValue = versionObj[versionKey];
module = await (await versionValue.get())();
}
}
if (module) {
return flattenModule(module, name);
}
let module = null;
if (globalThis?.__federation_shared__?.[shareScope]?.[name]) {
const versionObj = globalThis.__federation_shared__[shareScope][name];
const requiredVersion = moduleMap[name]?.requiredVersion;
const hasRequiredVersion = !!requiredVersion;
if (hasRequiredVersion) {
const versionKey = Object.keys(versionObj).find((version) =>
satisfy(version, requiredVersion)
);
if (versionKey) {
const versionValue = versionObj[versionKey];
module = await (await versionValue.get())();
} else {
console.log(
`provider support ${name}(${versionKey}) is not satisfied requiredVersion(\${moduleMap[name].requiredVersion})`
);
}
} else {
const versionKey = Object.keys(versionObj)[0];
const versionValue = versionObj[versionKey];
module = await (await versionValue.get())();
}
}
if (module) {
return flattenModule(module, name)
}
}
async function getSharedFromLocal(name) {
if (moduleMap[name]?.import) {
let module = await (await moduleMap[name].get())();
return flattenModule(module, name);
} else {
console.error(`consumer config import=false,so cant use callback shared module`);
}
if (moduleMap[name]?.import) {
let module = await (await moduleMap[name].get())();
return flattenModule(module, name)
} else {
console.error(
`consumer config import=false,so cant use callback shared module`
);
}
}
function flattenModule(module, name) {
// use a shared module which export default a function will getting error 'TypeError: xxx is not a function'
if (typeof module.default === "function") {
Object.keys(module).forEach((key) => {
if (key !== "default") {
module.default[key] = module[key];
}
});
moduleCache[name] = module.default;
return module.default;
}
if (module.default) module = Object.assign({}, module.default, module);
moduleCache[name] = module;
return module;
// use a shared module which export default a function will getting error 'TypeError: xxx is not a function'
if (typeof module.default === 'function') {
Object.keys(module).forEach((key) => {
if (key !== 'default') {
module.default[key] = module[key];
}
});
moduleCache[name] = module.default;
return module.default
}
if (module.default) module = Object.assign({}, module.default, module);
moduleCache[name] = module;
return module
}
export { importShared, getSharedFromLocal as importSharedLocal, getSharedFromRuntime as importSharedRuntime };

File diff suppressed because one or more lines are too long

View File

@ -1,7 +1,7 @@
import { g as getDefaultExportFromCjs } from "./_commonjsHelpers-B85MJLTf.js";
import { r as requireReact } from "./index-DQGM2Mpm.js";
import { g as getDefaultExportFromCjs } from './_commonjsHelpers-B85MJLTf.js';
import { r as requireReact } from './index-DQGM2Mpm.js';
var reactExports = requireReact();
const index = /*@__PURE__*/ getDefaultExportFromCjs(reactExports);
const index = /*@__PURE__*/getDefaultExportFromCjs(reactExports);
export { index as default };

View File

@ -1,7 +1,7 @@
import { g as getDefaultExportFromCjs } from "./_commonjsHelpers-B85MJLTf.js";
import { r as requireReactDom } from "./index-eoEhLOdg.js";
import { g as getDefaultExportFromCjs } from './_commonjsHelpers-B85MJLTf.js';
import { r as requireReactDom } from './index-eoEhLOdg.js';
var reactDomExports = requireReactDom();
const index = /*@__PURE__*/ getDefaultExportFromCjs(reactDomExports);
const index = /*@__PURE__*/getDefaultExportFromCjs(reactDomExports);
export { index as default };

View File

@ -1,5 +1,5 @@
function getDefaultExportFromCjs(x) {
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, "default") ? x["default"] : x;
function getDefaultExportFromCjs (x) {
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
}
export { getDefaultExportFromCjs as g };

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
import { importShared } from "./__federation_fn_import-hlt2XzeI.js";
import { X as requireShim } from "./index-Bs--Ol2m.js";
import { importShared } from './__federation_fn_import-hlt2XzeI.js';
import { X as requireShim } from './index-Bs--Ol2m.js';
/**
* @license lucide-react v0.468.0 - ISC
@ -9,13 +9,9 @@ import { X as requireShim } from "./index-Bs--Ol2m.js";
*/
const toKebabCase = (string) => string.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
const mergeClasses = (...classes) =>
classes
.filter((className, index, array) => {
return Boolean(className) && className.trim() !== "" && array.indexOf(className) === index;
})
.join(" ")
.trim();
const mergeClasses = (...classes) => classes.filter((className, index, array) => {
return Boolean(className) && className.trim() !== "" && array.indexOf(className) === index;
}).join(" ").trim();
/**
* @license lucide-react v0.468.0 - ISC
@ -25,15 +21,15 @@ const mergeClasses = (...classes) =>
*/
var defaultAttributes = {
xmlns: "http://www.w3.org/2000/svg",
width: 24,
height: 24,
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
strokeWidth: 2,
strokeLinecap: "round",
strokeLinejoin: "round",
xmlns: "http://www.w3.org/2000/svg",
width: 24,
height: 24,
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
strokeWidth: 2,
strokeLinecap: "round",
strokeLinejoin: "round"
};
/**
@ -43,24 +39,38 @@ var defaultAttributes = {
* See the LICENSE file in the root directory of this source tree.
*/
const { forwardRef: forwardRef$1, createElement: createElement$1 } = await importShared("react");
const {forwardRef: forwardRef$1,createElement: createElement$1} = await importShared('react');
const Icon = forwardRef$1(({ color = "currentColor", size = 24, strokeWidth = 2, absoluteStrokeWidth, className = "", children, iconNode, ...rest }, ref) => {
return createElement$1(
"svg",
{
ref,
...defaultAttributes,
width: size,
height: size,
stroke: color,
strokeWidth: absoluteStrokeWidth ? (Number(strokeWidth) * 24) / Number(size) : strokeWidth,
className: mergeClasses("lucide", className),
...rest,
},
[...iconNode.map(([tag, attrs]) => createElement$1(tag, attrs)), ...(Array.isArray(children) ? children : [children])],
);
});
const Icon = forwardRef$1(
({
color = "currentColor",
size = 24,
strokeWidth = 2,
absoluteStrokeWidth,
className = "",
children,
iconNode,
...rest
}, ref) => {
return createElement$1(
"svg",
{
ref,
...defaultAttributes,
width: size,
height: size,
stroke: color,
strokeWidth: absoluteStrokeWidth ? Number(strokeWidth) * 24 / Number(size) : strokeWidth,
className: mergeClasses("lucide", className),
...rest
},
[
...iconNode.map(([tag, attrs]) => createElement$1(tag, attrs)),
...Array.isArray(children) ? children : [children]
]
);
}
);
/**
* @license lucide-react v0.468.0 - ISC
@ -69,19 +79,19 @@ const Icon = forwardRef$1(({ color = "currentColor", size = 24, strokeWidth = 2,
* See the LICENSE file in the root directory of this source tree.
*/
const { forwardRef, createElement } = await importShared("react");
const {forwardRef,createElement} = await importShared('react');
const createLucideIcon = (iconName, iconNode) => {
const Component = forwardRef(({ className, ...props }, ref) =>
createElement(Icon, {
ref,
iconNode,
className: mergeClasses(`lucide-${toKebabCase(iconName)}`, className),
...props,
}),
);
Component.displayName = `${iconName}`;
return Component;
const Component = forwardRef(
({ className, ...props }, ref) => createElement(Icon, {
ref,
iconNode,
className: mergeClasses(`lucide-${toKebabCase(iconName)}`, className),
...props
})
);
Component.displayName = `${iconName}`;
return Component;
};
/**
@ -91,13 +101,14 @@ const createLucideIcon = (iconName, iconNode) => {
* See the LICENSE file in the root directory of this source tree.
*/
const List = createLucideIcon("List", [
["path", { d: "M3 12h.01", key: "nlz23k" }],
["path", { d: "M3 18h.01", key: "1tta3j" }],
["path", { d: "M3 6h.01", key: "1rqtza" }],
["path", { d: "M8 12h13", key: "1za7za" }],
["path", { d: "M8 18h13", key: "1lx6n3" }],
["path", { d: "M8 6h13", key: "ik3vkj" }],
["path", { d: "M3 12h.01", key: "nlz23k" }],
["path", { d: "M3 18h.01", key: "1tta3j" }],
["path", { d: "M3 6h.01", key: "1rqtza" }],
["path", { d: "M8 12h13", key: "1za7za" }],
["path", { d: "M8 18h13", key: "1lx6n3" }],
["path", { d: "M8 6h13", key: "ik3vkj" }]
]);
var shimExports = requireShim();

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,4 @@
var react = { exports: {} };
var react = {exports: {}};
var react_production_min = {};
@ -14,332 +14,36 @@ var react_production_min = {};
var hasRequiredReact_production_min;
function requireReact_production_min() {
function requireReact_production_min () {
if (hasRequiredReact_production_min) return react_production_min;
hasRequiredReact_production_min = 1;
var l = Symbol.for("react.element"),
n = Symbol.for("react.portal"),
p = Symbol.for("react.fragment"),
q = Symbol.for("react.strict_mode"),
r = Symbol.for("react.profiler"),
t = Symbol.for("react.provider"),
u = Symbol.for("react.context"),
v = Symbol.for("react.forward_ref"),
w = Symbol.for("react.suspense"),
x = Symbol.for("react.memo"),
y = Symbol.for("react.lazy"),
z = Symbol.iterator;
function A(a) {
if (null === a || "object" !== typeof a) return null;
a = (z && a[z]) || a["@@iterator"];
return "function" === typeof a ? a : null;
}
var B = {
isMounted: function () {
return false;
},
enqueueForceUpdate: function () {},
enqueueReplaceState: function () {},
enqueueSetState: function () {},
},
C = Object.assign,
D = {};
function E(a, b, e) {
this.props = a;
this.context = b;
this.refs = D;
this.updater = e || B;
}
E.prototype.isReactComponent = {};
E.prototype.setState = function (a, b) {
if ("object" !== typeof a && "function" !== typeof a && null != a)
throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");
this.updater.enqueueSetState(this, a, b, "setState");
};
E.prototype.forceUpdate = function (a) {
this.updater.enqueueForceUpdate(this, a, "forceUpdate");
};
function F() {}
F.prototype = E.prototype;
function G(a, b, e) {
this.props = a;
this.context = b;
this.refs = D;
this.updater = e || B;
}
var H = (G.prototype = new F());
H.constructor = G;
C(H, E.prototype);
H.isPureReactComponent = true;
var I = Array.isArray,
J = Object.prototype.hasOwnProperty,
K = { current: null },
L = { key: true, ref: true, __self: true, __source: true };
function M(a, b, e) {
var d,
c = {},
k = null,
h = null;
if (null != b) for (d in (void 0 !== b.ref && (h = b.ref), void 0 !== b.key && (k = "" + b.key), b)) J.call(b, d) && !L.hasOwnProperty(d) && (c[d] = b[d]);
var g = arguments.length - 2;
if (1 === g) c.children = e;
else if (1 < g) {
for (var f = Array(g), m = 0; m < g; m++) f[m] = arguments[m + 2];
c.children = f;
}
if (a && a.defaultProps) for (d in ((g = a.defaultProps), g)) void 0 === c[d] && (c[d] = g[d]);
return { $$typeof: l, type: a, key: k, ref: h, props: c, _owner: K.current };
}
function N(a, b) {
return { $$typeof: l, type: a.type, key: b, ref: a.ref, props: a.props, _owner: a._owner };
}
function O(a) {
return "object" === typeof a && null !== a && a.$$typeof === l;
}
function escape(a) {
var b = { "=": "=0", ":": "=2" };
return (
"$" +
a.replace(/[=:]/g, function (a) {
return b[a];
})
);
}
var P = /\/+/g;
function Q(a, b) {
return "object" === typeof a && null !== a && null != a.key ? escape("" + a.key) : b.toString(36);
}
function R(a, b, e, d, c) {
var k = typeof a;
if ("undefined" === k || "boolean" === k) a = null;
var h = false;
if (null === a) h = true;
else
switch (k) {
case "string":
case "number":
h = true;
break;
case "object":
switch (a.$$typeof) {
case l:
case n:
h = true;
}
}
if (h)
return (
(h = a),
(c = c(h)),
(a = "" === d ? "." + Q(h, 0) : d),
I(c) ?
((e = ""),
null != a && (e = a.replace(P, "$&/") + "/"),
R(c, b, e, "", function (a) {
return a;
}))
: null != c && (O(c) && (c = N(c, e + (!c.key || (h && h.key === c.key) ? "" : ("" + c.key).replace(P, "$&/") + "/") + a)), b.push(c)),
1
);
h = 0;
d = "" === d ? "." : d + ":";
if (I(a))
for (var g = 0; g < a.length; g++) {
k = a[g];
var f = d + Q(k, g);
h += R(k, b, e, f, c);
}
else if (((f = A(a)), "function" === typeof f)) for (a = f.call(a), g = 0; !(k = a.next()).done; ) ((k = k.value), (f = d + Q(k, g++)), (h += R(k, b, e, f, c)));
else if ("object" === k)
throw (
(b = String(a)),
Error(
"Objects are not valid as a React child (found: " +
("[object Object]" === b ? "object with keys {" + Object.keys(a).join(", ") + "}" : b) +
"). If you meant to render a collection of children, use an array instead.",
)
);
return h;
}
function S(a, b, e) {
if (null == a) return a;
var d = [],
c = 0;
R(a, d, "", "", function (a) {
return b.call(e, a, c++);
});
return d;
}
function T(a) {
if (-1 === a._status) {
var b = a._result;
b = b();
b.then(
function (b) {
if (0 === a._status || -1 === a._status) ((a._status = 1), (a._result = b));
},
function (b) {
if (0 === a._status || -1 === a._status) ((a._status = 2), (a._result = b));
},
);
-1 === a._status && ((a._status = 0), (a._result = b));
}
if (1 === a._status) return a._result.default;
throw a._result;
}
var U = { current: null },
V = { transition: null },
W = { ReactCurrentDispatcher: U, ReactCurrentBatchConfig: V, ReactCurrentOwner: K };
function X() {
throw Error("act(...) is not supported in production builds of React.");
}
react_production_min.Children = {
map: S,
forEach: function (a, b, e) {
S(
a,
function () {
b.apply(this, arguments);
},
e,
);
},
count: function (a) {
var b = 0;
S(a, function () {
b++;
});
return b;
},
toArray: function (a) {
return (
S(a, function (a) {
return a;
}) || []
);
},
only: function (a) {
if (!O(a)) throw Error("React.Children.only expected to receive a single React element child.");
return a;
},
};
react_production_min.Component = E;
react_production_min.Fragment = p;
react_production_min.Profiler = r;
react_production_min.PureComponent = G;
react_production_min.StrictMode = q;
react_production_min.Suspense = w;
react_production_min.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED = W;
react_production_min.act = X;
react_production_min.cloneElement = function (a, b, e) {
if (null === a || void 0 === a) throw Error("React.cloneElement(...): The argument must be a React element, but you passed " + a + ".");
var d = C({}, a.props),
c = a.key,
k = a.ref,
h = a._owner;
if (null != b) {
void 0 !== b.ref && ((k = b.ref), (h = K.current));
void 0 !== b.key && (c = "" + b.key);
if (a.type && a.type.defaultProps) var g = a.type.defaultProps;
for (f in b) J.call(b, f) && !L.hasOwnProperty(f) && (d[f] = void 0 === b[f] && void 0 !== g ? g[f] : b[f]);
}
var f = arguments.length - 2;
if (1 === f) d.children = e;
else if (1 < f) {
g = Array(f);
for (var m = 0; m < f; m++) g[m] = arguments[m + 2];
d.children = g;
}
return { $$typeof: l, type: a.type, key: c, ref: k, props: d, _owner: h };
};
react_production_min.createContext = function (a) {
a = { $$typeof: u, _currentValue: a, _currentValue2: a, _threadCount: 0, Provider: null, Consumer: null, _defaultValue: null, _globalName: null };
a.Provider = { $$typeof: t, _context: a };
return (a.Consumer = a);
};
react_production_min.createElement = M;
react_production_min.createFactory = function (a) {
var b = M.bind(null, a);
b.type = a;
return b;
};
react_production_min.createRef = function () {
return { current: null };
};
react_production_min.forwardRef = function (a) {
return { $$typeof: v, render: a };
};
react_production_min.isValidElement = O;
react_production_min.lazy = function (a) {
return { $$typeof: y, _payload: { _status: -1, _result: a }, _init: T };
};
react_production_min.memo = function (a, b) {
return { $$typeof: x, type: a, compare: void 0 === b ? null : b };
};
react_production_min.startTransition = function (a) {
var b = V.transition;
V.transition = {};
try {
a();
} finally {
V.transition = b;
}
};
react_production_min.unstable_act = X;
react_production_min.useCallback = function (a, b) {
return U.current.useCallback(a, b);
};
react_production_min.useContext = function (a) {
return U.current.useContext(a);
};
react_production_min.useDebugValue = function () {};
react_production_min.useDeferredValue = function (a) {
return U.current.useDeferredValue(a);
};
react_production_min.useEffect = function (a, b) {
return U.current.useEffect(a, b);
};
react_production_min.useId = function () {
return U.current.useId();
};
react_production_min.useImperativeHandle = function (a, b, e) {
return U.current.useImperativeHandle(a, b, e);
};
react_production_min.useInsertionEffect = function (a, b) {
return U.current.useInsertionEffect(a, b);
};
react_production_min.useLayoutEffect = function (a, b) {
return U.current.useLayoutEffect(a, b);
};
react_production_min.useMemo = function (a, b) {
return U.current.useMemo(a, b);
};
react_production_min.useReducer = function (a, b, e) {
return U.current.useReducer(a, b, e);
};
react_production_min.useRef = function (a) {
return U.current.useRef(a);
};
react_production_min.useState = function (a) {
return U.current.useState(a);
};
react_production_min.useSyncExternalStore = function (a, b, e) {
return U.current.useSyncExternalStore(a, b, e);
};
react_production_min.useTransition = function () {
return U.current.useTransition();
};
react_production_min.version = "18.3.1";
var l=Symbol.for("react.element"),n=Symbol.for("react.portal"),p=Symbol.for("react.fragment"),q=Symbol.for("react.strict_mode"),r=Symbol.for("react.profiler"),t=Symbol.for("react.provider"),u=Symbol.for("react.context"),v=Symbol.for("react.forward_ref"),w=Symbol.for("react.suspense"),x=Symbol.for("react.memo"),y=Symbol.for("react.lazy"),z=Symbol.iterator;function A(a){if(null===a||"object"!==typeof a)return null;a=z&&a[z]||a["@@iterator"];return "function"===typeof a?a:null}
var B={isMounted:function(){return false},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},C=Object.assign,D={};function E(a,b,e){this.props=a;this.context=b;this.refs=D;this.updater=e||B;}E.prototype.isReactComponent={};
E.prototype.setState=function(a,b){if("object"!==typeof a&&"function"!==typeof a&&null!=a)throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,a,b,"setState");};E.prototype.forceUpdate=function(a){this.updater.enqueueForceUpdate(this,a,"forceUpdate");};function F(){}F.prototype=E.prototype;function G(a,b,e){this.props=a;this.context=b;this.refs=D;this.updater=e||B;}var H=G.prototype=new F;
H.constructor=G;C(H,E.prototype);H.isPureReactComponent=true;var I=Array.isArray,J=Object.prototype.hasOwnProperty,K={current:null},L={key:true,ref:true,__self:true,__source:true};
function M(a,b,e){var d,c={},k=null,h=null;if(null!=b)for(d in void 0!==b.ref&&(h=b.ref),void 0!==b.key&&(k=""+b.key),b)J.call(b,d)&&!L.hasOwnProperty(d)&&(c[d]=b[d]);var g=arguments.length-2;if(1===g)c.children=e;else if(1<g){for(var f=Array(g),m=0;m<g;m++)f[m]=arguments[m+2];c.children=f;}if(a&&a.defaultProps)for(d in g=a.defaultProps,g) void 0===c[d]&&(c[d]=g[d]);return {$$typeof:l,type:a,key:k,ref:h,props:c,_owner:K.current}}
function N(a,b){return {$$typeof:l,type:a.type,key:b,ref:a.ref,props:a.props,_owner:a._owner}}function O(a){return "object"===typeof a&&null!==a&&a.$$typeof===l}function escape(a){var b={"=":"=0",":":"=2"};return "$"+a.replace(/[=:]/g,function(a){return b[a]})}var P=/\/+/g;function Q(a,b){return "object"===typeof a&&null!==a&&null!=a.key?escape(""+a.key):b.toString(36)}
function R(a,b,e,d,c){var k=typeof a;if("undefined"===k||"boolean"===k)a=null;var h=false;if(null===a)h=true;else switch(k){case "string":case "number":h=true;break;case "object":switch(a.$$typeof){case l:case n:h=true;}}if(h)return h=a,c=c(h),a=""===d?"."+Q(h,0):d,I(c)?(e="",null!=a&&(e=a.replace(P,"$&/")+"/"),R(c,b,e,"",function(a){return a})):null!=c&&(O(c)&&(c=N(c,e+(!c.key||h&&h.key===c.key?"":(""+c.key).replace(P,"$&/")+"/")+a)),b.push(c)),1;h=0;d=""===d?".":d+":";if(I(a))for(var g=0;g<a.length;g++){k=
a[g];var f=d+Q(k,g);h+=R(k,b,e,f,c);}else if(f=A(a),"function"===typeof f)for(a=f.call(a),g=0;!(k=a.next()).done;)k=k.value,f=d+Q(k,g++),h+=R(k,b,e,f,c);else if("object"===k)throw b=String(a),Error("Objects are not valid as a React child (found: "+("[object Object]"===b?"object with keys {"+Object.keys(a).join(", ")+"}":b)+"). If you meant to render a collection of children, use an array instead.");return h}
function S(a,b,e){if(null==a)return a;var d=[],c=0;R(a,d,"","",function(a){return b.call(e,a,c++)});return d}function T(a){if(-1===a._status){var b=a._result;b=b();b.then(function(b){if(0===a._status||-1===a._status)a._status=1,a._result=b;},function(b){if(0===a._status||-1===a._status)a._status=2,a._result=b;});-1===a._status&&(a._status=0,a._result=b);}if(1===a._status)return a._result.default;throw a._result;}
var U={current:null},V={transition:null},W={ReactCurrentDispatcher:U,ReactCurrentBatchConfig:V,ReactCurrentOwner:K};function X(){throw Error("act(...) is not supported in production builds of React.");}
react_production_min.Children={map:S,forEach:function(a,b,e){S(a,function(){b.apply(this,arguments);},e);},count:function(a){var b=0;S(a,function(){b++;});return b},toArray:function(a){return S(a,function(a){return a})||[]},only:function(a){if(!O(a))throw Error("React.Children.only expected to receive a single React element child.");return a}};react_production_min.Component=E;react_production_min.Fragment=p;react_production_min.Profiler=r;react_production_min.PureComponent=G;react_production_min.StrictMode=q;react_production_min.Suspense=w;
react_production_min.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED=W;react_production_min.act=X;
react_production_min.cloneElement=function(a,b,e){if(null===a||void 0===a)throw Error("React.cloneElement(...): The argument must be a React element, but you passed "+a+".");var d=C({},a.props),c=a.key,k=a.ref,h=a._owner;if(null!=b){ void 0!==b.ref&&(k=b.ref,h=K.current);void 0!==b.key&&(c=""+b.key);if(a.type&&a.type.defaultProps)var g=a.type.defaultProps;for(f in b)J.call(b,f)&&!L.hasOwnProperty(f)&&(d[f]=void 0===b[f]&&void 0!==g?g[f]:b[f]);}var f=arguments.length-2;if(1===f)d.children=e;else if(1<f){g=Array(f);
for(var m=0;m<f;m++)g[m]=arguments[m+2];d.children=g;}return {$$typeof:l,type:a.type,key:c,ref:k,props:d,_owner:h}};react_production_min.createContext=function(a){a={$$typeof:u,_currentValue:a,_currentValue2:a,_threadCount:0,Provider:null,Consumer:null,_defaultValue:null,_globalName:null};a.Provider={$$typeof:t,_context:a};return a.Consumer=a};react_production_min.createElement=M;react_production_min.createFactory=function(a){var b=M.bind(null,a);b.type=a;return b};react_production_min.createRef=function(){return {current:null}};
react_production_min.forwardRef=function(a){return {$$typeof:v,render:a}};react_production_min.isValidElement=O;react_production_min.lazy=function(a){return {$$typeof:y,_payload:{_status:-1,_result:a},_init:T}};react_production_min.memo=function(a,b){return {$$typeof:x,type:a,compare:void 0===b?null:b}};react_production_min.startTransition=function(a){var b=V.transition;V.transition={};try{a();}finally{V.transition=b;}};react_production_min.unstable_act=X;react_production_min.useCallback=function(a,b){return U.current.useCallback(a,b)};react_production_min.useContext=function(a){return U.current.useContext(a)};
react_production_min.useDebugValue=function(){};react_production_min.useDeferredValue=function(a){return U.current.useDeferredValue(a)};react_production_min.useEffect=function(a,b){return U.current.useEffect(a,b)};react_production_min.useId=function(){return U.current.useId()};react_production_min.useImperativeHandle=function(a,b,e){return U.current.useImperativeHandle(a,b,e)};react_production_min.useInsertionEffect=function(a,b){return U.current.useInsertionEffect(a,b)};react_production_min.useLayoutEffect=function(a,b){return U.current.useLayoutEffect(a,b)};
react_production_min.useMemo=function(a,b){return U.current.useMemo(a,b)};react_production_min.useReducer=function(a,b,e){return U.current.useReducer(a,b,e)};react_production_min.useRef=function(a){return U.current.useRef(a)};react_production_min.useState=function(a){return U.current.useState(a)};react_production_min.useSyncExternalStore=function(a,b,e){return U.current.useSyncExternalStore(a,b,e)};react_production_min.useTransition=function(){return U.current.useTransition()};react_production_min.version="18.3.1";
return react_production_min;
}
var hasRequiredReact;
function requireReact() {
function requireReact () {
if (hasRequiredReact) return react.exports;
hasRequiredReact = 1;
{
react.exports = requireReact_production_min();
react.exports = requireReact_production_min();
}
return react.exports;
}

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
import { r as requireReact } from "./index-DQGM2Mpm.js";
import { r as requireReact } from './index-DQGM2Mpm.js';
var jsxRuntime = { exports: {} };
var jsxRuntime = {exports: {}};
var reactJsxRuntime_production_min = {};
@ -16,40 +16,21 @@ var reactJsxRuntime_production_min = {};
var hasRequiredReactJsxRuntime_production_min;
function requireReactJsxRuntime_production_min() {
function requireReactJsxRuntime_production_min () {
if (hasRequiredReactJsxRuntime_production_min) return reactJsxRuntime_production_min;
hasRequiredReactJsxRuntime_production_min = 1;
var f = requireReact(),
k = Symbol.for("react.element"),
l = Symbol.for("react.fragment"),
m = Object.prototype.hasOwnProperty,
n = f.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,
p = { key: true, ref: true, __self: true, __source: true };
function q(c, a, g) {
var b,
d = {},
e = null,
h = null;
void 0 !== g && (e = "" + g);
void 0 !== a.key && (e = "" + a.key);
void 0 !== a.ref && (h = a.ref);
for (b in a) m.call(a, b) && !p.hasOwnProperty(b) && (d[b] = a[b]);
if (c && c.defaultProps) for (b in ((a = c.defaultProps), a)) void 0 === d[b] && (d[b] = a[b]);
return { $$typeof: k, type: c, key: e, ref: h, props: d, _owner: n.current };
}
reactJsxRuntime_production_min.Fragment = l;
reactJsxRuntime_production_min.jsx = q;
reactJsxRuntime_production_min.jsxs = q;
var f=requireReact(),k=Symbol.for("react.element"),l=Symbol.for("react.fragment"),m=Object.prototype.hasOwnProperty,n=f.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,p={key:true,ref:true,__self:true,__source:true};
function q(c,a,g){var b,d={},e=null,h=null;void 0!==g&&(e=""+g);void 0!==a.key&&(e=""+a.key);void 0!==a.ref&&(h=a.ref);for(b in a)m.call(a,b)&&!p.hasOwnProperty(b)&&(d[b]=a[b]);if(c&&c.defaultProps)for(b in a=c.defaultProps,a) void 0===d[b]&&(d[b]=a[b]);return {$$typeof:k,type:c,key:e,ref:h,props:d,_owner:n.current}}reactJsxRuntime_production_min.Fragment=l;reactJsxRuntime_production_min.jsx=q;reactJsxRuntime_production_min.jsxs=q;
return reactJsxRuntime_production_min;
}
var hasRequiredJsxRuntime;
function requireJsxRuntime() {
function requireJsxRuntime () {
if (hasRequiredJsxRuntime) return jsxRuntime.exports;
hasRequiredJsxRuntime = 1;
{
jsxRuntime.exports = requireReactJsxRuntime_production_min();
jsxRuntime.exports = requireReactJsxRuntime_production_min();
}
return jsxRuntime.exports;
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,88 +1,84 @@
const currentImports = {};
const exportSet = new Set(["Module", "__esModule", "default", "_export_sfc"]);
let moduleMap = {
"./editor": () => {
dynamicLoadingCss(["style-Bs2zy9jQ.css"], false, "./editor");
return __federation_import("./__federation_expose_Editor-BDCHVRx7.js").then((module) => (Object.keys(module).every((item) => exportSet.has(item)) ? () => module.default : () => module));
},
"./settings": () => {
dynamicLoadingCss(["style-Bs2zy9jQ.css"], false, "./settings");
return __federation_import("./__federation_expose_Settings-C7jFM1o4.js").then((module) => (Object.keys(module).every((item) => exportSet.has(item)) ? () => module.default : () => module));
},
};
const seen = {};
const dynamicLoadingCss = (cssFilePaths, dontAppendStylesToHead, exposeItemName) => {
const metaUrl = import.meta.url;
if (typeof metaUrl === "undefined") {
console.warn('The remote style takes effect only when the build.target option in the vite.config.ts file is higher than that of "es2020".');
return;
}
const exportSet = new Set(['Module', '__esModule', 'default', '_export_sfc']);
let moduleMap = {
"./editor":()=>{
dynamicLoadingCss(["style-Bs2zy9jQ.css"], false, './editor');
return __federation_import('./__federation_expose_Editor-DbTTOPh9.js').then(module =>Object.keys(module).every(item => exportSet.has(item)) ? () => module.default : () => module)},
"./settings":()=>{
dynamicLoadingCss(["style-Bs2zy9jQ.css"], false, './settings');
return __federation_import('./__federation_expose_Settings-C7jFM1o4.js').then(module =>Object.keys(module).every(item => exportSet.has(item)) ? () => module.default : () => module)},};
const seen = {};
const dynamicLoadingCss = (cssFilePaths, dontAppendStylesToHead, exposeItemName) => {
const metaUrl = import.meta.url;
if (typeof metaUrl === 'undefined') {
console.warn('The remote style takes effect only when the build.target option in the vite.config.ts file is higher than that of "es2020".');
return;
}
const curUrl = metaUrl.substring(0, metaUrl.lastIndexOf("remoteEntry.js"));
const base = "/";
("assets");
const curUrl = metaUrl.substring(0, metaUrl.lastIndexOf('remoteEntry.js'));
const base = '/';
'assets';
cssFilePaths.forEach((cssPath) => {
let href = "";
const baseUrl = base || curUrl;
if (baseUrl) {
const trimmer = {
trailing: (path) => (path.endsWith("/") ? path.slice(0, -1) : path),
leading: (path) => (path.startsWith("/") ? path.slice(1) : path),
};
const isAbsoluteUrl = (url) => url.startsWith("http") || url.startsWith("//");
cssFilePaths.forEach(cssPath => {
let href = '';
const baseUrl = base || curUrl;
if (baseUrl) {
const trimmer = {
trailing: (path) => (path.endsWith('/') ? path.slice(0, -1) : path),
leading: (path) => (path.startsWith('/') ? path.slice(1) : path)
};
const isAbsoluteUrl = (url) => url.startsWith('http') || url.startsWith('//');
const cleanBaseUrl = trimmer.trailing(baseUrl);
const cleanCssPath = trimmer.leading(cssPath);
const cleanCurUrl = trimmer.trailing(curUrl);
const cleanBaseUrl = trimmer.trailing(baseUrl);
const cleanCssPath = trimmer.leading(cssPath);
const cleanCurUrl = trimmer.trailing(curUrl);
if (isAbsoluteUrl(baseUrl)) {
href = [cleanBaseUrl, cleanCssPath].filter(Boolean).join("/");
} else {
if (cleanCurUrl.includes(cleanBaseUrl)) {
href = [cleanCurUrl, cleanCssPath].filter(Boolean).join("/");
} else {
href = [cleanCurUrl + cleanBaseUrl, cleanCssPath].filter(Boolean).join("/");
}
}
} else {
href = cssPath;
}
if (isAbsoluteUrl(baseUrl)) {
href = [cleanBaseUrl, cleanCssPath].filter(Boolean).join('/');
} else {
if (cleanCurUrl.includes(cleanBaseUrl)) {
href = [cleanCurUrl, cleanCssPath].filter(Boolean).join('/');
} else {
href = [cleanCurUrl + cleanBaseUrl, cleanCssPath].filter(Boolean).join('/');
}
}
} else {
href = cssPath;
}
if (dontAppendStylesToHead) {
const key = "css__calcomblock__" + exposeItemName;
window[key] = window[key] || [];
window[key].push(href);
return;
}
if (dontAppendStylesToHead) {
const key = 'css__calcomblock__' + exposeItemName;
window[key] = window[key] || [];
window[key].push(href);
return;
}
if (href in seen) return;
seen[href] = true;
if (href in seen) return;
seen[href] = true;
const element = document.createElement("link");
element.rel = "stylesheet";
element.href = href;
document.head.appendChild(element);
});
};
async function __federation_import(name) {
currentImports[name] ??= import(name);
return currentImports[name];
}
const get = (module) => {
if (!moduleMap[module]) throw new Error("Can not find remote module " + module);
return moduleMap[module]();
};
const init = (shareScope) => {
globalThis.__federation_shared__ = globalThis.__federation_shared__ || {};
Object.entries(shareScope).forEach(([key, value]) => {
for (const [versionKey, versionValue] of Object.entries(value)) {
const scope = versionValue.scope || "default";
globalThis.__federation_shared__[scope] = globalThis.__federation_shared__[scope] || {};
const shared = globalThis.__federation_shared__[scope];
(shared[key] = shared[key] || {})[versionKey] = versionValue;
}
});
};
const element = document.createElement('link');
element.rel = 'stylesheet';
element.href = href;
document.head.appendChild(element);
});
};
async function __federation_import(name) {
currentImports[name] ??= import(name);
return currentImports[name]
} const get =(module) => {
if(!moduleMap[module]) throw new Error('Can not find remote module ' + module)
return moduleMap[module]();
};
const init =(shareScope) => {
globalThis.__federation_shared__= globalThis.__federation_shared__|| {};
Object.entries(shareScope).forEach(([key, value]) => {
for (const [versionKey, versionValue] of Object.entries(value)) {
const scope = versionValue.scope || 'default';
globalThis.__federation_shared__[scope] = globalThis.__federation_shared__[scope] || {};
const shared= globalThis.__federation_shared__[scope];
(shared[key] = shared[key]||{})[versionKey] = versionValue;
}
});
};
export { dynamicLoadingCss, get, init };

File diff suppressed because one or more lines are too long

View File

@ -1,7 +1,7 @@
import { r as requireReact } from "./index-DQGM2Mpm.js";
import { X as requireShim } from "./index-Bs--Ol2m.js";
import { r as requireReact } from './index-DQGM2Mpm.js';
import { X as requireShim } from './index-Bs--Ol2m.js';
var withSelector = { exports: {} };
var withSelector = {exports: {}};
var withSelector_production = {};
@ -17,84 +17,93 @@ var withSelector_production = {};
var hasRequiredWithSelector_production;
function requireWithSelector_production() {
function requireWithSelector_production () {
if (hasRequiredWithSelector_production) return withSelector_production;
hasRequiredWithSelector_production = 1;
var React = requireReact(),
shim = requireShim();
shim = requireShim();
function is(x, y) {
return (x === y && (0 !== x || 1 / x === 1 / y)) || (x !== x && y !== y);
return (x === y && (0 !== x || 1 / x === 1 / y)) || (x !== x && y !== y);
}
var objectIs = "function" === typeof Object.is ? Object.is : is,
useSyncExternalStore = shim.useSyncExternalStore,
useRef = React.useRef,
useEffect = React.useEffect,
useMemo = React.useMemo,
useDebugValue = React.useDebugValue;
withSelector_production.useSyncExternalStoreWithSelector = function (subscribe, getSnapshot, getServerSnapshot, selector, isEqual) {
var instRef = useRef(null);
if (null === instRef.current) {
var inst = { hasValue: false, value: null };
instRef.current = inst;
} else inst = instRef.current;
instRef = useMemo(
function () {
function memoizedSelector(nextSnapshot) {
if (!hasMemo) {
hasMemo = true;
memoizedSnapshot = nextSnapshot;
nextSnapshot = selector(nextSnapshot);
if (void 0 !== isEqual && inst.hasValue) {
var currentSelection = inst.value;
if (isEqual(currentSelection, nextSnapshot)) return (memoizedSelection = currentSelection);
}
return (memoizedSelection = nextSnapshot);
}
currentSelection = memoizedSelection;
if (objectIs(memoizedSnapshot, nextSnapshot)) return currentSelection;
var nextSelection = selector(nextSnapshot);
if (void 0 !== isEqual && isEqual(currentSelection, nextSelection)) return ((memoizedSnapshot = nextSnapshot), currentSelection);
memoizedSnapshot = nextSnapshot;
return (memoizedSelection = nextSelection);
}
var hasMemo = false,
memoizedSnapshot,
memoizedSelection,
maybeGetServerSnapshot = void 0 === getServerSnapshot ? null : getServerSnapshot;
return [
function () {
return memoizedSelector(getSnapshot());
},
null === maybeGetServerSnapshot ? void 0 : (
function () {
return memoizedSelector(maybeGetServerSnapshot());
}
),
];
},
[getSnapshot, getServerSnapshot, selector, isEqual],
);
var value = useSyncExternalStore(subscribe, instRef[0], instRef[1]);
useEffect(
function () {
inst.hasValue = true;
inst.value = value;
},
[value],
);
useDebugValue(value);
return value;
useSyncExternalStore = shim.useSyncExternalStore,
useRef = React.useRef,
useEffect = React.useEffect,
useMemo = React.useMemo,
useDebugValue = React.useDebugValue;
withSelector_production.useSyncExternalStoreWithSelector = function (
subscribe,
getSnapshot,
getServerSnapshot,
selector,
isEqual
) {
var instRef = useRef(null);
if (null === instRef.current) {
var inst = { hasValue: false, value: null };
instRef.current = inst;
} else inst = instRef.current;
instRef = useMemo(
function () {
function memoizedSelector(nextSnapshot) {
if (!hasMemo) {
hasMemo = true;
memoizedSnapshot = nextSnapshot;
nextSnapshot = selector(nextSnapshot);
if (void 0 !== isEqual && inst.hasValue) {
var currentSelection = inst.value;
if (isEqual(currentSelection, nextSnapshot))
return (memoizedSelection = currentSelection);
}
return (memoizedSelection = nextSnapshot);
}
currentSelection = memoizedSelection;
if (objectIs(memoizedSnapshot, nextSnapshot)) return currentSelection;
var nextSelection = selector(nextSnapshot);
if (void 0 !== isEqual && isEqual(currentSelection, nextSelection))
return (memoizedSnapshot = nextSnapshot), currentSelection;
memoizedSnapshot = nextSnapshot;
return (memoizedSelection = nextSelection);
}
var hasMemo = false,
memoizedSnapshot,
memoizedSelection,
maybeGetServerSnapshot =
void 0 === getServerSnapshot ? null : getServerSnapshot;
return [
function () {
return memoizedSelector(getSnapshot());
},
null === maybeGetServerSnapshot
? void 0
: function () {
return memoizedSelector(maybeGetServerSnapshot());
}
];
},
[getSnapshot, getServerSnapshot, selector, isEqual]
);
var value = useSyncExternalStore(subscribe, instRef[0], instRef[1]);
useEffect(
function () {
inst.hasValue = true;
inst.value = value;
},
[value]
);
useDebugValue(value);
return value;
};
return withSelector_production;
}
var hasRequiredWithSelector;
function requireWithSelector() {
function requireWithSelector () {
if (hasRequiredWithSelector) return withSelector.exports;
hasRequiredWithSelector = 1;
{
withSelector.exports = requireWithSelector_production();
withSelector.exports = requireWithSelector_production();
}
return withSelector.exports;
}

2
web/dist/index.html vendored
View File

@ -2,7 +2,7 @@
<html>
<head>
<title>CalcomBlock Plugin</title>
<link rel="stylesheet" crossorigin href="/assets/style-Bs2zy9jQ.css" />
<link rel="stylesheet" crossorigin href="/assets/style-Bs2zy9jQ.css">
</head>
<body>
<!-- This is a placeholder for Module Federation remote entry -->

View File

@ -318,6 +318,20 @@ function CalcomBlockEditor({ content, onChange }: BlockEditorProps) {
aria-label={t("calcom.editor.showTimezone.label", "Show Timezone Selector")}
/>
</div>
<div className="flex items-center justify-between rounded-lg border p-3">
<div className="space-y-0.5">
<Label>{t("calcom.editor.captchaEnabled.label", "Enable captcha")}</Label>
<p className="text-xs text-muted-foreground">
{t("calcom.editor.captchaEnabled.help", "Require visitors to solve a privacy-friendly proof-of-work captcha before booking (spam protection).")}
</p>
</div>
<Switch
checked={getBool("captchaEnabled", false)}
onCheckedChange={(v) => handleFieldChange("captchaEnabled", v)}
aria-label={t("calcom.editor.captchaEnabled.label", "Enable captcha")}
/>
</div>
</CardContent>
</Card>

View File

@ -18,5 +18,10 @@
"react-dom": "^18.3.1",
"typescript": "^5.9.3",
"vite": "^6.4.1"
},
"pnpm": {
"onlyBuiltDependencies": [
"esbuild"
]
}
}