From b4e5cd7432d7d0e19e34cbcefa29fa49a1c044ec Mon Sep 17 00:00:00 2001 From: andrei-hasna Date: Mon, 27 Jul 2026 07:39:45 +0300 Subject: [PATCH 1/5] chore(security): remove brand-owned hostnames from source and shipped artifacts The published 1.4.0 tarball carried brand-owned apex hostnames. This replaces every occurrence with neutral placeholders, or makes the value configuration- driven where it is a real runtime endpoint. - connectors/zendesk: the remote API URL had a hardcoded deployment host as its fallback default. There is now no shipped default; the value comes from ZENDESK_REMOTE_API_URL or `config set-remote-url`, and getRemoteApiUrl() throws an actionable error when unset. `config show` uses a new findRemoteApiUrl() so it still renders "not set" instead of throwing. - connectors/zendesk nginx templates, Makefile, README, CLAUDE.md, SCAFFOLD.md: placeholders and env-var references instead of a real deployment host. The Makefile deploy banner reads an overridable REMOTE_API_URL. - connectors/googlephotos, connectors/yousearch: illustrative comment/example values replaced with reserved example values. - SECURITY.md: vulnerability reports now route through GitHub Security Advisories rather than an email address at an owned domain. - package.json: mark @hasna/events external in the CLI bundle. One owned name reached bin/index.js only as a bundled dependency literal with no occurrence in this repo's source, so every source-level grep passed while the shipped artifact carried it. It is a declared runtime dependency, so this matches the existing ink/react/chalk/conf externals and keeps the events commands working. Verified against the packed tarball rather than the working tree: npm pack, extract, then scan every extracted member against the full owned-domain portfolio. 1.4.0 tarball: 10 distinct owned hostnames in 12 files. 1.4.1 candidate tarball: zero. Bumps to 1.4.1. --- SECURITY.md | 4 +++- connectors/googlephotos/src/cli/index.ts | 2 +- connectors/yousearch/README.md | 2 +- connectors/zendesk/CLAUDE.md | 2 +- connectors/zendesk/Makefile | 4 +++- connectors/zendesk/README.md | 5 +++-- connectors/zendesk/SCAFFOLD.md | 4 ++-- connectors/zendesk/nginx-connector.conf | 16 ++++++++-------- connectors/zendesk/nginx.conf | 16 ++++++++-------- connectors/zendesk/src/cli/index.ts | 9 +++++---- connectors/zendesk/src/server/index.ts | 2 +- connectors/zendesk/src/utils/config.ts | 14 ++++++++++++-- package.json | 4 ++-- 13 files changed, 50 insertions(+), 34 deletions(-) diff --git a/SECURITY.md b/SECURITY.md index e8f9e16e6..81a14fc5b 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -5,7 +5,9 @@ If you discover a security vulnerability, please report it responsibly: 1. **Do not** open a public issue. -2. Email the maintainers at **security@hasna.dev** with: +2. Open a private report through + [GitHub Security Advisories](https://github.com/hasna/connectors/security/advisories/new) + with: - A description of the vulnerability - Steps to reproduce - Potential impact diff --git a/connectors/googlephotos/src/cli/index.ts b/connectors/googlephotos/src/cli/index.ts index 28ca94d6b..130926273 100644 --- a/connectors/googlephotos/src/cli/index.ts +++ b/connectors/googlephotos/src/cli/index.ts @@ -122,7 +122,7 @@ authCmd const userInfo = await getUserInfo(result.tokens!.accessToken); const email = userInfo.email; - // Convert email to profile slug: andrei@hasna.com → andreihasnacom + // Convert email to profile slug: user@example.com → userexamplecom const profileSlug = email.replace(/[^a-zA-Z0-9]/g, '').toLowerCase(); // Create profile if it doesn't exist diff --git a/connectors/yousearch/README.md b/connectors/yousearch/README.md index 9458e3d50..44bd3233e 100644 --- a/connectors/yousearch/README.md +++ b/connectors/yousearch/README.md @@ -25,7 +25,7 @@ export YOUSEARCH_API_KEY=your-api-key connect-yousearch search "latest AI news" --count 5 # Web search with domain filters (POST) -connect-yousearch search-post "alumia platform" --include-domains "hasna.com,github.com" +connect-yousearch search-post "agent frameworks" --include-domains "example.com,github.com" # Multi-step research connect-yousearch research "What are the latest developments in AI agents?" --effort deep diff --git a/connectors/zendesk/CLAUDE.md b/connectors/zendesk/CLAUDE.md index 273922985..658b40f65 100644 --- a/connectors/zendesk/CLAUDE.md +++ b/connectors/zendesk/CLAUDE.md @@ -43,7 +43,7 @@ connect-zendesk/ - **EC2 Instance**: `hasna-prod-connect-zendesk` - **RDS Database**: `hasna-prod-connect-zendesk` - **S3 Bucket**: `hasna-prod-connect-zendesk` -- **Remote API**: `https://connect.hasna.com/zendesk` +- **Remote API**: deployment-specific; set `ZENDESK_REMOTE_API_URL` (no shipped default) ## Key Patterns diff --git a/connectors/zendesk/Makefile b/connectors/zendesk/Makefile index c71369d9b..abf5c5a47 100644 --- a/connectors/zendesk/Makefile +++ b/connectors/zendesk/Makefile @@ -17,6 +17,8 @@ EC2_USER ?= ec2-user DEPLOY_PATH ?= /home/ec2-user/connectors/connect-zendesk SERVICE_NAME ?= connect-zendesk PORT ?= 21010 +# Public URL the deployed server is reachable at. Deployment-specific — no default. +REMOTE_API_URL ?= # Default target help: @@ -92,7 +94,7 @@ deploy-ec2: deploy-sync deploy: build deploy-ec2 @echo "✅ Deployment complete" - @echo "Server running at https://connect.hasna.com/zendesk" + @echo "Server running at $(if $(REMOTE_API_URL),$(REMOTE_API_URL),)" logs: ssh $(EC2_USER)@$(EC2_HOST) "sudo journalctl -u $(SERVICE_NAME) -f" diff --git a/connectors/zendesk/README.md b/connectors/zendesk/README.md index 199028483..9afbe2fbf 100644 --- a/connectors/zendesk/README.md +++ b/connectors/zendesk/README.md @@ -11,7 +11,7 @@ This connector provides programmatic access to Zendesk's Support API, including | EC2 Instance | `hasna-prod-connect-zendesk` | | RDS Database | `hasna-prod-connect-zendesk` | | S3 Bucket | `hasna-prod-connect-zendesk` | -| Remote API | `https://connect.hasna.com/zendesk` | +| Remote API | configured per deployment via `ZENDESK_REMOTE_API_URL` (no default) | ## Installation @@ -279,7 +279,8 @@ This connector is deployed to: - **Database**: `hasna-prod-connect-zendesk` - **S3**: `hasna-prod-connect-zendesk` -The remote API is accessible at `https://connect.hasna.com/zendesk` +The remote API host is deployment-specific and has no built-in default. Point the CLI at your +deployment with `ZENDESK_REMOTE_API_URL` or `connect-zendesk config set-remote-url `. ## License diff --git a/connectors/zendesk/SCAFFOLD.md b/connectors/zendesk/SCAFFOLD.md index 6b54cb4ad..578fbcdd5 100644 --- a/connectors/zendesk/SCAFFOLD.md +++ b/connectors/zendesk/SCAFFOLD.md @@ -43,7 +43,7 @@ Each connector follows this naming pattern: | EC2 Instance | `hasna-prod-connect-{name}` | `hasna-prod-connect-notion` | | RDS Database | `hasna-prod-connect-{name}` | `hasna-prod-connect-notion` | | S3 Bucket | `hasna-prod-connect-{name}` | `hasna-prod-connect-notion` | -| Remote API | `https://connect.hasna.com/{name}` | `https://connect.hasna.com/notion` | +| Remote API | `https:///{name}` | `https://connect.example.com/notion` | ## Project Structure @@ -175,4 +175,4 @@ The connector is deployed to AWS infrastructure: 1. Build the project: `make build` 2. Deploy to EC2: `make deploy-ec2` -3. Configure the remote API at `https://connect.hasna.com/{name}` +3. Point the CLI at the deployment via `_REMOTE_API_URL` (no default is shipped) diff --git a/connectors/zendesk/nginx-connector.conf b/connectors/zendesk/nginx-connector.conf index 36e76cef7..faf836cde 100644 --- a/connectors/zendesk/nginx-connector.conf +++ b/connectors/zendesk/nginx-connector.conf @@ -54,7 +54,7 @@ http { server { listen 80; - server_name connect.hasna.com; + server_name connect.example.com; location / { root /usr/share/nginx/html; @@ -127,7 +127,7 @@ http { server { listen 80; - server_name gmail.connect.hasna.com; + server_name gmail.connect.example.com; location / { proxy_pass http://127.0.0.1:3001; @@ -140,7 +140,7 @@ http { server { listen 80; - server_name googlecontacts.connect.hasna.com; + server_name googlecontacts.connect.example.com; location / { proxy_pass http://127.0.0.1:3002; @@ -153,7 +153,7 @@ http { server { listen 80; - server_name googledrive.connect.hasna.com; + server_name googledrive.connect.example.com; location / { proxy_pass http://127.0.0.1:3003; @@ -166,7 +166,7 @@ http { server { listen 80; - server_name linear.connect.hasna.com; + server_name linear.connect.example.com; location / { proxy_pass http://127.0.0.1:3004; @@ -179,7 +179,7 @@ http { server { listen 80; - server_name notion.connect.hasna.com; + server_name notion.connect.example.com; location / { proxy_pass http://127.0.0.1:3005; @@ -192,7 +192,7 @@ http { server { listen 80; - server_name clickbank.connect.hasna.com; + server_name clickbank.connect.example.com; location / { proxy_pass http://127.0.0.1:3013; @@ -205,7 +205,7 @@ http { server { listen 80; - server_name zendesk.connect.hasna.com; + server_name zendesk.connect.example.com; location / { proxy_pass http://127.0.0.1:21010; diff --git a/connectors/zendesk/nginx.conf b/connectors/zendesk/nginx.conf index 78eb3fa70..be8c9fc61 100644 --- a/connectors/zendesk/nginx.conf +++ b/connectors/zendesk/nginx.conf @@ -12,10 +12,10 @@ upstream connect_zendesk { keepalive 32; } -# Main server block for connect.hasna.com/zendesk -# Add this location block to the existing connect.hasna.com server +# Main server block for connect.example.com/zendesk +# Add this location block to the existing connect.example.com server # -# If you have a separate server block for connect.hasna.com, add: +# If you have a separate server block for connect.example.com, add: # # location /zendesk { # proxy_pass http://connect_zendesk; @@ -30,21 +30,21 @@ upstream connect_zendesk { # proxy_read_timeout 86400; # } -# Standalone server configuration (use if connect.hasna.com doesn't exist) +# Standalone server configuration (use if connect.example.com doesn't exist) # Uncomment and modify as needed: # # server { # listen 80; -# server_name connect.hasna.com; +# server_name connect.example.com; # return 301 https://$host$request_uri; # } # # server { # listen 443 ssl http2; -# server_name connect.hasna.com; +# server_name connect.example.com; # -# ssl_certificate /etc/letsencrypt/live/connect.hasna.com/fullchain.pem; -# ssl_certificate_key /etc/letsencrypt/live/connect.hasna.com/privkey.pem; +# ssl_certificate /etc/letsencrypt/live/connect.example.com/fullchain.pem; +# ssl_certificate_key /etc/letsencrypt/live/connect.example.com/privkey.pem; # # location /zendesk { # proxy_pass http://connect_zendesk; diff --git a/connectors/zendesk/src/cli/index.ts b/connectors/zendesk/src/cli/index.ts index 8872fd095..22997c863 100644 --- a/connectors/zendesk/src/cli/index.ts +++ b/connectors/zendesk/src/cli/index.ts @@ -17,6 +17,7 @@ import { getBaseConfigDir, getExportsDir, getRemoteApiUrl, + findRemoteApiUrl, setRemoteApiUrl, setProfileOverride, getCurrentProfile, @@ -238,7 +239,7 @@ configCmd configCmd .command('set-remote-url ') - .description('Set remote API URL (default: https://connect.hasna.com/zendesk)') + .description('Set remote API URL (no default; also settable via ZENDESK_REMOTE_API_URL)') .action((url: string) => { setRemoteApiUrl(url); success(`Remote API URL set to: ${url}`); @@ -253,13 +254,13 @@ configCmd const apiToken = getApiToken(); const baseUrl = getBaseUrl(); const account = getDefaultAccount(); - const remoteUrl = getRemoteApiUrl(); + const remoteUrl = findRemoteApiUrl(); info(`Profile: ${chalk.cyan(profile)}`); info(`Email: ${email || chalk.gray('not set')}`); info(`API Token: ${apiToken ? `${apiToken.substring(0, 6)}...${apiToken.substring(apiToken.length - 4)}` : chalk.gray('not set')}`); info(`Base URL: ${baseUrl || chalk.gray('not set')}`); info(`Default Account: ${account || chalk.gray('not set')}`); - info(`Remote API URL: ${remoteUrl}`); + info(`Remote API URL: ${remoteUrl || chalk.gray('not set')}`); info(`Config Directory: ${getBaseConfigDir()}`); info(`Profile Config: ${getConfigDir()}`); info(`Exports Directory: ${getExportsDir()}`); @@ -274,7 +275,7 @@ configCmd }); // ============================================ -// Remote API Commands (connect.hasna.com) +// Remote API Commands (host comes from ZENDESK_REMOTE_API_URL / config) // ============================================ const remoteCmd = program .command('remote') diff --git a/connectors/zendesk/src/server/index.ts b/connectors/zendesk/src/server/index.ts index 63096e617..6cf72cf3d 100644 --- a/connectors/zendesk/src/server/index.ts +++ b/connectors/zendesk/src/server/index.ts @@ -2,7 +2,7 @@ /** * connect-zendesk server * Remote API server for Zendesk connector - * Deployed at https://connect.hasna.com/zendesk + * Deployment host is environment-specific; see nginx.conf for the reverse-proxy template. */ const PORT = parseInt(process.env.PORT || '3000'); diff --git a/connectors/zendesk/src/utils/config.ts b/connectors/zendesk/src/utils/config.ts index 0d41a8d65..eef2924f0 100644 --- a/connectors/zendesk/src/utils/config.ts +++ b/connectors/zendesk/src/utils/config.ts @@ -499,10 +499,20 @@ export function clearConfig(): void { saveConfig({}); } -const DEFAULT_REMOTE_API_URL = 'https://connect.hasna.com/zendesk'; +// The remote API host is deployment-specific and has no shippable default. +// Configure it with ZENDESK_REMOTE_API_URL or `connect-zendesk config set-remote-url `. +export function findRemoteApiUrl(): string | undefined { + return process.env.ZENDESK_REMOTE_API_URL || loadConfig().remoteApiUrl || undefined; +} export function getRemoteApiUrl(): string { - return process.env.ZENDESK_REMOTE_API_URL || loadConfig().remoteApiUrl || DEFAULT_REMOTE_API_URL; + const url = findRemoteApiUrl(); + if (!url) { + throw new Error( + 'Remote API URL is not configured. Set ZENDESK_REMOTE_API_URL or run: connect-zendesk config set-remote-url ', + ); + } + return url; } export function setRemoteApiUrl(url: string): void { diff --git a/package.json b/package.json index 430dde50c..50f7644ca 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@hasna/connectors", - "version": "1.4.0", + "version": "1.4.1", "description": "Open source connector library - Install API connectors with a single command", "type": "module", "bin": { @@ -28,7 +28,7 @@ "main": "./dist/index.js", "types": "./dist/index.d.ts", "scripts": { - "build": "cd dashboard && bun run build && cd .. && bun build ./src/cli/index.tsx --outdir ./bin --target bun --external ink --external react --external chalk --external conf && bun build ./src/mcp/index.ts --outfile ./bin/mcp.js --target bun && bun build ./src/server/index.ts --outfile ./bin/serve.js --target bun && bun build ./src/index.ts --outdir ./dist --target bun && bun build ./src/social/index.ts --outfile ./dist/social/index.js --target bun && tsc --emitDeclarationOnly --outDir ./dist && tsc -p tsconfig.social.json", + "build": "cd dashboard && bun run build && cd .. && bun build ./src/cli/index.tsx --outdir ./bin --target bun --external ink --external react --external chalk --external conf --external @hasna/events && bun build ./src/mcp/index.ts --outfile ./bin/mcp.js --target bun && bun build ./src/server/index.ts --outfile ./bin/serve.js --target bun && bun build ./src/index.ts --outdir ./dist --target bun && bun build ./src/social/index.ts --outfile ./dist/social/index.js --target bun && tsc --emitDeclarationOnly --outDir ./dist && tsc -p tsconfig.social.json", "build:dashboard": "cd dashboard && bun run build", "postinstall": "mkdir -p $HOME/.hasna/connectors 2>/dev/null || true; [ \"$SKIP_DASHBOARD\" = \"1\" ] || [ ! -f dashboard/package.json ] || [ -d dashboard/node_modules ] || (cd dashboard && bun install)", "dev": "bun run ./src/cli/index.tsx", From 08e49056e277c29dde4aee09d7e7bdc10d66410e Mon Sep 17 00:00:00 2001 From: andrei-hasna Date: Mon, 27 Jul 2026 08:12:08 +0300 Subject: [PATCH 2/5] fix(security): restore a working disclosure path and cover remote-URL resolution Addresses two reviewer findings on the owned-hostname scrub. SECURITY.md pointed at the GitHub Security Advisories form while private vulnerability reporting was disabled on the repo, so an external reporter following the link got a 404 and the removed security@ address left no fallback. Private vulnerability reporting is now enabled on hasna/connectors, which makes the linked URL resolve and renders the public report button; the policy now states that any signed-in GitHub user can file, and adds a no-details-in-public fallback for the case where the form is unreachable. getRemoteApiUrl() changed from returning a hardcoded default host to throwing when unconfigured, but no test referenced it, so the suite was byte-identically green with the change reverted. Adds four tests that import the real config module rather than re-implementing the priority logic, isolated through the module's own profile override: unconfigured throws and findRemoteApiUrl() is undefined, env var resolves, stored config resolves, env beats stored config. Verified the unconfigured test fails on regression: reinstating a hardcoded default in getRemoteApiUrl() turns it red (16 pass / 1 fail), and reverting config.ts wholesale fails the file at import. Suite: 3973 pass, 0 fail, 35200 expect() calls (bun test, after bun run build as CI does). Typecheck clean. --- SECURITY.md | 9 ++- connectors/zendesk/src/utils/config.test.ts | 65 +++++++++++++++++++++ 2 files changed, 72 insertions(+), 2 deletions(-) diff --git a/SECURITY.md b/SECURITY.md index 81a14fc5b..b81f1fcb4 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -6,14 +6,19 @@ If you discover a security vulnerability, please report it responsibly: 1. **Do not** open a public issue. 2. Open a private report through - [GitHub Security Advisories](https://github.com/hasna/connectors/security/advisories/new) - with: + [GitHub Security Advisories](https://github.com/hasna/connectors/security/advisories/new). + Private vulnerability reporting is enabled on this repository, so any signed-in + GitHub user can file a report and only the maintainers can read it. Include: - A description of the vulnerability - Steps to reproduce - Potential impact 3. You will receive an acknowledgment within 48 hours. 4. We will work with you to understand and address the issue before any public disclosure. +If the advisory form is ever unreachable, open a public issue requesting a private +contact channel — with **no** vulnerability details in it — and a maintainer will +follow up privately. + ## Supported Versions | Version | Supported | diff --git a/connectors/zendesk/src/utils/config.test.ts b/connectors/zendesk/src/utils/config.test.ts index 217bc53ab..881577a4a 100644 --- a/connectors/zendesk/src/utils/config.test.ts +++ b/connectors/zendesk/src/utils/config.test.ts @@ -2,6 +2,14 @@ import { describe, test, expect, beforeEach, afterEach } from 'bun:test'; import { existsSync, rmSync, mkdirSync, writeFileSync, readFileSync } from 'fs'; import { homedir } from 'os'; import { join } from 'path'; +import { + findRemoteApiUrl, + getRemoteApiUrl, + setRemoteApiUrl, + setProfileOverride, + getConfigDir, + clearConfig, +} from './config'; // We need to test the config module with a custom config dir // to avoid messing with actual user config @@ -190,4 +198,61 @@ describe('Config utilities', () => { else delete process.env.ZENDESK_EMAIL; }); }); + + // The remote API URL no longer falls back to a hardcoded deployment host, so + // these exercise the real module instead of re-implementing the priority + // logic: an unconfigured URL has to fail loudly rather than silently resolve + // to a baked-in default, and each configured source has to round-trip. + describe('remote API URL resolution', () => { + const REMOTE_URL_TEST_PROFILE = 'remote-url-test'; + const ENV_URL = 'https://env.example.com/zendesk'; + const STORED_URL = 'https://stored.example.com/zendesk'; + + let originalEnv: string | undefined; + + beforeEach(() => { + originalEnv = process.env.ZENDESK_REMOTE_API_URL; + delete process.env.ZENDESK_REMOTE_API_URL; + // Profiles are the module's own isolation seam: point config reads and + // writes at a throwaway profile so the real one is never touched. + setProfileOverride(REMOTE_URL_TEST_PROFILE); + clearConfig(); + }); + + afterEach(() => { + const profileDir = getConfigDir(); + setProfileOverride(undefined); + rmSync(profileDir, { recursive: true, force: true }); + + if (originalEnv === undefined) delete process.env.ZENDESK_REMOTE_API_URL; + else process.env.ZENDESK_REMOTE_API_URL = originalEnv; + }); + + test('is unset and throws when neither env nor config provides a URL', () => { + expect(findRemoteApiUrl()).toBeUndefined(); + expect(() => getRemoteApiUrl()).toThrow(/ZENDESK_REMOTE_API_URL/); + }); + + test('resolves from the environment variable', () => { + process.env.ZENDESK_REMOTE_API_URL = ENV_URL; + + expect(findRemoteApiUrl()).toBe(ENV_URL); + expect(getRemoteApiUrl()).toBe(ENV_URL); + }); + + test('resolves from the stored config value', () => { + setRemoteApiUrl(STORED_URL); + + expect(findRemoteApiUrl()).toBe(STORED_URL); + expect(getRemoteApiUrl()).toBe(STORED_URL); + }); + + test('environment variable takes precedence over the stored config value', () => { + setRemoteApiUrl(STORED_URL); + process.env.ZENDESK_REMOTE_API_URL = ENV_URL; + + expect(findRemoteApiUrl()).toBe(ENV_URL); + expect(getRemoteApiUrl()).toBe(ENV_URL); + }); + }); }); From cb45d35958de01fca99a3fe32d9d0e30cb509113 Mon Sep 17 00:00:00 2001 From: andrei-hasna Date: Mon, 27 Jul 2026 08:17:14 +0300 Subject: [PATCH 3/5] test(zendesk): remove ordering hazard from remote-URL test cleanup The afterEach resolved the throwaway profile directory through getConfigDir(), which only returns the test profile while the override is still set. Reordering those two statements would have pointed the rmSync at the developer's real default profile. Derive the path by name from getBaseConfigDir() instead, so cleanup targets the test profile regardless of sequencing. Suite: 3973 pass, 0 fail, 35200 expect() calls (bun test). Typecheck clean. --- connectors/zendesk/src/utils/config.test.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/connectors/zendesk/src/utils/config.test.ts b/connectors/zendesk/src/utils/config.test.ts index 881577a4a..99f5c3d48 100644 --- a/connectors/zendesk/src/utils/config.test.ts +++ b/connectors/zendesk/src/utils/config.test.ts @@ -7,7 +7,7 @@ import { getRemoteApiUrl, setRemoteApiUrl, setProfileOverride, - getConfigDir, + getBaseConfigDir, clearConfig, } from './config'; @@ -220,9 +220,14 @@ describe('Config utilities', () => { }); afterEach(() => { - const profileDir = getConfigDir(); setProfileOverride(undefined); - rmSync(profileDir, { recursive: true, force: true }); + // Derive the throwaway path by name rather than from getConfigDir(), so + // this stays a delete of the test profile no matter how the override is + // sequenced above it. + rmSync(join(getBaseConfigDir(), 'profiles', REMOTE_URL_TEST_PROFILE), { + recursive: true, + force: true, + }); if (originalEnv === undefined) delete process.env.ZENDESK_REMOTE_API_URL; else process.env.ZENDESK_REMOTE_API_URL = originalEnv; From aff9745f31e0b8082348d7ac6d4ed7f1761032bb Mon Sep 17 00:00:00 2001 From: andrei-hasna Date: Mon, 27 Jul 2026 08:18:10 +0300 Subject: [PATCH 4/5] fix(security): address adversarial review before publishing 1.4.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An adversarial review returned BLOCK-PUBLISH on the scrub commit. The scrub itself was confirmed clean by an independent, more sensitive scanner; these are the three defects it found alongside it, two of which the scrub introduced. Three commands crashed instead of reporting an unset value. Removing the remote API URL default left `remote url`, `remote status` and `remote health` calling the throwing accessor outside their try blocks, so they exited with a raw stack trace. `remote url` was the worst of them: its whole job is to tell you whether the URL is set. They now go through a helper that reports the missing configuration using the connector's own error convention, and `remote url` reports "not set" like `config show` already did. Added a test that runs the real CLI against a throwaway HOME; it fails on the pre-fix code (3 of 7) and passes after, so it is a regression guard rather than decoration. The new disclosure link was dead. SECURITY.md now routes reports to GitHub Security Advisories, but private vulnerability reporting was disabled on the repository, so the link 404s for anyone who is not a collaborator — the change had removed the only working disclosure channel. Reporting is now enabled. Also corrected two stale claims in the same file: the supported-version table still said 0.2.x, and the credential path did not match what the code uses. The scanned artifact was not the artifact that would ship. `prepublishOnly` runs the test suite before npm packs, and the suite creates per-connector `.test-home` sandboxes and lockfiles that `files: ["connectors/"]` then sweeps in — 1.4.0 shipped a Bun install-cache blob exactly this way. `files` now carries explicit negations, verified by packing after a full test run. `scripts/check-package-secrets.ts` scanned `git ls-files`, so it structurally could not see those untracked-but-shipped files; a lockfile carrying an auth token would have passed. It now scans the union of tracked and packed files, and fails loudly rather than degrading if the pack list cannot be read. Adds the 1.4.1 changelog entry. --- CHANGELOG.md | 34 +++++++++ SECURITY.md | 8 +-- connectors/zendesk/src/cli/index.ts | 18 +++-- .../zendesk/src/utils/remote-url.test.ts | 72 +++++++++++++++++++ package.json | 10 ++- scripts/check-package-secrets.ts | 33 ++++++++- 6 files changed, 164 insertions(+), 11 deletions(-) create mode 100644 connectors/zendesk/src/utils/remote-url.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 09bc4658a..de9e088c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,40 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.4.1] - 2026-07-27 + +### Security + +- **Brand-owned hostnames removed from the published artifact.** 1.4.0 shipped + real deployment hostnames in `connectors/zendesk` config templates and docs, + in two other connectors' comments and examples, in `SECURITY.md`, and — via a + bundled dependency literal with no occurrence anywhere in this repo's source — + in the compiled `bin/index.js`. Every occurrence is gone. Verified against the + packed tarball rather than the working tree, because the working tree was + never where the whole problem was. + +### Changed + +- **`connect-zendesk` no longer ships a default remote API URL.** The value was + a hardcoded deployment host used as a fallback. It now comes from + `ZENDESK_REMOTE_API_URL` or `connect-zendesk config set-remote-url `. + `config show` and `remote url` report `not set`; `remote status` and + `remote health` exit non-zero with guidance naming both mechanisms. `make`'s + deploy banner reads a new overridable `REMOTE_API_URL`. +- **Vulnerability reports go through GitHub Security Advisories** instead of an + email address. Private vulnerability reporting is enabled on the repository. +- `@hasna/events` is marked external in the CLI bundle, joining the existing + `ink` / `react` / `chalk` / `conf` externals. It is a declared runtime + dependency, so npm resolves it at install time. + +### Fixed + +- `.test-home/` sandboxes and per-connector lockfiles are no longer swept into + the published tarball by `files: ["connectors/"]`. 1.4.0 shipped a Bun + install-cache blob this way. +- 1.4.0 shipped `.d.ts` files for two modules deleted in 1.4.0 itself, because + `dist/` was not clean at release time. + ## [1.4.0] - 2026-07-26 ### Removed diff --git a/SECURITY.md b/SECURITY.md index b81f1fcb4..27a97c5d4 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -23,8 +23,8 @@ follow up privately. | Version | Supported | |---------|-----------| -| 0.2.x | Yes | -| < 0.2 | No | +| 1.4.x | Yes | +| < 1.4 | No | ## Security Measures @@ -36,8 +36,8 @@ This project implements the following security measures: - **Security headers** (`X-Content-Type-Options: nosniff`, `X-Frame-Options: DENY`) - **Request body size limits** (1MB max) - **Fetch timeouts** (10s) on external HTTP requests -- **No secrets in source** — credentials stored in `~/.connectors/` only +- **No secrets in source** — credentials stored in `~/.hasna/connectors/` only ## Credential Storage -Connector credentials are stored locally at `~/.connectors/connect-{name}/`. OAuth tokens are written with `0o600` permissions. No credentials are ever sent to our servers or included in the npm package. +Connector credentials are stored locally at `~/.hasna/connectors/connect-{name}/`. OAuth tokens are written with `0o600` permissions. No credentials are ever sent to our servers or included in the npm package. diff --git a/connectors/zendesk/src/cli/index.ts b/connectors/zendesk/src/cli/index.ts index 22997c863..46d70cda8 100644 --- a/connectors/zendesk/src/cli/index.ts +++ b/connectors/zendesk/src/cli/index.ts @@ -16,7 +16,6 @@ import { getConfigDir, getBaseConfigDir, getExportsDir, - getRemoteApiUrl, findRemoteApiUrl, setRemoteApiUrl, setProfileOverride, @@ -281,11 +280,22 @@ const remoteCmd = program .command('remote') .description('Interact with the remote Zendesk connector API'); +// The remote host has no shipped default. Commands that need it exit with the +// connector's usual error convention rather than an uncaught throw. +function requireRemoteApiUrl(): string { + const remoteUrl = findRemoteApiUrl(); + if (!remoteUrl) { + error('Remote API URL is not configured. Set ZENDESK_REMOTE_API_URL or run: connect-zendesk config set-remote-url '); + process.exit(1); + } + return remoteUrl; +} + remoteCmd .command('status') .description('Check remote API status') .action(async () => { - const remoteUrl = getRemoteApiUrl(); + const remoteUrl = requireRemoteApiUrl(); logger.command('remote status', { remoteUrl }); try { const response = await fetch(`${remoteUrl}/status`); @@ -302,7 +312,7 @@ remoteCmd .command('health') .description('Check remote API health') .action(async () => { - const remoteUrl = getRemoteApiUrl(); + const remoteUrl = requireRemoteApiUrl(); logger.command('remote health', { remoteUrl }); try { const response = await fetch(`${remoteUrl}/health`); @@ -322,7 +332,7 @@ remoteCmd .command('url') .description('Show current remote API URL') .action(() => { - info(`Remote API URL: ${getRemoteApiUrl()}`); + info(`Remote API URL: ${findRemoteApiUrl() || chalk.gray('not set')}`); }); // ============================================ diff --git a/connectors/zendesk/src/utils/remote-url.test.ts b/connectors/zendesk/src/utils/remote-url.test.ts new file mode 100644 index 000000000..b3414f2ba --- /dev/null +++ b/connectors/zendesk/src/utils/remote-url.test.ts @@ -0,0 +1,72 @@ +import { describe, test, expect, beforeEach, afterEach } from 'bun:test'; +import { mkdtempSync, rmSync } from 'fs'; +import { tmpdir } from 'os'; +import { join } from 'path'; + +// The remote API URL used to fall back to a hardcoded deployment host. It no +// longer has a shipped default, so these lock in how the CLI behaves when it is +// unset: display commands must still print, and commands that genuinely need +// the URL must fail with actionable guidance rather than an uncaught throw. +// +// Resolution order itself is covered in config.test.ts. This file covers the +// CLI surface: os.homedir() does not observe runtime process.env.HOME mutation, +// so each case runs the real CLI in a subprocess with its own HOME. + +const CLI = join(import.meta.dir, '..', 'cli', 'index.ts'); +const ENV_KEY = 'ZENDESK_REMOTE_API_URL'; + +let home: string; + +function runCli(args: string[], env: Record = {}) { + const result = Bun.spawnSync({ + cmd: ['bun', 'run', CLI, ...args], + env: { ...process.env, HOME: home, [ENV_KEY]: '', ...env }, + stdout: 'pipe', + stderr: 'pipe', + }); + return { + code: result.exitCode, + out: new TextDecoder().decode(result.stdout) + new TextDecoder().decode(result.stderr), + }; +} + +describe('CLI behaviour when the remote API URL is unset', () => { + beforeEach(() => { + home = mkdtempSync(join(tmpdir(), 'connect-zendesk-remote-url-')); + }); + + afterEach(() => { + rmSync(home, { recursive: true, force: true }); + }); + + test('config show reports it as unset instead of throwing', () => { + const { code, out } = runCli(['config', 'show']); + expect(code).toBe(0); + expect(out).toContain('Remote API URL:'); + expect(out).toContain('not set'); + }); + + test('remote url reports it as unset instead of throwing', () => { + const { code, out } = runCli(['remote', 'url']); + expect(code).toBe(0); + expect(out).toContain('not set'); + expect(out).not.toContain('at getRemoteApiUrl'); + }); + + test.each(['status', 'health'])( + 'remote %s fails with actionable guidance, not a stack trace', + (sub) => { + const { code, out } = runCli(['remote', sub]); + expect(code).toBe(1); + expect(out).toContain(ENV_KEY); + expect(out).toContain('config set-remote-url'); + expect(out).not.toContain('at getRemoteApiUrl'); + }, + ); + + test('no deployment host is baked in as a fallback', () => { + // A regression here means someone reintroduced a literal default. + const { out } = runCli(['remote', 'url']); + expect(out).not.toMatch(/https?:\/\/\S+/); + }); +}); diff --git a/package.json b/package.json index 50f7644ca..a323e9d0a 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,15 @@ "dist/", "dashboard/dist/", "connectors/", - "README.md" + "README.md", + "!connectors/**/.test-home/**", + "!connectors/**/node_modules/**", + "!connectors/**/bun.lock", + "!connectors/**/package-lock.json", + "!connectors/**/yarn.lock", + "!connectors/**/pnpm-lock.yaml", + "!connectors/**/.env", + "!connectors/**/.env.local" ], "exports": { ".": { diff --git a/scripts/check-package-secrets.ts b/scripts/check-package-secrets.ts index 41f22f83a..80063504e 100644 --- a/scripts/check-package-secrets.ts +++ b/scripts/check-package-secrets.ts @@ -34,6 +34,35 @@ function trackedFiles(): string[] { return output.split("\0").filter(Boolean); } +// What npm will actually ship. This is deliberately not `git ls-files`: `files` +// in package.json is an allowlist over the working tree, so it picks up +// untracked build output and per-connector lockfiles that a tracked-only scan +// cannot see — which is exactly where a leaked credential would hide from us. +function packedFiles(): string[] { + const output = execFileSync("npm", ["pack", "--dry-run", "--json", "--ignore-scripts"], { + encoding: "utf-8", + maxBuffer: 256 * 1024 * 1024, + stdio: ["ignore", "pipe", "ignore"], + }); + const parsed = JSON.parse(output) as Array<{ files?: Array<{ path: string }> }>; + return (parsed[0]?.files ?? []).map((file) => file.path); +} + +// Union, so the guard keeps covering tracked files that never ship (CI config, +// scripts) while gaining everything that does. +function filesToScan(): string[] { + const paths = new Set(trackedFiles()); + try { + for (const path of packedFiles()) paths.add(path); + } catch (error) { + // A guard that silently degrades to a weaker scan is how this class of bug + // reaches the registry. Fail loudly instead. + console.error(`Could not enumerate the packed file list: ${String(error)}`); + process.exit(1); + } + return [...paths]; +} + function shouldScan(path: string): boolean { const name = basename(path); return isNpmrcName(name) || name === "bunfig.toml" || name === ".bunfig.toml" || LOCKFILE_NAMES.has(name); @@ -166,7 +195,7 @@ function isExactHasnaPackageName(item: string): boolean { const findings: Finding[] = []; let scanned = 0; -for (const path of trackedFiles().filter(shouldScan)) { +for (const path of filesToScan().filter(shouldScan)) { const text = readText(path); if (text === null) continue; scanned++; @@ -177,7 +206,7 @@ for (const path of trackedFiles().filter(shouldScan)) { } if (findings.length === 0) { - console.log(`Package-manager secret guard clean (${scanned} tracked file(s) scanned).`); + console.log(`Package-manager secret guard clean (${scanned} tracked + packed file(s) scanned).`); process.exit(0); } From 8cbb7f33588d4c5d3f26e71cb319f4d97e3c5c50 Mon Sep 17 00:00:00 2001 From: andrei-hasna Date: Mon, 27 Jul 2026 14:21:15 +0300 Subject: [PATCH 5/5] docs(security): scope the 1.4.1 claim to DNS domains, not all hostnames Adversarial review of this branch found the Security entry overclaiming. It said "hostnames removed" and "every occurrence is gone", but two classes are untouched: - the externalized runtime dependency still carries the literal in its own published package, so an install still puts an owned domain on disk; - deployment resource identifiers, and the pattern they are named by, are unchanged from 1.4.0 and still ship. A shipped Makefile two lines from an edit this branch made still names one. What the branch does achieve is real and independently reproduced: zero owned DNS domains across every member of the packed tarball, with the leaky predecessor as a positive control over a dataset of the same size. Narrowing the wording keeps that result honest instead of letting it read as a wider guarantee a consumer would then rely on. Documentation only. No code, no behaviour change. --- CHANGELOG.md | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index de9e088c9..0f18ab7d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,13 +11,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Security -- **Brand-owned hostnames removed from the published artifact.** 1.4.0 shipped +- **Brand-owned DNS domains removed from the published artifact.** 1.4.0 shipped real deployment hostnames in `connectors/zendesk` config templates and docs, in two other connectors' comments and examples, in `SECURITY.md`, and — via a bundled dependency literal with no occurrence anywhere in this repo's source — - in the compiled `bin/index.js`. Every occurrence is gone. Verified against the - packed tarball rather than the working tree, because the working tree was - never where the whole problem was. + in the compiled `bin/index.js`. Every occurrence of an owned DNS domain is gone + from the tarball this package publishes. Verified against the packed tarball + rather than the working tree, because the working tree was never where the + whole problem was. + + Two limits on that claim, stated because "hostnames removed" would overstate it: + + - `@hasna/events` is externalized out of `bin/index.js` (see *Changed*) but is + still a runtime dependency, and its own published package continues to carry + the literal. `npm install` therefore still places an owned domain on disk. + Removing it belongs to that package, and is tracked there. + - Scope here is DNS domains. Deployment **resource identifiers** — the + instance, database and bucket names in `connectors/zendesk`'s docs, `Makefile` + and `.env.example`, together with the naming pattern they follow — are + unchanged from 1.4.0 and still ship. They are a separate class with a + separate fix, tracked separately; this release does not address them and + should not be read as having done so. ### Changed