Skip to content

Conversation

@PeerRich
Copy link
Member

@PeerRich PeerRich commented Jan 24, 2026

What does this PR do?

Simplifies the link-as-an-app app template from generating multiple code files to just a config.json, DESCRIPTION.md, and /static/ folder. External link apps are now purely configuration-driven and don't create credentials since they're just redirects to external URLs.

Additionally, this PR migrates all 24 existing external link apps to the new simplified structure.

Key changes:

  • Remove api/, components/, index.ts, package.json from the link-as-an-app template
  • Add externalLink field to AppMetaSchema for external URL configuration
  • Update API handler to detect external link apps from metadata and return redirect URL directly (bypassing credential creation entirely)
  • Update CLI to prompt for externalLinkUrl when using link-as-an-app template
  • CLI now skips package.json update for apps without one

Migrated apps (24 total):
amie, autocheckin, baa-for-hipaa, bolna, chatbase, clic, cron, deel, elevenlabs, fonio-ai, framer, granola, greetmate-ai, lindy, millis-ai, monobot, n8n, pipedream, raycast, retell-ai, synthflow, telli, vimcal, wordpress

Link to Devin run: https://app.devin.ai/sessions/4c197fcfb40540df9c42a120d6178a27
Requested by: @PeerRich

Mandatory Tasks (DO NOT REMOVE)

  • I have self-reviewed the code (A decent size PR without self-review might be rejected).
  • I have updated the developer docs in /docs if this PR makes changes that would require a documentation change. N/A - internal tooling change
  • I confirm automated tests are in place that prove my fix is effective or that my feature works.

How should this be tested?

  1. Test a migrated app (e.g., baa-for-hipaa):

    • Enable the app in admin settings
    • Click "Add" on the app - should redirect to the external URL in a new tab
    • Verify no credential is created in the database
  2. Test creating a new external link app:

    • Run yarn create-app and select the link-as-an-app template
    • Verify it prompts for "External Link URL" after category selection
    • Check the generated app only has config.json, DESCRIPTION.md, and static/ folder
    • Verify config.json contains the externalLink field with the URL
  3. Verify non-external-link apps still work:

    • Apps like zapier (which has additional API endpoints in api/subscriptions/) should continue to work normally

Checklist

  • My code follows the style guidelines of this project
  • I have commented my code, particularly in hard-to-understand areas
  • I have checked if my changes generate no new warnings

Human Review Checklist

  • Verify the type assertion for externalLink in the API handler is safe (line 70 in [...args].ts)
  • Spot-check a few migrated apps to confirm their externalLink.url matches the original redirect.url from their deleted api/add.ts
  • Verify the API handler correctly falls back to 404 for apps without handlers AND without externalLink
  • Check that the CLI correctly handles both create and edit modes for link-as-an-app template
  • Confirm zapier (which has additional API functionality) was NOT migrated and still works

- Remove api/, components/, index.ts, package.json from link-as-an-app template
- Add externalLink field to AppMetaSchema for external URL configuration
- Update API handler to dynamically create handlers for external link apps
- External link apps no longer create credentials (just redirect to URL)
- Update CLI to handle externalLinkUrl field for link-as-an-app template
- CLI now skips package.json update for apps without package.json

Co-Authored-By: peer@cal.com <peer@cal.com>
@devin-ai-integration
Copy link
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR that start with 'DevinAI' or '@devin'.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

External link apps now directly return the redirect URL without going
through the AppDeclarativeHandler type, avoiding type conflicts with
the createCredential return type requirement.

Co-Authored-By: peer@cal.com <peer@cal.com>
@diffray-bot
Copy link

Changes Summary

This PR refactors the 'link-as-an-app' template to be purely configuration-driven, eliminating code files (api/, components/, index.ts, package.json) in favor of just config.json and static assets. External link apps now bypass credential creation entirely by reading the externalLink field from app metadata and returning the redirect URL directly from the API handler.

Type: refactoring

Components Affected: app-store template system, app integration API handler, app-store CLI, app metadata schema

Files Changed
File Summary Change Impact
...ce/apps/web/pages/api/integrations/[...args].ts Added fallback logic to check app metadata for externalLink field when no API handlers exist, bypassing credential creation for external link apps ✏️ 🔴
/tmp/workspace/packages/types/AppMetaSchema.ts Added optional externalLink field (url + newTab) to AppMetaSchema for storing external redirect configuration ✏️ 🟡
/tmp/workspace/packages/app-store-cli/src/core.ts Updated to handle externalLinkUrl parameter and skip package.json updates when file doesn't exist ✏️ 🟡
...tore-cli/src/components/AppCreateUpdateForm.tsx Added externalLinkUrl input field to CLI form when using link-as-an-app template ✏️ 🟢
...ace/packages/app-store/apps.server.generated.ts Removed link-as-an-app from apiHandlers map since it no longer has API handlers ✏️ 🟡
.../app-store/templates/link-as-an-app/config.json Added externalLink field with example URL configuration ✏️ 🟢
Architecture Impact
  • New Patterns: Configuration-driven external link apps (metadata-only, no code), Fallback pattern in API handler: check handlers first, then metadata for external links
  • Coupling: Reduced coupling for external link apps - they no longer need api/ handlers or credentials, just metadata configuration
  • Breaking Changes: Apps created with old link-as-an-app template may need migration to new structure, Removed link-as-an-app from apiHandlers map - existing apps using old structure must rely on their existing api/ folders

Risk Areas: Type assertion at line 70 in [...args].ts could fail if externalLink has unexpected structure, Existing apps created with old link-as-an-app template (zapier, pipedream, baa-for-hipaa mentioned in PR) need verification they still work, No authentication/authorization checks before returning external link URL - relies on existing session check at handler entry, Potential for misconfiguration if externalLinkUrl is malformed during CLI app creation, Generated apps.server.generated.ts file change suggests build process dependency

Suggestions
  • Consider adding runtime validation for externalLink structure instead of type assertion
  • Add integration tests verifying both old and new external link app structures work
  • Document migration path for existing apps using old link-as-an-app structure
  • Consider adding URL validation when setting externalLinkUrl in CLI
  • Verify that metadata fallback doesn't interfere with error handling for genuinely missing apps

Full review in progress... | Powered by diffray

) as AppMeta & { externalLink?: { url: string; newTab?: boolean } };
initialConfig = {
...config,
category: config.categories[0],

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 HIGH - Unsafe array access without null check on categories
Agent: bugs

Category: bug

Description:
Accessing config.categories[0] without checking if array exists or has elements could assign undefined to initialConfig.category. The schema defines categories as z.array(z.string()) which is always an array but could be empty.

Suggestion:
Add null/empty check: category: config.categories?.[0] || ''

Confidence: 85%
Rule: ts_unsafe_null_access
Review ID: 4f34730a-9b30-4ed7-99c0-82f62e8066fc
Rate it 👍 or 👎 to improve future reviews | Powered by diffray

Comment on lines +128 to +135
if (template === "link-as-an-app" && externalLinkUrl) {
config.externalLink = {
url: externalLinkUrl,
newTab: true,
};
// Also update the legacy url field for backwards compatibility
config.url = externalLinkUrl;
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 HIGH - Missing input validation for externalLinkUrl parameter
Agent: security

Category: security

Description:
The externalLinkUrl parameter from user input is written directly to config.json without validation of URL format or protocol. While this is a CLI tool run by developers, malicious or malformed URLs could be persisted and later served to end users.

Suggestion:
Add URL validation: try { const parsed = new URL(externalLinkUrl); if (!['http:', 'https:'].includes(parsed.protocol)) { throw new Error('Only http and https protocols allowed'); } } catch { throw new Error('Invalid URL format'); }

Confidence: 70%
Rule: security_missing_input_validation
Review ID: 4f34730a-9b30-4ed7-99c0-82f62e8066fc
Rate it 👍 or 👎 to improve future reviews | Powered by diffray

Comment on lines 119 to +137
].filter((f) => f);

// Add external link URL field for link-as-an-app template
// Use initialConfig.template since appInputData is not yet available
const templateForFields = initialConfig.template || cliTemplate;
if (templateForFields === "link-as-an-app") {
// Insert after category field
const categoryIndex = fields.findIndex((f) => f?.name === "category");
if (categoryIndex !== -1) {
fields.splice(categoryIndex + 1, 0, {
optional: false,
label: "External Link URL",
name: "externalLinkUrl",
type: "text",
explainer: "The URL users will be redirected to when they install this app (e.g., https://example.com/signup)",
defaultValue: "https://example.com",
});
}
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 MEDIUM - Array mutation in render function
Agent: react

Category: performance

Description:
The fields array is being mutated using splice() during render. While fields is recreated each render so this doesn't cause bugs, mutating arrays during render is not idiomatic React and could cause issues if the code is refactored.

Suggestion:
Build the fields array declaratively using useMemo or create a new array with spread/concat instead of splice

Confidence: 75%
Rule: react_component_consistency
Review ID: 4f34730a-9b30-4ed7-99c0-82f62e8066fc
Rate it 👍 or 👎 to improve future reviews | Powered by diffray

Comment on lines 41 to 51
try {
const config = JSON.parse(
fs.readFileSync(`${getAppDirPath(givenSlug, isTemplate)}/config.json`).toString()
) as AppMeta;
) as AppMeta & { externalLink?: { url: string; newTab?: boolean } };
initialConfig = {
...config,
category: config.categories[0],
template: config.__template,
externalLinkUrl: config.externalLink?.url || "",
};
} catch (e) {}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 HIGH - Empty catch block silently swallows errors
Agent: react

Category: bug

Description:
The catch block is completely empty (catch (e) {}), which silently swallows any errors that occur when reading and parsing the config.json file.

Suggestion:
Add error logging: catch (e) { console.error('Failed to load config for', givenSlug, e); }

Confidence: 95%
Rule: ts_avoid_empty_catch_blocks
Review ID: 4f34730a-9b30-4ed7-99c0-82f62e8066fc
Rate it 👍 or 👎 to improve future reviews | Powered by diffray

};
const currentConfig = JSON.parse(fs.readFileSync(`${appDirPath}/config.json`).toString());
config = {
...currentConfig,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 MEDIUM - Type assertion after JSON.parse without schema validation
Agent: typescript

Category: bug

Description:
The code reads a config.json file and uses JSON.parse without validating the result against a schema. Malformed data bypasses TypeScript's type checking.

Suggestion:
Validate the parsed JSON using Zod schema before merging with new config.

Confidence: 75%
Rule: ts_config_validate_before_cast
Review ID: 4f34730a-9b30-4ed7-99c0-82f62e8066fc
Rate it 👍 or 👎 to improve future reviews | Powered by diffray

Comment on lines 42 to +44
const config = JSON.parse(
fs.readFileSync(`${getAppDirPath(givenSlug, isTemplate)}/config.json`).toString()
) as AppMeta;
) as AppMeta & { externalLink?: { url: string; newTab?: boolean } };

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 MEDIUM - Type assertion after JSON.parse without schema validation
Agent: typescript

Category: bug

Description:
The code reads a JSON file and uses a type assertion (as AppMeta & {...}) without validating the parsed data. If the JSON is malformed, TypeScript's type safety is bypassed.

Suggestion:
Validate the parsed JSON using a Zod schema before casting to ensure type safety at runtime.

Confidence: 80%
Rule: ts_config_validate_before_cast
Review ID: 4f34730a-9b30-4ed7-99c0-82f62e8066fc
Rate it 👍 or 👎 to improve future reviews | Powered by diffray

initialConfig = {
...config,
category: config.categories[0],
template: config.__template,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 HIGH - Unsafe property access without null check on __template
Agent: bugs

Category: bug

Description:
Accessing config.__template without checking if it exists could assign undefined to initialConfig.template, potentially causing issues later.

Suggestion:
Use nullish coalescing: template: config.__template || ''

Confidence: 70%
Rule: ts_missing_optional_chaining
Review ID: 4f34730a-9b30-4ed7-99c0-82f62e8066fc
Rate it 👍 or 👎 to improve future reviews | Powered by diffray

Comment on lines 151 to 176
const formCompleted = inputIndex === fields.length;
if (field?.name === "appCategory") {
// Use template category as the default category
fieldValue = Templates.find((t) => t.value === appInputData["template"])?.category || "";
}
const slug = getSlugFromAppName(name) || givenSlug;

useEffect(() => {
// When all fields have been filled
(async () => {
if (formCompleted) {
await BaseAppFork.create({
category,
description,
name,
slug,
publisher,
email,
template,
editMode: isEditAction,
isTemplate,
oldSlug: givenSlug,
externalLinkUrl,
});

await generateAppFiles();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 HIGH - Async operations without error handling
Agent: react

Category: bug

Description:
The useEffect hook contains async operations (BaseAppFork.create() and generateAppFiles()) that are not wrapped in a try-catch block. If either of these async operations fail, the error will be unhandled and the component will be left in an inconsistent state with formCompleted=true but status still 'inProgress'.

Suggestion:
Wrap the async operations in try-catch: try { await BaseAppFork.create(...); await generateAppFiles(); setStatus('done'); } catch (error) { console.error('Failed to create app:', error); setStatus('error'); }

Confidence: 90%
Rule: ts_handle_async_operations_with_proper_erro
Review ID: 4f34730a-9b30-4ed7-99c0-82f62e8066fc
Rate it 👍 or 👎 to improve future reviews | Powered by diffray

}

const [appName, apiEndpoint] = args;
try {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 MEDIUM - Missing HTTP method validation in API handler
Agent: quality

Category: quality

Description:
Handler accepts all HTTP methods without validation. Line 84 calls defaultIntegrationAddHandler which creates credentials via GET, which is semantically incorrect.

Suggestion:
Add HTTP method validation: if (req.method !== 'POST') { return res.status(405).json({ message: 'Method not allowed' }); }

Confidence: 75%
Rule: api_wrong_http_method
Review ID: 4f34730a-9b30-4ed7-99c0-82f62e8066fc
Rate it 👍 or 👎 to improve future reviews | Powered by diffray

};
const currentConfig = JSON.parse(fs.readFileSync(`${appDirPath}/config.json`).toString());
config = {
...currentConfig,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 MEDIUM - JSON.parse without error handling
Agent: react

Category: bug

Description:
Line 121 reads config.json and parses it without try-catch. If the file doesn't exist or contains invalid JSON, the function will throw without graceful error handling.

Suggestion:
Wrap in try-catch or add existence check like line 39: if (!fs.existsSync(configPath)) { throw new Error('config.json not found'); }

Confidence: 75%
Rule: ts_handle_async_operations_with_proper_erro
Review ID: 4f34730a-9b30-4ed7-99c0-82f62e8066fc
Rate it 👍 or 👎 to improve future reviews | Powered by diffray

@diffray-bot
Copy link

Review Summary

Free public review - Want AI code reviews on your PRs? Check out diffray.ai

Validated 60 issues: 26 kept, 34 filtered

Issues Found: 26

💬 See 23 individual line comment(s) for details.

📊 20 unique issue type(s) across 26 location(s)

📋 Full issue list (click to expand)

🔴 CRITICAL - Open Redirect vulnerability - partial mitigation only

Agent: security

Category: security

File: packages/app-store/_utils/useAddAppMutation.ts:91-96

Description: Line 91 validates that URL uses http/https protocol, but doesn't validate the domain. An attacker who can control externalLink.url in app metadata could redirect users to any http/https malicious site for phishing.

Suggestion: Add domain allowlist validation or restrict to same-origin plus known trusted domains. The current regex check /https?:\/\// only validates protocol, not destination.

Confidence: 80%

Rule: sec_unsafe_url_navigation


🟠 HIGH - Unsafe array access without null check on categories

Agent: bugs

Category: bug

File: packages/app-store-cli/src/components/AppCreateUpdateForm.tsx:47

Description: Accessing config.categories[0] without checking if array exists or has elements could assign undefined to initialConfig.category. The schema defines categories as z.array(z.string()) which is always an array but could be empty.

Suggestion: Add null/empty check: category: config.categories?.[0] || ''

Confidence: 85%

Rule: ts_unsafe_null_access


🟠 HIGH - Missing input validation for externalLinkUrl parameter (2 occurrences)

Agent: security

Category: security

📍 View all locations
File Description Suggestion Confidence
packages/app-store-cli/src/core.ts:128-135 The externalLinkUrl parameter from user input is written directly to config.json without validation ... Add URL validation: try { const parsed = new URL(externalLinkUrl); if (!['http:', 'https:'].includes... 70%
packages/types/AppMetaSchema.ts:115-120 The externalLink.url field is defined as z.string() which only validates that it's a string type. Th... Change z.string() to z.string().url() and add protocol validation: .refine((url) => { const parsed =... 85%

Rule: security_missing_input_validation


🟠 HIGH - Empty catch block silently swallows errors

Agent: react

Category: bug

File: packages/app-store-cli/src/components/AppCreateUpdateForm.tsx:41-51

Description: The catch block is completely empty (catch (e) {}), which silently swallows any errors that occur when reading and parsing the config.json file.

Suggestion: Add error logging: catch (e) { console.error('Failed to load config for', givenSlug, e); }

Confidence: 95%

Rule: ts_avoid_empty_catch_blocks


🟠 HIGH - Unsafe property access without null check on __template

Agent: bugs

Category: bug

File: packages/app-store-cli/src/components/AppCreateUpdateForm.tsx:48

Description: Accessing config.__template without checking if it exists could assign undefined to initialConfig.template, potentially causing issues later.

Suggestion: Use nullish coalescing: template: config.__template || ''

Confidence: 70%

Rule: ts_missing_optional_chaining


🟠 HIGH - Async operations without error handling (2 occurrences)

Agent: react

Category: bug

📍 View all locations
File Description Suggestion Confidence
packages/app-store-cli/src/components/AppCreateUpdateForm.tsx:151-176 The useEffect hook contains async operations (BaseAppFork.create() and generateAppFiles()) that are ... Wrap the async operations in try-catch: `try { await BaseAppFork.create(...); await generateAppFiles... 90%
packages/app-store-cli/src/core.ts:123 Line 121 reads config.json and parses it without try-catch. If the file doesn't exist or contains in... Wrap in try-catch or add existence check like line 39: `if (!fs.existsSync(configPath)) { throw new ... 75%

Rule: ts_handle_async_operations_with_proper_erro


🟠 HIGH - Missing authentication check for external link apps

Agent: security

Category: security

File: apps/web/pages/api/integrations/[...args].ts:65-72

Description: The external link handler (lines 65-72) returns URL without verifying user authentication. While getServerSession is called at line 48, there's no check for req.session?.user before returning external link data. Auth validation only occurs in defaultIntegrationAddHandler which isn't called for external links.

Suggestion: Add authentication validation before returning external link URLs:

if (!req.session?.user?.id) {
throw new HttpError({ statusCode: 401, message: "You must be logged in to do this" });
}

Confidence: 85%

Rule: nextjs_api_route_missing_auth


🟠 HIGH - Optional chaining with -1 fallback misses undefined

Agent: typescript

Category: quality

File: packages/app-store-cli/src/components/AppCreateUpdateForm.tsx:295

Description: The code uses field?.options?.findIndex() which could return undefined (not -1) if field.options is undefined. The subsequent check 'selectedOptionIndex === -1 ? 0 : selectedOptionIndex' at line 353 won't properly handle undefined, potentially passing undefined to initialIndex.

Suggestion: Use explicit undefined check or nullish coalescing: field?.options?.findIndex((o) => o.value === fieldValue) ?? -1

Confidence: 85%

Rule: ts_falsy_zero_in_ternary


🟡 MEDIUM - Array mutation in render function

Agent: react

Category: performance

File: packages/app-store-cli/src/components/AppCreateUpdateForm.tsx:119-137

Description: The fields array is being mutated using splice() during render. While fields is recreated each render so this doesn't cause bugs, mutating arrays during render is not idiomatic React and could cause issues if the code is refactored.

Suggestion: Build the fields array declaratively using useMemo or create a new array with spread/concat instead of splice

Confidence: 75%

Rule: react_component_consistency


🟡 MEDIUM - Hardcoded app categories duplicate existing constants

Agent: architecture

Category: quality

File: packages/app-store-cli/src/components/AppCreateUpdateForm.tsx:89-99

Description: The app categories are hardcoded directly in the form component, duplicating the categories already defined in @calcom/app-store/_utils/getAppCategories.ts. This creates a maintenance burden where changes to categories must be made in multiple locations.

Suggestion: Import and reuse the existing app categories from getAppCategories() or create a shared constant. The TODO on line 88 acknowledges this issue.

Confidence: 95%

Rule: arch_reuse_existing_constants


🟡 MEDIUM - Type assertion after JSON.parse without schema validation (2 occurrences)

Agent: typescript

Category: bug

📍 View all locations
File Description Suggestion Confidence
packages/app-store-cli/src/core.ts:123 The code reads a config.json file and uses JSON.parse without validating the result against a schema... Validate the parsed JSON using Zod schema before merging with new config. 75%
packages/app-store-cli/src/components/AppCreateUpdateForm.tsx:42-44 The code reads a JSON file and uses a type assertion (as AppMeta & {...}) without validating the p... Validate the parsed JSON using a Zod schema before casting to ensure type safety at runtime. 80%

Rule: ts_config_validate_before_cast


🟡 MEDIUM - Missing HTTP method validation in API handler

Agent: quality

Category: quality

File: apps/web/pages/api/integrations/[...args].ts:57

Description: Handler accepts all HTTP methods without validation. Line 84 calls defaultIntegrationAddHandler which creates credentials via GET, which is semantically incorrect.

Suggestion: Add HTTP method validation: if (req.method !== 'POST') { return res.status(405).json({ message: 'Method not allowed' }); }

Confidence: 75%

Rule: api_wrong_http_method


🟡 MEDIUM - Reassigning function parameter

Agent: react

Category: quality

File: packages/app-store-cli/src/components/AppCreateUpdateForm.tsx:32

Description: The function parameter 'cliTemplate' is being reassigned on line 24. This mutates the input parameter and makes the code harder to reason about.

Suggestion: Use a different variable name: const processedTemplate = Templates.find((t) => t.value === cliTemplate)?.value || '';

Confidence: 80%

Rule: ts_do_not_reassign_imported_variables


🟡 MEDIUM - Synchronous file operations in CLI function

Agent: performance

Category: performance

File: packages/app-store-cli/src/core.ts:39-46

Description: updatePackageJson uses fs.existsSync, fs.readFileSync, and fs.writeFileSync. While blocking, this is a CLI context where it's less critical than server code.

Suggestion: Consider refactoring to use fs.promises API for consistency with async pattern already used in BaseAppFork.create

Confidence: 60%

Rule: node_blocking_sync_operations


🟡 MEDIUM - Shell command injection risk in file operations

Agent: security

Category: security

File: packages/app-store-cli/src/core.ts:78-101

Description: Shell commands are constructed using string concatenation with template and slug variables. While slug is sanitized via slugify(), template comes from Templates array without explicit runtime validation before shell execution.

Suggestion: Add explicit validation that template is in allowed Templates list, or use Node.js fs module functions (fs.mkdirSync, fs.cpSync) instead of shell commands.

Confidence: 60%

Rule: sec_shell_specific_escaping


🔵 LOW - Non-strict equality check

Agent: react

Category: quality

File: packages/app-store-cli/src/components/AppCreateUpdateForm.tsx:316

Description: Using loose equality (==) instead of strict equality (===) on line 316. Loose equality can lead to unexpected type coercion.

Suggestion: Use strict equality: if (field?.type === 'text')

Confidence: 95%

Rule: ts_always_use_strict_equality_checks


🔵 LOW - Prefer specific type over z.any() (2 occurrences)

Agent: react

Category: quality

📍 View all locations
File Description Suggestion Confidence
packages/types/AppMetaSchema.ts:30 The schema uses z.any() for the attrs property, which bypasses type safety. A more specific type wou... Replace z.any() with z.record(z.union([z.string(), z.number(), z.boolean()])) 75%
packages/types/AppMetaSchema.ts:84 The schema uses z.any() for the key property, which bypasses type safety. Define a specific schema for the key property or use z.unknown() if intentionally unspecified 70%

Rule: ts_prefer_specific_types_over_any_unknown_w


🔵 LOW - Template-specific field injection via array mutation

Agent: architecture

Category: quality

File: packages/app-store-cli/src/components/AppCreateUpdateForm.tsx:121-137

Description: The form dynamically injects fields for specific templates by mutating the fields array with splice (line 128). This mixed declarative/imperative approach reduces code clarity.

Suggestion: Use a declarative approach: build fields array with showForTemplates property on each field, then filter based on current template.

Confidence: 70%

Rule: arch_extract_complex_logic


🔵 LOW - Dual configuration fields for backwards compatibility

Agent: architecture

Category: quality

File: packages/app-store-cli/src/core.ts:133-134

Description: The code updates both config.externalLink.url and config.url with the same value. This duplication creates ambiguity about which field is the source of truth.

Suggestion: Add a deprecation plan with timeline for the legacy url field. Document which field is canonical.

Confidence: 60%

Rule: arch_duplicated_config_constants


🔵 LOW - New schema field 'externalLink' lacks test coverage (3 occurrences)

Agent: testing

Category: quality

📍 View all locations
File Description Suggestion Confidence
packages/types/AppMetaSchema.ts:115-120 AppMetaSchema added externalLink field with nested url (required) and newTab (optional) properties. ... Create test file packages/types/AppMetaSchema.test.ts covering valid/invalid externalLink scenarios. 85%
packages/app-store-cli/src/core.ts:63 BaseAppFork.create added externalLinkUrl parameter used to configure external link apps (lines 128-1... Create test file packages/app-store-cli/src/core.test.ts to cover externalLinkUrl parameter behavior... 80%
packages/app-store-cli/src/components/AppCreateUpdateForm.tsx:173 AppForm component now extracts externalLinkUrl from config (line 49) and passes it to BaseAppFork.cr... Create test file packages/app-store-cli/src/components/AppCreateUpdateForm.test.tsx to cover externa... 75%

Rule: test_new_parameter_coverage


ℹ️ 3 issue(s) outside PR diff (click to expand)

These issues were found in lines not modified in this PR.

🔴 CRITICAL - Open Redirect vulnerability - partial mitigation only

Agent: security

Category: security

File: packages/app-store/_utils/useAddAppMutation.ts:91-96

Description: Line 91 validates that URL uses http/https protocol, but doesn't validate the domain. An attacker who can control externalLink.url in app metadata could redirect users to any http/https malicious site for phishing.

Suggestion: Add domain allowlist validation or restrict to same-origin plus known trusted domains. The current regex check /https?:\/\// only validates protocol, not destination.

Confidence: 80%

Rule: sec_unsafe_url_navigation


🟠 HIGH - Optional chaining with -1 fallback misses undefined

Agent: typescript

Category: quality

File: packages/app-store-cli/src/components/AppCreateUpdateForm.tsx:295

Description: The code uses field?.options?.findIndex() which could return undefined (not -1) if field.options is undefined. The subsequent check 'selectedOptionIndex === -1 ? 0 : selectedOptionIndex' at line 353 won't properly handle undefined, potentially passing undefined to initialIndex.

Suggestion: Use explicit undefined check or nullish coalescing: field?.options?.findIndex((o) => o.value === fieldValue) ?? -1

Confidence: 85%

Rule: ts_falsy_zero_in_ternary


🟡 MEDIUM - Hardcoded app categories duplicate existing constants

Agent: architecture

Category: quality

File: packages/app-store-cli/src/components/AppCreateUpdateForm.tsx:89-99

Description: The app categories are hardcoded directly in the form component, duplicating the categories already defined in @calcom/app-store/_utils/getAppCategories.ts. This creates a maintenance burden where changes to categories must be made in multiple locations.

Suggestion: Import and reuse the existing app categories from getAppCategories() or create a shared constant. The TODO on line 88 acknowledges this issue.

Confidence: 95%

Rule: arch_reuse_existing_constants


🔇 6 low-severity issue(s) not posted (min: medium)

Issues below medium severity are saved but not posted as comments.
View all issues in the full review details.

📝 7 additional issue(s) shown in summary only (max: 10 inline)

To reduce noise, only 10 inline comments are posted.
All issues are listed above in the full issue list.

🔗 View full review details


Review ID: 4f34730a-9b30-4ed7-99c0-82f62e8066fc
Rate it 👍 or 👎 to improve future reviews | Powered by diffray

Migrated the following apps to use config.json with externalLink field
instead of api/add.ts handlers:

- amie, autocheckin, baa-for-hipaa, bolna, chatbase, clic, cron, deel
- elevenlabs, fonio-ai, framer, granola, greetmate-ai, lindy, millis-ai
- monobot, n8n, pipedream, raycast, retell-ai, synthflow, telli, vimcal
- wordpress

Each app now only contains: config.json, DESCRIPTION.md, static/
Removed: api/, components/, index.ts, package.json

Co-Authored-By: peer@cal.com <peer@cal.com>
@devin-ai-integration devin-ai-integration bot changed the title refactor: simplify link-as-an-app template to config.json only refactor: simplify link-as-an-app template and migrate 24 existing apps Jan 25, 2026
@PeerRich PeerRich marked this pull request as ready for review January 25, 2026 11:14
@PeerRich PeerRich requested a review from a team as a code owner January 25, 2026 11:14
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 154 files

Note: This PR contains a large number of files. cubic only reviews up to 75 files per PR, so some files may not have been reviewed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants