From 6bdaa4b1bbda8222105f17a378c8d4c59a14eb80 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 1 Nov 2026 11:46:47 +0000 Subject: [PATCH] fix(apps): the batch-1 connector apps carry their own iconUrl connector-github and connector-stripe each own a neutral connector glyph under assets/icon.svg, and objectstack.manifest.json points iconUrl at its raw.githubusercontent.com URL on main, the way HotCRM's manifest does. Each app's listing test now refuses a manifest whose iconUrl is absent or not http(s). Claude-Session: https://claude.ai/code/session_01FEkQ5p28hxU1qvesRWw9LU Co-authored-by: Claude --- .../apps/connector-github/assets/icon.svg | 7 +++ .../objectstack.manifest.json | 1 + .../connector-github/test/listing.test.ts | 47 ++++++++++++++++++- .../apps/connector-stripe/assets/icon.svg | 7 +++ .../objectstack.manifest.json | 1 + .../connector-stripe/test/listing.test.ts | 47 ++++++++++++++++++- 6 files changed, 106 insertions(+), 4 deletions(-) create mode 100644 packages/apps/connector-github/assets/icon.svg create mode 100644 packages/apps/connector-stripe/assets/icon.svg diff --git a/packages/apps/connector-github/assets/icon.svg b/packages/apps/connector-github/assets/icon.svg new file mode 100644 index 00000000000..2af96e89df5 --- /dev/null +++ b/packages/apps/connector-github/assets/icon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/packages/apps/connector-github/objectstack.manifest.json b/packages/apps/connector-github/objectstack.manifest.json index a3b7d7298d3..b1b94b0295d 100644 --- a/packages/apps/connector-github/objectstack.manifest.json +++ b/packages/apps/connector-github/objectstack.manifest.json @@ -8,6 +8,7 @@ "category": "devtools", "tags": ["github", "mcp", "connector", "issues"], "license": "Apache-2.0", + "iconUrl": "https://raw.githubusercontent.com/objectstack-ai/objectstack/main/packages/apps/connector-github/assets/icon.svg", "homepageUrl": "https://github.com/objectstack-ai/objectstack/tree/main/packages/apps/connector-github", "readmePath": "README.md" } diff --git a/packages/apps/connector-github/test/listing.test.ts b/packages/apps/connector-github/test/listing.test.ts index 6aad53d236b..cc4daef583d 100644 --- a/packages/apps/connector-github/test/listing.test.ts +++ b/packages/apps/connector-github/test/listing.test.ts @@ -3,14 +3,15 @@ /** * The listing this package publishes as: `objectstack.manifest.json` parses * against the spec's own schema for that file, names the same package id as - * the stack, points at a README that exists, and that README's credentials + * the stack, points at a README that exists and at the icon the package owns + * (a public http(s) `iconUrl`), and that README's credentials * section names every credential the app's connectors reference — the * marketplace handbook's "every credential it needs" rule, checked against * the metadata rather than trusted. */ import { describe, it, expect } from 'vitest'; -import { readFileSync, existsSync } from 'node:fs'; +import { readFileSync, existsSync, statSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import { TemplateManifestSchema } from '@objectstack/spec/marketplace'; import stack from '../objectstack.config.js'; @@ -18,6 +19,27 @@ import stack from '../objectstack.config.js'; const manifestPath = fileURLToPath(new URL('../objectstack.manifest.json', import.meta.url)); const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) as Record; +/** Where the listing icon is served from once this package's tree is on `main`. */ +const ICON_URL = 'https://raw.githubusercontent.com/objectstack-ai/objectstack/main/packages/apps/connector-github/assets/icon.svg'; + +/** + * Why a manifest's `iconUrl` cannot be the listing icon, or `null` when it can: + * `os package publish` sends the key as the package's `icon_url`, and the + * marketplace handbook asks for a public http(s) URL. `TemplateManifestSchema` + * accepts any URL scheme, so the scheme is checked here. + */ +function iconUrlProblem(m: Record): string | null { + const value = m.iconUrl; + if (typeof value !== 'string') return 'iconUrl is absent'; + let url: URL; + try { + url = new URL(value); + } catch { + return `iconUrl is not a URL: ${value}`; + } + return url.protocol === 'https:' || url.protocol === 'http:' ? null : `iconUrl is not http(s): ${value}`; +} + /** The README's `## Credentials` section, up to the next second-level heading. */ function credentialsSection(readme: string): string { const start = readme.indexOf('\n## Credentials\n'); @@ -55,6 +77,27 @@ describe('connector-github listing', () => { expect(section).toContain('https://github.com/settings/personal-access-tokens/new'); }); + it('carries a public http(s) iconUrl', () => { + expect(iconUrlProblem(manifest)).toBeNull(); + }); + + it('refuses an iconUrl that is absent or not http(s)', () => { + const withoutIcon = { ...manifest }; + delete withoutIcon.iconUrl; + expect(iconUrlProblem(withoutIcon)).toBe('iconUrl is absent'); + expect(iconUrlProblem({ ...manifest, iconUrl: 'ftp://example.com/icon.svg' })).toMatch(/not http\(s\)/); + expect(iconUrlProblem({ ...manifest, iconUrl: 'assets/icon.svg' })).toMatch(/not a URL/); + }); + + it('points iconUrl at the icon this package owns', () => { + expect(manifest.iconUrl).toBe(ICON_URL); + const iconPath = fileURLToPath(new URL('../assets/icon.svg', import.meta.url)); + expect(existsSync(iconPath)).toBe(true); + // `--icon-file` takes up to 256 KB; hold the owned asset to the same bound. + expect(statSync(iconPath).size).toBeLessThan(256 * 1024); + expect(readFileSync(iconPath, 'utf8')).toMatch(/^]*viewBox="0 0 256 256"/); + }); + it('carries no secret in metadata', () => { for (const c of stack.connectors ?? []) { expect(c).not.toHaveProperty('authentication.token'); diff --git a/packages/apps/connector-stripe/assets/icon.svg b/packages/apps/connector-stripe/assets/icon.svg new file mode 100644 index 00000000000..ef23a660a00 --- /dev/null +++ b/packages/apps/connector-stripe/assets/icon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/packages/apps/connector-stripe/objectstack.manifest.json b/packages/apps/connector-stripe/objectstack.manifest.json index b027a71df63..387d81ef611 100644 --- a/packages/apps/connector-stripe/objectstack.manifest.json +++ b/packages/apps/connector-stripe/objectstack.manifest.json @@ -8,6 +8,7 @@ "category": "finance", "tags": ["stripe", "openapi", "connector", "payments"], "license": "Apache-2.0", + "iconUrl": "https://raw.githubusercontent.com/objectstack-ai/objectstack/main/packages/apps/connector-stripe/assets/icon.svg", "homepageUrl": "https://github.com/objectstack-ai/objectstack/tree/main/packages/apps/connector-stripe", "readmePath": "README.md" } diff --git a/packages/apps/connector-stripe/test/listing.test.ts b/packages/apps/connector-stripe/test/listing.test.ts index d4142f3020d..0c840d2cab4 100644 --- a/packages/apps/connector-stripe/test/listing.test.ts +++ b/packages/apps/connector-stripe/test/listing.test.ts @@ -3,7 +3,8 @@ /** * The listing this package publishes as, and the document it packages: * `objectstack.manifest.json` parses against the spec's own schema for that - * file and names the same package id as the stack; the README's credentials + * file, names the same package id as the stack and points at the icon the + * package owns (a public http(s) `iconUrl`); the README's credentials * section names every credential the app's connectors reference (the * marketplace handbook's "every credential it needs" rule, checked against * the metadata rather than trusted); and the packaged OpenAPI document is the @@ -13,7 +14,7 @@ import { describe, it, expect } from 'vitest'; import { createHash } from 'node:crypto'; -import { readFileSync, existsSync } from 'node:fs'; +import { readFileSync, existsSync, statSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import { TemplateManifestSchema } from '@objectstack/spec/marketplace'; import stack from '../objectstack.config.js'; @@ -21,6 +22,27 @@ import { STRIPE_READ_OPERATIONS, StripeConnector } from '../src/connectors/strip const manifestPath = fileURLToPath(new URL('../objectstack.manifest.json', import.meta.url)); const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) as Record; + +/** Where the listing icon is served from once this package's tree is on `main`. */ +const ICON_URL = 'https://raw.githubusercontent.com/objectstack-ai/objectstack/main/packages/apps/connector-stripe/assets/icon.svg'; + +/** + * Why a manifest's `iconUrl` cannot be the listing icon, or `null` when it can: + * `os package publish` sends the key as the package's `icon_url`, and the + * marketplace handbook asks for a public http(s) URL. `TemplateManifestSchema` + * accepts any URL scheme, so the scheme is checked here. + */ +function iconUrlProblem(m: Record): string | null { + const value = m.iconUrl; + if (typeof value !== 'string') return 'iconUrl is absent'; + let url: URL; + try { + url = new URL(value); + } catch { + return `iconUrl is not a URL: ${value}`; + } + return url.protocol === 'https:' || url.protocol === 'http:' ? null : `iconUrl is not http(s): ${value}`; +} const readme = readFileSync(fileURLToPath(new URL('../README.md', import.meta.url)), 'utf8'); const documentBytes = readFileSync(fileURLToPath(new URL('../openapi/stripe.openapi.json', import.meta.url))); const document = JSON.parse(documentBytes.toString('utf8')) as { @@ -65,6 +87,27 @@ describe('connector-stripe listing', () => { expect(credentials).toContain('https://dashboard.stripe.com/apikeys'); }); + it('carries a public http(s) iconUrl', () => { + expect(iconUrlProblem(manifest)).toBeNull(); + }); + + it('refuses an iconUrl that is absent or not http(s)', () => { + const withoutIcon = { ...manifest }; + delete withoutIcon.iconUrl; + expect(iconUrlProblem(withoutIcon)).toBe('iconUrl is absent'); + expect(iconUrlProblem({ ...manifest, iconUrl: 'ftp://example.com/icon.svg' })).toMatch(/not http\(s\)/); + expect(iconUrlProblem({ ...manifest, iconUrl: 'assets/icon.svg' })).toMatch(/not a URL/); + }); + + it('points iconUrl at the icon this package owns', () => { + expect(manifest.iconUrl).toBe(ICON_URL); + const iconPath = fileURLToPath(new URL('../assets/icon.svg', import.meta.url)); + expect(existsSync(iconPath)).toBe(true); + // `--icon-file` takes up to 256 KB; hold the owned asset to the same bound. + expect(statSync(iconPath).size).toBeLessThan(256 * 1024); + expect(readFileSync(iconPath, 'utf8')).toMatch(/^]*viewBox="0 0 256 256"/); + }); + it('carries no secret in metadata', () => { for (const c of stack.connectors ?? []) { expect(c).not.toHaveProperty('authentication.token');