From 1ddd1210c503c0f353095d461fc461a12c943543 Mon Sep 17 00:00:00 2001 From: JOY <5027251+JOY@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:04:18 +0700 Subject: [PATCH 1/4] =?UTF-8?q?fix(deps):=20remove=20protobufjs=20override?= =?UTF-8?q?=20=E2=80=94=207.6.6=20breaks=20@grpc/grpc-js=20gRPC=20handshak?= =?UTF-8?q?e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The protobufjs override to ^7.5.5 (resolves 7.6.6) has a breaking change with @grpc/grpc-js@1.14.3 — gRPC handshake fails (error 14 UNAVAILABLE, 'Failed to connect before the deadline') even though TCP connects. This breaks ALL Temporal workflow creation (startWorkflow silently fails via D6 pattern). --- package.json | 1 - pnpm-lock.yaml | 1 - 2 files changed, 2 deletions(-) diff --git a/package.json b/package.json index 848251f570..ec3393edbd 100644 --- a/package.json +++ b/package.json @@ -326,7 +326,6 @@ "@types/react-dom": "19.1.6", "tar": "^7.5.19", "form-data": "^2.5.4", - "protobufjs": "^7.5.5", "shell-quote": "^1.8.4", "fast-uri": "^3.1.6", "hono": "^4.12.34", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b313a3f59d..a05131a1eb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,7 +14,6 @@ overrides: '@types/react-dom': 19.1.6 tar: ^7.5.19 form-data: ^2.5.4 - protobufjs: ^7.5.5 shell-quote: ^1.8.4 fast-uri: ^3.1.6 hono: ^4.12.34 From ee50fe657d85faaff566667ab41a78aaa1941369 Mon Sep 17 00:00:00 2001 From: JOY <5027251+JOY@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:12:08 +0700 Subject: [PATCH 2/4] fix(branding): close upstream endpoint leaks in customer-facing UI, SDK and MCP The branding guard only scanned for AGPL attribution (repo, image, author), so it reported "0 strict leaks" while runtime endpoints still pointed at upstream infrastructure: - the Developers page rendered `export POSTIZ_API_KEY=` with no POSTIZ_API_URL, so the upstream CLI sent Crove credentials to api.postiz.com and posted to the wrong account - `postiz auth:login` ran a device flow against cli-auth.postiz.com, signing customers into the upstream cloud - the rebranded @crove/node SDK still defaulted _path to api.postiz.com, and its README still documented the old @postiz/node package name - the MCP server announced itself as "Postiz MCP" on every OAuth path, including /mcp-oauth-dynamic which DOSClaw uses - "Add to Claude" pointed at the upstream directory listing and the Affiliate menu item at affiliate.postiz.com Guard changes: add a RUNTIME pattern family alongside the existing AGPL attribution patterns, bring apps/sdk/ into STRICT scope, drop the now-stale apps/sdk/package.json exception, report per-match line numbers instead of attributing every hit in a file to the first line, and add a `branding-guard-allow:` sentinel for references that are legitimately upstream. Allowed hits are still counted and printed so exceptions stay visible rather than silently accumulating. Add BRAND_CLAUDE_DIRECTORY_URL, defaulting to empty so the Add-to-Claude button fails closed instead of offering the upstream listing. Collapse the CLI section's Locally/CI toggle into one list: with no self-hosted CLI auth server both variants reduce to the same API-key flow. Visible behaviour changes, all fail-closed: the Add-to-Claude button and the Affiliate menu item are hidden until BRAND_CLAUDE_DIRECTORY_URL and BRAND_AFFILIATE_URL are set. Docs links now resolve through BRAND_DOCS_URL, which is still configured as docs.postiz.com in the deployment env files and needs a separate decision. Also fix docs/upstream-sync.md: the documented `pnpm exec tsx` invocation fails because tsx is not a dependency, and the workflow triggers on main/dev, not main/master. --- .env.example | 4 + .../developer/developer.component.tsx | 6 +- .../src/components/layout/top.menu.tsx | 7 +- .../onboarding/onboarding.modal.tsx | 20 +- .../public-api/public.component.tsx | 191 +++++++++--------- apps/sdk/README.md | 21 +- apps/sdk/src/index.ts | 4 +- docs/upstream-sync.md | 8 +- libraries/helpers/src/utils/brand.config.ts | 6 + .../nestjs-libraries/src/chat/start.mcp.ts | 4 +- scripts/branding-guard.ts | 82 ++++++-- 11 files changed, 208 insertions(+), 145 deletions(-) diff --git a/.env.example b/.env.example index f02d9b8f5b..4c8e69dfbb 100644 --- a/.env.example +++ b/.env.example @@ -181,6 +181,10 @@ BRAND_EXTENSION_STORE_URL="https://chromewebstore.google.com/detail/sample-exten BRAND_TUTORIAL_URL="https://example.com/tutorials" # Affiliate program URL BRAND_AFFILIATE_URL="https://example.com/affiliates" +# Claude connector directory listing URL. Leave EMPTY to hide the +# "Add to Claude" button — it defaults to empty on purpose so a deployment +# that forgets to set it cannot offer the upstream listing by accident. +BRAND_CLAUDE_DIRECTORY_URL="" # ============================================================================== # 8. Single Sign-On (Generic OAuth 2.0 / DOS ID via PKCE Bridge) diff --git a/apps/frontend/src/components/developer/developer.component.tsx b/apps/frontend/src/components/developer/developer.component.tsx index 65414061f3..d557b51d13 100644 --- a/apps/frontend/src/components/developer/developer.component.tsx +++ b/apps/frontend/src/components/developer/developer.component.tsx @@ -8,6 +8,7 @@ import { useDecisionModal, useModals } from '@gitroom/frontend/components/layout import { MediaBox } from '@gitroom/frontend/components/media/media.component'; import copy from 'copy-to-clipboard'; import { useT } from '@gitroom/react/translation/get.transation.service.client'; +import { useVariables } from '@gitroom/react/helpers/variable.context'; const useOAuthApp = () => { const fetch = useFetch(); @@ -67,6 +68,7 @@ export const DeveloperComponent: FC = () => { const decision = useDecisionModal(); const modals = useModals(); const t = useT(); + const { brandConfig } = useVariables(); const { data: app, mutate } = useOAuthApp(); const [plaintextSecret, setPlaintextSecret] = useState(null); const [creating, setCreating] = useState(false); @@ -244,7 +246,7 @@ export const DeveloperComponent: FC = () => {
@@ -407,7 +409,7 @@ export const DeveloperComponent: FC = () => {
diff --git a/apps/frontend/src/components/layout/top.menu.tsx b/apps/frontend/src/components/layout/top.menu.tsx index defa47bb65..435fb385d9 100644 --- a/apps/frontend/src/components/layout/top.menu.tsx +++ b/apps/frontend/src/components/layout/top.menu.tsx @@ -19,7 +19,7 @@ interface MenuItemInterface { } export const useMenuItem = () => { - const { isGeneral } = useVariables(); + const { isGeneral, brandConfig } = useVariables(); const t = useT(); const { openModal } = useModals(); @@ -246,7 +246,10 @@ export const useMenuItem = () => { /> ), - path: 'https://affiliate.postiz.com', + path: brandConfig?.affiliateUrl || '#', + // No affiliate program configured for this deployment — hide the entry + // rather than sending customers to the upstream program. + hide: !brandConfig?.affiliateUrl, role: ['ADMIN', 'SUPERADMIN', 'USER'], requireBilling: true, }, diff --git a/apps/frontend/src/components/onboarding/onboarding.modal.tsx b/apps/frontend/src/components/onboarding/onboarding.modal.tsx index 7e95193f34..5e598aeca8 100644 --- a/apps/frontend/src/components/onboarding/onboarding.modal.tsx +++ b/apps/frontend/src/components/onboarding/onboarding.modal.tsx @@ -274,7 +274,16 @@ const otherAgents = mcpClients.filter( const apiTab = 'API' as const; type OnboardingTab = OnboardingAgent | typeof otherTab | typeof apiTab; -const cliCommands = localCliSteps.map((step) => step.code); +// localCliSteps carries {API_URL} / {API_KEY} placeholders. This panel +// substitutes the instance URL but deliberately not the key: the API tab +// already reveals it behind its own toggle, and echoing the secret into a +// second panel puts it in screenshots and screen-shares. +const getCliCommands = (apiBaseUrl: string) => + localCliSteps.map((step) => + step.code + .replace('{API_URL}', apiBaseUrl) + .replace('{API_KEY}', '') + ); // Cursor one-click install: https://cursor.com/docs/mcp/install-links const getCursorInstallUrl = ( @@ -300,7 +309,7 @@ const OnboardingStep2: FC<{ onBack: () => void; onNext: () => void }> = ({ }) => { const t = useT(); const user = useUser(); - const { backendUrl, mcpUrl, billingEnabled } = useVariables(); + const { backendUrl, mcpUrl, billingEnabled, brandConfig } = useVariables(); const [tab, setTab] = useState('Claude'); const [otherAgent, setOtherAgent] = useState(otherAgents[0]); // The client the cards describe: the tab itself, or the pick inside "Other agents" @@ -311,6 +320,7 @@ const OnboardingStep2: FC<{ onBack: () => void; onNext: () => void }> = ({ const mcpBase = mcpUrl || backendUrl; const apiKey = user?.publicApi || ''; const available = !!apiKey && !!user?.tier?.public_api; + const cliCommands = getCliCommands(mcpBase); const { config, hint } = agent === apiTab @@ -326,9 +336,9 @@ const OnboardingStep2: FC<{ onBack: () => void; onNext: () => void }> = ({ ); const connector = - agent === 'Claude' && billingEnabled + agent === 'Claude' && billingEnabled && brandConfig?.claudeDirectoryUrl ? { - href: 'https://claude.ai/directory/postiz', + href: brandConfig.claudeDirectoryUrl, label: t('add_to_claude', 'Add to Claude'), } : agent === 'Cursor' @@ -398,7 +408,7 @@ const OnboardingStep2: FC<{ onBack: () => void; onNext: () => void }> = ({
diff --git a/apps/frontend/src/components/public-api/public.component.tsx b/apps/frontend/src/components/public-api/public.component.tsx index 24afb92bcc..a583fa9568 100644 --- a/apps/frontend/src/components/public-api/public.component.tsx +++ b/apps/frontend/src/components/public-api/public.component.tsx @@ -22,10 +22,13 @@ export const remoteMcpClients = { } as const; // Clients with no MCP or CLI settings: you paste instructions into the chat, -// the agent installs the CLI itself and asks you for the API key +// the agent installs the CLI itself and asks you for the API key. +// A function of the API base, because the upstream CLI defaults to the +// upstream cloud — omitting POSTIZ_API_URL sends this deployment's API key to +// api.postiz.com and posts to the wrong account. export const chatOnlyMcpClients = { - 'Grok Bot': - 'Install the Postiz CLI with `npm install -g postiz`, then install the Postiz skill with `npx skills add gitroomhq/postiz-agent`. Ask me for my Postiz API key and set it as the POSTIZ_API_KEY environment variable before using the CLI.', + 'Grok Bot': (apiBase: string) => + `Install the CLI with \`npm install -g postiz\`, then install the skill with \`npx skills add gitroomhq/postiz-agent\`. Ask me for my API key, then set BOTH environment variables before using the CLI: POSTIZ_API_URL="${apiBase}" and POSTIZ_API_KEY. Do not skip POSTIZ_API_URL.`, // branding-guard-allow: upstream publishes the only CLI and skill package; Crove has no equivalent yet } as const; export const mcpClients = [ @@ -69,7 +72,7 @@ export const getMcpConfig = ( ): { config: string; hint: string } => { if (isChatOnlyMcpClient(client)) { return { - config: chatOnlyMcpClients[client], + config: chatOnlyMcpClients[client](mcpBase), hint: 'Paste this into the chat. The agent will ask you for your API key.', }; } @@ -293,7 +296,7 @@ const McpSection = ({ mcpBase: string; }) => { const t = useT(); - const { billingEnabled } = useVariables(); + const { billingEnabled, brandConfig } = useVariables(); const [activeClient, setActiveClient] = useState('Claude'); const [auth, setAuth] = useState('oauth'); const [revealed, setRevealed] = useState(false); @@ -332,10 +335,10 @@ const McpSection = ({
- {billingEnabled && ( + {!!brandConfig?.claudeDirectoryUrl && billingEnabled && ( @@ -344,7 +347,7 @@ const McpSection = ({ )} @@ -458,16 +461,18 @@ const McpSection = ({ {!isRemoteMcpClient(activeClient) && !chatOnly && ( )} - {activeClient === 'Claude' && billingEnabled && ( - - - {t('add_to_claude', 'Add to Claude')} - - )} + {activeClient === 'Claude' && + billingEnabled && + !!brandConfig?.claudeDirectoryUrl && ( + + + {t('add_to_claude', 'Add to Claude')} + + )}
@@ -475,59 +480,64 @@ const McpSection = ({ ); }; +// One canonical list, not separate "Locally" and "CI" variants. This +// deployment runs no CLI auth server, so the upstream `postiz auth:login` +// device flow would authenticate against cli-auth.postiz.com — into the +// upstream cloud, not this instance. Every step is therefore API-key based. +// +// POSTIZ_API_URL is mandatory, not optional: without it the CLI defaults to +// api.postiz.com, so the key in step 3 is transmitted upstream and the posts +// land on the wrong account. +// +// Placeholders are substituted at render time by CliSection and by the +// onboarding modal, which is why this stays a module-level constant. export const localCliSteps = [ { label: 'Install the CLI', - code: 'npm install -g postiz', - }, - { - label: 'Run: postiz auth:login', - code: 'postiz auth:login', - }, - { - label: 'Install the Postiz skill for your AI agent', - code: 'npx skills add gitroomhq/postiz-agent', + code: 'npm install -g postiz', // branding-guard-allow: upstream publishes the only CLI; Crove ships no equivalent yet }, -] as const; - -const ciCliSteps = [ { - label: 'Install the CLI', - code: 'npm install -g postiz', + label: 'Point the CLI at this instance (required)', + code: 'export POSTIZ_API_URL="{API_URL}"', }, { label: 'Set your API key as an environment variable', code: 'export POSTIZ_API_KEY="{API_KEY}"', }, { - label: 'Install the Postiz skill for your AI agent', - code: 'npx skills add gitroomhq/postiz-agent', + label: 'Install the skill for your AI agent', + code: 'npx skills add gitroomhq/postiz-agent', // branding-guard-allow: upstream publishes the only agent skill; Crove ships no equivalent yet }, ] as const; -const CliSection = ({ apiKey }: { apiKey: string }) => { +const CliSection = ({ + apiKey, + apiBaseUrl, +}: { + apiKey: string; + apiBaseUrl: string; +}) => { const t = useT(); - const [mode, setMode] = useState<'local' | 'ci'>('local'); + const { brandConfig } = useVariables(); const [revealed, setRevealed] = useState(false); - const steps = - mode === 'local' - ? localCliSteps.map((step) => ({ ...step })) - : ciCliSteps.map((step) => ({ - ...step, - code: step.code.replace('{API_KEY}', apiKey), - })); + const steps = localCliSteps.map((step) => ({ + ...step, + code: step.code + .replace('{API_URL}', apiBaseUrl) + .replace('{API_KEY}', apiKey), + })); const displaySteps = - mode === 'ci' && !revealed - ? steps.map((step) => ({ + revealed || !apiKey + ? steps + : steps.map((step) => ({ ...step, code: step.code.replace( new RegExp(apiKey.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), '*'.repeat(apiKey.length) ), - })) - : steps; + })); return (
@@ -546,7 +556,7 @@ const CliSection = ({ apiKey }: { apiKey: string }) => {
@@ -555,25 +565,6 @@ const CliSection = ({ apiKey }: { apiKey: string }) => {
-
- {(['local', 'ci'] as const).map((m) => ( - - ))} -
{displaySteps.map((step, i) => (
@@ -585,38 +576,36 @@ const CliSection = ({ apiKey }: { apiKey: string }) => {
))}
- {mode === 'ci' && ( - - )} + {revealed ? ( + <> + + + + + ) : ( + <> + + + + )} + + {revealed ? t('hide', 'Hide') : t('reveal', 'Reveal')} + s.code).join(' && ')} label={t('copy_all', 'Copy All')} @@ -629,7 +618,7 @@ const CliSection = ({ apiKey }: { apiKey: string }) => { const PublicApiContent = () => { const user = useUser(); - const { backendUrl, frontEndUrl, mcpUrl } = useVariables(); + const { backendUrl, frontEndUrl, mcpUrl, brandConfig } = useVariables(); const toaster = useToaster(); const fetch = useFetch(); const decision = useDecisionModal(); @@ -702,7 +691,7 @@ const PublicApiContent = () => {
- +
diff --git a/apps/sdk/README.md b/apps/sdk/README.md index c5b77c7447..1779a40ca6 100644 --- a/apps/sdk/README.md +++ b/apps/sdk/README.md @@ -1,24 +1,29 @@ -# Postiz NodeJS SDK +# Crove NodeJS SDK -This is the NodeJS SDK for [Postiz](https://postiz.com). +This is the NodeJS SDK for [Crove](https://crove.com). You can start by installing the package: ```bash -npm install @postiz/node +npm install @crove/node ``` ## Usage ```typescript -import Postiz from '@postiz/node'; -const postiz = new Postiz('your api key', 'your self-hosted instance (optional)'); +import Crove from '@crove/node'; +const crove = new Crove('your api key', 'your self-hosted instance (optional)'); ``` +The second argument defaults to `https://post.crove.com/api`. Pass your own +base URL if you run a self-hosted instance. + The available methods are: -- `post(posts: CreatePostDto)` - Schedule a post to Postiz +- `post(posts: CreatePostDto)` - Schedule a post to Crove - `postList(filters: GetPostsDto)` - Get a list of posts -- `upload(file: Buffer, extension: string)` - Upload a file to Postiz +- `upload(file: Buffer, extension: string)` - Upload a file to Crove - `integrations()` - Get a list of connected channels - `deletePost(id: string)` - Delete a post by ID -Alternatively you can use the SDK with curl, check the [Postiz API documentation](https://docs.postiz.com/public-api) for more information. \ No newline at end of file +Your API key is available in the application under **Settings → Developer**. +Alternatively you can use the SDK with curl against the same +`/public/v1/*` routes. diff --git a/apps/sdk/src/index.ts b/apps/sdk/src/index.ts index d92d0d0ca7..b74fb44d1d 100644 --- a/apps/sdk/src/index.ts +++ b/apps/sdk/src/index.ts @@ -15,7 +15,9 @@ function toQueryString(obj: Record): string { export default class Postiz { constructor( private _apiKey: string, - private _path = 'https://api.postiz.com' + // Matches NEXT_PUBLIC_BACKEND_URL: the public API is served under /api, + // so every call below resolves to /public/v1/... + private _path = 'https://post.crove.com/api' ) {} async post(posts: CreatePostDto) { diff --git a/docs/upstream-sync.md b/docs/upstream-sync.md index 6ff3ee05c9..e4f4a521b2 100644 --- a/docs/upstream-sync.md +++ b/docs/upstream-sync.md @@ -23,15 +23,19 @@ File kịch bản `scripts/branding-guard.ts` thực thi các kiểm tra nghiêm ### Chạy Branding Guard cục bộ: ```powershell -pnpm exec tsx scripts/branding-guard.ts +npx tsx scripts/branding-guard.ts ``` +> `tsx` không nằm trong `dependencies` của repo, nên `pnpm exec tsx` sẽ fail +> với `ERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL`. Dùng `npx tsx` — đúng cách mà +> `branding-guard.yml` đang chạy trong CI. + --- ## 3. GitHub Actions Workflows ### 3.1. Branding Guard CI (`.github/workflows/branding-guard.yml`) -- Kích hoạt khi có `push` hoặc `pull_request` vào nhánh `main` hoặc `master`. +- Kích hoạt khi có `push` hoặc `pull_request` vào nhánh `main` hoặc `dev`. - Đảm bảo không có bất kỳ commit nào phá vỡ các quy tắc branding và bảo mật. ### 3.2. Upstream Sync Workflow (`.github/workflows/sync-upstream.yml`) diff --git a/libraries/helpers/src/utils/brand.config.ts b/libraries/helpers/src/utils/brand.config.ts index d1d5417b63..dcd77aff47 100644 --- a/libraries/helpers/src/utils/brand.config.ts +++ b/libraries/helpers/src/utils/brand.config.ts @@ -20,6 +20,7 @@ export interface BrandConfig { extensionStoreUrl?: string; tutorialUrl?: string; affiliateUrl?: string; + claudeDirectoryUrl?: string; } export interface PublicBrandConfig extends BrandConfig { @@ -48,6 +49,9 @@ export const DEFAULT_BRAND_CONFIG: BrandConfig = { extensionStoreUrl: '', tutorialUrl: '', affiliateUrl: '', + // Deliberately empty rather than the upstream listing: a fork that forgets + // to set this must hide the button, not install a competitor's connector. + claudeDirectoryUrl: '', }; const DANGEROUS_PROTOCOLS = ['javascript:', 'data:', 'vbscript:', 'file:']; @@ -121,6 +125,7 @@ export function getBrandConfig(env: Record = process const extensionStoreUrl = sanitizeUrl(env.BRAND_EXTENSION_STORE_URL || env.NEXT_PUBLIC_BRAND_EXTENSION_STORE_URL); const tutorialUrl = sanitizeUrl(env.BRAND_TUTORIAL_URL || env.NEXT_PUBLIC_BRAND_TUTORIAL_URL); const affiliateUrl = sanitizeUrl(env.BRAND_AFFILIATE_URL || env.NEXT_PUBLIC_BRAND_AFFILIATE_URL); + const claudeDirectoryUrl = sanitizeUrl(env.BRAND_CLAUDE_DIRECTORY_URL || env.NEXT_PUBLIC_BRAND_CLAUDE_DIRECTORY_URL); const isCustomBrand = brandName.toLowerCase() !== 'postiz' && brandName.toLowerCase() !== 'gitroom'; @@ -146,6 +151,7 @@ export function getBrandConfig(env: Record = process extensionStoreUrl, tutorialUrl, affiliateUrl, + claudeDirectoryUrl, isCustomBrand, }; } diff --git a/libraries/nestjs-libraries/src/chat/start.mcp.ts b/libraries/nestjs-libraries/src/chat/start.mcp.ts index ab2c771902..fbafa97687 100644 --- a/libraries/nestjs-libraries/src/chat/start.mcp.ts +++ b/libraries/nestjs-libraries/src/chat/start.mcp.ts @@ -83,13 +83,13 @@ export const startMcp = async (app: INestApplication) => { // exposed as an annotation-less catch-all ask_postiz tool, which the // ChatGPT and Claude directory reviews reject const oauthServer = new MCPServer({ - name: 'Postiz MCP', + name: `${brand.name} MCP`, version: '1.0.0', tools, }); const claudeOauthServer = new MCPServer({ - name: 'Postiz MCP', + name: `${brand.name} MCP`, version: '1.0.0', tools: claudeTools, }); diff --git a/scripts/branding-guard.ts b/scripts/branding-guard.ts index d36c82fe07..663a40763a 100644 --- a/scripts/branding-guard.ts +++ b/scripts/branding-guard.ts @@ -34,6 +34,9 @@ console.log('=== Running Branding Guard Validations ===\n'); assert(defaults.isCustomBrand === false, 'Default isCustomBrand should be false'); assert(defaults.sourceUrl === DEFAULT_BRAND_CONFIG.sourceUrl, 'Default sourceUrl must point to upstream repository'); assert(defaults.primaryColor === '#612BD3', 'Default primaryColor should be #612BD3'); + // Fail closed: a deployment that never sets BRAND_CLAUDE_DIRECTORY_URL must + // hide the button, not fall back to the upstream listing. + assert(!defaults.claudeDirectoryUrl, 'Default claudeDirectoryUrl must be empty so the Add-to-Claude button fails closed'); } // 2. Custom branding test @@ -46,6 +49,7 @@ console.log('=== Running Branding Guard Validations ===\n'); BRAND_LOGO_URL: 'https://crove.app/logo.png', BRAND_DEFAULT_EMAIL_DOMAIN: 'crove.app', MAIN_URL: 'https://crove.app', + BRAND_CLAUDE_DIRECTORY_URL: 'https://claude.ai/directory/crove', }); assert(custom.name === 'Crove', 'Custom brand name matches'); @@ -58,6 +62,10 @@ console.log('=== Running Branding Guard Validations ===\n'); custom.websiteUrl === 'https://crove.app' || custom.websiteUrl === 'https://crove.app/', 'websiteUrl auto fallbacks to MAIN_URL when BRAND_WEBSITE_URL omitted' ); + assert( + custom.claudeDirectoryUrl === 'https://claude.ai/directory/crove', + 'claudeDirectoryUrl parsed from BRAND_CLAUDE_DIRECTORY_URL' + ); } // 3. Security sanitization tests @@ -128,7 +136,6 @@ console.log('=== Running Branding Guard Validations ===\n'); 'sonar-project.properties', // upstream tenant key, pending deletion 'Jenkins/', 'railway.toml', '.devcontainer/', // dead upstream infra '.github/workflows/issue-label-triggers.yml', // upstream automation - 'apps/sdk/package.json', // upstream author field, pending rewrite 'libraries/nestjs-libraries/src/sentry/initialize.sentry.ts', 'CHANGELOG.md', 'ROADMAP.md', ]; @@ -142,16 +149,34 @@ console.log('=== Running Branding Guard Validations ===\n'); // Directories we own and actively edit — leaks here are strict. const STRICT_PREFIXES = [ 'apps/backend/src/', 'apps/frontend/src/', 'apps/crove-sso/', + 'apps/sdk/', 'libraries/nestjs-libraries/src/', 'libraries/helpers/src/', 'libraries/react-shared-libraries/src/', 'scripts/', ]; + // Two families of leak, deliberately separate: + // ATTRIBUTION — AGPL-3.0 obligations (repo, image, author). Must survive + // in LICENSE/README/docs, so those stay in INHERITED. + // RUNTIME — endpoints and install commands a customer actually + // executes. These are never attribution: they send Crove + // traffic or Crove credentials to upstream infrastructure. + // The RUNTIME family was missing entirely, which is how a rebranded + // @crove/node SDK shipped with a default _path of https://api.postiz.com. const PATTERNS: Array<[RegExp, string]> = [ + // --- attribution --- [/platform\.postiz\.com/gi, 'upstream platform domain'], [/gitroomhq\/postiz-app/gi, 'upstream container image'], [/github\.com\/gitroomhq/gi, 'upstream repository URL'], [/\bNevo David\b/g, 'upstream author name'], [/\bpostiz-app\b/gi, 'upstream repository name'], + // --- runtime: upstream endpoints --- + [/\b(?:api|docs|affiliate|cli-auth)\.postiz\.com\b/gi, 'upstream runtime endpoint'], + // --- runtime: upstream install/skill instructions --- + [/gitroomhq\/postiz-agent/gi, 'upstream agent skill package'], + [/install\s+-g\s+postiz\b/gi, 'upstream CLI install command'], + // --- runtime: upstream-branded destinations shown to customers --- + [/claude\.ai\/directory\/postiz/gi, 'upstream Claude directory listing'], + [/'Postiz MCP'|"Postiz MCP"/g, 'upstream MCP server name'], ]; const TEXT_EXTS = new Set([ '.ts', '.tsx', '.js', '.mjs', '.cjs', '.json', '.jsonc', '.yaml', @@ -179,6 +204,7 @@ console.log('=== Running Branding Guard Validations ===\n'); const repoRoot = join(__dirname, '..'); let strictHits = 0; let inheritedHits = 0; + let allowedHits = 0; for (const file of walk(repoRoot)) { const rel = file.slice(repoRoot.length + 1).replace(/\\/g, '/'); const ext = extname(file); @@ -190,28 +216,37 @@ console.log('=== Running Branding Guard Validations ===\n'); } catch { continue; } + const strict = + STRICT_EXTRA.includes(rel) || + STRICT_PREFIXES.some((p) => rel.startsWith(p)); + const lines = content.split('\n'); for (const [pattern, label] of PATTERNS) { - const matches = content.match(pattern); - if (!matches) continue; - const strict = - STRICT_EXTRA.includes(rel) || - STRICT_PREFIXES.some((p) => rel.startsWith(p)); - const line = content - .slice(0, content.search(pattern)) - .split('\n').length; - // Commented-out examples of BRAND_* attribution are documentation of - // the AGPL knob, not shipped config — never a strict leak. - const matchedLine = content.split('\n')[line - 1] ?? ''; - if (/^\s*(#|\/\/|\/\*|\{\/\*)/.test(matchedLine)) continue; - if (strict) { - strictHits += matches.length; - console.error( - `[FAIL] ${rel}:${line} — ${label} (${matches.length}×)` - ); - failed = true; - } else { - inheritedHits += matches.length; - console.warn(`[INHERITED] ${rel} — ${label} (${matches.length}×)`); + // Per-match line numbers: the previous version reported one line for + // every match in the file, so a file with three leaks on three lines + // showed as "(3×)" against the first line only. + for (const match of content.matchAll(pattern)) { + const lineNo = content.slice(0, match.index ?? 0).split('\n').length; + const matchedLine = lines[lineNo - 1] ?? ''; + const prevLine = lines[lineNo - 2] ?? ''; + // Commented-out examples of BRAND_* attribution are documentation of + // the AGPL knob, not shipped config — never a strict leak. + if (/^\s*(#|\/\/|\/\*|\{\/\*)/.test(matchedLine)) continue; + // Deliberate upstream reference on the matched line or the line above + // it. Must carry a reason so the exception is reviewable, and is + // still counted and printed — exceptions stay visible, never silent. + if (/branding-guard-allow:/.test(`${prevLine}\n${matchedLine}`)) { + allowedHits += 1; + console.warn(`[ALLOWED] ${rel}:${lineNo} — ${label}`); + continue; + } + if (strict) { + strictHits += 1; + console.error(`[FAIL] ${rel}:${lineNo} — ${label}`); + failed = true; + } else { + inheritedHits += 1; + console.warn(`[INHERITED] ${rel}:${lineNo} — ${label}`); + } } } } @@ -219,6 +254,9 @@ console.log('=== Running Branding Guard Validations ===\n'); console.log( `Repo scan: ${inheritedHits} inherited upstream mentions (tolerated, see INHERITED list)` ); + console.log( + `Repo scan: ${allowedHits} deliberate upstream references (see branding-guard-allow comments)` + ); } console.log('\n=== Branding Guard Summary ==='); From a1191cdee8330940d535098a0c828d69a34438b5 Mon Sep 17 00:00:00 2001 From: JOY <5027251+JOY@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:51:53 +0700 Subject: [PATCH 3/4] fix(branding): drive the MCP connector name from brand config instead of hardcoding it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every generated client snippet registered this server under the literal key "postiz" — `claude mcp add postiz`, `{ mcpServers: { postiz: … } }`, `[mcp_servers.postiz]`, `cursor://…?name=postiz` — so customers saw an upstream-branded connector inside their own Claude/Cursor/Codex. Add BrandConfig.mcpConnectorName, derived from BRAND_SHORT_NAME and overridable via BRAND_MCP_CONNECTOR_NAME. It is always slugified so it stays safe as a shell argument, JSON/YAML key, TOML table name and URL query param; prod resolves to "crove" and beta to "crove-beta", so the two environments no longer collide in the same client. Verified by rendering every snippet against the real deployment env files. Add joinBrandUrl and use it for every docs link. sanitizeUrl normalises a bare origin to a trailing slash, so the previous `${docsUrl}/path` concatenation emitted a double slash — a regression introduced while de-hardcoding those URLs in the previous commit. Extend the guard with patterns for connector keys baked into generated config, and record the three identifiers that must stay upstream: the compose service name (container_name, the postiz-postgres dependency, the volume names and validate-beta-compose.mjs all key off it) and the two backward-compatible `postiz` agent aliases documented in docs/architecture.md §8.2. Also rename the SDK's default export Postiz -> Crove to match the @crove/node package name and its README. --- .env.example | 5 ++ .../developer/developer.component.tsx | 5 +- .../onboarding/onboarding.modal.tsx | 17 ++-- .../public-api/public.component.tsx | 77 +++++++++++-------- apps/sdk/src/index.ts | 2 +- docker-compose.yaml | 2 +- libraries/helpers/src/utils/brand.config.ts | 43 +++++++++++ .../src/chat/mastra.service.ts | 1 + .../nestjs-libraries/src/chat/start.mcp.ts | 1 + scripts/branding-guard.ts | 61 +++++++++++++++ 10 files changed, 173 insertions(+), 41 deletions(-) diff --git a/.env.example b/.env.example index 4c8e69dfbb..1c616e5c27 100644 --- a/.env.example +++ b/.env.example @@ -143,6 +143,11 @@ LISTMONK_WELCOME_TEMPLATE_ID="1" BRAND_NAME="Crove Post" # Short application brand name BRAND_SHORT_NAME="Crove" +# Machine identifier for the MCP server in generated client config +# (`claude mcp add `, `{ mcpServers: { : … } }`, Cursor deep links). +# Must be a lowercase hyphenated slug. Leave EMPTY to derive it automatically +# from BRAND_SHORT_NAME ("Crove" -> "crove", "Crove (Beta)" -> "crove-beta"). +BRAND_MCP_CONNECTOR_NAME="" # Brand meta description BRAND_DESCRIPTION="The AI-powered multi-channel social media management platform" # Company legal entity name diff --git a/apps/frontend/src/components/developer/developer.component.tsx b/apps/frontend/src/components/developer/developer.component.tsx index d557b51d13..a49e00c802 100644 --- a/apps/frontend/src/components/developer/developer.component.tsx +++ b/apps/frontend/src/components/developer/developer.component.tsx @@ -9,6 +9,7 @@ import { MediaBox } from '@gitroom/frontend/components/media/media.component'; import copy from 'copy-to-clipboard'; import { useT } from '@gitroom/react/translation/get.transation.service.client'; import { useVariables } from '@gitroom/react/helpers/variable.context'; +import { joinBrandUrl } from '@gitroom/helpers/utils/brand.config'; const useOAuthApp = () => { const fetch = useFetch(); @@ -246,7 +247,7 @@ export const DeveloperComponent: FC = () => {
@@ -409,7 +410,7 @@ export const DeveloperComponent: FC = () => {
diff --git a/apps/frontend/src/components/onboarding/onboarding.modal.tsx b/apps/frontend/src/components/onboarding/onboarding.modal.tsx index 5e598aeca8..ed74769750 100644 --- a/apps/frontend/src/components/onboarding/onboarding.modal.tsx +++ b/apps/frontend/src/components/onboarding/onboarding.modal.tsx @@ -2,6 +2,7 @@ import React, { FC, Fragment, useCallback, useMemo, useState } from 'react'; import { useFetch } from '@gitroom/helpers/utils/custom.fetch'; +import { joinBrandUrl } from '@gitroom/helpers/utils/brand.config'; import useSWR from 'swr'; import { orderBy } from 'lodash'; import clsx from 'clsx'; @@ -289,7 +290,8 @@ const getCliCommands = (apiBaseUrl: string) => const getCursorInstallUrl = ( auth: McpAuth, mcpBase: string, - apiKey: string + apiKey: string, + connectorName: string ) => { const server = auth === 'oauth' @@ -298,9 +300,9 @@ const getCursorInstallUrl = ( url: `${mcpBase}/mcp`, headers: { Authorization: `Bearer ${apiKey}` }, }; - return `cursor://anysphere.cursor-deeplink/mcp/install?name=postiz&config=${btoa( - JSON.stringify(server) - )}`; + return `cursor://anysphere.cursor-deeplink/mcp/install?name=${encodeURIComponent( + connectorName + )}&config=${btoa(JSON.stringify(server))}`; }; const OnboardingStep2: FC<{ onBack: () => void; onNext: () => void }> = ({ @@ -321,11 +323,12 @@ const OnboardingStep2: FC<{ onBack: () => void; onNext: () => void }> = ({ const apiKey = user?.publicApi || ''; const available = !!apiKey && !!user?.tier?.public_api; const cliCommands = getCliCommands(mcpBase); + const connectorName = brandConfig?.mcpConnectorName || 'mcp'; const { config, hint } = agent === apiTab ? { config: '', hint: '' } - : getMcpConfig(agent, auth, mcpBase, apiKey); + : getMcpConfig(agent, auth, mcpBase, apiKey, connectorName); const maskedConfig = revealed || auth === 'oauth' || !apiKey @@ -343,7 +346,7 @@ const OnboardingStep2: FC<{ onBack: () => void; onNext: () => void }> = ({ } : agent === 'Cursor' ? { - href: getCursorInstallUrl(auth, mcpBase, apiKey), + href: getCursorInstallUrl(auth, mcpBase, apiKey, connectorName), label: t('add_to_cursor', 'Add to Cursor'), } : null; @@ -408,7 +411,7 @@ const OnboardingStep2: FC<{ onBack: () => void; onNext: () => void }> = ({
diff --git a/apps/frontend/src/components/public-api/public.component.tsx b/apps/frontend/src/components/public-api/public.component.tsx index a583fa9568..5261826ceb 100644 --- a/apps/frontend/src/components/public-api/public.component.tsx +++ b/apps/frontend/src/components/public-api/public.component.tsx @@ -8,6 +8,7 @@ import { useToaster } from '@gitroom/react/toaster/toaster'; import { useVariables } from '@gitroom/react/helpers/variable.context'; import { useT } from '@gitroom/react/translation/get.transation.service.client'; import { useFetch } from '@gitroom/helpers/utils/custom.fetch'; +import { joinBrandUrl } from '@gitroom/helpers/utils/brand.config'; import { useDecisionModal } from '@gitroom/frontend/components/layout/new-modal'; import { DeveloperComponent } from '@gitroom/frontend/components/developer/developer.component'; import { McpClientIcon } from '@gitroom/frontend/components/public-api/mcp.client.icons'; @@ -68,7 +69,11 @@ export const getMcpConfig = ( client: AnyMcpClient, auth: McpAuth, mcpBase: string, - apiKey: string + apiKey: string, + // brandConfig.mcpConnectorName — the key every generated snippet registers + // this server under. Passed in rather than read from context so the helper + // stays pure and testable. + connectorName: string ): { config: string; hint: string } => { if (isChatOnlyMcpClient(client)) { return { @@ -94,62 +99,62 @@ export const getMcpConfig = ( switch (client) { case 'Claude Code': return { - config: `claude mcp add postiz --transport http "${oauthUrl}"`, + config: `claude mcp add ${connectorName} --transport http "${oauthUrl}"`, hint: 'Run this command in your terminal.', }; case 'Cursor': return { - config: json({ mcpServers: { postiz: { url: oauthUrl } } }), + config: json({ mcpServers: { [connectorName]: { url: oauthUrl } } }), hint: 'Add to .cursor/mcp.json in your project root.', }; case 'VS Code / Copilot': return { config: json({ - servers: { postiz: { type: 'http', url: oauthUrl } }, + servers: { [connectorName]: { type: 'http', url: oauthUrl } }, }), hint: 'Add to .vscode/mcp.json in your project root.', }; case 'Windsurf': return { config: json({ - mcpServers: { postiz: { serverUrl: oauthUrl } }, + mcpServers: { [connectorName]: { serverUrl: oauthUrl } }, }), hint: 'Add to ~/.codeium/windsurf/mcp_config.json', }; case 'Amp': return { - config: `amp mcp add postiz ${oauthUrl}`, + config: `amp mcp add ${connectorName} ${oauthUrl}`, hint: 'Run this command in your terminal.', }; case 'Codex': return { - config: `# ~/.codex/config.toml\n\n[mcp_servers.postiz]\nurl = "${oauthUrl}"`, - hint: 'Add to ~/.codex/config.toml, then run: codex mcp login postiz', + config: `# ~/.codex/config.toml\n\n[mcp_servers.${connectorName}]\nurl = "${oauthUrl}"`, + hint: `Add to ~/.codex/config.toml, then run: codex mcp login ${connectorName}`, }; case 'Gemini CLI': return { - config: json({ mcpServers: { postiz: { url: oauthUrl } } }), + config: json({ mcpServers: { [connectorName]: { url: oauthUrl } } }), hint: 'Add to ~/.gemini/settings.json', }; case 'Warp': return { - config: json({ postiz: { url: oauthUrl } }), + config: json({ [connectorName]: { url: oauthUrl } }), hint: 'Settings > MCP Servers > + Add, then paste this config.', }; case 'Hermes': return { - config: `# ~/.hermes/config.yaml\n\nmcp_servers:\n postiz:\n url: "${oauthUrl}"\n auth: oauth`, + config: `# ~/.hermes/config.yaml\n\nmcp_servers:\n ${connectorName}:\n url: "${oauthUrl}"\n auth: oauth`, hint: 'Add to ~/.hermes/config.yaml, then run /reload-mcp in the chat.', }; case 'OpenClaw': return { - config: `openclaw mcp add postiz --url ${oauthUrl} --transport streamable-http --auth oauth && openclaw mcp login postiz`, + config: `openclaw mcp add ${connectorName} --url ${oauthUrl} --transport streamable-http --auth oauth && openclaw mcp login ${connectorName}`, hint: 'Run this command in your terminal.', }; case 'NanoClaw': return { - config: `ncl groups config add-mcp-server --id --name postiz --url ${oauthUrl}`, - hint: 'Run this in your terminal, replace with the agent group that should get Postiz.', + config: `ncl groups config add-mcp-server --id --name ${connectorName} --url ${oauthUrl}`, + hint: 'Run this in your terminal, replace with the agent group that should get this server.', }; } } @@ -157,14 +162,17 @@ export const getMcpConfig = ( switch (client) { case 'Claude Code': return { - config: `claude mcp add --transport http postiz ${urlBase} --header "Authorization: ${bearer}"`, + config: `claude mcp add --transport http ${connectorName} ${urlBase} --header "Authorization: ${bearer}"`, hint: 'Run this command in your terminal.', }; case 'Cursor': return { config: json({ mcpServers: { - postiz: { url: urlBase, headers: { Authorization: bearer } }, + [connectorName]: { + url: urlBase, + headers: { Authorization: bearer }, + }, }, }), hint: 'Add to .cursor/mcp.json in your project root.', @@ -173,7 +181,7 @@ export const getMcpConfig = ( return { config: json({ servers: { - postiz: { + [connectorName]: { type: 'http', url: urlBase, headers: { Authorization: bearer }, @@ -186,7 +194,7 @@ export const getMcpConfig = ( return { config: json({ mcpServers: { - postiz: { + [connectorName]: { serverUrl: urlBase, headers: { Authorization: bearer }, }, @@ -198,21 +206,27 @@ export const getMcpConfig = ( return { config: json({ 'amp.mcpServers': { - postiz: { url: urlBase, headers: { Authorization: bearer } }, + [connectorName]: { + url: urlBase, + headers: { Authorization: bearer }, + }, }, }), hint: 'Add to your Amp settings.json', }; case 'Codex': return { - config: `# ~/.codex/config.toml\n\n[mcp_servers.postiz]\nurl = "${urlBase}"\nhttp_headers = { "Authorization" = "${bearer}" }`, + config: `# ~/.codex/config.toml\n\n[mcp_servers.${connectorName}]\nurl = "${urlBase}"\nhttp_headers = { "Authorization" = "${bearer}" }`, hint: 'Add to ~/.codex/config.toml', }; case 'Gemini CLI': return { config: json({ mcpServers: { - postiz: { url: urlBase, headers: { Authorization: bearer } }, + [connectorName]: { + url: urlBase, + headers: { Authorization: bearer }, + }, }, }), hint: 'Add to ~/.gemini/settings.json', @@ -220,13 +234,13 @@ export const getMcpConfig = ( case 'Warp': return { config: json({ - postiz: { url: urlBase, headers: { Authorization: bearer } }, + [connectorName]: { url: urlBase, headers: { Authorization: bearer } }, }), hint: 'Settings > MCP Servers > + Add, then paste this config.', }; case 'Hermes': return { - config: `# ~/.hermes/config.yaml\n\nmcp_servers:\n postiz:\n url: "${urlBase}"\n headers:\n Authorization: "${bearer}"`, + config: `# ~/.hermes/config.yaml\n\nmcp_servers:\n ${connectorName}:\n url: "${urlBase}"\n headers:\n Authorization: "${bearer}"`, hint: 'Add to ~/.hermes/config.yaml, then run /reload-mcp in the chat.', }; case 'OpenClaw': @@ -234,7 +248,7 @@ export const getMcpConfig = ( config: json({ mcp: { servers: { - postiz: { + [connectorName]: { url: urlBase, transport: 'streamable-http', headers: { Authorization: bearer }, @@ -247,8 +261,8 @@ export const getMcpConfig = ( case 'NanoClaw': // No headers flag, the key travels inside the URL like remote clients return { - config: `ncl groups config add-mcp-server --id --name postiz --url ${mcpBase}/mcp/${apiKey}`, - hint: 'Run this in your terminal, replace with the agent group that should get Postiz.', + config: `ncl groups config add-mcp-server --id --name ${connectorName} --url ${mcpBase}/mcp/${apiKey}`, + hint: 'Run this in your terminal, replace with the agent group that should get this server.', }; } }; @@ -301,11 +315,14 @@ const McpSection = ({ const [auth, setAuth] = useState('oauth'); const [revealed, setRevealed] = useState(false); + const connectorName = brandConfig?.mcpConnectorName || 'mcp'; + const { config, hint } = getMcpConfig( activeClient, auth, mcpBase, - user.publicApi + user.publicApi, + connectorName ); const baseUrl = auth === 'oauth' ? getMcpOauthUrl(mcpBase) : `${mcpBase}/mcp`; @@ -347,7 +364,7 @@ const McpSection = ({ )} @@ -556,7 +573,7 @@ const CliSection = ({
@@ -691,7 +708,7 @@ const PublicApiContent = () => {
diff --git a/apps/sdk/src/index.ts b/apps/sdk/src/index.ts index b74fb44d1d..5d414e89e7 100644 --- a/apps/sdk/src/index.ts +++ b/apps/sdk/src/index.ts @@ -12,7 +12,7 @@ function toQueryString(obj: Record): string { return params.toString(); } -export default class Postiz { +export default class Crove { constructor( private _apiKey: string, // Matches NEXT_PUBLIC_BACKEND_URL: the public API is served under /api, diff --git a/docker-compose.yaml b/docker-compose.yaml index 8141c200a5..2da1aa402f 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,5 +1,5 @@ services: - postiz: + postiz: # branding-guard-allow: compose service name — container_name, the postiz-postgres dependency, the volume names and scripts/validate-beta-compose.mjs all key off it. Renaming is an infra migration, not a branding fix. image: ghcr.io/dos/crove-post:latest container_name: postiz restart: always diff --git a/libraries/helpers/src/utils/brand.config.ts b/libraries/helpers/src/utils/brand.config.ts index dcd77aff47..5944d3020c 100644 --- a/libraries/helpers/src/utils/brand.config.ts +++ b/libraries/helpers/src/utils/brand.config.ts @@ -1,6 +1,13 @@ export interface BrandConfig { name: string; shortName: string; + /** + * Machine identifier for this deployment's MCP server, used as the key in + * generated client config (`claude mcp add `, `{ mcpServers: { : … } }`). + * Always a lowercase hyphenated slug so it stays safe as a shell argument, + * JSON key, YAML key and TOML table name. + */ + mcpConnectorName: string; description: string; companyName: string; logoUrl?: string; @@ -30,6 +37,7 @@ export interface PublicBrandConfig extends BrandConfig { export const DEFAULT_BRAND_CONFIG: BrandConfig = { name: 'Postiz', shortName: 'Postiz', + mcpConnectorName: 'postiz', description: 'The open-source social media management platform', companyName: 'Postiz', logoUrl: '', @@ -98,9 +106,43 @@ export function sanitizeHexColor(color?: string | null): string | undefined { return undefined; } +/** + * Reduce a display name to a machine-safe identifier: lowercase, hyphenated, + * no leading/trailing hyphen. "Crove (Beta)" -> "crove-beta". + * Returns '' when nothing alphanumeric survives, so callers can fall back. + */ +export function slugifyIdentifier(value?: string | null): string { + if (!value || typeof value !== 'string') return ''; + return value + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, ''); +} + +/** + * Join a brand URL base with a path, tolerating a trailing slash on the base + * and a leading slash on the path. Needed because sanitizeUrl normalises + * through the URL constructor, so a bare origin such as + * BRAND_DOCS_URL=https://docs.example.com comes back as + * "https://docs.example.com/" and naive concatenation yields a double slash. + */ +export function joinBrandUrl(base?: string | null, path?: string | null): string { + const trimmedBase = (base || '').replace(/\/+$/, ''); + const trimmedPath = (path || '').replace(/^\/+/, ''); + if (!trimmedBase) return trimmedPath ? `/${trimmedPath}` : ''; + return trimmedPath ? `${trimmedBase}/${trimmedPath}` : trimmedBase; +} + export function getBrandConfig(env: Record = process.env): PublicBrandConfig { const brandName = (env.BRAND_NAME || env.NEXT_PUBLIC_BRAND_NAME || '').trim() || DEFAULT_BRAND_CONFIG.name; const brandShortName = (env.BRAND_SHORT_NAME || env.NEXT_PUBLIC_BRAND_SHORT_NAME || '').trim() || brandName; + // Explicit override wins; otherwise derive from the short name so a + // deployment that never sets it still gets a correctly branded connector + // key ("Crove" -> "crove") instead of the upstream one. + const mcpConnectorName = + slugifyIdentifier(env.BRAND_MCP_CONNECTOR_NAME || env.NEXT_PUBLIC_BRAND_MCP_CONNECTOR_NAME) || + slugifyIdentifier(brandShortName) || + 'mcp'; const brandDescription = (env.BRAND_DESCRIPTION || env.NEXT_PUBLIC_BRAND_DESCRIPTION || '').trim() || DEFAULT_BRAND_CONFIG.description; const brandCompanyName = (env.BRAND_COMPANY_NAME || env.NEXT_PUBLIC_BRAND_COMPANY_NAME || '').trim() || brandName; @@ -132,6 +174,7 @@ export function getBrandConfig(env: Record = process return { name: brandName, shortName: brandShortName, + mcpConnectorName, description: brandDescription, companyName: brandCompanyName, logoUrl, diff --git a/libraries/nestjs-libraries/src/chat/mastra.service.ts b/libraries/nestjs-libraries/src/chat/mastra.service.ts index e95f2dd582..39d8d062b4 100644 --- a/libraries/nestjs-libraries/src/chat/mastra.service.ts +++ b/libraries/nestjs-libraries/src/chat/mastra.service.ts @@ -14,6 +14,7 @@ export class MastraService { new Mastra({ storage: pStore, agents: { + // branding-guard-allow: 'postiz' is a backward-compatibility agent alias. Clients that registered the server under the upstream name must keep resolving; crove_post and post are the branded equivalents (docs/architecture.md §8.2). postiz: await this._loadToolsService.agent('postiz'), crove_post: await this._loadToolsService.agent('crove_post'), post: await this._loadToolsService.agent('post'), diff --git a/libraries/nestjs-libraries/src/chat/start.mcp.ts b/libraries/nestjs-libraries/src/chat/start.mcp.ts index fbafa97687..1d52aa83f2 100644 --- a/libraries/nestjs-libraries/src/chat/start.mcp.ts +++ b/libraries/nestjs-libraries/src/chat/start.mcp.ts @@ -71,6 +71,7 @@ export const startMcp = async (app: INestApplication) => { version: '1.0.0', tools, agents: { + // branding-guard-allow: 'postiz' is a backward-compatibility agent alias. Clients that registered the server under the upstream name must keep resolving; crove_post and post are the branded equivalents (docs/architecture.md §8.2). postiz: agent, crove_post: agent, post: agent, diff --git a/scripts/branding-guard.ts b/scripts/branding-guard.ts index 663a40763a..5ef04c6065 100644 --- a/scripts/branding-guard.ts +++ b/scripts/branding-guard.ts @@ -9,6 +9,8 @@ import { sanitizeUrl, sanitizeHexColor, applyBrandToString, + slugifyIdentifier, + joinBrandUrl, DEFAULT_BRAND_CONFIG, } from '../libraries/helpers/src/utils/brand.config'; import { readdirSync, readFileSync, statSync } from 'fs'; @@ -68,6 +70,56 @@ console.log('=== Running Branding Guard Validations ===\n'); ); } +// 2b. MCP connector name. This string is emitted into generated client config +// as a shell argument, a JSON key, a YAML key, a TOML table name and a URL +// query param, so it must always be a safe lowercase slug — never the display +// name, which can contain spaces and punctuation. +{ + assert(slugifyIdentifier('Crove (Beta)') === 'crove-beta', 'slugifyIdentifier hyphenates spaces and punctuation'); + assert(slugifyIdentifier(' --Crove Post-- ') === 'crove-post', 'slugifyIdentifier trims leading/trailing hyphens'); + assert(slugifyIdentifier('!!!') === '', 'slugifyIdentifier returns empty when nothing alphanumeric survives'); + assert(slugifyIdentifier(undefined) === '', 'slugifyIdentifier tolerates undefined'); + + const derived = getBrandConfig({ BRAND_SHORT_NAME: 'Crove' }); + assert(derived.mcpConnectorName === 'crove', 'mcpConnectorName derives from BRAND_SHORT_NAME when unset'); + + const beta = getBrandConfig({ BRAND_SHORT_NAME: 'Crove (Beta)' }); + assert(beta.mcpConnectorName === 'crove-beta', 'mcpConnectorName slugifies a short name that is not shell-safe'); + + const explicit = getBrandConfig({ + BRAND_SHORT_NAME: 'Crove', + BRAND_MCP_CONNECTOR_NAME: 'crove-mcp', + }); + assert(explicit.mcpConnectorName === 'crove-mcp', 'BRAND_MCP_CONNECTOR_NAME overrides the derived slug'); + + const dirty = getBrandConfig({ BRAND_MCP_CONNECTOR_NAME: ' --Crove Post-- ' }); + assert(dirty.mcpConnectorName === 'crove-post', 'An unsafe BRAND_MCP_CONNECTOR_NAME is still slugified, not passed through'); + + const unusable = getBrandConfig({ BRAND_NAME: '!!!', BRAND_SHORT_NAME: '!!!' }); + assert(unusable.mcpConnectorName === 'mcp', 'mcpConnectorName falls back to "mcp" rather than emitting an empty key'); + + assert( + /^[a-z0-9]+(-[a-z0-9]+)*$/.test(getBrandConfig({}).mcpConnectorName), + 'Default mcpConnectorName is a safe slug' + ); +} + +// 2c. joinBrandUrl. sanitizeUrl normalises a bare origin through the URL +// constructor, so BRAND_DOCS_URL="https://docs.example.com" is stored as +// "https://docs.example.com/". Naive `${docsUrl}/path` concatenation then +// produces a double slash — this is the regression the helper exists to stop. +{ + const normalised = sanitizeUrl('https://docs.example.com')!; + assert(normalised === 'https://docs.example.com/', 'sanitizeUrl appends a trailing slash to a bare origin (precondition for the join tests)'); + assert(joinBrandUrl(normalised, 'public-api') === 'https://docs.example.com/public-api', 'joinBrandUrl never emits a double slash'); + assert(joinBrandUrl('https://docs.example.com', 'public-api') === 'https://docs.example.com/public-api', 'joinBrandUrl handles a base without a trailing slash'); + assert(joinBrandUrl('https://docs.example.com/', '/public-api') === 'https://docs.example.com/public-api', 'joinBrandUrl handles a leading slash on the path'); + assert(joinBrandUrl('https://docs.example.com//', '//public-api') === 'https://docs.example.com/public-api', 'joinBrandUrl collapses repeated slashes at the join'); + assert(joinBrandUrl('https://docs.example.com/', '') === 'https://docs.example.com', 'joinBrandUrl with an empty path returns the base'); + assert(joinBrandUrl(undefined, 'public-api') === '/public-api', 'joinBrandUrl with no base degrades to a root-relative path'); + assert(joinBrandUrl(undefined, undefined) === '', 'joinBrandUrl with nothing returns an empty string rather than "undefined"'); +} + // 3. Security sanitization tests { assert(sanitizeUrl('javascript:alert(1)') === undefined, 'Reject javascript: URLs'); @@ -177,6 +229,15 @@ console.log('=== Running Branding Guard Validations ===\n'); // --- runtime: upstream-branded destinations shown to customers --- [/claude\.ai\/directory\/postiz/gi, 'upstream Claude directory listing'], [/'Postiz MCP'|"Postiz MCP"/g, 'upstream MCP server name'], + // --- runtime: upstream identifier baked into generated client config --- + // These are the connector keys customers paste into Claude/Cursor/Codex/ + // etc. They must come from brandConfig.mcpConnectorName, not a literal. + // `postiz://` is deliberately NOT matched: it is a registered mobile URL + // scheme, not a connector key. [ \t] rather than \s so a match can never + // span a newline and report the wrong line number. + [/(?:^|[{,])[ \t]*postiz[ \t]*:/gm, 'upstream MCP connector key'], + [/\b(?:add|login)\s+postiz\b|(?:--name|name=)postiz\b/g, 'upstream MCP connector name in a command'], + [/mcp_servers\.postiz\b/g, 'upstream MCP connector name in a TOML table'], ]; const TEXT_EXTS = new Set([ '.ts', '.tsx', '.js', '.mjs', '.cjs', '.json', '.jsonc', '.yaml', From f8ff8801684ddd5b8cf02709e21d61e587dd6249 Mon Sep 17 00:00:00 2001 From: JOY <5027251+JOY@users.noreply.github.com> Date: Sat, 12 Sep 2026 19:48:55 +0700 Subject: [PATCH 4/4] =?UTF-8?q?fix(deps):=20downgrade=20@grpc/grpc-js=20to?= =?UTF-8?q?=201.12.4=20=E2=80=94=20fix=20gRPC=20IPv6=20resolution=20in=20N?= =?UTF-8?q?ode=2022?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes Facebook post publishing failure: @grpc/grpc-js@1.14.3 resolves ALL hostnames to IPv6 ::1 in Node 22 Docker containers, causing ECONNREFUSED for every Temporal workflow start. Downgrading to 1.12.4 restores working DNS resolution. --- package.json | 4 ++- pnpm-lock.yaml | 70 ++++++++++++++++++++++++++++---------------------- 2 files changed, 42 insertions(+), 32 deletions(-) diff --git a/package.json b/package.json index ec3393edbd..43fc495fe7 100644 --- a/package.json +++ b/package.json @@ -326,12 +326,14 @@ "@types/react-dom": "19.1.6", "tar": "^7.5.19", "form-data": "^2.5.4", + "protobufjs": "7.5.5", "shell-quote": "^1.8.4", "fast-uri": "^3.1.6", "hono": "^4.12.34", "immutable": "^5.1.8", "handlebars": "^4.7.9", - "happy-dom": "^20.0.0" + "happy-dom": "^20.0.0", + "@grpc/grpc-js": "1.12.4" }, "onlyBuiltDependencies": [ "bcrypt", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a05131a1eb..3e9254d401 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,12 +14,14 @@ overrides: '@types/react-dom': 19.1.6 tar: ^7.5.19 form-data: ^2.5.4 + protobufjs: 7.5.5 shell-quote: ^1.8.4 fast-uri: ^3.1.6 hono: ^4.12.34 immutable: ^5.1.8 handlebars: ^4.7.9 happy-dom: ^20.0.0 + '@grpc/grpc-js': 1.12.4 importers: @@ -117,7 +119,7 @@ importers: version: 11.1.21(@nestjs/common@11.1.21(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/microservices@11.1.21)(@nestjs/platform-express@11.1.21)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/microservices': specifier: ^11.1.21 - version: 11.1.21(@grpc/grpc-js@1.14.3)(@nestjs/common@11.1.21(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.21)(cache-manager@7.2.8)(ioredis@5.10.0)(reflect-metadata@0.2.2)(rxjs@7.8.2) + version: 11.1.21(@grpc/grpc-js@1.12.4)(@nestjs/common@11.1.21(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.21)(cache-manager@7.2.8)(ioredis@5.10.0)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/platform-express': specifier: ^11.1.21 version: 11.1.21(@nestjs/common@11.1.21(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.21) @@ -2991,12 +2993,12 @@ packages: resolution: {integrity: sha512-ZpJxMqB+Qfe3rp6uszCQoag4nSw42icURnBRfFYSOmTgEeOe4rD0vYlbA8spvCu2TlCesNTlEN9BLWtQqLxabA==} engines: {node: '>=18.0.0'} - '@grpc/grpc-js@1.14.3': - resolution: {integrity: sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==} + '@grpc/grpc-js@1.12.4': + resolution: {integrity: sha512-NBhrxEWnFh0FxeA0d//YP95lRFsSx2TNLEUQg4/W+5f/BMxcCjgOOIT24iD+ZB/tZw057j44DaIxja7w4XMrhg==} engines: {node: '>=12.10.0'} - '@grpc/proto-loader@0.8.0': - resolution: {integrity: sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==} + '@grpc/proto-loader@0.7.15': + resolution: {integrity: sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==} engines: {node: '>=6'} hasBin: true @@ -5104,7 +5106,7 @@ packages: '@nestjs/microservices@11.1.21': resolution: {integrity: sha512-NRDK/lCSD5ul53NE5YHuXKygI7P73OLSR5gljFBrUfLip37L/+s96MqUBHCxWt2YCd0GhPJfqO9zttJBxpX5sA==} peerDependencies: - '@grpc/grpc-js': '*' + '@grpc/grpc-js': 1.12.4 '@nestjs/common': ^11.0.0 '@nestjs/core': ^11.0.0 '@nestjs/websockets': ^11.0.0 @@ -6258,6 +6260,9 @@ packages: '@protobufjs/float@1.0.2': resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + '@protobufjs/inquire@1.1.2': + resolution: {integrity: sha512-pa0vFRuws4wkvaXKK1uXZMAwAX4/t8ANaJo45iw/oQHNQ9q5xUzwgFmVJGXiga2BeN+zpX7Vf9vmsiIa2J+MUw==} + '@protobufjs/path@1.1.2': resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} @@ -14677,8 +14682,8 @@ packages: resolution: {integrity: sha512-SAzp/O4Yh02jGdRc+uIrGoe87dkN/XtwxfZ4ZyafJHymd79ozp5VG5nyZ7ygqPM5+cpLDjjGnYFUkngonyDPOQ==} engines: {node: '>=14.0.0'} - protobufjs@7.6.6: - resolution: {integrity: sha512-dYDWdjSl5RNb7SgPxGQcRU+GtvP7s2fpkrY0r432PcOIaZ0/rBcxEZnQN67iJhFuQiVw754JDoPruPCNdGsbjg==} + protobufjs@7.5.5: + resolution: {integrity: sha512-3wY1AxV+VBNW8Yypfd1yQY9pXnqTAN+KwQxL8iYm3/BjKYMNg4i0owhEe26PWDOMaIrzeeF98Lqd5NGz4omiIg==} engines: {node: '>=12.0.0'} proxy-addr@2.0.7: @@ -20296,16 +20301,16 @@ snapshots: '@repeaterjs/repeater': 3.0.6 tslib: 2.8.1 - '@grpc/grpc-js@1.14.3': + '@grpc/grpc-js@1.12.4': dependencies: - '@grpc/proto-loader': 0.8.0 + '@grpc/proto-loader': 0.7.15 '@js-sdsl/ordered-map': 4.4.2 - '@grpc/proto-loader@0.8.0': + '@grpc/proto-loader@0.7.15': dependencies: lodash.camelcase: 4.3.0 long: 5.3.2 - protobufjs: 7.6.6 + protobufjs: 7.5.5 yargs: 17.7.2 '@headlessui/react@2.2.9(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': @@ -22040,7 +22045,7 @@ snapshots: tslib: 2.8.1 uid: 2.0.2 optionalDependencies: - '@nestjs/microservices': 11.1.21(@grpc/grpc-js@1.14.3)(@nestjs/common@11.1.21(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.21)(cache-manager@7.2.8)(ioredis@5.10.0)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/microservices': 11.1.21(@grpc/grpc-js@1.12.4)(@nestjs/common@11.1.21(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.21)(cache-manager@7.2.8)(ioredis@5.10.0)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/platform-express': 11.1.21(@nestjs/common@11.1.21(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.21) '@nestjs/core@11.1.21(@nestjs/common@11.1.21(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/microservices@11.1.21)(@nestjs/platform-express@11.1.21)(reflect-metadata@0.2.2)(rxjs@7.8.2)': @@ -22055,7 +22060,7 @@ snapshots: tslib: 2.8.1 uid: 2.0.2 optionalDependencies: - '@nestjs/microservices': 11.1.21(@grpc/grpc-js@1.14.3)(@nestjs/common@11.1.21(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.21)(cache-manager@7.2.8)(ioredis@5.10.0)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/microservices': 11.1.21(@grpc/grpc-js@1.12.4)(@nestjs/common@11.1.21(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.21)(cache-manager@7.2.8)(ioredis@5.10.0)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/platform-express': 11.1.21(@nestjs/common@11.1.21(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.21) '@nestjs/mapped-types@2.1.1(@nestjs/common@11.1.21(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)': @@ -22066,7 +22071,7 @@ snapshots: class-transformer: 0.5.1 class-validator: 0.14.4 - '@nestjs/microservices@11.1.21(@grpc/grpc-js@1.14.3)(@nestjs/common@11.1.21(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.21)(cache-manager@7.2.8)(ioredis@5.10.0)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + '@nestjs/microservices@11.1.21(@grpc/grpc-js@1.12.4)(@nestjs/common@11.1.21(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.21)(cache-manager@7.2.8)(ioredis@5.10.0)(reflect-metadata@0.2.2)(rxjs@7.8.2)': dependencies: '@nestjs/common': 11.1.21(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/core': 11.1.21(@nestjs/common@11.1.21(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/microservices@11.1.21)(@nestjs/platform-express@11.1.21)(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -22075,7 +22080,7 @@ snapshots: rxjs: 7.8.2 tslib: 2.8.1 optionalDependencies: - '@grpc/grpc-js': 1.14.3 + '@grpc/grpc-js': 1.12.4 cache-manager: 7.2.8 ioredis: 5.10.0 @@ -22144,7 +22149,7 @@ snapshots: '@nestjs/core': 11.1.21(@nestjs/common@11.1.21(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/microservices@11.1.21)(@nestjs/platform-express@11.1.21)(reflect-metadata@0.2.2)(rxjs@7.8.2) tslib: 2.8.1 optionalDependencies: - '@nestjs/microservices': 11.1.21(@grpc/grpc-js@1.14.3)(@nestjs/common@11.1.21(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.21)(cache-manager@7.2.8)(ioredis@5.10.0)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/microservices': 11.1.21(@grpc/grpc-js@1.12.4)(@nestjs/common@11.1.21(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.21)(cache-manager@7.2.8)(ioredis@5.10.0)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/platform-express': 11.1.21(@nestjs/common@11.1.21(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.21) '@nestjs/throttler@6.5.0(@nestjs/common@11.1.21(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.21)(reflect-metadata@0.2.2)': @@ -22401,7 +22406,7 @@ snapshots: '@opentelemetry/exporter-logs-otlp-grpc@0.203.0(@opentelemetry/api@1.9.0)': dependencies: - '@grpc/grpc-js': 1.14.3 + '@grpc/grpc-js': 1.12.4 '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) '@opentelemetry/otlp-exporter-base': 0.203.0(@opentelemetry/api@1.9.0) @@ -22440,7 +22445,7 @@ snapshots: '@opentelemetry/exporter-metrics-otlp-grpc@0.203.0(@opentelemetry/api@1.9.0)': dependencies: - '@grpc/grpc-js': 1.14.3 + '@grpc/grpc-js': 1.12.4 '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) '@opentelemetry/exporter-metrics-otlp-http': 0.203.0(@opentelemetry/api@1.9.0) @@ -22478,7 +22483,7 @@ snapshots: '@opentelemetry/exporter-trace-otlp-grpc@0.203.0(@opentelemetry/api@1.9.0)': dependencies: - '@grpc/grpc-js': 1.14.3 + '@grpc/grpc-js': 1.12.4 '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) '@opentelemetry/otlp-exporter-base': 0.203.0(@opentelemetry/api@1.9.0) @@ -23102,7 +23107,7 @@ snapshots: '@opentelemetry/otlp-grpc-exporter-base@0.203.0(@opentelemetry/api@1.9.0)': dependencies: - '@grpc/grpc-js': 1.14.3 + '@grpc/grpc-js': 1.12.4 '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) '@opentelemetry/otlp-exporter-base': 0.203.0(@opentelemetry/api@1.9.0) @@ -23117,7 +23122,7 @@ snapshots: '@opentelemetry/sdk-logs': 0.203.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-metrics': 2.0.1(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0) - protobufjs: 7.6.6 + protobufjs: 7.5.5 '@opentelemetry/otlp-transformer@0.208.0(@opentelemetry/api@1.9.0)': dependencies: @@ -23128,7 +23133,7 @@ snapshots: '@opentelemetry/sdk-logs': 0.208.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-metrics': 2.2.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-base': 2.2.0(@opentelemetry/api@1.9.0) - protobufjs: 7.6.6 + protobufjs: 7.5.5 '@opentelemetry/propagator-b3@2.0.1(@opentelemetry/api@1.9.0)': dependencies: @@ -23510,6 +23515,8 @@ snapshots: '@protobufjs/float@1.0.2': {} + '@protobufjs/inquire@1.1.2': {} + '@protobufjs/path@1.1.2': {} '@protobufjs/pool@1.1.0': {} @@ -25130,7 +25137,7 @@ snapshots: '@temporalio/client@1.15.0': dependencies: - '@grpc/grpc-js': 1.14.3 + '@grpc/grpc-js': 1.12.4 '@temporalio/common': 1.15.0 '@temporalio/proto': 1.15.0 abort-controller: 3.0.0 @@ -25147,7 +25154,7 @@ snapshots: '@temporalio/core-bridge@1.15.0': dependencies: - '@grpc/grpc-js': 1.14.3 + '@grpc/grpc-js': 1.12.4 '@temporalio/common': 1.15.0 '@temporalio/nexus@1.15.0': @@ -25161,11 +25168,11 @@ snapshots: '@temporalio/proto@1.15.0': dependencies: long: 5.3.2 - protobufjs: 7.6.6 + protobufjs: 7.5.5 '@temporalio/worker@1.15.0(@swc/helpers@0.5.13)(esbuild@0.27.7)(tslib@2.8.1)': dependencies: - '@grpc/grpc-js': 1.14.3 + '@grpc/grpc-js': 1.12.4 '@swc/core': 1.5.7(@swc/helpers@0.5.13) '@temporalio/activity': 1.15.0 '@temporalio/client': 1.15.0 @@ -25179,7 +25186,7 @@ snapshots: memfs: 4.56.11(tslib@2.8.1) nexus-rpc: 0.0.1 proto3-json-serializer: 2.0.2 - protobufjs: 7.6.6 + protobufjs: 7.5.5 rxjs: 7.8.2 source-map: 0.7.6 source-map-loader: 4.0.2(webpack@5.105.4(@swc/core@1.5.7(@swc/helpers@0.5.13))(esbuild@0.27.7)) @@ -32563,7 +32570,7 @@ snapshots: nice-grpc@2.1.14: dependencies: - '@grpc/grpc-js': 1.14.3 + '@grpc/grpc-js': 1.12.4 abort-controller-x: 0.4.3 nice-grpc-common: 2.0.2 @@ -33580,9 +33587,9 @@ snapshots: proto3-json-serializer@2.0.2: dependencies: - protobufjs: 7.6.6 + protobufjs: 7.5.5 - protobufjs@7.6.6: + protobufjs@7.5.5: dependencies: '@protobufjs/aspromise': 1.1.2 '@protobufjs/base64': 1.1.2 @@ -33590,6 +33597,7 @@ snapshots: '@protobufjs/eventemitter': 1.1.1 '@protobufjs/fetch': 1.1.1 '@protobufjs/float': 1.0.2 + '@protobufjs/inquire': 1.1.2 '@protobufjs/path': 1.1.2 '@protobufjs/pool': 1.1.0 '@protobufjs/utf8': 1.1.2