Files
PaperClipAI/packages/shared/src/app-definitions.test.ts
T
Dotta a560b48d6d feat(apps): refine Postman and Shopify setup (#12357)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Apps give those agents governed access to external tools.
> - Provider catalogs must match each provider's current protocol and
credential contract.
> - Postman method labels and API-key placement were outdated.
> - Shopify now offers a UCP commerce endpoint that needs a managed
agent-profile argument.
> - This pull request updates both providers and documents the complete
connection-authoring workflow.
> - The benefit is accurate setup, safer runtime defaults, and a
repeatable provider review process.

## Linked Issues or Issue Description

Refs #11965

This is stack 10 of 11. It depends on stack 9 and preserves the final
catalog work recovered from #11965.

Related: #5904 covers Shopify skill routing. This pull request covers
the Apps connection contract instead.

## What Changed

- Update Postman hosted MCP methods, capability choices, default
selection, and bearer-token placement.
- Add Shopify UCP commerce and Storefront compatibility methods with
public-store prerequisites.
- Inject the reviewed Shopify UCP agent profile at runtime and remove
that managed field from user input schemas.
- Classify Shopify checkout completion and cancellation as destructive
actions.
- Expand the connection authoring runbook from provider research through
verification and pull request handoff.
- Add focused shared, server, and UI coverage.
- Make the approved-execution waiter phase-aware so slow preparation
cannot consume the provider execution timeout and grace period.
- Settle legacy pre-execute-on-approve requests and invocations as
failed, clear their stale idempotency key, and allow a fresh governed
approval instead of leaving work stuck in `executing`.

## Verification

- `pnpm exec vitest run packages/shared/src/app-definitions.test.ts
server/src/__tests__/tool-access-service.test.ts
ui/src/pages/apps/AppsConnect.test.tsx -t
"Postman|Shopify|normalizeConnectionMethodConfig|classifyRisk"` (16
passed)
- `pnpm exec vitest run
server/src/services/approved-execution-wait.test.ts` (4 passed)
- `pnpm exec vitest run server/src/__tests__/tool-gateway.test.ts -t
"enforces policy, approvals, retries, rate limits, and company
boundaries for connected remote MCP calls"` (1 passed)
- `pnpm exec vitest run
server/src/__tests__/tool-gateway-service.test.ts` (21 passed; includes
legacy approval settlement and fresh-approval recovery)
- `pnpm --filter @paperclipai/server typecheck`
- `pnpm check:token-gates`
- `pnpm -r typecheck`
- `pnpm build`

## Risks

- Shopify UCP calls now include a Paperclip-managed agent profile that
overrides caller input at the same path.
- Postman EU credentials now use the hosted MCP server's bearer-token
contract instead of the general REST API header.
- The catalog generator and checked-in definitions change together to
prevent regeneration drift.
- Approved execution preparation has an explicit two-minute bound;
provider execution retains its own 65-second timeout and persistence
grace starting from durable provider start.
- Legacy approvals created before execute-on-approve are intentionally
terminalized and must be requested again under the current signed
contract.

> I checked `ROADMAP.md`. This provider update does not duplicate
planned core work. The related open Shopify PR addresses skill routing,
not Apps connections.

## Model Used

OpenAI Codex, GPT-5. The runtime exact model ID and context window were
not exposed. The model used reasoning, tool use, and code execution.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change and contains no internal
Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge
2026-08-29 12:08:35 -05:00

209 lines
17 KiB
TypeScript

import { describe,expect,it } from "vitest";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { APP_DEFINITIONS } from "./app-definitions.generated.js";
import { APP_STORE_DEFINITIONS, APP_STORE_HIDDEN_SLUGS, CONNECTABLE_APP_DEFINITIONS, appSupportsCatalogSetup, getAvailableConnectionMethod, getRecommendedConnectionMethod, recommendedDefaultsForApp, resolveConnectionMethodServerUrl } from "./app-definitions.js";
import { BLOCKED_MCP_PROVIDERS, SELF_SERVE_MCP_CANDIDATES, SELF_SERVE_MCP_RESEARCH } from "./self-serve-mcp-research.js";
import { appDefinitionsSchema } from "./validators/app-definition.js";
describe("AppDefinition catalog",()=>{
it("validates all Wave 1 definitions",()=>expect(()=>appDefinitionsSchema.parse(APP_DEFINITIONS)).not.toThrow());
it("contains every established provider plus the reviewed self-serve catalog",()=>{
expect(APP_DEFINITIONS.map((app)=>app.slug)).toEqual(expect.arrayContaining(["zapier","github","slack","notion","posthog","linear","google-sheets","context7","composio","oauth-generic","api-key-generic","sentry","vercel","anthropic","gmail","google-drive","google-docs","google-slides","google-calendar","google-chat","google-people","google-workspace-search"]));
expect(SELF_SERVE_MCP_CANDIDATES).toHaveLength(43);
expect(BLOCKED_MCP_PROVIDERS.map((entry)=>entry.slug)).toEqual(["g2","vercel","zomato"]);
const definitionSlugs=new Set(APP_DEFINITIONS.map((app)=>app.slug));
const connectableSlugs=new Set(CONNECTABLE_APP_DEFINITIONS.map((app)=>app.slug));
expect(SELF_SERVE_MCP_CANDIDATES.filter((entry)=>!definitionSlugs.has(entry.slug))).toEqual([]);
expect(SELF_SERVE_MCP_CANDIDATES.filter((entry)=>!connectableSlugs.has(entry.slug))).toEqual([]);
for(const entry of BLOCKED_MCP_PROVIDERS)expect(connectableSlugs.has(entry.slug)).toBe(false);
});
it("keeps a complete, unique, dated evidence ledger for all 46 researched MCP providers",()=>{
expect(SELF_SERVE_MCP_RESEARCH.verifiedAt).toBe("2026-08-26");
expect(SELF_SERVE_MCP_RESEARCH.entries).toHaveLength(46);
expect(new Set(SELF_SERVE_MCP_RESEARCH.entries.map((entry)=>entry.slug))).toHaveProperty("size",46);
for(const entry of SELF_SERVE_MCP_RESEARCH.entries){
expect(new URL(entry.docsUrl).protocol).toBe("https:");
expect(new URL(entry.serverUrl).protocol).toBe("https:");
expect(entry.authMode).toBeTruthy();
expect(entry.prerequisite.length).toBeGreaterThan(10);
expect(["S1","S2","S3","S4"]).toContain(entry.riskTier);
}
});
it("uses the reviewed current endpoints and configuration modes",()=>{
const method=(slug:string,key?:string)=>APP_DEFINITIONS.find((app)=>app.slug===slug)?.methods.find((candidate)=>!key||candidate.key===key);
expect(method("jira")?.defaults?.serverUrl).toBe("https://mcp.atlassian.com/v1/mcp/authv2");
expect(method("jira")?.defaults?.scopesHint).toEqual([
"read:me",
"read:account",
"offline_access",
"email",
"read:jira-work",
"write:jira-work",
"search:confluence",
"read:confluence-user",
"read:page:confluence",
"write:page:confluence",
"read:comment:confluence",
"write:comment:confluence",
"read:space:confluence",
"read:hierarchical-content:confluence",
"write:component:compass",
"read:component:compass",
"read:scorecard:compass",
"write:scorecard:compass",
"read:event:compass",
"read:metric:compass",
"read:all:twg",
"write:all:twg",
]);
expect(method("cloudinary")?.defaults?.serverUrl).toBe("https://asset-management.mcp.cloudinary.com/mcp");
expect(method("kernel")?.defaults?.serverUrl).toBe("https://mcp.onkernel.com/mcp");
expect(method("resend")?.defaults?.serverUrl).toBe("https://mcp.resend.com/mcp");
expect(method("clickhouse")?.defaults?.serverUrl).toBe("https://mcp.clickhouse.cloud/clickstack");
expect(method("clickhouse")?.tenantFields?.[0]?.transport).toEqual({location:"header",name:"x-service-id"});
expect(method("mem0")).toMatchObject({auth:"api_key",keyPlacement:{location:"header",name:"Authorization",prefix:"Bearer "}});
expect(method("mem0")?.defaults?.serverUrl).toBe("https://mcp.mem0.ai/mcp/");
expect(method("xero")?.defaults?.scopesHint).toEqual([
"openid","profile","email","offline_access","accounting.settings","accounting.invoices.read","accounting.reports.aged.read","accounting.reports.balancesheet.read","accounting.reports.profitandloss.read",
]);
expect(APP_DEFINITIONS.find((app)=>app.slug==="pagerduty")?.methods.map((candidate)=>({key:candidate.key,serverUrl:candidate.defaults?.serverUrl}))).toEqual([
{key:"mcp-api-key-us",serverUrl:"https://mcp.pagerduty.com/mcp"},
{key:"mcp-api-key-eu",serverUrl:"https://mcp.eu.pagerduty.com/mcp"},
]);
expect(method("context7")).toMatchObject({auth:"none",defaults:{serverUrl:"https://mcp.context7.com/mcp"}});
expect(APP_DEFINITIONS.find((app)=>app.slug==="planetscale")?.methods.map((candidate)=>candidate.key)).toEqual(["mcp-oauth","mcp-insights-only"]);
const postman=APP_DEFINITIONS.find((app)=>app.slug==="postman");
expect(postman?.methods.map((candidate)=>candidate.key)).toEqual([
"mcp-oauth-minimal","mcp-oauth-code","mcp-oauth-full","mcp-eu-key-minimal","mcp-eu-key-code","mcp-eu-key-full",
]);
expect(getAvailableConnectionMethod(postman!)?.key).toBe("mcp-oauth-full");
expect(postman?.methods.filter((candidate)=>candidate.auth==="api_key").every((candidate)=>
candidate.keyPlacement?.name==="Authorization"&&candidate.keyPlacement.prefix==="Bearer "
)).toBe(true);
expect(method("supabase")?.tenantFields?.find((field)=>field.key==="readOnly")?.defaultValue).toBe(false);
expect(method("asana")?.ownershipModes).toEqual(["customer"]);
expect(method("zapier")).toMatchObject({key:"generated-url",auth:"none",defaults:{}});
expect(method("zapier")?.credentialFields).toBeUndefined();
});
it("uses discovery-first Notion MCP OAuth metadata",()=>{
const notion=APP_DEFINITIONS.find((app)=>app.slug==="notion");
expect(notion?.redirectConstraints).toBe("https-or-loopback-http");
expect(notion?.methods[0]?.defaults).toEqual({serverUrl:"https://mcp.notion.com/mcp"});
});
it("preserves required Linear OAuth scopes",()=>expect(APP_DEFINITIONS.find((app)=>app.slug==="linear")?.methods[0]?.defaults?.scopesHint).toEqual(["read","write"]));
it("requests only Hugging Face's MCP read scope",()=>expect(APP_DEFINITIONS.find((app)=>app.slug==="hugging-face")?.methods[0]?.defaults?.scopesHint).toEqual(["read-mcp"]));
it("defaults S2-S4 write and destructive actions to ask-first",()=>{for(const app of APP_DEFINITIONS)for(const method of app.methods)expect(recommendedDefaultsForApp(app,method.key)).toEqual({access:"all_agents",askFirstRiskLevels:method.riskTier==="S1"?[]:["write","destructive"]})});
it("defaults explicit read/write capability groups to their write-capable method",()=>{
const drive=APP_DEFINITIONS.find((app)=>app.slug==="google-drive")!;
const gmail=APP_DEFINITIONS.find((app)=>app.slug==="gmail")!;
expect(getAvailableConnectionMethod(drive)?.key).toBe("customer-write-oauth");
expect(getAvailableConnectionMethod(gmail)?.key).toBe("customer-draft-oauth");
expect(getRecommendedConnectionMethod(drive.methods.filter((candidate)=>candidate.ownershipModes.includes("customer")))?.key).toBe("customer-write-oauth");
});
it("explains Google Workspace Developer Preview enrollment before connection",()=>{
const googleWorkspaceMcpSlugs=["gmail","google-drive","google-docs","google-slides","google-calendar","google-chat","google-people","google-workspace-search"];
for(const slug of googleWorkspaceMcpSlugs){
const prerequisite=APP_DEFINITIONS.find((app)=>app.slug===slug)?.setupPrerequisite;
expect(prerequisite?.actionUrl,slug).toBe("https://developers.google.com/workspace/preview");
expect(prerequisite?.description,slug).toContain("does not enable unrelated Paperclip customers");
expect(prerequisite?.steps?.join(" "),slug).toContain("final project-registration email");
}
});
it("withholds unverified and reserved providers from the app store without deleting their definitions",()=>{
expect([...APP_STORE_HIDDEN_SLUGS].sort()).toEqual([
"beehiiv","bitly","brex","candid","coda","composio","context7","egnyte","embat","github","kernel","local-falcon","make","manufact","oreilly","planetscale","razorpay","sanity","similarweb","slack","ticket-tailor","ticktick","xero",
]);
expect(APP_STORE_DEFINITIONS).toHaveLength(35);
const connectableSlugs=new Set(CONNECTABLE_APP_DEFINITIONS.map((entry)=>entry.slug));
const storeSlugs=new Set(APP_STORE_DEFINITIONS.map((entry)=>entry.slug));
for(const slug of APP_STORE_HIDDEN_SLUGS){
expect(connectableSlugs.has(slug),slug).toBe(true);
expect(storeSlugs.has(slug),slug).toBe(false);
}
});
it("ships complete local branding provenance for all 35 store-visible providers",()=>{
const uiPublic=path.resolve(path.dirname(fileURLToPath(import.meta.url)),"../../../ui/public");
const manifest=JSON.parse(fs.readFileSync(path.join(uiPublic,"brands/apps/manifest.json"),"utf8")) as {providers:Array<{slug:string;catalogVisible:boolean;localAsset:string;darkAsset?:string;officialSourceUrl:string;upstreamAssetUrl:string;assetType:"svg"|"png";darkVariantRequired:boolean}>};
const visible=manifest.providers.filter((entry)=>entry.catalogVisible);
expect(visible).toHaveLength(35);
expect(new Set(visible.map((entry)=>entry.slug))).toHaveProperty("size",35);
expect(new Set(visible.map((entry)=>entry.localAsset))).toHaveProperty("size",35);
expect(new Set(APP_STORE_DEFINITIONS.map((entry)=>entry.slug))).toEqual(new Set(visible.map((entry)=>entry.slug)));
for(const app of APP_STORE_DEFINITIONS){
const provenance=visible.find((entry)=>entry.slug===app.slug)!;
expect(provenance).toBeTruthy();
expect(provenance.localAsset).toBe(app.branding.logoUrl);
expect(provenance.darkAsset).toBe(app.branding.darkLogoUrl);
expect(provenance.darkVariantRequired).toBe(Boolean(provenance.darkAsset));
expect(new URL(provenance.officialSourceUrl).protocol).toBe("https:");
expect(new URL(provenance.upstreamAssetUrl).protocol).toBe("https:");
expect(provenance.localAsset).toMatch(/^\/brands\/apps\/.+\.(svg|png)$/);
expect(provenance.localAsset).not.toContain("google.com/s2/favicons");
const asset=fs.readFileSync(path.join(uiPublic,provenance.localAsset));
if(provenance.assetType==="png"){
expect(asset.subarray(0,8).toString("hex")).toBe("89504e470d0a1a0a");
expect(asset.readUInt32BE(16)).toBeGreaterThanOrEqual(128);
expect(asset.readUInt32BE(20)).toBeGreaterThanOrEqual(128);
}else{
const svg=asset.toString("utf8");
expect(svg).toMatch(/^<svg\b/);
expect(svg).not.toMatch(/<script|<foreignObject|\son[a-z]+\s*=/i);
}
if(provenance.darkAsset)expect(fs.existsSync(path.join(uiPublic,provenance.darkAsset))).toBe(true);
}
});
it("keeps every researched self-serve candidate implemented while blocked providers stay absent",()=>{
const definitions=new Map(CONNECTABLE_APP_DEFINITIONS.map((entry)=>[entry.slug,entry]));
for(const candidate of SELF_SERVE_MCP_CANDIDATES)expect(appSupportsCatalogSetup(definitions.get(candidate.slug))).toBe(true);
for(const blocked of BLOCKED_MCP_PROVIDERS)expect(definitions.has(blocked.slug)).toBe(false);
});
it("ships each Google Workspace surface as an independent personal OAuth app",()=>{
const expected={
gmail:"https://gmailmcp.googleapis.com/mcp/v1",
"google-drive":"https://drivemcp.googleapis.com/mcp/v1",
"google-docs":"https://docsmcp.googleapis.com/mcp/v1",
"google-sheets":"https://sheetsmcp.googleapis.com/mcp/v1",
"google-slides":"https://slidesmcp.googleapis.com/mcp/v1",
"google-calendar":"https://calendarmcp.googleapis.com/mcp/v1",
"google-chat":"https://chatmcp.googleapis.com/mcp/v1",
"google-people":"https://people.googleapis.com/mcp/v1",
"google-workspace-search":"https://workspacemcp.googleapis.com/mcp/v1",
} as const;
for(const [slug,serverUrl] of Object.entries(expected)){
const app=APP_DEFINITIONS.find((candidate)=>candidate.slug===slug);
expect(app,slug).toBeTruthy();
expect(app?.methods.some((method)=>method.oauthStrategy==="paperclip_id_connector")).toBe(true);
for(const method of app?.methods.filter((candidate)=>candidate.auth==="oauth")??[]){
expect(method.grantKinds,`${slug}:${method.key}`).toEqual(["user"]);
expect(method.defaults?.serverUrl,`${slug}:${method.key}`).toBe(serverUrl);
expect(method.capabilityProfile,`${slug}:${method.key}`).toBeTruthy();
}
}
});
it("advertises managed Gmail methods with their profile-scoped grants",()=>{
const gmail=APP_DEFINITIONS.find((app)=>app.slug==="gmail");
const managedMethods=gmail?.methods.filter((method)=>method.oauthStrategy==="paperclip_id_connector")??[];
expect(managedMethods.map((method)=>method.key)).toEqual(["paperclip-read","paperclip-draft"]);
expect(managedMethods.map((method)=>[method.connectorProfile,method.defaults?.scopesHint])).toEqual([
["gmail.read",["https://www.googleapis.com/auth/gmail.readonly"]],
["gmail.draft",[
"https://www.googleapis.com/auth/gmail.readonly",
"https://www.googleapis.com/auth/gmail.compose",
]],
]);
});
it("configures Shopify's current UCP and compatibility MCP methods without OAuth",()=>{const shopify=APP_DEFINITIONS.find((app)=>app.slug==="shopify");expect(shopify?.methods.map((method)=>method.key)).toEqual(["ucp-commerce","storefront-mcp"]);const ucp=shopify?.methods[0];const compatibility=shopify?.methods[1];expect(ucp).toMatchObject({auth:"none",defaults:{serverUrlTemplate:"https://{storeDomain}/api/ucp/mcp",toolArgumentDefaults:{meta:{"ucp-agent":{profile:"https://shopify.dev/ucp/agent-profiles/examples/2026-04-08/valid-with-capabilities.json"}}}},tenantFields:[expect.objectContaining({key:"storeDomain",required:true})]});expect(compatibility).toMatchObject({auth:"none",defaults:{serverUrlTemplate:"https://{storeDomain}/api/mcp"}});expect(resolveConnectionMethodServerUrl(ucp!,{storeDomain:"paperclip-demo.myshopify.com"})).toBe("https://paperclip-demo.myshopify.com/api/ucp/mcp");expect(resolveConnectionMethodServerUrl(compatibility!,{storeDomain:"paperclip-demo.myshopify.com"})).toBe("https://paperclip-demo.myshopify.com/api/mcp");expect(resolveConnectionMethodServerUrl(ucp!,{})).toBeNull();expect(shopify?.setupPrerequisite).toMatchObject({title:"Launch the storefront before connecting",actionUrl:"https://admin.shopify.com/"});expect(shopify?.setupPrerequisite?.steps?.join(" ")).toContain("Storefront visibility to Public")});
it("offers PostHog OAuth and API-key methods with zero-config defaults and advanced narrowing",()=>{const posthog=APP_DEFINITIONS.find((app)=>app.slug==="posthog");expect(posthog?.methods.map((method)=>method.key)).toEqual(["mcp-oauth","mcp-api-key"]);for(const method of posthog?.methods??[]){const projectField=method.tenantFields?.find((field)=>field.key==="projectId");expect(method.riskTier).toBe("S3");expect(method.tenantFields?.find((field)=>field.key==="readOnly")).toMatchObject({defaultValue:false,advanced:true});expect(projectField).toMatchObject({advanced:true,transport:{location:"header",name:"x-posthog-project-id"}});expect(projectField?.required).not.toBe(true);expect(method.tenantFields?.filter((field)=>field.advanced).map((field)=>field.key)).toEqual(["projectId","readOnly","features","tools"]);expect(method.tenantFields?.find((field)=>field.key==="mode")).toMatchObject({hidden:true,defaultValue:"tools",transport:{location:"query",name:"mode"}});expect(method.configRequirements).toBeUndefined();expect(method.requiredResourceFilters).toBeUndefined();expect(method.guidanceMd).toContain("optional advanced controls")}});
it("requires only reviewed provider or safety-boundary configuration on the default path",()=>{const required=APP_DEFINITIONS.flatMap((app)=>app.methods.flatMap((method)=>[...(method.tenantFields??[]),...(method.extensionFields??[])].filter((field)=>field.required&&field.advanced!==true&&!field.hidden).map((field)=>`${app.slug}:${method.key}:${field.key}`))).sort();expect(required).toEqual(["clickhouse:mcp-oauth:serviceId","shopify:storefront-mcp:storeDomain","shopify:ucp-commerce:storeDomain","supabase:mcp-api-key:projectRef","supabase:mcp-oauth:projectRef"])});
it("limits Vercel Connect setup to the reviewed pilot methods",()=>{
const reviewed=APP_DEFINITIONS.flatMap((app)=>app.methods.flatMap((method)=>method.credentialSources?.vercelConnect?[{slug:app.slug,key:method.key,review:method.credentialSources.vercelConnect}]:[]));
expect(reviewed.map(({slug,key})=>`${slug}:${key}`).sort()).toEqual(["linear:mcp-oauth","notion:mcp-oauth","posthog:mcp-api-key","posthog:mcp-oauth"]);
expect(reviewed.find(({slug})=>slug==="linear")?.review).toMatchObject({services:["linear"],principalModes:["user"],scopes:["read","write"],header:{name:"Authorization",prefix:"Bearer "}});
expect(reviewed.find(({slug,key})=>slug==="posthog"&&key==="mcp-oauth")?.review.services).toEqual(["posthog","mcp.posthog.com/mcp"]);
expect(reviewed.find(({slug,key})=>slug==="posthog"&&key==="mcp-api-key")?.review.principalModes).toEqual(["app"]);
expect(APP_DEFINITIONS.find((app)=>app.slug==="vercel")?.availability?.available).toBe(false);
});
it("enforces method and field invariants",()=>{for(const app of APP_DEFINITIONS)for(const method of app.methods){if(method.auth==="api_key")expect(method.keyPlacement).toBeTruthy();if(method.auth==="oauth")expect(method.ownershipModes.length).toBeGreaterThan(0);for(const field of method.credentialFields??[])if(field.required&&field.type!=="checkbox")expect(field.placeholder).toBeTruthy()}});
});