From e90390008800a6e5d425b04d5060f4fc47cfdfcd Mon Sep 17 00:00:00 2001 From: Evan Hu Date: Fri, 18 Sep 2026 23:12:02 +0900 Subject: [PATCH 1/2] refactor(api): extract the webhook routes into internal/webhooks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit internal/project is 172 Go files and 37.7k lines because it accumulated everything the session API serves, not because those things belong together. The webhook routes are the one group that can leave without dragging anything behind: nothing outside webhook_handler.go referenced a single thing it declared, the webhooks and webhook_logs models are its own, its four tests are pure unit tests over webhookJSON and the url checks that build no router, and the three Settings fields it reads (WebhookAllowedIPs, WebhookAllowedHosts, WebhookDisallowedDomains) were read by no other file in the package. Moving it is a rename plus a package clause, which is what makes it worth doing first: it proves the seam is real on the cheapest possible subject rather than on a group that would need a query engine duplicated to follow it. The small kit the handlers need — authenticated, requireWorkspaceRole and its role lookup, invalidDetail, internalError, isUniqueViolation, newUUID — is copied byte-for-byte into the new package rather than exported from internal/project. That is this repo's established pattern and not laziness: authenticated, internalError, newUUID and decodeJSON each already exist five times over across internal/space, internal/workspace, internal/externalapi, internal/project and internal/user, and the file_assets model seven times. Ninety duplicated lines buys a package that imports nothing from a 37k-line one; exporting instead would have coupled the two permanently for the same ninety lines. Only roleAdmin is copied of the three role constants, since every route here is admin only and the other two would be dead. Route behaviour is unchanged and the counts say so: internal/project's inventory test goes from 304 registrations to 297 and the new TestWebhookRouteInventory asserts the missing 7, so the composed router still serves exactly what it did. TestEveryCutOverPathIsFullyServed in internal/server is the independent check — the community Caddyfile cuts over all of /api/*, so it requires every /api/ row of the 640-row Django fixture to be served, and a webhook path lost in the move would fail it by name. apps/api/README.md records why the package exists, next to the module note it belongs to, so the next reader does not have to re-derive it from the file listing. --- apps/api/README.md | 6 + apps/api/internal/project/handler.go | 6 - apps/api/internal/project/handler_test.go | 7 -- apps/api/internal/server/router.go | 25 ++-- apps/api/internal/webhooks/handler.go | 116 ++++++++++++++++++ apps/api/internal/webhooks/handler_test.go | 32 +++++ .../{project => webhooks}/webhook_handler.go | 2 +- .../webhook_handler_test.go | 2 +- 8 files changed, 171 insertions(+), 25 deletions(-) create mode 100644 apps/api/internal/webhooks/handler.go create mode 100644 apps/api/internal/webhooks/handler_test.go rename apps/api/internal/{project => webhooks}/webhook_handler.go (99%) rename apps/api/internal/{project => webhooks}/webhook_handler_test.go (99%) diff --git a/apps/api/README.md b/apps/api/README.md index bfc3f8c2..db7084ca 100644 --- a/apps/api/README.md +++ b/apps/api/README.md @@ -767,6 +767,12 @@ The chart endpoints — `analytics/`, `saved-analytic-view/`, `default-analytics The seven routes under `webhooks/` and `webhook-logs/`. Admin only, at the workspace level. +### They have their own package + +They live in `internal/webhooks` rather than in `internal/project`, which is where they were first written. The charter boundary in this repo is by application rather than by url prefix — same session, same error vocabulary, so this is still the session API — but `internal/project` is that API's **work-item** surface, and these routes touch neither a work item nor a project: the `webhooks` and `webhook_logs` models are theirs alone, nothing outside the file referenced anything it declared, and the three allowlists that decide which urls a workspace may be told to call (`WEBHOOK_ALLOWED_IPS`, `WEBHOOK_ALLOWED_HOSTS`, `WEBHOOK_DISALLOWED_DOMAINS`) are read by nothing else in it. The move is a rename plus a package clause; every path, status and body is the one Django answered. + +The kit the handlers need — `authenticated`, `requireWorkspaceRole` and its role lookup, `invalidDetail`, `internalError`, `isUniqueViolation`, `newUUID` — is **copied** rather than imported. That is deliberate and it is what every handler package here already does: `authenticated`, `internalError`, `newUUID` and `decodeJSON` each exist five times over across `internal/space`, `internal/workspace`, `internal/externalapi`, `internal/project` and `internal/user`, and the `file_assets` model seven times. Ninety duplicated lines is the price of a package that imports nothing from a 37k-line one, and the copies are kept byte-identical so diffing the two shows nothing. + ### The secret is shown twice and no more often On creation and on a regenerate. Everywhere else it is dropped, and the **only** thing dropping it is a context flag. diff --git a/apps/api/internal/project/handler.go b/apps/api/internal/project/handler.go index 409650c8..c287da0f 100644 --- a/apps/api/internal/project/handler.go +++ b/apps/api/internal/project/handler.go @@ -15,7 +15,6 @@ import ( "github.com/yldm-tech/pace/apps/api/internal/projects" "github.com/yldm-tech/pace/apps/api/internal/storage" "gorm.io/gorm" - "net/netip" ) const ( @@ -47,10 +46,6 @@ type Settings struct { Environment map[string]string // LLMBaseURL is where the assistant's completions are asked for. The python client points at OpenAI unless it is told otherwise. LLMBaseURL string - // The three webhook settings, which together decide which urls a workspace may be told to call. - WebhookAllowedIPs []netip.Prefix - WebhookAllowedHosts []string - WebhookDisallowedDomains []string // SecretKey is what an encrypted configuration value is read back with. SecretKey string } @@ -194,7 +189,6 @@ func (handler *Handler) Register(router gin.IRouter) { handler.registerIssueSearchRoutes(router) handler.registerGlobalSearchRoutes(router) handler.registerEntitySearchRoutes(router) - handler.registerWebhookRoutes(router) handler.registerAnalyticViewRoutes(router) handler.registerAnalyticsRoutes(router) handler.registerAnalyticsSummaryRoutes(router) diff --git a/apps/api/internal/project/handler_test.go b/apps/api/internal/project/handler_test.go index 4c6334f4..b6753368 100644 --- a/apps/api/internal/project/handler_test.go +++ b/apps/api/internal/project/handler_test.go @@ -128,7 +128,6 @@ func TestProjectRouteInventory(t *testing.T) { "GET /api/workspaces/:slug/projects/:id/search-issues/": true, "GET /api/workspaces/:slug/search/": true, "GET /api/workspaces/:slug/entity-search/": true, - "GET /api/workspaces/:slug/webhooks/": true, "GET /api/workspaces/:slug/analytic-view/": true, "GET /api/workspaces/:slug/analytics/": true, "GET /api/workspaces/:slug/default-analytics/": true, @@ -139,12 +138,6 @@ func TestProjectRouteInventory(t *testing.T) { "PATCH /api/workspaces/:slug/analytic-view/:view/": true, "DELETE /api/workspaces/:slug/analytic-view/:view/": true, "POST /api/workspaces/:slug/export-analytics/": true, - "POST /api/workspaces/:slug/webhooks/": true, - "GET /api/workspaces/:slug/webhooks/:webhook/": true, - "PATCH /api/workspaces/:slug/webhooks/:webhook/": true, - "DELETE /api/workspaces/:slug/webhooks/:webhook/": true, - "POST /api/workspaces/:slug/webhooks/:webhook/regenerate/": true, - "GET /api/workspaces/:slug/webhook-logs/:webhook/": true, "GET /api/workspaces/:slug/projects/:id/intake-issues/": true, "POST /api/workspaces/:slug/projects/:id/intake-issues/": true, "GET /api/workspaces/:slug/projects/:id/intake-issues/:issue/": true, diff --git a/apps/api/internal/server/router.go b/apps/api/internal/server/router.go index aa8eea50..ce359f82 100644 --- a/apps/api/internal/server/router.go +++ b/apps/api/internal/server/router.go @@ -14,6 +14,7 @@ import ( spaceapi "github.com/yldm-tech/pace/apps/api/internal/space" "github.com/yldm-tech/pace/apps/api/internal/storage" userapi "github.com/yldm-tech/pace/apps/api/internal/user" + webhooksapi "github.com/yldm-tech/pace/apps/api/internal/webhooks" workspaceapi "github.com/yldm-tech/pace/apps/api/internal/workspace" "gorm.io/gorm" ) @@ -142,16 +143,13 @@ func NewRouter(dependencies Dependencies) *gin.Engine { } workspaceHandler.Register(router) projectHandler := projectapi.NewHandler(dependencies.Database, sessions, projectapi.Settings{ - AppBaseURL: dependencies.AuthSettings.AppBaseURL, - WebURL: dependencies.AuthSettings.WebURL, - FileSizeLimit: dependencies.AuthSettings.FileSizeLimit, - WebhookAllowedIPs: dependencies.AuthSettings.WebhookAllowedIPs, - WebhookAllowedHosts: dependencies.AuthSettings.WebhookAllowedHosts, - WebhookDisallowedDomains: dependencies.AuthSettings.WebhookDisallowedDomains, - SecretKey: dependencies.AuthSettings.SecretKey, - SkipEnvironmentConfig: dependencies.AuthSkipEnvironmentConfig, - Environment: dependencies.AuthSettings.Environment, - LLMBaseURL: dependencies.LLMBaseURL, + AppBaseURL: dependencies.AuthSettings.AppBaseURL, + WebURL: dependencies.AuthSettings.WebURL, + FileSizeLimit: dependencies.AuthSettings.FileSizeLimit, + SecretKey: dependencies.AuthSettings.SecretKey, + SkipEnvironmentConfig: dependencies.AuthSkipEnvironmentConfig, + Environment: dependencies.AuthSettings.Environment, + LLMBaseURL: dependencies.LLMBaseURL, }) if err == nil { projectHandler.SetStorage(attachmentStore) @@ -163,6 +161,13 @@ func NewRouter(dependencies Dependencies) *gin.Engine { projectHandler.SetCache(auth.NewRedisCacheInvalidator(dependencies.AuthRedis)) } projectHandler.Register(router) + // The webhook routes are session-authenticated like the project ones and read the same auth settings, but they share nothing else with the work-item surface, so they are their own package. + webhooksHandler := webhooksapi.NewHandler(dependencies.Database, sessions, webhooksapi.Settings{ + WebhookAllowedIPs: dependencies.AuthSettings.WebhookAllowedIPs, + WebhookAllowedHosts: dependencies.AuthSettings.WebhookAllowedHosts, + WebhookDisallowedDomains: dependencies.AuthSettings.WebhookDisallowedDomains, + }) + webhooksHandler.Register(router) spaceHandler := spaceapi.NewHandler(dependencies.Database) spaceHandler.SetSessions(sessions) spaceHandler.SetFileSizeLimit(dependencies.AuthSettings.FileSizeLimit) diff --git a/apps/api/internal/webhooks/handler.go b/apps/api/internal/webhooks/handler.go new file mode 100644 index 00000000..a8095b35 --- /dev/null +++ b/apps/api/internal/webhooks/handler.go @@ -0,0 +1,116 @@ +// Package webhooks serves the seven routes under `webhooks/` and `webhook-logs/`: where a workspace wants to be told about things, and which things. +// +// It is part of the session API rather than a third application — same cookie, same error vocabulary — but it is its own package because it shares nothing with the work-item surface `internal/project` owns. Its two models are its own, it answers no project-scoped path, and the three allowlists that decide which urls a workspace may be told to call are read by nothing else. The delivery side of the same chain lives in `internal/worker`; this is only the registration surface. +// +// The small kit below — authenticated, the role constants, workspaceRole, requireWorkspaceRole, invalidDetail, internalError, isUniqueViolation and newUUID — is copied from `internal/project` rather than imported from it. That is this repo's pattern for a handler package and not an oversight: `authenticated`, `internalError`, `newUUID` and `decodeJSON` each already exist five times over in internal/space, internal/workspace, internal/externalapi, internal/project and internal/user, and the alternative — exporting them from a 37k-line package — would couple the two packages together for the sake of ninety lines. Keep the copies byte-identical so a reader diffing the two sees no difference. +package webhooks + +import ( + "context" + "crypto/rand" + "fmt" + "net/http" + "net/netip" + "strings" + "time" + + "github.com/gin-gonic/gin" + "github.com/yldm-tech/pace/apps/api/internal/auth" + "gorm.io/gorm" +) + +// roleAdmin is WorkspaceMember.Role's administrator value. Every route here is admin only, so it is the only one of the three this package has a use for. +const roleAdmin = 20 + +type Settings struct { + // The three webhook settings, which together decide which urls a workspace may be told to call. + WebhookAllowedIPs []netip.Prefix + WebhookAllowedHosts []string + WebhookDisallowedDomains []string +} + +type Handler struct { + db *gorm.DB + sessions *auth.SessionManager + settings Settings + clock func() time.Time +} + +func NewHandler(db *gorm.DB, sessions *auth.SessionManager, settings Settings) *Handler { + return &Handler{db: db, sessions: sessions, settings: settings, clock: time.Now} +} + +func (handler *Handler) Register(router gin.IRouter) { + handler.registerWebhookRoutes(router) +} + +func (handler *Handler) authenticated(next func(*gin.Context, *auth.User)) gin.HandlerFunc { + return func(c *gin.Context) { + if handler.sessions == nil { + c.JSON(http.StatusUnauthorized, gin.H{"detail": "Authentication credentials were not provided."}) + return + } + user, _, err := handler.sessions.Authenticate(c.Request.Context(), c.Request, c.Writer) + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"detail": "Authentication credentials were not provided."}) + return + } + next(c, user) + } +} + +func (handler *Handler) workspaceRole(ctx context.Context, slug, userID string) (int, error) { + var role *int + err := handler.db.WithContext(ctx).Table("workspace_members wm"). + Joins("JOIN workspaces w ON w.id = wm.workspace_id"). + Where("w.slug = ? AND wm.member_id = ? AND wm.is_active = TRUE AND wm.deleted_at IS NULL", slug, userID). + Select("wm.role").Limit(1).Scan(&role).Error + if err != nil { + return 0, err + } + if role == nil { + return 0, nil + } + return *role, nil +} + +func (handler *Handler) requireWorkspaceRole(c *gin.Context, user *auth.User, allowed ...int) bool { + role, err := handler.workspaceRole(c.Request.Context(), c.Param("slug"), user.ID) + if err != nil { + handler.internalError(c, err) + return false + } + for _, candidate := range allowed { + if role == candidate { + return true + } + } + c.JSON(http.StatusForbidden, gin.H{"error": "You don't have the required permissions."}) + return false +} + +func (handler *Handler) invalidDetail(c *gin.Context) { + c.JSON(http.StatusBadRequest, gin.H{"error": "Please provide valid detail"}) +} + +func (handler *Handler) internalError(c *gin.Context, err error) { + if err != nil { + c.Error(err) + } + c.JSON(http.StatusInternalServerError, gin.H{"error": "Something went wrong please try again later"}) +} + +func isUniqueViolation(err error) bool { + lowered := strings.ToLower(err.Error()) + return strings.Contains(lowered, "duplicate key") || strings.Contains(lowered, "unique constraint") +} + +func newUUID() (string, error) { + value := make([]byte, 16) + if _, err := rand.Read(value); err != nil { + return "", err + } + value[6] = (value[6] & 0x0f) | 0x40 + value[8] = (value[8] & 0x3f) | 0x80 + return fmt.Sprintf("%08x-%04x-%04x-%04x-%012x", value[0:4], value[4:6], value[6:8], value[8:10], value[10:16]), nil +} diff --git a/apps/api/internal/webhooks/handler_test.go b/apps/api/internal/webhooks/handler_test.go new file mode 100644 index 00000000..e8badbc7 --- /dev/null +++ b/apps/api/internal/webhooks/handler_test.go @@ -0,0 +1,32 @@ +package webhooks + +import ( + "testing" + + "github.com/gin-gonic/gin" +) + +func TestWebhookRouteInventory(t *testing.T) { + gin.SetMode(gin.TestMode) + router := gin.New() + NewHandler(nil, nil, Settings{}).Register(router) + expected := map[string]bool{ + "GET /api/workspaces/:slug/webhooks/": true, + "POST /api/workspaces/:slug/webhooks/": true, + "GET /api/workspaces/:slug/webhooks/:webhook/": true, + "PATCH /api/workspaces/:slug/webhooks/:webhook/": true, + "DELETE /api/workspaces/:slug/webhooks/:webhook/": true, + "POST /api/workspaces/:slug/webhooks/:webhook/regenerate/": true, + "GET /api/workspaces/:slug/webhook-logs/:webhook/": true, + } + for _, route := range router.Routes() { + key := route.Method + " " + route.Path + if !expected[key] { + t.Fatalf("unexpected webhook route %s", key) + } + delete(expected, key) + } + if len(expected) != 0 { + t.Fatalf("missing webhook routes: %#v", expected) + } +} diff --git a/apps/api/internal/project/webhook_handler.go b/apps/api/internal/webhooks/webhook_handler.go similarity index 99% rename from apps/api/internal/project/webhook_handler.go rename to apps/api/internal/webhooks/webhook_handler.go index 2436954f..1e738e12 100644 --- a/apps/api/internal/project/webhook_handler.go +++ b/apps/api/internal/webhooks/webhook_handler.go @@ -1,4 +1,4 @@ -package project +package webhooks import ( "crypto/rand" diff --git a/apps/api/internal/project/webhook_handler_test.go b/apps/api/internal/webhooks/webhook_handler_test.go similarity index 99% rename from apps/api/internal/project/webhook_handler_test.go rename to apps/api/internal/webhooks/webhook_handler_test.go index be36815b..04f0bfcd 100644 --- a/apps/api/internal/project/webhook_handler_test.go +++ b/apps/api/internal/webhooks/webhook_handler_test.go @@ -1,4 +1,4 @@ -package project +package webhooks import ( "strings" From a24dbf1fd91fe676410c87cf4a176082f2a95056 Mon Sep 17 00:00:00 2001 From: Evan Hu Date: Fri, 18 Sep 2026 23:49:55 +0900 Subject: [PATCH 2/2] chore: bump the workspace version, which check-version requires of every PR --- apps/admin/package.json | 2 +- apps/space/package.json | 2 +- apps/web/package.json | 2 +- package.json | 2 +- packages/codemods/package.json | 2 +- packages/constants/package.json | 2 +- packages/editor/package.json | 2 +- packages/hooks/package.json | 2 +- packages/i18n/package.json | 2 +- packages/propel/package.json | 2 +- packages/services/package.json | 2 +- packages/shared-state/package.json | 2 +- packages/tailwind-config/package.json | 2 +- packages/types/package.json | 2 +- packages/typescript-config/package.json | 2 +- packages/ui/package.json | 2 +- packages/utils/package.json | 2 +- 17 files changed, 17 insertions(+), 17 deletions(-) diff --git a/apps/admin/package.json b/apps/admin/package.json index 1a415ed9..5b339758 100644 --- a/apps/admin/package.json +++ b/apps/admin/package.json @@ -1,6 +1,6 @@ { "name": "admin", - "version": "1.4.35", + "version": "1.4.36", "private": true, "description": "Admin UI for Pace", "license": "AGPL-3.0", diff --git a/apps/space/package.json b/apps/space/package.json index 20139853..bcfff121 100644 --- a/apps/space/package.json +++ b/apps/space/package.json @@ -1,6 +1,6 @@ { "name": "space", - "version": "1.4.35", + "version": "1.4.36", "private": true, "license": "AGPL-3.0", "type": "module", diff --git a/apps/web/package.json b/apps/web/package.json index 976e0d6b..c010bcee 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "web", - "version": "1.4.35", + "version": "1.4.36", "private": true, "license": "AGPL-3.0", "type": "module", diff --git a/package.json b/package.json index 17b03a5d..40dd2525 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pace", - "version": "1.4.35", + "version": "1.4.36", "private": true, "description": "Project management that keeps pace with the work", "license": "AGPL-3.0", diff --git a/packages/codemods/package.json b/packages/codemods/package.json index 759f42a7..45ea3f64 100644 --- a/packages/codemods/package.json +++ b/packages/codemods/package.json @@ -1,6 +1,6 @@ { "name": "@pace/codemods", - "version": "1.4.35", + "version": "1.4.36", "private": true, "scripts": { "check:format": "oxfmt --check .", diff --git a/packages/constants/package.json b/packages/constants/package.json index d82b7b66..76a427eb 100644 --- a/packages/constants/package.json +++ b/packages/constants/package.json @@ -1,6 +1,6 @@ { "name": "@pace/constants", - "version": "1.4.35", + "version": "1.4.36", "private": true, "license": "AGPL-3.0", "type": "module", diff --git a/packages/editor/package.json b/packages/editor/package.json index f79902bd..6f2b7a04 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -1,6 +1,6 @@ { "name": "@pace/editor", - "version": "1.4.35", + "version": "1.4.36", "private": true, "description": "Core Editor that powers Pace", "keywords": [ diff --git a/packages/hooks/package.json b/packages/hooks/package.json index da746818..46ed80c9 100644 --- a/packages/hooks/package.json +++ b/packages/hooks/package.json @@ -1,6 +1,6 @@ { "name": "@pace/hooks", - "version": "1.4.35", + "version": "1.4.36", "private": true, "description": "React hooks that are shared across multiple apps internally", "license": "AGPL-3.0", diff --git a/packages/i18n/package.json b/packages/i18n/package.json index 8ddc7a6a..fae25017 100644 --- a/packages/i18n/package.json +++ b/packages/i18n/package.json @@ -1,6 +1,6 @@ { "name": "@pace/i18n", - "version": "1.4.35", + "version": "1.4.36", "private": true, "description": "I18n shared across multiple apps internally", "license": "AGPL-3.0", diff --git a/packages/propel/package.json b/packages/propel/package.json index d49584d1..a8a98136 100644 --- a/packages/propel/package.json +++ b/packages/propel/package.json @@ -1,6 +1,6 @@ { "name": "@pace/propel", - "version": "1.4.35", + "version": "1.4.36", "private": true, "license": "AGPL-3.0", "type": "module", diff --git a/packages/services/package.json b/packages/services/package.json index 5be54051..691d598f 100644 --- a/packages/services/package.json +++ b/packages/services/package.json @@ -1,6 +1,6 @@ { "name": "@pace/services", - "version": "1.4.35", + "version": "1.4.36", "private": true, "license": "AGPL-3.0", "type": "module", diff --git a/packages/shared-state/package.json b/packages/shared-state/package.json index 33674806..96720a0a 100644 --- a/packages/shared-state/package.json +++ b/packages/shared-state/package.json @@ -1,6 +1,6 @@ { "name": "@pace/shared-state", - "version": "1.4.35", + "version": "1.4.36", "private": true, "description": "Shared state shared across multiple apps internally", "license": "AGPL-3.0", diff --git a/packages/tailwind-config/package.json b/packages/tailwind-config/package.json index 728d404c..4ce960a2 100644 --- a/packages/tailwind-config/package.json +++ b/packages/tailwind-config/package.json @@ -1,6 +1,6 @@ { "name": "@pace/tailwind-config", - "version": "1.4.35", + "version": "1.4.36", "private": true, "description": "common tailwind configuration across monorepo", "license": "AGPL-3.0", diff --git a/packages/types/package.json b/packages/types/package.json index 0f9f9bf3..6d538abe 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@pace/types", - "version": "1.4.35", + "version": "1.4.36", "private": true, "license": "AGPL-3.0", "type": "module", diff --git a/packages/typescript-config/package.json b/packages/typescript-config/package.json index 66c97bcd..e16decc6 100644 --- a/packages/typescript-config/package.json +++ b/packages/typescript-config/package.json @@ -1,6 +1,6 @@ { "name": "@pace/typescript-config", - "version": "1.4.35", + "version": "1.4.36", "private": true, "license": "AGPL-3.0", "files": [ diff --git a/packages/ui/package.json b/packages/ui/package.json index 0279cb5a..82c69db0 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@pace/ui", - "version": "1.4.35", + "version": "1.4.36", "private": true, "description": "UI components shared across multiple apps internally", "license": "AGPL-3.0", diff --git a/packages/utils/package.json b/packages/utils/package.json index 7b0ef7b6..0193eb31 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -1,6 +1,6 @@ { "name": "@pace/utils", - "version": "1.4.35", + "version": "1.4.36", "private": true, "description": "Helper functions shared across multiple apps internally", "license": "AGPL-3.0",