thinkwork-cli 0.12.11 → 0.12.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -38
- package/dist/{api-client-ASJWIG3Z.js → api-client-4VT3FWOH.js} +1 -1
- package/dist/{chunk-JEHV35EU.js → chunk-H7AB42ES.js} +340 -30
- package/dist/cli.js +22157 -9229
- package/dist/commands/enterprise/templates/deploy-repo/.github/workflows/deploy.yml +17 -6
- package/dist/commands/enterprise/templates/deploy-repo/docs/runbook.md +81 -9
- package/dist/commands/enterprise/templates/deploy-repo/scripts/apply-release.mjs +183 -21
- package/dist/commands/enterprise/templates/deploy-repo/scripts/smoke.mjs +1 -2
- package/dist/commands/enterprise/templates/deploy-repo/terraform/main.tf +286 -0
- package/dist/drizzle/0000_petite_marvel_apes.sql +1188 -0
- package/dist/drizzle/0001_milky_surge.sql +103 -0
- package/dist/drizzle/0002_aberrant_mattie_franklin.sql +41 -0
- package/dist/drizzle/0003_exotic_ogun.sql +3 -0
- package/dist/drizzle/0004_glamorous_magneto.sql +2 -0
- package/dist/drizzle/0005_ancient_nocturne.sql +2 -0
- package/dist/drizzle/0006_new_colonel_america.sql +2 -0
- package/dist/drizzle/0007_unique_external_task_id.sql +31 -0
- package/dist/drizzle/0008_external_task_id_column.sql +43 -0
- package/dist/drizzle/0009_evaluations.sql +64 -0
- package/dist/drizzle/0010_eval_agent_template.sql +2 -0
- package/dist/drizzle/0011_eval_run_template.sql +2 -0
- package/dist/drizzle/0012_eval_seed_unique.sql +16 -0
- package/dist/drizzle/0013_motionless_white_queen.sql +140 -0
- package/dist/drizzle/0014_wiki_aggregation.sql +11 -0
- package/dist/drizzle/0015_pg_trgm_alias_title_indexes.sql +9 -0
- package/dist/drizzle/0016_wiki_schema_drops.sql +24 -0
- package/dist/drizzle/0017_wiki_places.sql +46 -0
- package/dist/drizzle/0018_agent_workspace_overlay.sql +26 -0
- package/dist/drizzle/0018_cool_firebird.sql +24 -0
- package/dist/drizzle/0018_skill_runs.sql +84 -0
- package/dist/drizzle/0019_condemned_dragon_man.sql +95 -0
- package/dist/drizzle/0019_webhook_skill_runs.sql +58 -0
- package/dist/drizzle/0020_crazy_the_anarchist.sql +9 -0
- package/dist/drizzle/0020_mutation_idempotency.sql +64 -0
- package/dist/drizzle/0021_crm_work_links.sql +76 -0
- package/dist/drizzle/0021_skill_runs_completion_hmac.sql +33 -0
- package/dist/drizzle/0022_add_billing_tables.sql +70 -0
- package/dist/drizzle/0022_seed_thinkwork_admin_permissions.sql +160 -0
- package/dist/drizzle/0022_stripe_billing_indexes.sql +48 -0
- package/dist/drizzle/0023_tenants_deactivation.sql +27 -0
- package/dist/drizzle/0024_tenant_mcp_admin_keys.sql +63 -0
- package/dist/drizzle/0025_v1_agent_architecture.sql +182 -0
- package/dist/drizzle/0026_thread_turns_add_kind.sql +28 -0
- package/dist/drizzle/0027_capability_catalog_and_manifests.sql +219 -0
- package/dist/drizzle/0028_aws_s3_extension.sql +26 -0
- package/dist/drizzle/0029_collapse_execution_types.sql +42 -0
- package/dist/drizzle/0030_retire_composition_primitives.sql +48 -0
- package/dist/drizzle/0031_thread_cleanup_drops.sql +131 -0
- package/dist/drizzle/0032_add_browser_automation_template_config.sql +24 -0
- package/dist/drizzle/0033_agent_operation_leases.sql +32 -0
- package/dist/drizzle/0034_agent_workspace_events.sql +116 -0
- package/dist/drizzle/0035_workspace_review_decisions.sql +19 -0
- package/dist/drizzle/0036_user_scoped_memory_wiki.sql +152 -0
- package/dist/drizzle/0037_user_profile_operating_model.sql +34 -0
- package/dist/drizzle/0042_agent_runtime_selector.sql +41 -0
- package/dist/drizzle/0043_wiki_search_tsv_tokenization.sql +48 -0
- package/dist/drizzle/0044_add_web_search_template_config.sql +7 -0
- package/dist/drizzle/0045_add_send_email_template_config.sql +7 -0
- package/dist/drizzle/0046_tenant_mcp_context_tools.sql +26 -0
- package/dist/drizzle/0047_add_context_engine_template_config.sql +7 -0
- package/dist/drizzle/0048_tenant_context_provider_settings.sql +22 -0
- package/dist/drizzle/0050_brain_v0_entity_subtype.sql +29 -0
- package/dist/drizzle/0051_brain_v0_tenant_entity_pages.sql +170 -0
- package/dist/drizzle/0052_brain_v0_external_refs.sql +41 -0
- package/dist/drizzle/0053_brain_v0_unresolved_mentions_subtype.sql +48 -0
- package/dist/drizzle/0054_wiki_compile_jobs_input.sql +28 -0
- package/dist/drizzle/0055_routines_stepfunctions_substrate.sql +202 -0
- package/dist/drizzle/0056_routine_step_events_dedup_idx.sql +25 -0
- package/dist/drizzle/0057_archive_legacy_python_routines.sql +36 -0
- package/dist/drizzle/0058_routines_visibility_and_owning_agent.sql +61 -0
- package/dist/drizzle/0061_routine_execution_asl_version_id.sql +29 -0
- package/dist/drizzle/0062_migrate_pi_to_flue.sql +110 -0
- package/dist/drizzle/0063_threads_session_data.sql +23 -0
- package/dist/drizzle/0064_tenant_credentials.sql +65 -0
- package/dist/drizzle/0067_thinkwork_computers_phase_one.sql +174 -0
- package/dist/drizzle/0068_drop_system_workflows_and_activation.sql +115 -0
- package/dist/drizzle/0069_compliance_schema.sql +364 -0
- package/dist/drizzle/0070_compliance_aurora_roles.sql +171 -0
- package/dist/drizzle/0072_threads_computer_ownership.sql +24 -0
- package/dist/drizzle/0073_compliance_tenant_anchor_state.sql +105 -0
- package/dist/drizzle/0074_compliance_event_hash_index.sql +49 -0
- package/dist/drizzle/0075_scheduled_jobs_computer_ownership.sql +27 -0
- package/dist/drizzle/0076_scheduled_jobs_marco_backfill.sql +69 -0
- package/dist/drizzle/0077_computers_primary_agent_id.sql +38 -0
- package/dist/drizzle/0078_tenant_customize_catalog.sql +46 -0
- package/dist/drizzle/0079_seed_tenant_customize_catalog.sql +53 -0
- package/dist/drizzle/0081_routines_catalog_slug.sql +42 -0
- package/dist/drizzle/0082_messages_parts_jsonb.sql +52 -0
- package/dist/drizzle/0083_computer_runbooks.sql +103 -0
- package/dist/drizzle/0084_artifacts_favorited_at.sql +32 -0
- package/dist/drizzle/0085_seed_thinkwork_computer_default_template.sql +46 -0
- package/dist/drizzle/0086_update_default_template_models.sql +19 -0
- package/dist/drizzle/0087_retire_oss_connectors.sql +77 -0
- package/dist/drizzle/0088_compliance_event_types_finance_pilot.sql +37 -0
- package/dist/drizzle/0089_remove_maniflow_eval_seeds.sql +81 -0
- package/dist/drizzle/0089_wiki_schema_extraction.sql +315 -0
- package/dist/drizzle/0090_brain_schema_extraction.sql +274 -0
- package/dist/drizzle/0091_drop_wiki_brain_compat_views.sql +118 -0
- package/dist/drizzle/0092_finish_oss_connector_retirement.sql +97 -0
- package/dist/drizzle/0093_eval_runs_scheduled_job_id.sql +23 -0
- package/dist/drizzle/0094_slack_workspace_app.sql +140 -0
- package/dist/drizzle/0095_eval_runs_computer_id.sql +22 -0
- package/dist/drizzle/0096_true_redteam_eval_seed_cleanup.sql +67 -0
- package/dist/drizzle/0097_shared_computers.sql +191 -0
- package/dist/drizzle/0098_business_ontology.sql +587 -0
- package/dist/drizzle/0099_thread_idle_learning.sql +145 -0
- package/dist/drizzle/0100_ontology_gated_brain_dynamic_subtypes.sql +7 -0
- package/dist/drizzle/0101_ontology_gated_unresolved_mentions_dynamic_subtypes.sql +7 -0
- package/dist/drizzle/0102_wiki_brain_owner_repair.sql +87 -0
- package/dist/drizzle/0103_brain_section_source_trigger_repair.sql +75 -0
- package/dist/drizzle/0104_expand_seed_ontology_for_user_memory.sql +166 -0
- package/dist/drizzle/0105_spaces_domain.sql +349 -0
- package/dist/drizzle/0106_expand_seed_ontology_for_triples.sql +173 -0
- package/dist/drizzle/0106_space_threads_participants.sql +197 -0
- package/dist/drizzle/0107_linked_task_mirror.sql +245 -0
- package/dist/drizzle/0108_message_mentions.sql +95 -0
- package/dist/drizzle/0109_space_owned_threads.sql +217 -0
- package/dist/drizzle/0110_thread_participant_read_state.sql +21 -0
- package/dist/drizzle/0111_agents_tenant_name_unique.sql +38 -0
- package/dist/drizzle/0112_recast_spaces_as_contextual_workrooms.sql +138 -0
- package/dist/drizzle/0113_agents_own_runtime_fields.sql +48 -0
- package/dist/drizzle/0114_migrate_templates_to_agents_and_spaces.sql +361 -0
- package/dist/drizzle/0115_remove_computer_eval_template_dependencies.sql +69 -0
- package/dist/drizzle/0116_backfill_thread_participants_access.sql +126 -0
- package/dist/drizzle/0117_space_access_mode.sql +40 -0
- package/dist/drizzle/0118_normalize_tenant_member_principal_type.sql +41 -0
- package/dist/drizzle/0119_space_knowledge_bases.sql +126 -0
- package/dist/drizzle/0120_space_automations.sql +58 -0
- package/dist/drizzle/0121_drop_computer_runbooks.sql +26 -0
- package/dist/drizzle/0122_space_email_triggers.sql +24 -0
- package/dist/drizzle/0123_single_platform_agent_and_overrides.sql +58 -0
- package/dist/drizzle/0124_thread_turns_finalized_at.sql +17 -0
- package/dist/drizzle/0125_drop_space_agent_assignments.sql +42 -0
- package/dist/drizzle/0126_migrate_flue_to_pi.sql +42 -0
- package/dist/drizzle/0127_runtime_type_observability.sql +28 -0
- package/dist/drizzle/0128_drop_eval_test_cases_agent_id.sql +37 -0
- package/dist/drizzle/0129_drop_code_factory_tables.sql +43 -0
- package/dist/drizzle/0129_eval_results_system_prompt.sql +30 -0
- package/dist/drizzle/0130_drop_teams_and_team_id_columns.sql +54 -0
- package/dist/drizzle/0131_drop_skill_catalog_and_tenant_skills.sql +33 -0
- package/dist/drizzle/0132_drop_computer_tables.sql +31 -0
- package/dist/drizzle/0133_drop_seed_view_tombstones.sql +79 -0
- package/dist/drizzle/0133_thread_turns_system_prompt.sql +22 -0
- package/dist/drizzle/0134_drop_dead_agent_runtime_tables.sql +53 -0
- package/dist/drizzle/0135_native_checklist_linked_tasks.sql +48 -0
- package/dist/drizzle/0136_goal_ledger.sql +55 -0
- package/dist/drizzle/0136_space_email_trigger_status.sql +66 -0
- package/dist/drizzle/0137_thread_participant_pins.sql +25 -0
- package/dist/drizzle/0138_users_cognito_sub.sql +30 -0
- package/dist/drizzle/0139_mobile_turn_client_id.sql +23 -0
- package/dist/drizzle/0140_workspace_folder_names.sql +36 -0
- package/dist/drizzle/0141_eval_runs_desktop_pi_provenance.sql +31 -0
- package/dist/drizzle/0142_pi_only_agent_runtime.sql +61 -0
- package/dist/drizzle/0143_add_web_extract_builtin_config.sql +28 -0
- package/dist/drizzle/0144_skill_catalog_index.sql +40 -0
- package/dist/drizzle/0145_knowledge_graph_thread_ingest.sql +512 -0
- package/dist/drizzle/0146_knowledge_graph_source_scope.sql +413 -0
- package/dist/drizzle/0147_knowledge_graph_scope_trigger_qualification.sql +211 -0
- package/dist/drizzle/0148_user_cost_attribution.sql +171 -0
- package/dist/drizzle/0149_managed_mcp_servers.sql +64 -0
- package/dist/drizzle/0149_user_model_approvals.sql +89 -0
- package/dist/drizzle/0150_bootstrap_first_admin_claim.sql +11 -0
- package/dist/drizzle/0151_managed_deployments.sql +91 -0
- package/dist/drizzle/0152_agent_profiles.sql +81 -0
- package/dist/drizzle/0153_message_mentions_agent_profile.sql +9 -0
- package/dist/drizzle/0154_customer_deployment_sessions.sql +61 -0
- package/dist/drizzle/0155_tenant_model_catalog.sql +152 -0
- package/dist/drizzle/0156_bootstrap_credential_leases.sql +52 -0
- package/dist/drizzle/0157_knowledge_graph_observations_source.sql +79 -0
- package/dist/drizzle/0158_pending_user_questions.sql +80 -0
- package/dist/drizzle/0158_wiki_tenant_scope.sql +38 -0
- package/dist/drizzle/0159_agent_profiles_space_local.sql +49 -0
- package/dist/drizzle/0159_application_plugins.sql +193 -0
- package/dist/drizzle/0159_eval_scoring_version.sql +46 -0
- package/dist/drizzle/0160_compliance_event_types_plugins.sql +37 -0
- package/dist/drizzle/0160_eval_datasets.sql +75 -0
- package/dist/drizzle/0160_eval_results_thread_turn.sql +35 -0
- package/dist/drizzle/0161_eval_run_pinning.sql +54 -0
- package/dist/drizzle/0161_tenant_mcp_servers_plugin_source.sql +62 -0
- package/dist/drizzle/0162_eval_result_override.sql +49 -0
- package/dist/drizzle/0163_eval_replay_tool_allowlist.sql +50 -0
- package/dist/drizzle/0164_eval_replay_tool_override_mode.sql +52 -0
- package/dist/drizzle/0165_plugin_premium_entitlements.sql +153 -0
- package/dist/drizzle/0166_company_brain_substrate_contract.sql +386 -0
- package/dist/drizzle/0167_company_brain_artifact_manifest_runtime.sql +85 -0
- package/dist/drizzle/0168_eval_skill_gate.sql +44 -0
- package/dist/drizzle/0169_release_update_jobs.sql +67 -0
- package/dist/drizzle/0170_email_channel_plugin.sql +291 -0
- package/dist/drizzle/0171_linked_task_external_providers.sql +30 -0
- package/dist/drizzle/0172_email_channel_sendgrid_provider.sql +8 -0
- package/dist/drizzle/0173_auth_provider_resources.sql +83 -0
- package/dist/drizzle/0174_workos_auth_bridges.sql +37 -0
- package/dist/drizzle/0175_workos_auth_sessions.sql +38 -0
- package/dist/drizzle/0176_n8n_agent_step_runs.sql +107 -0
- package/dist/drizzle/0177_workflow_control_plane.sql +281 -0
- package/dist/drizzle/0178_workflow_backfill_existing_routines.sql +525 -0
- package/dist/drizzle/0179_tenant_goal_budget_settings.sql +21 -0
- package/dist/drizzle/0180_skill_drafts.sql +89 -0
- package/dist/drizzle/0181_skill_draft_publish_status.sql +21 -0
- package/dist/drizzle/0182_agent_loops.sql +260 -0
- package/dist/drizzle/0183_okf_artifact_manifests.sql +52 -0
- package/dist/drizzle/0184_skill_catalog_trust_cache.sql +35 -0
- package/dist/drizzle/0185_agent_loop_space.sql +16 -0
- package/dist/drizzle/0186_tenant_mcp_runtime_metadata.sql +10 -0
- package/dist/drizzle/0187_native_work_items.sql +220 -0
- package/dist/drizzle/0188_company_etl_plugin_rename.sql +99 -0
- package/dist/drizzle/0189_trace_cost_substrate.sql +247 -0
- package/dist/drizzle/0190_billing_export_reconciliation.sql +101 -0
- package/dist/drizzle/0191_open_engine_work_item_queue.sql +66 -0
- package/dist/drizzle/0192_work_item_labels.sql +44 -0
- package/dist/drizzle/0193_work_item_documents.sql +31 -0
- package/dist/drizzle/0194_memory_retain_attempts.sql +103 -0
- package/dist/drizzle/0194_work_item_comments.sql +35 -0
- package/dist/drizzle/0195_plugin_app_overlays.sql +45 -0
- package/dist/drizzle/0196_pi_extensions.sql +118 -0
- package/dist/drizzle/0197_eval_profiles.sql +150 -0
- package/dist/drizzle/0198_eval_runs_unstable.sql +27 -0
- package/dist/drizzle/0199_eval_runs_cost_partial.sql +27 -0
- package/dist/drizzle/meta/_journal.json +160 -0
- package/dist/plugins/README.md +162 -0
- package/dist/plugins/catalog/package.json +40 -0
- package/dist/plugins/catalog/scripts/build-catalog.ts +120 -0
- package/dist/plugins/catalog/scripts/generate-plugin-registry.ts +144 -0
- package/dist/plugins/catalog/src/__tests__/build-catalog.test.ts +86 -0
- package/dist/plugins/catalog/src/__tests__/catalog.test.ts +308 -0
- package/dist/plugins/catalog/src/__tests__/contracts.test.ts +861 -0
- package/dist/plugins/catalog/src/__tests__/plugin-package.test.ts +124 -0
- package/dist/plugins/catalog/src/__tests__/plugin-registry.test.ts +120 -0
- package/dist/plugins/catalog/src/catalog.ts +434 -0
- package/dist/plugins/catalog/src/contracts.ts +1498 -0
- package/dist/plugins/catalog/src/index.ts +4 -0
- package/dist/plugins/catalog/src/plugin-package.ts +171 -0
- package/dist/plugins/catalog/src/registry/generated-first-party.ts +76 -0
- package/dist/plugins/catalog/src/registry/index.ts +1 -0
- package/dist/plugins/catalog/tsconfig.json +8 -0
- package/dist/plugins/company-brain/README.md +37 -0
- package/dist/plugins/company-brain/package.json +33 -0
- package/dist/plugins/company-brain/runtime/cognee/Dockerfile +11 -0
- package/dist/plugins/company-brain/smoke/cognee-managed-app-smoke.mjs +458 -0
- package/dist/plugins/company-brain/smoke/cognee-memory-cutover-smoke.mjs +539 -0
- package/dist/plugins/company-brain/smoke/company-brain-context-engine-smoke.mjs +312 -0
- package/dist/plugins/company-brain/smoke/company-brain-operations-smoke.mjs +449 -0
- package/dist/plugins/company-brain/smoke/company-brain-plugin-smoke.mjs +655 -0
- package/dist/plugins/company-brain/smoke/hindsight-memory-isolation-smoke.mjs +474 -0
- package/dist/plugins/company-brain/src/api/cognee-client.ts +896 -0
- package/dist/plugins/company-brain/src/api/cognee-cluster-identity.ts +49 -0
- package/dist/plugins/company-brain/src/api/cognee-memory-scope.ts +84 -0
- package/dist/plugins/company-brain/src/api/context-engine-provider.ts +888 -0
- package/dist/plugins/company-brain/src/api/migration.ts +665 -0
- package/dist/plugins/company-brain/src/deployment/cognee-managed-app.ts +396 -0
- package/dist/plugins/company-brain/src/index.ts +62 -0
- package/dist/plugins/company-brain/src/manifest.ts +112 -0
- package/dist/plugins/company-brain/terraform/cognee/README.md +263 -0
- package/dist/plugins/company-brain/terraform/cognee/main.tf +766 -0
- package/dist/plugins/company-brain/terraform/cognee/outputs.tf +144 -0
- package/dist/plugins/company-brain/terraform/cognee/variables.tf +402 -0
- package/dist/plugins/company-brain/test/api/cognee-client.test.ts +562 -0
- package/dist/plugins/company-brain/test/api/cognee-memory-scope.test.ts +74 -0
- package/dist/plugins/company-brain/test/api/context-engine-provider.test.ts +447 -0
- package/dist/plugins/company-brain/test/api/migration.test.ts +465 -0
- package/dist/plugins/company-brain/test/manifest.test.ts +95 -0
- package/dist/plugins/company-brain/tsconfig.json +8 -0
- package/dist/plugins/company-data/README.md +49 -0
- package/dist/plugins/company-data/package.json +21 -0
- package/dist/plugins/company-data/src/index.ts +30 -0
- package/dist/plugins/company-data/src/manifest.ts +26 -0
- package/dist/plugins/company-data/test/manifest.test.ts +116 -0
- package/dist/plugins/company-data/tsconfig.json +8 -0
- package/dist/plugins/company-etl/README.md +46 -0
- package/dist/plugins/company-etl/package.json +21 -0
- package/dist/plugins/company-etl/src/index.ts +29 -0
- package/dist/plugins/company-etl/src/manifest.ts +26 -0
- package/dist/plugins/company-etl/test/manifest.test.ts +108 -0
- package/dist/plugins/company-etl/tsconfig.json +8 -0
- package/dist/plugins/email-channel/README.md +41 -0
- package/dist/plugins/email-channel/package.json +22 -0
- package/dist/plugins/email-channel/src/index.ts +36 -0
- package/dist/plugins/email-channel/src/manifest.ts +44 -0
- package/dist/plugins/email-channel/src/provider-contract.ts +36 -0
- package/dist/plugins/email-channel/test/manifest.test.ts +119 -0
- package/dist/plugins/email-channel/tsconfig.json +8 -0
- package/dist/plugins/lastmile/README.md +50 -0
- package/dist/plugins/lastmile/package.json +23 -0
- package/dist/plugins/lastmile/smoke/lastmile-plugin-smoke.mjs +661 -0
- package/dist/plugins/lastmile/src/api/tasks-adapter.ts +572 -0
- package/dist/plugins/lastmile/src/discovery.fixture.ts +64 -0
- package/dist/plugins/lastmile/src/index.ts +46 -0
- package/dist/plugins/lastmile/src/manifest.ts +139 -0
- package/dist/plugins/lastmile/test/api/tasks-adapter.test.ts +266 -0
- package/dist/plugins/lastmile/test/discovery.test.ts +88 -0
- package/dist/plugins/lastmile/tsconfig.json +8 -0
- package/dist/plugins/n8n/README.md +201 -0
- package/dist/plugins/n8n/n8n-app/README.md +33 -0
- package/dist/plugins/n8n/n8n-app/package.json +23 -0
- package/dist/plugins/n8n/n8n-app/src/application-config.ts +45 -0
- package/dist/plugins/n8n/n8n-app/src/front-components/thinkwork-workflows.front-component.tsx +970 -0
- package/dist/plugins/n8n/n8n-app/src/lib/n8n-app-data.ts +190 -0
- package/dist/plugins/n8n/n8n-app/src/vite-env.d.ts +7 -0
- package/dist/plugins/n8n/n8n-app/tsconfig.json +15 -0
- package/dist/plugins/n8n/package.json +35 -0
- package/dist/plugins/n8n/runtime/Dockerfile +18 -0
- package/dist/plugins/n8n/runtime/n8n-task-runners.json +14 -0
- package/dist/plugins/n8n/runtime/n8n-task-runners.json.template +14 -0
- package/dist/plugins/n8n/runtime/package-lock.json +13 -0
- package/dist/plugins/n8n/runtime/package.json +7 -0
- package/dist/plugins/n8n/scripts/sync-thinkwork-app.mjs +268 -0
- package/dist/plugins/n8n/smoke/README.md +95 -0
- package/dist/plugins/n8n/smoke/n8n-agent-step-bridge-smoke.mjs +866 -0
- package/dist/plugins/n8n/smoke/n8n-integrated-app-smoke.mjs +262 -0
- package/dist/plugins/n8n/smoke/n8n-managed-app-smoke.mjs +409 -0
- package/dist/plugins/n8n/smoke/n8n-mcp-smoke.mjs +624 -0
- package/dist/plugins/n8n/src/deployment/image-build.ts +172 -0
- package/dist/plugins/n8n/src/deployment/managed-app.ts +429 -0
- package/dist/plugins/n8n/src/index.ts +79 -0
- package/dist/plugins/n8n/src/manifest.ts +403 -0
- package/dist/plugins/n8n/src/package-config.ts +186 -0
- package/dist/plugins/n8n/src/skills/n8n-workflow-operator/SKILL.md +76 -0
- package/dist/plugins/n8n/src/skills/n8n-workflow-operator/references/mcp-tooling.md +40 -0
- package/dist/plugins/n8n/src/skills/n8n-workflow-operator/references/validation-and-handoff.md +40 -0
- package/dist/plugins/n8n/src/skills/n8n-workflow-operator/references/workflow-authoring.md +45 -0
- package/dist/plugins/n8n/terraform/n8n/README.md +113 -0
- package/dist/plugins/n8n/terraform/n8n/main.tf +918 -0
- package/dist/plugins/n8n/terraform/n8n/outputs.tf +124 -0
- package/dist/plugins/n8n/terraform/n8n/scripts/sync-database.py +250 -0
- package/dist/plugins/n8n/terraform/n8n/scripts/test_sync_database.py +55 -0
- package/dist/plugins/n8n/terraform/n8n/variables.tf +354 -0
- package/dist/plugins/n8n/test/image-build.test.ts +118 -0
- package/dist/plugins/n8n/test/integrated-app-contract.test.ts +353 -0
- package/dist/plugins/n8n/test/manifest.test.ts +278 -0
- package/dist/plugins/n8n/test/package-config.test.ts +57 -0
- package/dist/plugins/n8n/tsconfig.json +8 -0
- package/dist/plugins/sendgrid/README.md +30 -0
- package/dist/plugins/sendgrid/package.json +22 -0
- package/dist/plugins/sendgrid/src/index.ts +36 -0
- package/dist/plugins/sendgrid/src/manifest.ts +37 -0
- package/dist/plugins/sendgrid/src/provider-contract.ts +14 -0
- package/dist/plugins/sendgrid/test/manifest.test.ts +80 -0
- package/dist/plugins/sendgrid/tsconfig.json +8 -0
- package/dist/plugins/twenty/README.md +78 -0
- package/dist/plugins/twenty/package.json +27 -0
- package/dist/plugins/twenty/scripts/__tests__/sync-thinkwork-app.test.mjs +67 -0
- package/dist/plugins/twenty/scripts/sync-thinkwork-app.mjs +185 -0
- package/dist/plugins/twenty/scripts/wire-thinkwork-workflow.mjs +658 -0
- package/dist/plugins/twenty/smoke/twenty-managed-app-smoke.mjs +359 -0
- package/dist/plugins/twenty/smoke/twenty-mcp-oauth-smoke.mjs +574 -0
- package/dist/plugins/twenty/src/api/cutover.ts +209 -0
- package/dist/plugins/twenty/src/deployment/managed-app.ts +253 -0
- package/dist/plugins/twenty/src/index.ts +55 -0
- package/dist/plugins/twenty/src/manifest.ts +259 -0
- package/dist/plugins/twenty/terraform/twenty/README.md +97 -0
- package/dist/plugins/twenty/terraform/twenty/main.tf +829 -0
- package/dist/plugins/twenty/terraform/twenty/outputs.tf +79 -0
- package/dist/plugins/twenty/terraform/twenty/variables.tf +268 -0
- package/dist/plugins/twenty/test/api/cutover.test.ts +183 -0
- package/dist/plugins/twenty/test/manifest.test.ts +463 -0
- package/dist/plugins/twenty/tsconfig.json +9 -0
- package/dist/plugins/twenty/twenty-app/.nvmrc +1 -0
- package/dist/plugins/twenty/twenty-app/README.md +148 -0
- package/dist/plugins/twenty/twenty-app/package.json +30 -0
- package/dist/plugins/twenty/twenty-app/src/application-config.ts +44 -0
- package/dist/plugins/twenty/twenty-app/src/constants/universal-identifiers.ts +18 -0
- package/dist/plugins/twenty/twenty-app/src/default-role.ts +24 -0
- package/dist/plugins/twenty/twenty-app/src/front-components/thinkwork-settings.front-component.tsx +299 -0
- package/dist/plugins/twenty/twenty-app/src/logic-functions/thinkwork-webhook.logic-function.ts +354 -0
- package/dist/plugins/twenty/twenty-app/tsconfig.json +14 -0
- package/dist/plugins/twenty/twenty-app/yarn.lock +4144 -0
- package/dist/plugins/workos-auth/package.json +22 -0
- package/dist/plugins/workos-auth/src/index.ts +30 -0
- package/dist/plugins/workos-auth/src/manifest.ts +40 -0
- package/dist/plugins/workos-auth/src/provider-contract.ts +39 -0
- package/dist/plugins/workos-auth/test/manifest.test.ts +94 -0
- package/dist/plugins/workos-auth/tsconfig.json +8 -0
- package/dist/scripts/bootstrap-workspace.sh +162 -0
- package/dist/scripts/post-deploy.sh +290 -0
- package/dist/terraform/examples/greenfield/main.tf +903 -75
- package/dist/terraform/examples/greenfield/terraform.tfvars.example +67 -8
- package/dist/terraform/modules/app/agentcore-code-interpreter/Dockerfile.sandbox-base +2 -2
- package/dist/terraform/modules/app/agentcore-code-interpreter/README.md +8 -9
- package/dist/terraform/modules/app/agentcore-code-interpreter/sandbox/sitecustomize.py +227 -0
- package/dist/terraform/modules/app/agentcore-code-interpreter/sandbox/test_sitecustomize.py +277 -0
- package/dist/terraform/modules/app/agentcore-memory/README.md +6 -6
- package/dist/terraform/modules/app/agentcore-memory/main.tf +1 -1
- package/dist/terraform/modules/app/agentcore-memory/scripts/create_or_find_memory.sh +40 -9
- package/dist/terraform/modules/app/agentcore-pi/README.md +61 -0
- package/dist/terraform/modules/app/agentcore-pi/main.tf +453 -0
- package/dist/terraform/modules/app/agentcore-pi/outputs.tf +38 -0
- package/dist/terraform/modules/app/agentcore-pi/variables.tf +145 -0
- package/dist/terraform/modules/app/agentcore-platform/main.tf +83 -0
- package/dist/terraform/modules/app/agentcore-runtime/main.tf +9 -11
- package/dist/terraform/modules/app/appsync-subscriptions/main.tf +5 -1
- package/dist/terraform/modules/app/customer-domain/main.tf +483 -0
- package/dist/terraform/modules/app/deployment-control-plane/README.md +47 -0
- package/dist/terraform/modules/app/deployment-control-plane/buildspec.yml +24 -0
- package/dist/terraform/modules/app/deployment-control-plane/main.tf +614 -0
- package/dist/terraform/modules/app/deployment-control-plane/outputs.tf +54 -0
- package/dist/terraform/modules/app/deployment-control-plane/runner.py +4720 -0
- package/dist/terraform/modules/app/deployment-control-plane/test_runner_bundle.py +3026 -0
- package/dist/terraform/modules/app/deployment-control-plane/variables.tf +96 -0
- package/dist/terraform/modules/app/hindsight-memory/README.md +25 -23
- package/dist/terraform/modules/app/hindsight-memory/main.tf +26 -2
- package/dist/terraform/modules/app/lambda-api/eval-fanout.tf +17 -50
- package/dist/terraform/modules/app/lambda-api/handlers.tf +1290 -679
- package/dist/terraform/modules/app/lambda-api/iam-grouped.tf +933 -0
- package/dist/terraform/modules/app/lambda-api/main.tf +14 -527
- package/dist/terraform/modules/app/lambda-api/mcp-oauth.tf +3 -19
- package/dist/terraform/modules/app/lambda-api/outputs.tf +50 -6
- package/dist/terraform/modules/app/lambda-api/runtime-config.tf +163 -0
- package/dist/terraform/modules/app/lambda-api/slack-app-secrets.tf +3 -1
- package/dist/terraform/modules/app/lambda-api/stripe-secrets.tf +3 -1
- package/dist/terraform/modules/app/lambda-api/variables.tf +240 -53
- package/dist/terraform/modules/app/lambda-api/workspace-events.tf +2 -25
- package/dist/terraform/modules/app/ses-email/main.tf +143 -30
- package/dist/terraform/modules/app/static-site/main.tf +4 -3
- package/dist/terraform/modules/app/www-dns/main.tf +45 -5
- package/dist/terraform/modules/app/www-dns/outputs.tf +10 -0
- package/dist/terraform/modules/app/www-dns/variables.tf +24 -0
- package/dist/terraform/modules/data/aurora-postgres/main.tf +15 -1
- package/dist/terraform/modules/data/compliance-exports-bucket/main.tf +1 -1
- package/dist/terraform/modules/data/compliance-exports-bucket/variables.tf +6 -0
- package/dist/terraform/modules/foundation/cognito/main.tf +167 -11
- package/dist/terraform/modules/foundation/cognito/outputs.tf +5 -0
- package/dist/terraform/modules/foundation/cognito/variables.tf +110 -0
- package/dist/terraform/modules/foundation/vpc/main.tf +45 -3
- package/dist/terraform/modules/foundation/vpc/outputs.tf +20 -0
- package/dist/terraform/modules/foundation/vpc/variables.tf +18 -0
- package/dist/terraform/modules/thinkwork/README.md +71 -0
- package/dist/terraform/modules/thinkwork/main.tf +1325 -174
- package/dist/terraform/modules/thinkwork/outputs.tf +457 -24
- package/dist/terraform/modules/thinkwork/variables.tf +1169 -93
- package/dist/terraform/schema.graphql +82 -9
- package/dist/workspace-defaults/files/AGENTS.md +344 -0
- package/dist/workspace-defaults/files/CONTEXT.md +8 -0
- package/dist/workspace-defaults/files/GUARDRAILS.md +51 -0
- package/dist/workspace-defaults/files/MEMORY_GUIDE.md +65 -0
- package/dist/workspace-defaults/files/ROUTER.md +24 -0
- package/dist/workspace-defaults/files/SPACE.md +62 -0
- package/dist/workspace-defaults/files/TOOLS.md +65 -0
- package/dist/workspace-defaults/files/USER.md +40 -0
- package/dist/workspace-defaults/files/mcp.json +3 -0
- package/dist/workspace-defaults/files/memory/contacts.md +13 -0
- package/dist/workspace-defaults/files/memory/lessons.md +13 -0
- package/dist/workspace-defaults/files/memory/preferences.md +12 -0
- package/dist/workspace-defaults/files/skills/.gitkeep +1 -0
- package/dist/workspace-defaults/files/skills/artifact-builder/SKILL.md +79 -0
- package/dist/workspace-defaults/files/skills/artifact-builder/references/crm-dashboard.md +166 -0
- package/dist/workspace-defaults/files/skills/automation-loop-designer/SKILL.md +120 -0
- package/dist/workspace-defaults/files/skills/skill-creator/LICENSE.txt +202 -0
- package/dist/workspace-defaults/files/skills/skill-creator/SKILL.md +485 -0
- package/dist/workspace-defaults/files/skills/skill-creator/UPSTREAM.json +124 -0
- package/dist/workspace-defaults/files/skills/skill-creator/agents/analyzer.md +274 -0
- package/dist/workspace-defaults/files/skills/skill-creator/agents/comparator.md +202 -0
- package/dist/workspace-defaults/files/skills/skill-creator/agents/grader.md +223 -0
- package/dist/workspace-defaults/files/skills/skill-creator/assets/eval_review.html +146 -0
- package/dist/workspace-defaults/files/skills/skill-creator/eval-viewer/generate_review.py +471 -0
- package/dist/workspace-defaults/files/skills/skill-creator/eval-viewer/viewer.html +1325 -0
- package/dist/workspace-defaults/files/skills/skill-creator/references/schemas.md +430 -0
- package/dist/workspace-defaults/files/skills/skill-creator/scripts/__init__.py +0 -0
- package/dist/workspace-defaults/files/skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
- package/dist/workspace-defaults/files/skills/skill-creator/scripts/generate_report.py +326 -0
- package/dist/workspace-defaults/files/skills/skill-creator/scripts/improve_description.py +247 -0
- package/dist/workspace-defaults/files/skills/skill-creator/scripts/package_skill.py +136 -0
- package/dist/workspace-defaults/files/skills/skill-creator/scripts/quick_validate.py +103 -0
- package/dist/workspace-defaults/files/skills/skill-creator/scripts/run_eval.py +310 -0
- package/dist/workspace-defaults/files/skills/skill-creator/scripts/run_loop.py +328 -0
- package/dist/workspace-defaults/files/skills/skill-creator/scripts/utils.py +47 -0
- package/package.json +1 -1
- package/dist/terraform/modules/app/agentcore-flue/README.md +0 -58
- package/dist/terraform/modules/app/agentcore-flue/main.tf +0 -323
- package/dist/terraform/modules/app/agentcore-flue/outputs.tf +0 -23
- package/dist/terraform/modules/app/agentcore-flue/variables.tf +0 -97
- package/dist/terraform/modules/app/computer-runtime/README.md +0 -15
- package/dist/terraform/modules/app/computer-runtime/main.tf +0 -406
- package/dist/terraform/modules/app/computer-runtime/outputs.tf +0 -75
- package/dist/terraform/modules/app/computer-runtime/variables.tf +0 -66
|
@@ -0,0 +1,1498 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin manifest contracts.
|
|
3
|
+
*
|
|
4
|
+
* A plugin is a versioned package described by a manifest declaring identity
|
|
5
|
+
* and components. Component types:
|
|
6
|
+
*
|
|
7
|
+
* - `mcp-server` — a hosted MCP endpoint the agent dispatches to
|
|
8
|
+
* - `skills` — bundled skill folders seeded into the tenant catalog
|
|
9
|
+
* - `infrastructure` — a managed-app Terraform deployment
|
|
10
|
+
* - `ui-surface` — declared or launchable UI identity
|
|
11
|
+
* - `auth-provider` — declared/admin-configured login federation capability
|
|
12
|
+
*
|
|
13
|
+
* Capability declarations are catalog metadata, not install lifecycle
|
|
14
|
+
* components. They let a plugin advertise channel contracts whose runtime is
|
|
15
|
+
* owned by shared platform handlers.
|
|
16
|
+
*
|
|
17
|
+
* This module is pure: types + validation only. No DB, GraphQL, or AWS
|
|
18
|
+
* imports — the SSM-backed catalog verification wrapper lives in
|
|
19
|
+
* `packages/api`.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
// Mirrors packages/api/src/types/catalog-skill.ts SLUG_RE — plugin skill
|
|
23
|
+
// slugs land in the same tenant skill-catalog namespace, so they must obey
|
|
24
|
+
// the same shape. Namespacing is by hyphen convention (`lastmile--crm-basics`),
|
|
25
|
+
// never slashes.
|
|
26
|
+
export const SLUG_RE = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
27
|
+
|
|
28
|
+
// Full semver 2.0.0 shape including prerelease/build metadata.
|
|
29
|
+
export const SEMVER_RE =
|
|
30
|
+
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-(?:[0-9A-Za-z-]+)(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
|
31
|
+
|
|
32
|
+
export const PLUGIN_COMPONENT_TYPES = [
|
|
33
|
+
"mcp-server",
|
|
34
|
+
"skills",
|
|
35
|
+
"infrastructure",
|
|
36
|
+
"ui-surface",
|
|
37
|
+
"auth-provider",
|
|
38
|
+
] as const;
|
|
39
|
+
|
|
40
|
+
export type PluginComponentType = (typeof PLUGIN_COMPONENT_TYPES)[number];
|
|
41
|
+
|
|
42
|
+
export const EMAIL_CHANNEL_PROVIDER_KEYS = [
|
|
43
|
+
"resend",
|
|
44
|
+
"sendgrid",
|
|
45
|
+
"ses",
|
|
46
|
+
] as const;
|
|
47
|
+
|
|
48
|
+
export type EmailChannelProviderKey =
|
|
49
|
+
(typeof EMAIL_CHANNEL_PROVIDER_KEYS)[number];
|
|
50
|
+
|
|
51
|
+
export type McpServerAuth =
|
|
52
|
+
| {
|
|
53
|
+
mode: "oauth";
|
|
54
|
+
/** Authorization-server reference (the plugin-wide auth domain). */
|
|
55
|
+
authDomain: string;
|
|
56
|
+
/** RFC 8707 resource indicator the minted token must be bound to. */
|
|
57
|
+
resourceIndicator: string;
|
|
58
|
+
}
|
|
59
|
+
| {
|
|
60
|
+
/**
|
|
61
|
+
* Per-instance OAuth (e.g. Twenty CRM): the server endpoint is
|
|
62
|
+
* tenant-specific (`endpointFrom`), and both the resource indicator
|
|
63
|
+
* AND the authorization server are derived from the RESOLVED
|
|
64
|
+
* endpoint at provision/activation time — the resource indicator is
|
|
65
|
+
* the resolved endpoint URL, and the auth domain comes from the
|
|
66
|
+
* endpoint's RFC 9728 protected-resource metadata. Requires
|
|
67
|
+
* `endpointFrom` on the component.
|
|
68
|
+
*/
|
|
69
|
+
mode: "oauth-per-instance";
|
|
70
|
+
}
|
|
71
|
+
| {
|
|
72
|
+
/**
|
|
73
|
+
* User-provided HTTP headers for PAT/header-based MCP auth. Activation stores
|
|
74
|
+
* the caller's values as a per-user plugin secret, then dispatch emits
|
|
75
|
+
* only these declared headers to the MCP transport. Never use this for
|
|
76
|
+
* Authorization-bearing OAuth; OAuth must stay in the handle-shaped
|
|
77
|
+
* bearer path.
|
|
78
|
+
*/
|
|
79
|
+
mode: "user-provided-headers";
|
|
80
|
+
/**
|
|
81
|
+
* Optional first-class bearer token credential to send as
|
|
82
|
+
* `Authorization: Bearer <value>`, alongside any declared headers.
|
|
83
|
+
* Use this when the MCP transport/framework expects Authorization as
|
|
84
|
+
* its auth token path but still needs extra per-user headers.
|
|
85
|
+
*/
|
|
86
|
+
bearer?: McpUserProvidedBearer;
|
|
87
|
+
headers: McpUserProvidedHeader[];
|
|
88
|
+
}
|
|
89
|
+
| {
|
|
90
|
+
/**
|
|
91
|
+
* Tenant-owned service credential (e.g. n8n instance-level MCP access).
|
|
92
|
+
* The manifest declares only the credential kind, the managed-app
|
|
93
|
+
* desired_config key that holds a Secrets Manager secret ref, and the
|
|
94
|
+
* HTTP header shape. Secret values are resolved server-side during MCP
|
|
95
|
+
* dispatch and are never exposed through plugin activation.
|
|
96
|
+
*/
|
|
97
|
+
mode: "tenant-service-credential";
|
|
98
|
+
/** Stable non-secret kind for audit/evidence, e.g. n8n-mcp-access-token. */
|
|
99
|
+
credentialKind: string;
|
|
100
|
+
/** managed_applications.desired_config key holding the secret ARN/name. */
|
|
101
|
+
secretRefConfigKey: string;
|
|
102
|
+
/** Header bindings sourced from JSON keys inside the tenant secret. */
|
|
103
|
+
headers: McpTenantServiceCredentialHeader[];
|
|
104
|
+
}
|
|
105
|
+
| { mode: "none" };
|
|
106
|
+
|
|
107
|
+
export interface McpUserProvidedBearer {
|
|
108
|
+
/** Activation input key whose value supplies the bearer token. */
|
|
109
|
+
credentialKey: string;
|
|
110
|
+
/** Human label for activation UI/operator docs. */
|
|
111
|
+
displayName: string;
|
|
112
|
+
/** Marks secret fields for UI copy; values are always stored as secrets. */
|
|
113
|
+
secret?: boolean;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface McpUserProvidedHeader {
|
|
117
|
+
/** HTTP header name to send to the MCP server, e.g. `x-api-key`. */
|
|
118
|
+
name: string;
|
|
119
|
+
/** Activation input key whose value supplies this header. */
|
|
120
|
+
credentialKey: string;
|
|
121
|
+
/** Human label for activation UI/operator docs. */
|
|
122
|
+
displayName: string;
|
|
123
|
+
/** Marks secret fields for UI copy; values are always stored as secrets. */
|
|
124
|
+
secret?: boolean;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export interface McpTenantServiceCredentialHeader {
|
|
128
|
+
/** HTTP header name required by the service, e.g. `Authorization`. */
|
|
129
|
+
name: string;
|
|
130
|
+
/** JSON key inside the tenant service credential secret. */
|
|
131
|
+
secretJsonKey: string;
|
|
132
|
+
/**
|
|
133
|
+
* Static prefix prepended at dispatch. Authorization headers must use
|
|
134
|
+
* `Bearer ` so the Pi runtime can route the secret through its handle store.
|
|
135
|
+
*/
|
|
136
|
+
valuePrefix?: string;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* The ONE allowed endpoint indirection (plan 2026-06-12-001 U10): a
|
|
141
|
+
* manifest cannot carry a tenant-specific URL, so the MCP handler resolves
|
|
142
|
+
* the endpoint at provision time from the tenant's `managed_applications`
|
|
143
|
+
* row for `managedApp` — `desired_config[configKey]` holds the
|
|
144
|
+
* application's public URL (for Twenty, `publicUrl`, which the adapter
|
|
145
|
+
* echoes verbatim as the `twenty_url` Terraform output), and `path`
|
|
146
|
+
* replaces the URL path (e.g. `/mcp`).
|
|
147
|
+
*/
|
|
148
|
+
export interface McpEndpointFrom {
|
|
149
|
+
/** Managed-app adapter key whose managed_applications row carries the URL. */
|
|
150
|
+
managedApp: string;
|
|
151
|
+
/** `desired_config` key holding the application's public http(s) URL. */
|
|
152
|
+
configKey: string;
|
|
153
|
+
/** Replacement URL path for the MCP endpoint (must start with `/`). */
|
|
154
|
+
path?: string;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export interface McpRecordLinkRouteHint {
|
|
158
|
+
/** Provider-neutral object key, e.g. `opportunity`. */
|
|
159
|
+
objectType: string;
|
|
160
|
+
/** Relative browser route template containing exactly one `{id}` placeholder. */
|
|
161
|
+
routeTemplate: string;
|
|
162
|
+
/** Candidate fields in the MCP result that can contain the record id. */
|
|
163
|
+
idFields?: string[];
|
|
164
|
+
/** Candidate fields in the MCP result that can contain a human label. */
|
|
165
|
+
labelFields?: string[];
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export interface McpRecordLinkWorkspaceHint {
|
|
169
|
+
/** Optional non-secret field whose value can be appended as a URL hash. */
|
|
170
|
+
hashField?: string;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export interface McpRecordLinkHints {
|
|
174
|
+
/** Contract version for runtime metadata repair/audit decisions. */
|
|
175
|
+
schemaVersion: 1;
|
|
176
|
+
/** Declares where the static route hints came from. */
|
|
177
|
+
source: "plugin-manifest";
|
|
178
|
+
/** Supported record routes. Browser origins are resolved during provisioning. */
|
|
179
|
+
routes: McpRecordLinkRouteHint[];
|
|
180
|
+
/** Optional workspace/hash metadata. Never stores a credential. */
|
|
181
|
+
workspace?: McpRecordLinkWorkspaceHint;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export interface McpServerComponent {
|
|
185
|
+
type: "mcp-server";
|
|
186
|
+
key: string;
|
|
187
|
+
displayName: string;
|
|
188
|
+
description?: string;
|
|
189
|
+
/** Static endpoint URL. Exactly one of `endpointUrl` / `endpointFrom`. */
|
|
190
|
+
endpointUrl?: string;
|
|
191
|
+
/** Provision-time endpoint resolution from a managed-app row. */
|
|
192
|
+
endpointFrom?: McpEndpointFrom;
|
|
193
|
+
auth: McpServerAuth;
|
|
194
|
+
/** Optional non-secret hints for generating links from trusted MCP results. */
|
|
195
|
+
recordLinkHints?: McpRecordLinkHints;
|
|
196
|
+
/** Optional human notes about the tools the server exposes. */
|
|
197
|
+
toolNotes?: string[];
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export interface SkillSupportingFile {
|
|
201
|
+
/** Path relative to the skill folder (e.g. `references/guide.md`). */
|
|
202
|
+
path: string;
|
|
203
|
+
content: string;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export interface PluginSkillSource {
|
|
207
|
+
/**
|
|
208
|
+
* Namespaced slug satisfying SLUG_RE — hyphen-namespaced like
|
|
209
|
+
* `lastmile--crm-basics`, never slash-delimited. Seeds into
|
|
210
|
+
* `tenants/<tenant-slug>/skill-catalog/<slug>/`.
|
|
211
|
+
*/
|
|
212
|
+
slug: string;
|
|
213
|
+
/** Full SKILL.md content (frontmatter + body). */
|
|
214
|
+
skillMd: string;
|
|
215
|
+
supportingFiles?: SkillSupportingFile[];
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export interface SkillsComponent {
|
|
219
|
+
type: "skills";
|
|
220
|
+
key: string;
|
|
221
|
+
skills: PluginSkillSource[];
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export interface TerraformInputSpec {
|
|
225
|
+
description: string;
|
|
226
|
+
/** Terraform type expression, e.g. `string`, `number`, `list(string)`. */
|
|
227
|
+
type: string;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export interface InfrastructureComponent {
|
|
231
|
+
type: "infrastructure";
|
|
232
|
+
key: string;
|
|
233
|
+
/** Managed-app adapter key in the deployment-runner registry. */
|
|
234
|
+
managedAppKey: string;
|
|
235
|
+
/** Required Terraform input names → their contract. */
|
|
236
|
+
terraformInputs: Record<string, TerraformInputSpec>;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export interface UiSurfaceComponent {
|
|
240
|
+
type: "ui-surface";
|
|
241
|
+
key: string;
|
|
242
|
+
displayName: string;
|
|
243
|
+
/** Intended mount point identity for settings/reserved surfaces. */
|
|
244
|
+
intendedMount: string;
|
|
245
|
+
/**
|
|
246
|
+
* Optional safe launch metadata for trusted first-party plugin apps.
|
|
247
|
+
* This is route identity only: no URLs, credentials, or executable code.
|
|
248
|
+
*/
|
|
249
|
+
launch?: UiSurfaceLaunchMetadata;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export const UI_SURFACE_LAUNCH_MOUNTS = ["main-shell"] as const;
|
|
253
|
+
|
|
254
|
+
export type UiSurfaceLaunchMount = (typeof UI_SURFACE_LAUNCH_MOUNTS)[number];
|
|
255
|
+
|
|
256
|
+
export const UI_SURFACE_LAUNCH_RUNTIMES = ["trusted-bundled-react"] as const;
|
|
257
|
+
|
|
258
|
+
export type UiSurfaceLaunchRuntime =
|
|
259
|
+
(typeof UI_SURFACE_LAUNCH_RUNTIMES)[number];
|
|
260
|
+
|
|
261
|
+
export interface UiSurfaceLaunchMetadata {
|
|
262
|
+
schemaVersion: 1;
|
|
263
|
+
type: "app";
|
|
264
|
+
/** Stable app identity within the plugin package/catalog. */
|
|
265
|
+
appKey: string;
|
|
266
|
+
/** URL segment used by the shell host, e.g. /apps/twenty/client-engagement. */
|
|
267
|
+
routeSegment: string;
|
|
268
|
+
/** Trusted host mount point for the app. */
|
|
269
|
+
mount: UiSurfaceLaunchMount;
|
|
270
|
+
/** Runtime implementation class. V1 only supports bundled React routes. */
|
|
271
|
+
runtime: UiSurfaceLaunchRuntime;
|
|
272
|
+
/** Optional sidebar/menu description. */
|
|
273
|
+
description?: string;
|
|
274
|
+
/** Optional non-secret icon token understood by the web shell. */
|
|
275
|
+
icon?: string;
|
|
276
|
+
/** Future-friendly product/entitlement key. Metadata only in v1. */
|
|
277
|
+
entitlementProductKey?: string;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export const AUTH_PROVIDER_KEYS = ["workos"] as const;
|
|
281
|
+
|
|
282
|
+
export type AuthProviderKey = (typeof AUTH_PROVIDER_KEYS)[number];
|
|
283
|
+
|
|
284
|
+
export const AUTH_PROVIDER_CONFIG_FIELD_STORAGE = [
|
|
285
|
+
"metadata",
|
|
286
|
+
"secret-ref",
|
|
287
|
+
] as const;
|
|
288
|
+
|
|
289
|
+
export type AuthProviderConfigFieldStorage =
|
|
290
|
+
(typeof AUTH_PROVIDER_CONFIG_FIELD_STORAGE)[number];
|
|
291
|
+
|
|
292
|
+
export interface AuthProviderConfigField {
|
|
293
|
+
/** Operator-facing input key; values are never present in the manifest. */
|
|
294
|
+
key: string;
|
|
295
|
+
displayName: string;
|
|
296
|
+
required: boolean;
|
|
297
|
+
/**
|
|
298
|
+
* metadata: non-secret config such as issuer/client id.
|
|
299
|
+
* secret-ref: stored only as an operator-managed secret reference.
|
|
300
|
+
*/
|
|
301
|
+
storage: AuthProviderConfigFieldStorage;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export interface AuthProviderPublicOption {
|
|
305
|
+
/**
|
|
306
|
+
* Public-safe option key. For the U1-approved fallback this is `sso`; do not
|
|
307
|
+
* use provider-specific Google/Microsoft keys unless routing evidence exists.
|
|
308
|
+
*/
|
|
309
|
+
key: string;
|
|
310
|
+
displayName: string;
|
|
311
|
+
providerSpecific: boolean;
|
|
312
|
+
recommended?: boolean;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export interface AuthProviderComponent {
|
|
316
|
+
type: "auth-provider";
|
|
317
|
+
key: string;
|
|
318
|
+
displayName: string;
|
|
319
|
+
provider: AuthProviderKey;
|
|
320
|
+
settingsSurface: string;
|
|
321
|
+
/** Cognito IdP name the validated bridge will use; not a secret. */
|
|
322
|
+
cognitoIdentityProviderName: string;
|
|
323
|
+
configFields: AuthProviderConfigField[];
|
|
324
|
+
publicOptions: AuthProviderPublicOption[];
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export type PluginComponent =
|
|
328
|
+
| McpServerComponent
|
|
329
|
+
| SkillsComponent
|
|
330
|
+
| InfrastructureComponent
|
|
331
|
+
| UiSurfaceComponent
|
|
332
|
+
| AuthProviderComponent;
|
|
333
|
+
|
|
334
|
+
export interface EmailChannelProviderOption {
|
|
335
|
+
key: EmailChannelProviderKey;
|
|
336
|
+
displayName: string;
|
|
337
|
+
recommended?: boolean;
|
|
338
|
+
compatibility?: boolean;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export interface EmailChannelCapability {
|
|
342
|
+
type: "email-channel";
|
|
343
|
+
key: string;
|
|
344
|
+
displayName: string;
|
|
345
|
+
providers: EmailChannelProviderOption[];
|
|
346
|
+
settingsSurface: string;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export type PluginCapability = EmailChannelCapability;
|
|
350
|
+
|
|
351
|
+
export interface PluginVersion {
|
|
352
|
+
/** Semver string (SEMVER_RE). */
|
|
353
|
+
version: string;
|
|
354
|
+
/** OAuth scopes this version's MCP servers require at activation. */
|
|
355
|
+
requiredOauthScopes: string[];
|
|
356
|
+
/**
|
|
357
|
+
* Declared catalog capabilities that are implemented by shared platform
|
|
358
|
+
* handlers or reserved UI surfaces, rather than plugin-engine components.
|
|
359
|
+
*/
|
|
360
|
+
capabilities?: PluginCapability[];
|
|
361
|
+
components: PluginComponent[];
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
export interface PremiumPluginMetadata {
|
|
365
|
+
/**
|
|
366
|
+
* Stable product/entitlement key used by the premium entitlement layer.
|
|
367
|
+
* Usually matches `pluginKey`, but remains explicit so future paid
|
|
368
|
+
* packages can share or migrate entitlement products deliberately.
|
|
369
|
+
*/
|
|
370
|
+
entitlementProductKey: string;
|
|
371
|
+
/** V1 premium plugins install through a ThinkWork-provided one-time key. */
|
|
372
|
+
installKeyRequired: true;
|
|
373
|
+
/** Customer-facing prompt copy shown when the tenant lacks entitlement. */
|
|
374
|
+
installKeyPrompt: string;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export interface PluginManifest {
|
|
378
|
+
/** Plugin key satisfying SLUG_RE; unique within a catalog. */
|
|
379
|
+
pluginKey: string;
|
|
380
|
+
displayName: string;
|
|
381
|
+
description: string;
|
|
382
|
+
/** Present for paid/key-gated plugins. Omitted for free/included plugins. */
|
|
383
|
+
premium?: PremiumPluginMetadata;
|
|
384
|
+
versions: PluginVersion[];
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
export class PluginManifestError extends Error {
|
|
388
|
+
constructor(message: string) {
|
|
389
|
+
super(message);
|
|
390
|
+
this.name = "PluginManifestError";
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
export function validatePluginManifest(value: unknown): PluginManifest {
|
|
395
|
+
const manifest = value as Partial<PluginManifest>;
|
|
396
|
+
if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) {
|
|
397
|
+
throw new PluginManifestError("Plugin manifest must be an object");
|
|
398
|
+
}
|
|
399
|
+
requireString(manifest.pluginKey, "pluginKey");
|
|
400
|
+
if (!SLUG_RE.test(manifest.pluginKey)) {
|
|
401
|
+
throw new PluginManifestError(
|
|
402
|
+
`pluginKey "${manifest.pluginKey}" must match ${SLUG_RE.source}`,
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
requireString(manifest.displayName, "displayName");
|
|
406
|
+
requireString(manifest.description, "description");
|
|
407
|
+
if (manifest.premium !== undefined) {
|
|
408
|
+
validatePremiumPluginMetadata(manifest.premium, manifest.pluginKey);
|
|
409
|
+
}
|
|
410
|
+
if (!Array.isArray(manifest.versions) || manifest.versions.length === 0) {
|
|
411
|
+
throw new PluginManifestError(
|
|
412
|
+
`plugin ${manifest.pluginKey}: versions must be a non-empty array`,
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
const seenVersions = new Set<string>();
|
|
416
|
+
for (const version of manifest.versions) {
|
|
417
|
+
validatePluginVersion(version, manifest.pluginKey);
|
|
418
|
+
if (seenVersions.has(version.version)) {
|
|
419
|
+
throw new PluginManifestError(
|
|
420
|
+
`plugin ${manifest.pluginKey}: duplicate version ${version.version}`,
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
seenVersions.add(version.version);
|
|
424
|
+
}
|
|
425
|
+
return manifest as PluginManifest;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function validatePremiumPluginMetadata(
|
|
429
|
+
value: unknown,
|
|
430
|
+
pluginKey: string,
|
|
431
|
+
): void {
|
|
432
|
+
const premium = value as Partial<PremiumPluginMetadata>;
|
|
433
|
+
const prefix = `plugin ${pluginKey}: premium`;
|
|
434
|
+
if (!premium || typeof premium !== "object" || Array.isArray(premium)) {
|
|
435
|
+
throw new PluginManifestError(`${prefix} must be an object`);
|
|
436
|
+
}
|
|
437
|
+
requireString(
|
|
438
|
+
premium.entitlementProductKey,
|
|
439
|
+
`${prefix}.entitlementProductKey`,
|
|
440
|
+
);
|
|
441
|
+
if (!SLUG_RE.test(premium.entitlementProductKey)) {
|
|
442
|
+
throw new PluginManifestError(
|
|
443
|
+
`${prefix}.entitlementProductKey "${premium.entitlementProductKey}" must match ${SLUG_RE.source}`,
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
if (premium.installKeyRequired !== true) {
|
|
447
|
+
throw new PluginManifestError(`${prefix}.installKeyRequired must be true`);
|
|
448
|
+
}
|
|
449
|
+
requireString(premium.installKeyPrompt, `${prefix}.installKeyPrompt`);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function validatePluginVersion(value: unknown, pluginKey: string): void {
|
|
453
|
+
const version = value as Partial<PluginVersion>;
|
|
454
|
+
if (!version || typeof version !== "object" || Array.isArray(version)) {
|
|
455
|
+
throw new PluginManifestError(
|
|
456
|
+
`plugin ${pluginKey}: each version must be an object`,
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
requireString(version.version, `plugin ${pluginKey}: version`);
|
|
460
|
+
if (!SEMVER_RE.test(version.version)) {
|
|
461
|
+
throw new PluginManifestError(
|
|
462
|
+
`plugin ${pluginKey}: version "${version.version}" is not valid semver`,
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
const label = `plugin ${pluginKey}@${version.version}`;
|
|
466
|
+
if (
|
|
467
|
+
!Array.isArray(version.requiredOauthScopes) ||
|
|
468
|
+
version.requiredOauthScopes.some(
|
|
469
|
+
(scope) => typeof scope !== "string" || scope.length === 0,
|
|
470
|
+
)
|
|
471
|
+
) {
|
|
472
|
+
throw new PluginManifestError(
|
|
473
|
+
`${label}: requiredOauthScopes must be an array of non-empty strings`,
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
if (!Array.isArray(version.components) || version.components.length === 0) {
|
|
477
|
+
throw new PluginManifestError(
|
|
478
|
+
`${label}: components must be a non-empty array`,
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const seenComponentKeys = new Set<string>();
|
|
483
|
+
const seenCapabilityKeys = new Set<string>();
|
|
484
|
+
const seenSkillSlugs = new Set<string>();
|
|
485
|
+
let hasOauthServer = false;
|
|
486
|
+
|
|
487
|
+
if (version.capabilities !== undefined) {
|
|
488
|
+
if (!Array.isArray(version.capabilities)) {
|
|
489
|
+
throw new PluginManifestError(`${label}: capabilities must be an array`);
|
|
490
|
+
}
|
|
491
|
+
for (const capability of version.capabilities as Partial<PluginCapability>[]) {
|
|
492
|
+
validatePluginCapability(capability, label, seenCapabilityKeys);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
for (const component of version.components as Partial<PluginComponent>[]) {
|
|
497
|
+
if (
|
|
498
|
+
!component ||
|
|
499
|
+
typeof component !== "object" ||
|
|
500
|
+
Array.isArray(component)
|
|
501
|
+
) {
|
|
502
|
+
throw new PluginManifestError(
|
|
503
|
+
`${label}: each component must be an object`,
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
if (
|
|
507
|
+
!(PLUGIN_COMPONENT_TYPES as readonly string[]).includes(
|
|
508
|
+
component.type as string,
|
|
509
|
+
)
|
|
510
|
+
) {
|
|
511
|
+
throw new PluginManifestError(
|
|
512
|
+
`${label}: unknown component type "${String(component.type)}"`,
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
requireString(component.key, `${label}: component.key`);
|
|
516
|
+
if (!SLUG_RE.test(component.key)) {
|
|
517
|
+
throw new PluginManifestError(
|
|
518
|
+
`${label}: component key "${component.key}" must match ${SLUG_RE.source}`,
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
if (seenComponentKeys.has(component.key)) {
|
|
522
|
+
throw new PluginManifestError(
|
|
523
|
+
`${label}: duplicate component key "${component.key}"`,
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
seenComponentKeys.add(component.key);
|
|
527
|
+
|
|
528
|
+
switch (component.type) {
|
|
529
|
+
case "mcp-server":
|
|
530
|
+
if (
|
|
531
|
+
validateMcpServerComponent(component as McpServerComponent, label)
|
|
532
|
+
) {
|
|
533
|
+
hasOauthServer = true;
|
|
534
|
+
}
|
|
535
|
+
break;
|
|
536
|
+
case "skills":
|
|
537
|
+
validateSkillsComponent(
|
|
538
|
+
component as SkillsComponent,
|
|
539
|
+
label,
|
|
540
|
+
seenSkillSlugs,
|
|
541
|
+
);
|
|
542
|
+
break;
|
|
543
|
+
case "infrastructure":
|
|
544
|
+
validateInfrastructureComponent(
|
|
545
|
+
component as InfrastructureComponent,
|
|
546
|
+
label,
|
|
547
|
+
);
|
|
548
|
+
break;
|
|
549
|
+
case "ui-surface":
|
|
550
|
+
validateUiSurfaceComponent(component as UiSurfaceComponent, label);
|
|
551
|
+
break;
|
|
552
|
+
case "auth-provider":
|
|
553
|
+
validateAuthProviderComponent(
|
|
554
|
+
component as AuthProviderComponent,
|
|
555
|
+
label,
|
|
556
|
+
);
|
|
557
|
+
break;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// OAuth servers without declared scopes would mint an unauditable grant —
|
|
562
|
+
// the activation flow needs the scope set up front. Per-instance OAuth
|
|
563
|
+
// servers are exempt: their authorization server (and its supported
|
|
564
|
+
// scopes) are only discoverable per tenant instance, so an empty scope
|
|
565
|
+
// list degrades to the activation flow's default scope set.
|
|
566
|
+
if (hasOauthServer && version.requiredOauthScopes!.length === 0) {
|
|
567
|
+
throw new PluginManifestError(
|
|
568
|
+
`${label}: OAuth mcp-server components require non-empty requiredOauthScopes`,
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function validatePluginCapability(
|
|
574
|
+
capability: Partial<PluginCapability>,
|
|
575
|
+
label: string,
|
|
576
|
+
seenCapabilityKeys: Set<string>,
|
|
577
|
+
): void {
|
|
578
|
+
if (
|
|
579
|
+
!capability ||
|
|
580
|
+
typeof capability !== "object" ||
|
|
581
|
+
Array.isArray(capability)
|
|
582
|
+
) {
|
|
583
|
+
throw new PluginManifestError(
|
|
584
|
+
`${label}: each capability must be an object`,
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
if (capability.type !== "email-channel") {
|
|
588
|
+
throw new PluginManifestError(
|
|
589
|
+
`${label}: unknown capability type "${String(capability.type)}"`,
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
requireString(capability.key, `${label}: capability.key`);
|
|
593
|
+
if (!SLUG_RE.test(capability.key)) {
|
|
594
|
+
throw new PluginManifestError(
|
|
595
|
+
`${label}: capability key "${capability.key}" must match ${SLUG_RE.source}`,
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
if (seenCapabilityKeys.has(capability.key)) {
|
|
599
|
+
throw new PluginManifestError(
|
|
600
|
+
`${label}: duplicate capability key "${capability.key}"`,
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
seenCapabilityKeys.add(capability.key);
|
|
604
|
+
validateEmailChannelCapability(capability as EmailChannelCapability, label);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function validateEmailChannelCapability(
|
|
608
|
+
capability: Partial<EmailChannelCapability>,
|
|
609
|
+
label: string,
|
|
610
|
+
): void {
|
|
611
|
+
const prefix = `${label}: email-channel "${capability.key}"`;
|
|
612
|
+
requireString(capability.displayName, `${prefix}.displayName`);
|
|
613
|
+
requireString(capability.settingsSurface, `${prefix}.settingsSurface`);
|
|
614
|
+
if (
|
|
615
|
+
!Array.isArray(capability.providers) ||
|
|
616
|
+
capability.providers.length === 0
|
|
617
|
+
) {
|
|
618
|
+
throw new PluginManifestError(
|
|
619
|
+
`${prefix}.providers must be a non-empty array`,
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
const seenProviders = new Set<string>();
|
|
624
|
+
let recommendedCount = 0;
|
|
625
|
+
for (const [index, provider] of capability.providers.entries()) {
|
|
626
|
+
const providerPrefix = `${prefix}.providers[${index}]`;
|
|
627
|
+
if (!provider || typeof provider !== "object" || Array.isArray(provider)) {
|
|
628
|
+
throw new PluginManifestError(`${providerPrefix} must be an object`);
|
|
629
|
+
}
|
|
630
|
+
requireString(provider.key, `${providerPrefix}.key`);
|
|
631
|
+
if (
|
|
632
|
+
!(EMAIL_CHANNEL_PROVIDER_KEYS as readonly string[]).includes(provider.key)
|
|
633
|
+
) {
|
|
634
|
+
throw new PluginManifestError(
|
|
635
|
+
`${providerPrefix}.key "${provider.key}" is not a supported email-channel provider`,
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
if (seenProviders.has(provider.key)) {
|
|
639
|
+
throw new PluginManifestError(
|
|
640
|
+
`${prefix}.providers declares duplicate provider "${provider.key}"`,
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
seenProviders.add(provider.key);
|
|
644
|
+
requireString(provider.displayName, `${providerPrefix}.displayName`);
|
|
645
|
+
if (
|
|
646
|
+
provider.recommended !== undefined &&
|
|
647
|
+
typeof provider.recommended !== "boolean"
|
|
648
|
+
) {
|
|
649
|
+
throw new PluginManifestError(
|
|
650
|
+
`${providerPrefix}.recommended must be a boolean`,
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
if (
|
|
654
|
+
provider.compatibility !== undefined &&
|
|
655
|
+
typeof provider.compatibility !== "boolean"
|
|
656
|
+
) {
|
|
657
|
+
throw new PluginManifestError(
|
|
658
|
+
`${providerPrefix}.compatibility must be a boolean`,
|
|
659
|
+
);
|
|
660
|
+
}
|
|
661
|
+
if (provider.recommended === true) recommendedCount += 1;
|
|
662
|
+
}
|
|
663
|
+
if (recommendedCount !== 1) {
|
|
664
|
+
throw new PluginManifestError(
|
|
665
|
+
`${prefix}.providers must declare exactly one recommended provider`,
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/** Returns true when the component uses static (plugin-wide) OAuth. */
|
|
671
|
+
function validateMcpServerComponent(
|
|
672
|
+
component: Partial<McpServerComponent>,
|
|
673
|
+
label: string,
|
|
674
|
+
): boolean {
|
|
675
|
+
const prefix = `${label}: mcp-server "${component.key}"`;
|
|
676
|
+
requireString(component.displayName, `${prefix}.displayName`);
|
|
677
|
+
if (
|
|
678
|
+
component.description !== undefined &&
|
|
679
|
+
typeof component.description !== "string"
|
|
680
|
+
) {
|
|
681
|
+
throw new PluginManifestError(`${prefix}.description must be a string`);
|
|
682
|
+
}
|
|
683
|
+
if (
|
|
684
|
+
(component.endpointUrl === undefined) ===
|
|
685
|
+
(component.endpointFrom === undefined)
|
|
686
|
+
) {
|
|
687
|
+
throw new PluginManifestError(
|
|
688
|
+
`${prefix} must declare exactly one of endpointUrl / endpointFrom`,
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
if (component.endpointUrl !== undefined) {
|
|
692
|
+
requireString(component.endpointUrl, `${prefix}.endpointUrl`);
|
|
693
|
+
requireHttpUrl(component.endpointUrl, `${prefix}.endpointUrl`);
|
|
694
|
+
} else {
|
|
695
|
+
validateEndpointFrom(component.endpointFrom!, prefix);
|
|
696
|
+
}
|
|
697
|
+
if (component.recordLinkHints !== undefined) {
|
|
698
|
+
validateRecordLinkHints(component.recordLinkHints, prefix);
|
|
699
|
+
}
|
|
700
|
+
if (component.toolNotes !== undefined) {
|
|
701
|
+
if (
|
|
702
|
+
!Array.isArray(component.toolNotes) ||
|
|
703
|
+
component.toolNotes.some((note) => typeof note !== "string")
|
|
704
|
+
) {
|
|
705
|
+
throw new PluginManifestError(
|
|
706
|
+
`${prefix}.toolNotes must be an array of strings`,
|
|
707
|
+
);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
const auth = component.auth as Partial<McpServerAuth> | undefined;
|
|
711
|
+
if (!auth || typeof auth !== "object" || Array.isArray(auth)) {
|
|
712
|
+
throw new PluginManifestError(`${prefix}.auth is required`);
|
|
713
|
+
}
|
|
714
|
+
if (auth.mode === "none") return false;
|
|
715
|
+
if (auth.mode === "oauth-per-instance") {
|
|
716
|
+
if (component.endpointFrom === undefined) {
|
|
717
|
+
throw new PluginManifestError(
|
|
718
|
+
`${prefix}.auth.mode "oauth-per-instance" requires endpointFrom (the resolved endpoint anchors the per-instance auth)`,
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
return false;
|
|
722
|
+
}
|
|
723
|
+
if (auth.mode === "user-provided-headers") {
|
|
724
|
+
validateUserProvidedHeadersAuth(auth, prefix);
|
|
725
|
+
return false;
|
|
726
|
+
}
|
|
727
|
+
if (auth.mode === "tenant-service-credential") {
|
|
728
|
+
if (component.endpointFrom === undefined) {
|
|
729
|
+
throw new PluginManifestError(
|
|
730
|
+
`${prefix}.auth.mode "tenant-service-credential" requires endpointFrom so the service credential secret ref can resolve from the managed app desired_config`,
|
|
731
|
+
);
|
|
732
|
+
}
|
|
733
|
+
validateTenantServiceCredentialAuth(auth, prefix);
|
|
734
|
+
return false;
|
|
735
|
+
}
|
|
736
|
+
if (auth.mode !== "oauth") {
|
|
737
|
+
throw new PluginManifestError(
|
|
738
|
+
`${prefix}.auth.mode must be "oauth", "oauth-per-instance", "user-provided-headers", "tenant-service-credential", or "none"`,
|
|
739
|
+
);
|
|
740
|
+
}
|
|
741
|
+
const oauth = auth as Partial<Extract<McpServerAuth, { mode: "oauth" }>>;
|
|
742
|
+
requireString(oauth.authDomain, `${prefix}.auth.authDomain`);
|
|
743
|
+
requireHttpUrl(oauth.authDomain, `${prefix}.auth.authDomain`);
|
|
744
|
+
requireString(oauth.resourceIndicator, `${prefix}.auth.resourceIndicator`);
|
|
745
|
+
return true;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
const RECORD_LINK_FIELD_RE = /^[A-Za-z][A-Za-z0-9_.-]{0,127}$/;
|
|
749
|
+
const RECORD_LINK_ALLOWED_HINT_KEYS = [
|
|
750
|
+
"schemaVersion",
|
|
751
|
+
"source",
|
|
752
|
+
"routes",
|
|
753
|
+
"workspace",
|
|
754
|
+
] as const;
|
|
755
|
+
const RECORD_LINK_ALLOWED_ROUTE_KEYS = [
|
|
756
|
+
"objectType",
|
|
757
|
+
"routeTemplate",
|
|
758
|
+
"idFields",
|
|
759
|
+
"labelFields",
|
|
760
|
+
] as const;
|
|
761
|
+
const RECORD_LINK_ALLOWED_WORKSPACE_KEYS = ["hashField"] as const;
|
|
762
|
+
const RECORD_LINK_TEMPLATE_SEGMENT_RE = /^[A-Za-z0-9._~-]+$|^\{id\}$/;
|
|
763
|
+
const RECORD_LINK_FORBIDDEN_FIELD_PARTS = [
|
|
764
|
+
"auth_config",
|
|
765
|
+
"authorization",
|
|
766
|
+
"cookie",
|
|
767
|
+
"token",
|
|
768
|
+
"secret",
|
|
769
|
+
"password",
|
|
770
|
+
"credential",
|
|
771
|
+
"header",
|
|
772
|
+
] as const;
|
|
773
|
+
|
|
774
|
+
function validateRecordLinkHints(
|
|
775
|
+
hints: Partial<McpRecordLinkHints>,
|
|
776
|
+
prefix: string,
|
|
777
|
+
): void {
|
|
778
|
+
const label = `${prefix}.recordLinkHints`;
|
|
779
|
+
if (!hints || typeof hints !== "object" || Array.isArray(hints)) {
|
|
780
|
+
throw new PluginManifestError(`${label} must be an object`);
|
|
781
|
+
}
|
|
782
|
+
rejectUnknownKeys(
|
|
783
|
+
hints as Record<string, unknown>,
|
|
784
|
+
RECORD_LINK_ALLOWED_HINT_KEYS,
|
|
785
|
+
label,
|
|
786
|
+
);
|
|
787
|
+
if (hints.schemaVersion !== 1) {
|
|
788
|
+
throw new PluginManifestError(`${label}.schemaVersion must be 1`);
|
|
789
|
+
}
|
|
790
|
+
if (hints.source !== "plugin-manifest") {
|
|
791
|
+
throw new PluginManifestError(`${label}.source must be "plugin-manifest"`);
|
|
792
|
+
}
|
|
793
|
+
if (!Array.isArray(hints.routes) || hints.routes.length === 0) {
|
|
794
|
+
throw new PluginManifestError(`${label}.routes must be a non-empty array`);
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
const seenObjectTypes = new Set<string>();
|
|
798
|
+
for (const [index, route] of hints.routes.entries()) {
|
|
799
|
+
const routeLabel = `${label}.routes[${index}]`;
|
|
800
|
+
if (!route || typeof route !== "object" || Array.isArray(route)) {
|
|
801
|
+
throw new PluginManifestError(`${routeLabel} must be an object`);
|
|
802
|
+
}
|
|
803
|
+
rejectUnknownKeys(
|
|
804
|
+
route as unknown as Record<string, unknown>,
|
|
805
|
+
RECORD_LINK_ALLOWED_ROUTE_KEYS,
|
|
806
|
+
routeLabel,
|
|
807
|
+
);
|
|
808
|
+
requireString(route.objectType, `${routeLabel}.objectType`);
|
|
809
|
+
if (!SLUG_RE.test(route.objectType)) {
|
|
810
|
+
throw new PluginManifestError(
|
|
811
|
+
`${routeLabel}.objectType "${route.objectType}" must match ${SLUG_RE.source}`,
|
|
812
|
+
);
|
|
813
|
+
}
|
|
814
|
+
if (seenObjectTypes.has(route.objectType)) {
|
|
815
|
+
throw new PluginManifestError(
|
|
816
|
+
`${label}.routes declares duplicate objectType "${route.objectType}"`,
|
|
817
|
+
);
|
|
818
|
+
}
|
|
819
|
+
seenObjectTypes.add(route.objectType);
|
|
820
|
+
|
|
821
|
+
requireString(route.routeTemplate, `${routeLabel}.routeTemplate`);
|
|
822
|
+
validateRecordLinkRouteTemplate(
|
|
823
|
+
route.routeTemplate,
|
|
824
|
+
`${routeLabel}.routeTemplate`,
|
|
825
|
+
);
|
|
826
|
+
|
|
827
|
+
validateOptionalFieldList(route.idFields, `${routeLabel}.idFields`);
|
|
828
|
+
validateOptionalFieldList(route.labelFields, `${routeLabel}.labelFields`);
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
if (hints.workspace !== undefined) {
|
|
832
|
+
if (
|
|
833
|
+
!hints.workspace ||
|
|
834
|
+
typeof hints.workspace !== "object" ||
|
|
835
|
+
Array.isArray(hints.workspace)
|
|
836
|
+
) {
|
|
837
|
+
throw new PluginManifestError(`${label}.workspace must be an object`);
|
|
838
|
+
}
|
|
839
|
+
rejectUnknownKeys(
|
|
840
|
+
hints.workspace as Record<string, unknown>,
|
|
841
|
+
RECORD_LINK_ALLOWED_WORKSPACE_KEYS,
|
|
842
|
+
`${label}.workspace`,
|
|
843
|
+
);
|
|
844
|
+
if (hints.workspace.hashField !== undefined) {
|
|
845
|
+
validateFieldName(
|
|
846
|
+
hints.workspace.hashField,
|
|
847
|
+
`${label}.workspace.hashField`,
|
|
848
|
+
);
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
function validateRecordLinkRouteTemplate(value: string, label: string): void {
|
|
854
|
+
if (!value.startsWith("/") || value.startsWith("//")) {
|
|
855
|
+
throw new PluginManifestError(
|
|
856
|
+
`${label} must be a relative path containing exactly one "{id}" segment`,
|
|
857
|
+
);
|
|
858
|
+
}
|
|
859
|
+
if (
|
|
860
|
+
/[?#\\%\s<>\[\]()"']/.test(value) ||
|
|
861
|
+
/[\u0000-\u001F\u007F]/.test(value)
|
|
862
|
+
) {
|
|
863
|
+
throw new PluginManifestError(
|
|
864
|
+
`${label} must not contain query, fragment, encoded separator, whitespace, control, or markup characters`,
|
|
865
|
+
);
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
const placeholders = value.match(/\{[^}]*\}/g) ?? [];
|
|
869
|
+
if (placeholders.length !== 1 || placeholders[0] !== "{id}") {
|
|
870
|
+
throw new PluginManifestError(
|
|
871
|
+
`${label} must contain exactly one "{id}" placeholder and no other placeholders`,
|
|
872
|
+
);
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
const segments = value.slice(1).split("/");
|
|
876
|
+
if (segments.some((segment) => segment.length === 0)) {
|
|
877
|
+
throw new PluginManifestError(`${label} must not contain empty segments`);
|
|
878
|
+
}
|
|
879
|
+
let idSegmentCount = 0;
|
|
880
|
+
for (const segment of segments) {
|
|
881
|
+
if (segment === "." || segment === "..") {
|
|
882
|
+
throw new PluginManifestError(`${label} must not contain dot segments`);
|
|
883
|
+
}
|
|
884
|
+
if (!RECORD_LINK_TEMPLATE_SEGMENT_RE.test(segment)) {
|
|
885
|
+
throw new PluginManifestError(
|
|
886
|
+
`${label} contains an unsupported path segment "${segment}"`,
|
|
887
|
+
);
|
|
888
|
+
}
|
|
889
|
+
if (segment === "{id}") idSegmentCount += 1;
|
|
890
|
+
}
|
|
891
|
+
if (idSegmentCount !== 1) {
|
|
892
|
+
throw new PluginManifestError(
|
|
893
|
+
`${label} must use "{id}" as exactly one full path segment`,
|
|
894
|
+
);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
function validateOptionalFieldList(
|
|
899
|
+
fields: string[] | undefined,
|
|
900
|
+
label: string,
|
|
901
|
+
): void {
|
|
902
|
+
if (fields === undefined) return;
|
|
903
|
+
if (!Array.isArray(fields) || fields.length === 0) {
|
|
904
|
+
throw new PluginManifestError(`${label} must be a non-empty array`);
|
|
905
|
+
}
|
|
906
|
+
const seen = new Set<string>();
|
|
907
|
+
for (const [index, field] of fields.entries()) {
|
|
908
|
+
validateFieldName(field, `${label}[${index}]`);
|
|
909
|
+
if (seen.has(field)) {
|
|
910
|
+
throw new PluginManifestError(
|
|
911
|
+
`${label} declares duplicate field "${field}"`,
|
|
912
|
+
);
|
|
913
|
+
}
|
|
914
|
+
seen.add(field);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
function validateFieldName(
|
|
919
|
+
value: unknown,
|
|
920
|
+
label: string,
|
|
921
|
+
): asserts value is string {
|
|
922
|
+
requireString(value, label);
|
|
923
|
+
if (!RECORD_LINK_FIELD_RE.test(value)) {
|
|
924
|
+
throw new PluginManifestError(
|
|
925
|
+
`${label} "${value}" must match ${RECORD_LINK_FIELD_RE.source}`,
|
|
926
|
+
);
|
|
927
|
+
}
|
|
928
|
+
const normalized = value.toLowerCase();
|
|
929
|
+
const parts = normalized.split(/[_.-]+/);
|
|
930
|
+
const hasSensitivePart =
|
|
931
|
+
parts.includes("auth") ||
|
|
932
|
+
RECORD_LINK_FORBIDDEN_FIELD_PARTS.some((part) => normalized.includes(part));
|
|
933
|
+
if (hasSensitivePart) {
|
|
934
|
+
throw new PluginManifestError(
|
|
935
|
+
`${label} must not reference credential-shaped data`,
|
|
936
|
+
);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
const HTTP_HEADER_NAME_RE = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
|
941
|
+
const CREDENTIAL_KEY_RE = /^[A-Za-z][A-Za-z0-9_]{0,63}$/;
|
|
942
|
+
const FORBIDDEN_USER_HEADER_NAMES = new Set([
|
|
943
|
+
"authorization",
|
|
944
|
+
"proxy-authorization",
|
|
945
|
+
"cookie",
|
|
946
|
+
"set-cookie",
|
|
947
|
+
]);
|
|
948
|
+
const FORBIDDEN_SERVICE_CREDENTIAL_HEADER_NAMES = new Set([
|
|
949
|
+
"proxy-authorization",
|
|
950
|
+
"cookie",
|
|
951
|
+
"set-cookie",
|
|
952
|
+
]);
|
|
953
|
+
|
|
954
|
+
function validateUserProvidedHeadersAuth(
|
|
955
|
+
auth: Partial<Extract<McpServerAuth, { mode: "user-provided-headers" }>>,
|
|
956
|
+
prefix: string,
|
|
957
|
+
): void {
|
|
958
|
+
if (!Array.isArray(auth.headers) || auth.headers.length === 0) {
|
|
959
|
+
throw new PluginManifestError(
|
|
960
|
+
`${prefix}.auth.headers must be a non-empty array`,
|
|
961
|
+
);
|
|
962
|
+
}
|
|
963
|
+
const seenHeaderNames = new Set<string>();
|
|
964
|
+
const seenCredentialKeys = new Set<string>();
|
|
965
|
+
if (auth.bearer !== undefined) {
|
|
966
|
+
const label = `${prefix}.auth.bearer`;
|
|
967
|
+
if (
|
|
968
|
+
!auth.bearer ||
|
|
969
|
+
typeof auth.bearer !== "object" ||
|
|
970
|
+
Array.isArray(auth.bearer)
|
|
971
|
+
) {
|
|
972
|
+
throw new PluginManifestError(`${label} must be an object`);
|
|
973
|
+
}
|
|
974
|
+
requireString(auth.bearer.credentialKey, `${label}.credentialKey`);
|
|
975
|
+
if (!CREDENTIAL_KEY_RE.test(auth.bearer.credentialKey)) {
|
|
976
|
+
throw new PluginManifestError(
|
|
977
|
+
`${label}.credentialKey "${auth.bearer.credentialKey}" must match ${CREDENTIAL_KEY_RE.source}`,
|
|
978
|
+
);
|
|
979
|
+
}
|
|
980
|
+
seenCredentialKeys.add(auth.bearer.credentialKey);
|
|
981
|
+
requireString(auth.bearer.displayName, `${label}.displayName`);
|
|
982
|
+
if (
|
|
983
|
+
auth.bearer.secret !== undefined &&
|
|
984
|
+
typeof auth.bearer.secret !== "boolean"
|
|
985
|
+
) {
|
|
986
|
+
throw new PluginManifestError(`${label}.secret must be a boolean`);
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
for (const [index, header] of auth.headers.entries()) {
|
|
990
|
+
const label = `${prefix}.auth.headers[${index}]`;
|
|
991
|
+
if (!header || typeof header !== "object" || Array.isArray(header)) {
|
|
992
|
+
throw new PluginManifestError(`${label} must be an object`);
|
|
993
|
+
}
|
|
994
|
+
requireString(header.name, `${label}.name`);
|
|
995
|
+
if (!HTTP_HEADER_NAME_RE.test(header.name)) {
|
|
996
|
+
throw new PluginManifestError(
|
|
997
|
+
`${label}.name "${header.name}" must be a valid HTTP header name`,
|
|
998
|
+
);
|
|
999
|
+
}
|
|
1000
|
+
const normalizedName = header.name.toLowerCase();
|
|
1001
|
+
if (FORBIDDEN_USER_HEADER_NAMES.has(normalizedName)) {
|
|
1002
|
+
throw new PluginManifestError(
|
|
1003
|
+
`${label}.name "${header.name}" is not allowed for user-provided header auth`,
|
|
1004
|
+
);
|
|
1005
|
+
}
|
|
1006
|
+
if (seenHeaderNames.has(normalizedName)) {
|
|
1007
|
+
throw new PluginManifestError(
|
|
1008
|
+
`${prefix}.auth.headers declares duplicate header "${header.name}"`,
|
|
1009
|
+
);
|
|
1010
|
+
}
|
|
1011
|
+
seenHeaderNames.add(normalizedName);
|
|
1012
|
+
|
|
1013
|
+
requireString(header.credentialKey, `${label}.credentialKey`);
|
|
1014
|
+
if (!CREDENTIAL_KEY_RE.test(header.credentialKey)) {
|
|
1015
|
+
throw new PluginManifestError(
|
|
1016
|
+
`${label}.credentialKey "${header.credentialKey}" must match ${CREDENTIAL_KEY_RE.source}`,
|
|
1017
|
+
);
|
|
1018
|
+
}
|
|
1019
|
+
if (seenCredentialKeys.has(header.credentialKey)) {
|
|
1020
|
+
throw new PluginManifestError(
|
|
1021
|
+
`${prefix}.auth.headers declares duplicate credentialKey "${header.credentialKey}"`,
|
|
1022
|
+
);
|
|
1023
|
+
}
|
|
1024
|
+
seenCredentialKeys.add(header.credentialKey);
|
|
1025
|
+
|
|
1026
|
+
requireString(header.displayName, `${label}.displayName`);
|
|
1027
|
+
if (header.secret !== undefined && typeof header.secret !== "boolean") {
|
|
1028
|
+
throw new PluginManifestError(`${label}.secret must be a boolean`);
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
function validateTenantServiceCredentialAuth(
|
|
1034
|
+
auth: Partial<Extract<McpServerAuth, { mode: "tenant-service-credential" }>>,
|
|
1035
|
+
prefix: string,
|
|
1036
|
+
): void {
|
|
1037
|
+
requireString(auth.credentialKind, `${prefix}.auth.credentialKind`);
|
|
1038
|
+
if (!SLUG_RE.test(auth.credentialKind)) {
|
|
1039
|
+
throw new PluginManifestError(
|
|
1040
|
+
`${prefix}.auth.credentialKind "${auth.credentialKind}" must match ${SLUG_RE.source}`,
|
|
1041
|
+
);
|
|
1042
|
+
}
|
|
1043
|
+
requireString(auth.secretRefConfigKey, `${prefix}.auth.secretRefConfigKey`);
|
|
1044
|
+
if (!CREDENTIAL_KEY_RE.test(auth.secretRefConfigKey)) {
|
|
1045
|
+
throw new PluginManifestError(
|
|
1046
|
+
`${prefix}.auth.secretRefConfigKey "${auth.secretRefConfigKey}" must match ${CREDENTIAL_KEY_RE.source}`,
|
|
1047
|
+
);
|
|
1048
|
+
}
|
|
1049
|
+
if (!Array.isArray(auth.headers) || auth.headers.length === 0) {
|
|
1050
|
+
throw new PluginManifestError(
|
|
1051
|
+
`${prefix}.auth.headers must be a non-empty array`,
|
|
1052
|
+
);
|
|
1053
|
+
}
|
|
1054
|
+
const seenHeaderNames = new Set<string>();
|
|
1055
|
+
for (const [index, header] of auth.headers.entries()) {
|
|
1056
|
+
const label = `${prefix}.auth.headers[${index}]`;
|
|
1057
|
+
if (!header || typeof header !== "object" || Array.isArray(header)) {
|
|
1058
|
+
throw new PluginManifestError(`${label} must be an object`);
|
|
1059
|
+
}
|
|
1060
|
+
rejectManifestValueCarrier(
|
|
1061
|
+
header as unknown as Record<string, unknown>,
|
|
1062
|
+
label,
|
|
1063
|
+
"tenant-service-credential manifests",
|
|
1064
|
+
);
|
|
1065
|
+
requireString(header.name, `${label}.name`);
|
|
1066
|
+
if (!HTTP_HEADER_NAME_RE.test(header.name)) {
|
|
1067
|
+
throw new PluginManifestError(
|
|
1068
|
+
`${label}.name "${header.name}" must be a valid HTTP header name`,
|
|
1069
|
+
);
|
|
1070
|
+
}
|
|
1071
|
+
const normalizedName = header.name.toLowerCase();
|
|
1072
|
+
if (FORBIDDEN_SERVICE_CREDENTIAL_HEADER_NAMES.has(normalizedName)) {
|
|
1073
|
+
throw new PluginManifestError(
|
|
1074
|
+
`${label}.name "${header.name}" is not allowed for tenant service credential auth`,
|
|
1075
|
+
);
|
|
1076
|
+
}
|
|
1077
|
+
if (seenHeaderNames.has(normalizedName)) {
|
|
1078
|
+
throw new PluginManifestError(
|
|
1079
|
+
`${prefix}.auth.headers declares duplicate header "${header.name}"`,
|
|
1080
|
+
);
|
|
1081
|
+
}
|
|
1082
|
+
seenHeaderNames.add(normalizedName);
|
|
1083
|
+
|
|
1084
|
+
requireString(header.secretJsonKey, `${label}.secretJsonKey`);
|
|
1085
|
+
if (!CREDENTIAL_KEY_RE.test(header.secretJsonKey)) {
|
|
1086
|
+
throw new PluginManifestError(
|
|
1087
|
+
`${label}.secretJsonKey "${header.secretJsonKey}" must match ${CREDENTIAL_KEY_RE.source}`,
|
|
1088
|
+
);
|
|
1089
|
+
}
|
|
1090
|
+
if (
|
|
1091
|
+
header.valuePrefix !== undefined &&
|
|
1092
|
+
typeof header.valuePrefix !== "string"
|
|
1093
|
+
) {
|
|
1094
|
+
throw new PluginManifestError(`${label}.valuePrefix must be a string`);
|
|
1095
|
+
}
|
|
1096
|
+
if (
|
|
1097
|
+
normalizedName === "authorization" &&
|
|
1098
|
+
header.valuePrefix !== "Bearer "
|
|
1099
|
+
) {
|
|
1100
|
+
throw new PluginManifestError(
|
|
1101
|
+
`${label}.valuePrefix must be "Bearer " for Authorization service credentials`,
|
|
1102
|
+
);
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
function validateEndpointFrom(
|
|
1108
|
+
endpointFrom: Partial<McpEndpointFrom>,
|
|
1109
|
+
prefix: string,
|
|
1110
|
+
): void {
|
|
1111
|
+
if (
|
|
1112
|
+
!endpointFrom ||
|
|
1113
|
+
typeof endpointFrom !== "object" ||
|
|
1114
|
+
Array.isArray(endpointFrom)
|
|
1115
|
+
) {
|
|
1116
|
+
throw new PluginManifestError(`${prefix}.endpointFrom must be an object`);
|
|
1117
|
+
}
|
|
1118
|
+
requireString(endpointFrom.managedApp, `${prefix}.endpointFrom.managedApp`);
|
|
1119
|
+
if (!SLUG_RE.test(endpointFrom.managedApp)) {
|
|
1120
|
+
throw new PluginManifestError(
|
|
1121
|
+
`${prefix}.endpointFrom.managedApp "${endpointFrom.managedApp}" must match ${SLUG_RE.source}`,
|
|
1122
|
+
);
|
|
1123
|
+
}
|
|
1124
|
+
requireString(endpointFrom.configKey, `${prefix}.endpointFrom.configKey`);
|
|
1125
|
+
if (endpointFrom.path !== undefined) {
|
|
1126
|
+
if (
|
|
1127
|
+
typeof endpointFrom.path !== "string" ||
|
|
1128
|
+
!endpointFrom.path.startsWith("/") ||
|
|
1129
|
+
/[?#]/.test(endpointFrom.path)
|
|
1130
|
+
) {
|
|
1131
|
+
throw new PluginManifestError(
|
|
1132
|
+
`${prefix}.endpointFrom.path must start with "/" and carry no query/fragment`,
|
|
1133
|
+
);
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
function validateSkillsComponent(
|
|
1139
|
+
component: Partial<SkillsComponent>,
|
|
1140
|
+
label: string,
|
|
1141
|
+
seenSkillSlugs: Set<string>,
|
|
1142
|
+
): void {
|
|
1143
|
+
const prefix = `${label}: skills "${component.key}"`;
|
|
1144
|
+
if (!Array.isArray(component.skills) || component.skills.length === 0) {
|
|
1145
|
+
throw new PluginManifestError(`${prefix}.skills must be a non-empty array`);
|
|
1146
|
+
}
|
|
1147
|
+
for (const skill of component.skills as Partial<PluginSkillSource>[]) {
|
|
1148
|
+
if (!skill || typeof skill !== "object" || Array.isArray(skill)) {
|
|
1149
|
+
throw new PluginManifestError(`${prefix}: each skill must be an object`);
|
|
1150
|
+
}
|
|
1151
|
+
requireString(skill.slug, `${prefix}: skill.slug`);
|
|
1152
|
+
if (!SLUG_RE.test(skill.slug)) {
|
|
1153
|
+
throw new PluginManifestError(
|
|
1154
|
+
`${prefix}: skill slug "${skill.slug}" must match ${SLUG_RE.source} (hyphen-namespaced, no slashes)`,
|
|
1155
|
+
);
|
|
1156
|
+
}
|
|
1157
|
+
if (seenSkillSlugs.has(skill.slug)) {
|
|
1158
|
+
throw new PluginManifestError(
|
|
1159
|
+
`${label}: duplicate skill slug "${skill.slug}"`,
|
|
1160
|
+
);
|
|
1161
|
+
}
|
|
1162
|
+
seenSkillSlugs.add(skill.slug);
|
|
1163
|
+
requireString(skill.skillMd, `${prefix}: skill "${skill.slug}".skillMd`);
|
|
1164
|
+
if (skill.supportingFiles !== undefined) {
|
|
1165
|
+
if (!Array.isArray(skill.supportingFiles)) {
|
|
1166
|
+
throw new PluginManifestError(
|
|
1167
|
+
`${prefix}: skill "${skill.slug}".supportingFiles must be an array`,
|
|
1168
|
+
);
|
|
1169
|
+
}
|
|
1170
|
+
const seenPaths = new Set<string>();
|
|
1171
|
+
for (const file of skill.supportingFiles as Partial<SkillSupportingFile>[]) {
|
|
1172
|
+
if (!file || typeof file !== "object") {
|
|
1173
|
+
throw new PluginManifestError(
|
|
1174
|
+
`${prefix}: skill "${skill.slug}" supporting files must be objects`,
|
|
1175
|
+
);
|
|
1176
|
+
}
|
|
1177
|
+
requireString(file.path, `${prefix}: skill "${skill.slug}" file.path`);
|
|
1178
|
+
if (
|
|
1179
|
+
file.path.startsWith("/") ||
|
|
1180
|
+
file.path.includes("\\") ||
|
|
1181
|
+
file.path.split("/").includes("..")
|
|
1182
|
+
) {
|
|
1183
|
+
throw new PluginManifestError(
|
|
1184
|
+
`${prefix}: skill "${skill.slug}" supporting file path "${file.path}" must be folder-relative`,
|
|
1185
|
+
);
|
|
1186
|
+
}
|
|
1187
|
+
if (seenPaths.has(file.path)) {
|
|
1188
|
+
throw new PluginManifestError(
|
|
1189
|
+
`${prefix}: skill "${skill.slug}" duplicate supporting file path "${file.path}"`,
|
|
1190
|
+
);
|
|
1191
|
+
}
|
|
1192
|
+
seenPaths.add(file.path);
|
|
1193
|
+
if (typeof file.content !== "string") {
|
|
1194
|
+
throw new PluginManifestError(
|
|
1195
|
+
`${prefix}: skill "${skill.slug}" file "${file.path}" content must be a string`,
|
|
1196
|
+
);
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
function validateInfrastructureComponent(
|
|
1204
|
+
component: Partial<InfrastructureComponent>,
|
|
1205
|
+
label: string,
|
|
1206
|
+
): void {
|
|
1207
|
+
const prefix = `${label}: infrastructure "${component.key}"`;
|
|
1208
|
+
requireString(component.managedAppKey, `${prefix}.managedAppKey`);
|
|
1209
|
+
const inputs = component.terraformInputs;
|
|
1210
|
+
if (!inputs || typeof inputs !== "object" || Array.isArray(inputs)) {
|
|
1211
|
+
throw new PluginManifestError(
|
|
1212
|
+
`${prefix}.terraformInputs must be an object`,
|
|
1213
|
+
);
|
|
1214
|
+
}
|
|
1215
|
+
for (const [name, spec] of Object.entries(inputs)) {
|
|
1216
|
+
if (name.length === 0) {
|
|
1217
|
+
throw new PluginManifestError(
|
|
1218
|
+
`${prefix}.terraformInputs has an empty input name`,
|
|
1219
|
+
);
|
|
1220
|
+
}
|
|
1221
|
+
const inputSpec = spec as Partial<TerraformInputSpec>;
|
|
1222
|
+
if (
|
|
1223
|
+
!inputSpec ||
|
|
1224
|
+
typeof inputSpec !== "object" ||
|
|
1225
|
+
Array.isArray(inputSpec)
|
|
1226
|
+
) {
|
|
1227
|
+
throw new PluginManifestError(
|
|
1228
|
+
`${prefix}.terraformInputs["${name}"] must be an object`,
|
|
1229
|
+
);
|
|
1230
|
+
}
|
|
1231
|
+
requireString(
|
|
1232
|
+
inputSpec.description,
|
|
1233
|
+
`${prefix}.terraformInputs["${name}"].description`,
|
|
1234
|
+
);
|
|
1235
|
+
requireString(inputSpec.type, `${prefix}.terraformInputs["${name}"].type`);
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
function validateUiSurfaceComponent(
|
|
1240
|
+
component: Partial<UiSurfaceComponent>,
|
|
1241
|
+
label: string,
|
|
1242
|
+
): void {
|
|
1243
|
+
const prefix = `${label}: ui-surface "${component.key}"`;
|
|
1244
|
+
requireString(component.displayName, `${prefix}.displayName`);
|
|
1245
|
+
requireString(component.intendedMount, `${prefix}.intendedMount`);
|
|
1246
|
+
if (component.launch !== undefined) {
|
|
1247
|
+
validateUiSurfaceLaunchMetadata(component.launch, prefix);
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
const UI_SURFACE_LAUNCH_ALLOWED_KEYS = [
|
|
1252
|
+
"schemaVersion",
|
|
1253
|
+
"type",
|
|
1254
|
+
"appKey",
|
|
1255
|
+
"routeSegment",
|
|
1256
|
+
"mount",
|
|
1257
|
+
"runtime",
|
|
1258
|
+
"description",
|
|
1259
|
+
"icon",
|
|
1260
|
+
"entitlementProductKey",
|
|
1261
|
+
] as const;
|
|
1262
|
+
|
|
1263
|
+
function validateUiSurfaceLaunchMetadata(
|
|
1264
|
+
launch: Partial<UiSurfaceLaunchMetadata>,
|
|
1265
|
+
prefix: string,
|
|
1266
|
+
): void {
|
|
1267
|
+
const label = `${prefix}.launch`;
|
|
1268
|
+
if (!launch || typeof launch !== "object" || Array.isArray(launch)) {
|
|
1269
|
+
throw new PluginManifestError(`${label} must be an object`);
|
|
1270
|
+
}
|
|
1271
|
+
rejectUnknownKeys(
|
|
1272
|
+
launch as Record<string, unknown>,
|
|
1273
|
+
UI_SURFACE_LAUNCH_ALLOWED_KEYS,
|
|
1274
|
+
label,
|
|
1275
|
+
);
|
|
1276
|
+
if (launch.schemaVersion !== 1) {
|
|
1277
|
+
throw new PluginManifestError(`${label}.schemaVersion must be 1`);
|
|
1278
|
+
}
|
|
1279
|
+
if (launch.type !== "app") {
|
|
1280
|
+
throw new PluginManifestError(`${label}.type must be "app"`);
|
|
1281
|
+
}
|
|
1282
|
+
requireString(launch.appKey, `${label}.appKey`);
|
|
1283
|
+
if (!SLUG_RE.test(launch.appKey)) {
|
|
1284
|
+
throw new PluginManifestError(
|
|
1285
|
+
`${label}.appKey "${launch.appKey}" must match ${SLUG_RE.source}`,
|
|
1286
|
+
);
|
|
1287
|
+
}
|
|
1288
|
+
requireString(launch.routeSegment, `${label}.routeSegment`);
|
|
1289
|
+
if (!SLUG_RE.test(launch.routeSegment)) {
|
|
1290
|
+
throw new PluginManifestError(
|
|
1291
|
+
`${label}.routeSegment "${launch.routeSegment}" must match ${SLUG_RE.source}`,
|
|
1292
|
+
);
|
|
1293
|
+
}
|
|
1294
|
+
if (
|
|
1295
|
+
typeof launch.mount !== "string" ||
|
|
1296
|
+
!(UI_SURFACE_LAUNCH_MOUNTS as readonly string[]).includes(launch.mount)
|
|
1297
|
+
) {
|
|
1298
|
+
throw new PluginManifestError(
|
|
1299
|
+
`${label}.mount "${String(launch.mount)}" is not supported`,
|
|
1300
|
+
);
|
|
1301
|
+
}
|
|
1302
|
+
if (
|
|
1303
|
+
typeof launch.runtime !== "string" ||
|
|
1304
|
+
!(UI_SURFACE_LAUNCH_RUNTIMES as readonly string[]).includes(launch.runtime)
|
|
1305
|
+
) {
|
|
1306
|
+
throw new PluginManifestError(
|
|
1307
|
+
`${label}.runtime "${String(launch.runtime)}" is not supported`,
|
|
1308
|
+
);
|
|
1309
|
+
}
|
|
1310
|
+
if (launch.description !== undefined) {
|
|
1311
|
+
requireString(launch.description, `${label}.description`);
|
|
1312
|
+
}
|
|
1313
|
+
if (launch.icon !== undefined) {
|
|
1314
|
+
requireString(launch.icon, `${label}.icon`);
|
|
1315
|
+
if (!SLUG_RE.test(launch.icon)) {
|
|
1316
|
+
throw new PluginManifestError(
|
|
1317
|
+
`${label}.icon "${launch.icon}" must match ${SLUG_RE.source}`,
|
|
1318
|
+
);
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
if (launch.entitlementProductKey !== undefined) {
|
|
1322
|
+
requireString(
|
|
1323
|
+
launch.entitlementProductKey,
|
|
1324
|
+
`${label}.entitlementProductKey`,
|
|
1325
|
+
);
|
|
1326
|
+
if (!SLUG_RE.test(launch.entitlementProductKey)) {
|
|
1327
|
+
throw new PluginManifestError(
|
|
1328
|
+
`${label}.entitlementProductKey "${launch.entitlementProductKey}" must match ${SLUG_RE.source}`,
|
|
1329
|
+
);
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
function validateAuthProviderComponent(
|
|
1335
|
+
component: Partial<AuthProviderComponent>,
|
|
1336
|
+
label: string,
|
|
1337
|
+
): void {
|
|
1338
|
+
const prefix = `${label}: auth-provider "${component.key}"`;
|
|
1339
|
+
requireString(component.displayName, `${prefix}.displayName`);
|
|
1340
|
+
if (
|
|
1341
|
+
typeof component.provider !== "string" ||
|
|
1342
|
+
!(AUTH_PROVIDER_KEYS as readonly string[]).includes(component.provider)
|
|
1343
|
+
) {
|
|
1344
|
+
throw new PluginManifestError(
|
|
1345
|
+
`${prefix}.provider "${String(component.provider)}" is not supported`,
|
|
1346
|
+
);
|
|
1347
|
+
}
|
|
1348
|
+
requireString(component.settingsSurface, `${prefix}.settingsSurface`);
|
|
1349
|
+
requireString(
|
|
1350
|
+
component.cognitoIdentityProviderName,
|
|
1351
|
+
`${prefix}.cognitoIdentityProviderName`,
|
|
1352
|
+
);
|
|
1353
|
+
|
|
1354
|
+
if (
|
|
1355
|
+
!Array.isArray(component.configFields) ||
|
|
1356
|
+
component.configFields.length === 0
|
|
1357
|
+
) {
|
|
1358
|
+
throw new PluginManifestError(
|
|
1359
|
+
`${prefix}.configFields must be a non-empty array`,
|
|
1360
|
+
);
|
|
1361
|
+
}
|
|
1362
|
+
const seenConfigKeys = new Set<string>();
|
|
1363
|
+
for (const [index, field] of component.configFields.entries()) {
|
|
1364
|
+
const fieldPrefix = `${prefix}.configFields[${index}]`;
|
|
1365
|
+
if (!field || typeof field !== "object" || Array.isArray(field)) {
|
|
1366
|
+
throw new PluginManifestError(`${fieldPrefix} must be an object`);
|
|
1367
|
+
}
|
|
1368
|
+
rejectManifestValueCarrier(
|
|
1369
|
+
field as unknown as Record<string, unknown>,
|
|
1370
|
+
fieldPrefix,
|
|
1371
|
+
);
|
|
1372
|
+
requireString(field.key, `${fieldPrefix}.key`);
|
|
1373
|
+
if (!CREDENTIAL_KEY_RE.test(field.key)) {
|
|
1374
|
+
throw new PluginManifestError(
|
|
1375
|
+
`${fieldPrefix}.key "${field.key}" must match ${CREDENTIAL_KEY_RE.source}`,
|
|
1376
|
+
);
|
|
1377
|
+
}
|
|
1378
|
+
if (seenConfigKeys.has(field.key)) {
|
|
1379
|
+
throw new PluginManifestError(
|
|
1380
|
+
`${prefix}.configFields declares duplicate key "${field.key}"`,
|
|
1381
|
+
);
|
|
1382
|
+
}
|
|
1383
|
+
seenConfigKeys.add(field.key);
|
|
1384
|
+
requireString(field.displayName, `${fieldPrefix}.displayName`);
|
|
1385
|
+
if (typeof field.required !== "boolean") {
|
|
1386
|
+
throw new PluginManifestError(
|
|
1387
|
+
`${fieldPrefix}.required must be a boolean`,
|
|
1388
|
+
);
|
|
1389
|
+
}
|
|
1390
|
+
if (
|
|
1391
|
+
!(AUTH_PROVIDER_CONFIG_FIELD_STORAGE as readonly string[]).includes(
|
|
1392
|
+
field.storage,
|
|
1393
|
+
)
|
|
1394
|
+
) {
|
|
1395
|
+
throw new PluginManifestError(
|
|
1396
|
+
`${fieldPrefix}.storage must be one of ${AUTH_PROVIDER_CONFIG_FIELD_STORAGE.join(", ")}`,
|
|
1397
|
+
);
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
if (
|
|
1402
|
+
!Array.isArray(component.publicOptions) ||
|
|
1403
|
+
component.publicOptions.length === 0
|
|
1404
|
+
) {
|
|
1405
|
+
throw new PluginManifestError(
|
|
1406
|
+
`${prefix}.publicOptions must be a non-empty array`,
|
|
1407
|
+
);
|
|
1408
|
+
}
|
|
1409
|
+
const seenOptionKeys = new Set<string>();
|
|
1410
|
+
let recommendedCount = 0;
|
|
1411
|
+
for (const [index, option] of component.publicOptions.entries()) {
|
|
1412
|
+
const optionPrefix = `${prefix}.publicOptions[${index}]`;
|
|
1413
|
+
if (!option || typeof option !== "object" || Array.isArray(option)) {
|
|
1414
|
+
throw new PluginManifestError(`${optionPrefix} must be an object`);
|
|
1415
|
+
}
|
|
1416
|
+
rejectManifestValueCarrier(
|
|
1417
|
+
option as unknown as Record<string, unknown>,
|
|
1418
|
+
optionPrefix,
|
|
1419
|
+
);
|
|
1420
|
+
requireString(option.key, `${optionPrefix}.key`);
|
|
1421
|
+
if (!SLUG_RE.test(option.key)) {
|
|
1422
|
+
throw new PluginManifestError(
|
|
1423
|
+
`${optionPrefix}.key "${option.key}" must match ${SLUG_RE.source}`,
|
|
1424
|
+
);
|
|
1425
|
+
}
|
|
1426
|
+
if (seenOptionKeys.has(option.key)) {
|
|
1427
|
+
throw new PluginManifestError(
|
|
1428
|
+
`${prefix}.publicOptions declares duplicate key "${option.key}"`,
|
|
1429
|
+
);
|
|
1430
|
+
}
|
|
1431
|
+
seenOptionKeys.add(option.key);
|
|
1432
|
+
requireString(option.displayName, `${optionPrefix}.displayName`);
|
|
1433
|
+
if (typeof option.providerSpecific !== "boolean") {
|
|
1434
|
+
throw new PluginManifestError(
|
|
1435
|
+
`${optionPrefix}.providerSpecific must be a boolean`,
|
|
1436
|
+
);
|
|
1437
|
+
}
|
|
1438
|
+
if (
|
|
1439
|
+
option.recommended !== undefined &&
|
|
1440
|
+
typeof option.recommended !== "boolean"
|
|
1441
|
+
) {
|
|
1442
|
+
throw new PluginManifestError(
|
|
1443
|
+
`${optionPrefix}.recommended must be a boolean`,
|
|
1444
|
+
);
|
|
1445
|
+
}
|
|
1446
|
+
if (option.recommended === true) recommendedCount += 1;
|
|
1447
|
+
}
|
|
1448
|
+
if (recommendedCount !== 1) {
|
|
1449
|
+
throw new PluginManifestError(
|
|
1450
|
+
`${prefix}.publicOptions must declare exactly one recommended option`,
|
|
1451
|
+
);
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
function rejectManifestValueCarrier(
|
|
1456
|
+
value: Record<string, unknown>,
|
|
1457
|
+
prefix: string,
|
|
1458
|
+
context = "auth-provider manifests",
|
|
1459
|
+
): void {
|
|
1460
|
+
for (const key of ["value", "secret", "defaultValue", "currentValue"]) {
|
|
1461
|
+
if (Object.prototype.hasOwnProperty.call(value, key)) {
|
|
1462
|
+
throw new PluginManifestError(
|
|
1463
|
+
`${prefix}.${key} is not allowed in ${context}`,
|
|
1464
|
+
);
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
function rejectUnknownKeys(
|
|
1470
|
+
value: Record<string, unknown>,
|
|
1471
|
+
allowedKeys: readonly string[],
|
|
1472
|
+
prefix: string,
|
|
1473
|
+
): void {
|
|
1474
|
+
const allowed = new Set(allowedKeys);
|
|
1475
|
+
for (const key of Object.keys(value)) {
|
|
1476
|
+
if (!allowed.has(key)) {
|
|
1477
|
+
throw new PluginManifestError(`${prefix}.${key} is not allowed`);
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
function requireString(value: unknown, path: string): asserts value is string {
|
|
1483
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
1484
|
+
throw new PluginManifestError(`${path} is required`);
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
function requireHttpUrl(value: string, path: string): void {
|
|
1489
|
+
let parsed: URL;
|
|
1490
|
+
try {
|
|
1491
|
+
parsed = new URL(value);
|
|
1492
|
+
} catch {
|
|
1493
|
+
throw new PluginManifestError(`${path} must be a valid URL`);
|
|
1494
|
+
}
|
|
1495
|
+
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
|
|
1496
|
+
throw new PluginManifestError(`${path} must be an http(s) URL`);
|
|
1497
|
+
}
|
|
1498
|
+
}
|