diff --git a/CHANGELOG.md b/CHANGELOG.md index 09bc4658a..0f18ab7d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,54 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.4.1] - 2026-07-27 + +### Security + +- **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 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 + +- **`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 e8f9e16e6..27a97c5d4 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -5,19 +5,26 @@ 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). + 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 | |---------|-----------| -| 0.2.x | Yes | -| < 0.2 | No | +| 1.4.x | Yes | +| < 1.4 | No | ## Security Measures @@ -29,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/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..46d70cda8 100644 --- a/connectors/zendesk/src/cli/index.ts +++ b/connectors/zendesk/src/cli/index.ts @@ -16,7 +16,7 @@ import { getConfigDir, getBaseConfigDir, getExportsDir, - getRemoteApiUrl, + findRemoteApiUrl, setRemoteApiUrl, setProfileOverride, getCurrentProfile, @@ -238,7 +238,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 +253,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,17 +274,28 @@ configCmd }); // ============================================ -// Remote API Commands (connect.hasna.com) +// Remote API Commands (host comes from ZENDESK_REMOTE_API_URL / config) // ============================================ 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`); @@ -301,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`); @@ -321,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/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.test.ts b/connectors/zendesk/src/utils/config.test.ts index 217bc53ab..99f5c3d48 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, + getBaseConfigDir, + 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,66 @@ 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(() => { + setProfileOverride(undefined); + // 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; + }); + + 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); + }); + }); }); 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/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 430dde50c..a323e9d0a 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": { @@ -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": { ".": { @@ -28,7 +36,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", 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); }