Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/apps/connector-github/assets/icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions packages/apps/connector-github/objectstack.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
47 changes: 45 additions & 2 deletions packages/apps/connector-github/test/listing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,43 @@
/**
* 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';

const manifestPath = fileURLToPath(new URL('../objectstack.manifest.json', import.meta.url));
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) as Record<string, unknown>;

/** 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, unknown>): 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');
Expand Down Expand Up @@ -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(/^<svg [^>]*viewBox="0 0 256 256"/);
});

it('carries no secret in metadata', () => {
for (const c of stack.connectors ?? []) {
expect(c).not.toHaveProperty('authentication.token');
Expand Down
7 changes: 7 additions & 0 deletions packages/apps/connector-stripe/assets/icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions packages/apps/connector-stripe/objectstack.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
47 changes: 45 additions & 2 deletions packages/apps/connector-stripe/test/listing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -13,14 +14,35 @@

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';
import { STRIPE_READ_OPERATIONS, StripeConnector } from '../src/connectors/stripe.connector.js';

const manifestPath = fileURLToPath(new URL('../objectstack.manifest.json', import.meta.url));
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) as Record<string, unknown>;

/** 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, unknown>): 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 {
Expand Down Expand Up @@ -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(/^<svg [^>]*viewBox="0 0 256 256"/);
});

it('carries no secret in metadata', () => {
for (const c of stack.connectors ?? []) {
expect(c).not.toHaveProperty('authentication.token');
Expand Down
Loading