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,124 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
PluginPackageError,
|
|
5
|
+
defineFirstPartyPluginPackage,
|
|
6
|
+
} from "../plugin-package";
|
|
7
|
+
import {
|
|
8
|
+
allPluginManifests,
|
|
9
|
+
firstPartyPluginPackages,
|
|
10
|
+
n8nPluginPackage,
|
|
11
|
+
} from "../registry";
|
|
12
|
+
|
|
13
|
+
describe("first-party plugin packages", () => {
|
|
14
|
+
it("registers n8n from its root plugin package boundary", () => {
|
|
15
|
+
expect(firstPartyPluginPackages.map((entry) => entry.packageKey)).toEqual([
|
|
16
|
+
"company-brain",
|
|
17
|
+
"company-data",
|
|
18
|
+
"company-etl",
|
|
19
|
+
"email-channel",
|
|
20
|
+
"lastmile",
|
|
21
|
+
"n8n",
|
|
22
|
+
"sendgrid",
|
|
23
|
+
"twenty",
|
|
24
|
+
"workos-auth",
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
expect(n8nPluginPackage.sourceRoot).toBe("plugins/n8n");
|
|
28
|
+
expect(n8nPluginPackage.manifest.pluginKey).toBe("n8n");
|
|
29
|
+
expect(n8nPluginPackage.ownedSources).toContainEqual({
|
|
30
|
+
kind: "manifest",
|
|
31
|
+
path: "plugins/n8n/src/manifest.ts",
|
|
32
|
+
description: "n8n catalog manifest and versioned component contract.",
|
|
33
|
+
});
|
|
34
|
+
expect(n8nPluginPackage.ownedSources).toContainEqual({
|
|
35
|
+
kind: "deployment",
|
|
36
|
+
path: "plugins/n8n/src/deployment",
|
|
37
|
+
description: "n8n managed-app adapter and package image build contract.",
|
|
38
|
+
});
|
|
39
|
+
expect(n8nPluginPackage.ownedSources).toContainEqual({
|
|
40
|
+
kind: "skills",
|
|
41
|
+
path: "plugins/n8n/src/skills",
|
|
42
|
+
description:
|
|
43
|
+
"n8n workflow operator instructions seeded through the plugin catalog.",
|
|
44
|
+
});
|
|
45
|
+
expect(n8nPluginPackage.compatibilityLinks).toEqual([]);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("publishes every first-party plugin manifest through the catalog aggregate", () => {
|
|
49
|
+
expect(allPluginManifests.map((manifest) => manifest.pluginKey)).toEqual([
|
|
50
|
+
"lastmile",
|
|
51
|
+
"n8n",
|
|
52
|
+
"email-channel",
|
|
53
|
+
"sendgrid",
|
|
54
|
+
"company-brain",
|
|
55
|
+
"company-data",
|
|
56
|
+
"company-etl",
|
|
57
|
+
"twenty",
|
|
58
|
+
"workos-auth",
|
|
59
|
+
]);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("rejects package descriptors whose source root does not match the key", () => {
|
|
63
|
+
expect(() =>
|
|
64
|
+
defineFirstPartyPluginPackage({
|
|
65
|
+
...n8nPluginPackage,
|
|
66
|
+
sourceRoot: "plugins/not-n8n",
|
|
67
|
+
}),
|
|
68
|
+
).toThrow(PluginPackageError);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("rejects package descriptors whose manifest key does not match", () => {
|
|
72
|
+
expect(() =>
|
|
73
|
+
defineFirstPartyPluginPackage({
|
|
74
|
+
...n8nPluginPackage,
|
|
75
|
+
packageKey: "not-n8n",
|
|
76
|
+
}),
|
|
77
|
+
).toThrow(PluginPackageError);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("rejects owned source descriptors outside the package root", () => {
|
|
81
|
+
expect(() =>
|
|
82
|
+
defineFirstPartyPluginPackage({
|
|
83
|
+
...n8nPluginPackage,
|
|
84
|
+
ownedSources: [
|
|
85
|
+
{
|
|
86
|
+
kind: "deployment",
|
|
87
|
+
path: "packages/deployment-runner/src/apps/n8n.ts",
|
|
88
|
+
description: "Misplaced n8n deployment adapter.",
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
}),
|
|
92
|
+
).toThrow(/ownedSources\[0\]\.path must live under plugins\/n8n\//);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("rejects compatibility links without migration debt documentation", () => {
|
|
96
|
+
expect(() =>
|
|
97
|
+
defineFirstPartyPluginPackage({
|
|
98
|
+
...n8nPluginPackage,
|
|
99
|
+
compatibilityLinks: [
|
|
100
|
+
{
|
|
101
|
+
path: "packages/deployment-runner/src/apps/n8n.ts",
|
|
102
|
+
reason: "",
|
|
103
|
+
removal: "THNK-31 U3",
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
}),
|
|
107
|
+
).toThrow(/compatibilityLinks\[0\]\.reason/);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("rejects compatibility links that point back inside the package", () => {
|
|
111
|
+
expect(() =>
|
|
112
|
+
defineFirstPartyPluginPackage({
|
|
113
|
+
...n8nPluginPackage,
|
|
114
|
+
compatibilityLinks: [
|
|
115
|
+
{
|
|
116
|
+
path: "plugins/n8n/src/manifest.ts",
|
|
117
|
+
reason: "Already owned source should not be compatibility debt.",
|
|
118
|
+
removal: "Remove the compatibility link.",
|
|
119
|
+
},
|
|
120
|
+
],
|
|
121
|
+
}),
|
|
122
|
+
).toThrow(/should describe legacy source outside plugins\/n8n\//);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { describe, expect, it } from "vitest";
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
discoverPluginRegistryEntries,
|
|
9
|
+
expectedPluginRegistry,
|
|
10
|
+
} from "../../scripts/generate-plugin-registry";
|
|
11
|
+
|
|
12
|
+
describe("generated first-party plugin registry", () => {
|
|
13
|
+
it("discovers plugin packages from plugins/* in deterministic order", () => {
|
|
14
|
+
expect(
|
|
15
|
+
discoverPluginRegistryEntries().map((entry) => ({
|
|
16
|
+
packageKey: entry.packageKey,
|
|
17
|
+
packageName: entry.packageName,
|
|
18
|
+
exportName: entry.exportName,
|
|
19
|
+
})),
|
|
20
|
+
).toEqual([
|
|
21
|
+
{
|
|
22
|
+
packageKey: "company-brain",
|
|
23
|
+
packageName: "@thinkwork/plugin-company-brain",
|
|
24
|
+
exportName: "companyBrainPluginPackage",
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
packageKey: "company-data",
|
|
28
|
+
packageName: "@thinkwork/plugin-company-data",
|
|
29
|
+
exportName: "companyDataPluginPackage",
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
packageKey: "company-etl",
|
|
33
|
+
packageName: "@thinkwork/plugin-company-etl",
|
|
34
|
+
exportName: "companyEtlPluginPackage",
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
packageKey: "email-channel",
|
|
38
|
+
packageName: "@thinkwork/plugin-email-channel",
|
|
39
|
+
exportName: "emailChannelPluginPackage",
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
packageKey: "lastmile",
|
|
43
|
+
packageName: "@thinkwork/plugin-lastmile",
|
|
44
|
+
exportName: "lastmilePluginPackage",
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
packageKey: "n8n",
|
|
48
|
+
packageName: "@thinkwork/plugin-n8n",
|
|
49
|
+
exportName: "n8nPluginPackage",
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
packageKey: "sendgrid",
|
|
53
|
+
packageName: "@thinkwork/plugin-sendgrid",
|
|
54
|
+
exportName: "sendgridPluginPackage",
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
packageKey: "twenty",
|
|
58
|
+
packageName: "@thinkwork/plugin-twenty",
|
|
59
|
+
exportName: "twentyPluginPackage",
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
packageKey: "workos-auth",
|
|
63
|
+
packageName: "@thinkwork/plugin-workos-auth",
|
|
64
|
+
exportName: "workosAuthPluginPackage",
|
|
65
|
+
},
|
|
66
|
+
]);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("skips packages whose catalog publication is deferred", () => {
|
|
70
|
+
const repoRoot = mkdtempRepo();
|
|
71
|
+
try {
|
|
72
|
+
writePluginPackage(repoRoot, "alpha", "@thinkwork/plugin-alpha");
|
|
73
|
+
writePluginPackage(repoRoot, "n8n", "@thinkwork/plugin-n8n", {
|
|
74
|
+
catalogPublication: "deferred",
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
expect(discoverPluginRegistryEntries({ repoRoot })).toEqual([
|
|
78
|
+
{
|
|
79
|
+
packageKey: "alpha",
|
|
80
|
+
packageName: "@thinkwork/plugin-alpha",
|
|
81
|
+
exportName: "alphaPluginPackage",
|
|
82
|
+
manifestExportName: "alphaManifest",
|
|
83
|
+
rawExportName: "rawAlphaPluginPackage",
|
|
84
|
+
},
|
|
85
|
+
]);
|
|
86
|
+
} finally {
|
|
87
|
+
rmSync(repoRoot, { recursive: true, force: true });
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("keeps the checked-in generated aggregate fresh", () => {
|
|
92
|
+
const generatedPath = fileURLToPath(
|
|
93
|
+
new URL("../registry/generated-first-party.ts", import.meta.url),
|
|
94
|
+
);
|
|
95
|
+
expect(readFileSync(generatedPath, "utf8")).toBe(expectedPluginRegistry());
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
function mkdtempRepo() {
|
|
100
|
+
const repoRoot = join(
|
|
101
|
+
tmpdir(),
|
|
102
|
+
`thinkwork-plugin-registry-${Date.now()}-${Math.random().toString(16).slice(2)}`,
|
|
103
|
+
);
|
|
104
|
+
mkdirSync(join(repoRoot, "plugins"), { recursive: true });
|
|
105
|
+
return repoRoot;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function writePluginPackage(
|
|
109
|
+
repoRoot: string,
|
|
110
|
+
packageKey: string,
|
|
111
|
+
packageName: string,
|
|
112
|
+
thinkworkPlugin?: { catalogPublication?: string },
|
|
113
|
+
) {
|
|
114
|
+
const packageRoot = join(repoRoot, "plugins", packageKey);
|
|
115
|
+
mkdirSync(packageRoot, { recursive: true });
|
|
116
|
+
writeFileSync(
|
|
117
|
+
join(packageRoot, "package.json"),
|
|
118
|
+
`${JSON.stringify({ name: packageName, thinkworkPlugin }, null, 2)}\n`,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Signed plugin catalog document.
|
|
3
|
+
*
|
|
4
|
+
* The catalog carries every published plugin's version list with the full
|
|
5
|
+
* per-version payload, a sha256 digest per payload (the install-time pin),
|
|
6
|
+
* and one ed25519 signature over the canonical document. Signing follows
|
|
7
|
+
* the `packages/release-manifest` conventions (canonical sorted-key JSON,
|
|
8
|
+
* raw ed25519 over the bytes) adapted to this document shape.
|
|
9
|
+
*
|
|
10
|
+
* Verification fails closed: unknown schema version, digest mismatch, and
|
|
11
|
+
* bad signature each throw a typed error. The trusted public key is a
|
|
12
|
+
* parameter — the SSM-backed wrapper lives in `packages/api`, keeping this
|
|
13
|
+
* package free of AWS imports.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
createHash,
|
|
18
|
+
createPrivateKey,
|
|
19
|
+
createPublicKey,
|
|
20
|
+
sign as signBytes,
|
|
21
|
+
verify as verifyBytes,
|
|
22
|
+
} from "node:crypto";
|
|
23
|
+
|
|
24
|
+
import {
|
|
25
|
+
validatePluginManifest,
|
|
26
|
+
type PluginManifest,
|
|
27
|
+
type PremiumPluginMetadata,
|
|
28
|
+
type PluginVersion,
|
|
29
|
+
} from "./contracts";
|
|
30
|
+
|
|
31
|
+
export const PLUGIN_CATALOG_SCHEMA_VERSION = 1;
|
|
32
|
+
export const PLUGIN_CATALOG_SIGNATURE_ALGORITHM = "ed25519";
|
|
33
|
+
|
|
34
|
+
export interface PluginCatalogVersionEntry {
|
|
35
|
+
version: string;
|
|
36
|
+
/** sha256 of the canonical JSON of `payload` — the install-time pin. */
|
|
37
|
+
payloadSha256: string;
|
|
38
|
+
payload: PluginVersion;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface PluginCatalogEntry {
|
|
42
|
+
pluginKey: string;
|
|
43
|
+
displayName: string;
|
|
44
|
+
description: string;
|
|
45
|
+
premium?: PremiumPluginMetadata;
|
|
46
|
+
versions: PluginCatalogVersionEntry[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface PluginCatalogSourceProvenance {
|
|
50
|
+
/** GitHub repository that authored the catalog source, e.g. thinkwork-ai/thinkwork. */
|
|
51
|
+
repository: string;
|
|
52
|
+
/** Source ref or channel used by the publisher, e.g. main or refs/heads/main. */
|
|
53
|
+
ref: string;
|
|
54
|
+
/** Git commit SHA that the generated catalog was built from. */
|
|
55
|
+
commitSha: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface PluginCatalog {
|
|
59
|
+
schemaVersion: typeof PLUGIN_CATALOG_SCHEMA_VERSION;
|
|
60
|
+
generatedAt: string;
|
|
61
|
+
source?: PluginCatalogSourceProvenance;
|
|
62
|
+
plugins: PluginCatalogEntry[];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface PluginCatalogSignature {
|
|
66
|
+
algorithm: typeof PLUGIN_CATALOG_SIGNATURE_ALGORITHM;
|
|
67
|
+
catalogSha256: string;
|
|
68
|
+
signedAt: string;
|
|
69
|
+
signature: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface SignedPluginCatalogDocument {
|
|
73
|
+
catalog: PluginCatalog;
|
|
74
|
+
signature: PluginCatalogSignature;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export class PluginCatalogError extends Error {
|
|
78
|
+
constructor(message: string) {
|
|
79
|
+
super(message);
|
|
80
|
+
this.name = "PluginCatalogError";
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export class PluginCatalogSchemaError extends PluginCatalogError {
|
|
85
|
+
constructor(message: string) {
|
|
86
|
+
super(message);
|
|
87
|
+
this.name = "PluginCatalogSchemaError";
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export class PluginCatalogDigestError extends PluginCatalogError {
|
|
92
|
+
constructor(message: string) {
|
|
93
|
+
super(message);
|
|
94
|
+
this.name = "PluginCatalogDigestError";
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export class PluginCatalogSignatureError extends PluginCatalogError {
|
|
99
|
+
constructor(message: string) {
|
|
100
|
+
super(message);
|
|
101
|
+
this.name = "PluginCatalogSignatureError";
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function sha256Hex(bytes: Buffer | Uint8Array | string): string {
|
|
106
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function pluginVersionSha256(payload: PluginVersion): string {
|
|
110
|
+
return sha256Hex(Buffer.from(stableStringify(payload), "utf8"));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function canonicalPluginCatalogBytes(catalog: PluginCatalog): Buffer {
|
|
114
|
+
return Buffer.from(stableStringify(catalog), "utf8");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function pluginCatalogSha256(catalog: PluginCatalog): string {
|
|
118
|
+
return sha256Hex(canonicalPluginCatalogBytes(catalog));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Build a catalog document from validated manifests. Rejects duplicate
|
|
123
|
+
* plugin keys; computes the per-version payload digest.
|
|
124
|
+
*/
|
|
125
|
+
export function buildPluginCatalog(options: {
|
|
126
|
+
manifests: readonly PluginManifest[];
|
|
127
|
+
generatedAt?: Date | string;
|
|
128
|
+
source?: PluginCatalogSourceProvenance;
|
|
129
|
+
}): PluginCatalog {
|
|
130
|
+
const seenKeys = new Set<string>();
|
|
131
|
+
const plugins: PluginCatalogEntry[] = [];
|
|
132
|
+
for (const candidate of options.manifests) {
|
|
133
|
+
const manifest = validatePluginManifest(candidate);
|
|
134
|
+
if (seenKeys.has(manifest.pluginKey)) {
|
|
135
|
+
throw new PluginCatalogError(
|
|
136
|
+
`Duplicate plugin key in catalog: ${manifest.pluginKey}`,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
seenKeys.add(manifest.pluginKey);
|
|
140
|
+
plugins.push({
|
|
141
|
+
pluginKey: manifest.pluginKey,
|
|
142
|
+
displayName: manifest.displayName,
|
|
143
|
+
description: manifest.description,
|
|
144
|
+
premium: manifest.premium,
|
|
145
|
+
versions: manifest.versions.map((payload) => ({
|
|
146
|
+
version: payload.version,
|
|
147
|
+
payloadSha256: pluginVersionSha256(payload),
|
|
148
|
+
payload,
|
|
149
|
+
})),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
plugins.sort(compareCatalogPluginNames);
|
|
153
|
+
return {
|
|
154
|
+
schemaVersion: PLUGIN_CATALOG_SCHEMA_VERSION,
|
|
155
|
+
generatedAt: toIso(options.generatedAt ?? new Date()),
|
|
156
|
+
source: options.source
|
|
157
|
+
? validateSourceProvenance(options.source)
|
|
158
|
+
: undefined,
|
|
159
|
+
plugins,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function compareCatalogPluginNames(
|
|
164
|
+
left: PluginCatalogEntry,
|
|
165
|
+
right: PluginCatalogEntry,
|
|
166
|
+
) {
|
|
167
|
+
const byName = left.displayName.localeCompare(right.displayName, undefined, {
|
|
168
|
+
sensitivity: "base",
|
|
169
|
+
});
|
|
170
|
+
return byName === 0 ? left.pluginKey.localeCompare(right.pluginKey) : byName;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function signPluginCatalog(options: {
|
|
174
|
+
catalog: PluginCatalog;
|
|
175
|
+
privateKeyPem: string;
|
|
176
|
+
signedAt?: Date | string;
|
|
177
|
+
}): SignedPluginCatalogDocument {
|
|
178
|
+
const catalog = validatePluginCatalog(options.catalog);
|
|
179
|
+
const privateKey = createPrivateKey(options.privateKeyPem);
|
|
180
|
+
const signature = signBytes(
|
|
181
|
+
null,
|
|
182
|
+
canonicalPluginCatalogBytes(catalog),
|
|
183
|
+
privateKey,
|
|
184
|
+
).toString("base64");
|
|
185
|
+
return {
|
|
186
|
+
catalog,
|
|
187
|
+
signature: {
|
|
188
|
+
algorithm: PLUGIN_CATALOG_SIGNATURE_ALGORITHM,
|
|
189
|
+
catalogSha256: pluginCatalogSha256(catalog),
|
|
190
|
+
signedAt: toIso(options.signedAt ?? new Date()),
|
|
191
|
+
signature,
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Verify a signed catalog document against the trusted public key.
|
|
198
|
+
* Returns the verified catalog; throws a typed PluginCatalogError subclass
|
|
199
|
+
* on any failure (fail closed — never returns a partially trusted catalog).
|
|
200
|
+
*/
|
|
201
|
+
export function verifyPluginCatalog(options: {
|
|
202
|
+
document: unknown;
|
|
203
|
+
trustedPublicKeyPem: string;
|
|
204
|
+
}): PluginCatalog {
|
|
205
|
+
const document = options.document as Partial<SignedPluginCatalogDocument>;
|
|
206
|
+
if (!document || typeof document !== "object" || Array.isArray(document)) {
|
|
207
|
+
throw new PluginCatalogError("Plugin catalog document must be an object");
|
|
208
|
+
}
|
|
209
|
+
const catalog = validatePluginCatalog(document.catalog);
|
|
210
|
+
const signature = validateSignatureShape(document.signature);
|
|
211
|
+
|
|
212
|
+
const catalogSha256 = pluginCatalogSha256(catalog);
|
|
213
|
+
if (signature.catalogSha256 !== catalogSha256) {
|
|
214
|
+
throw new PluginCatalogDigestError(
|
|
215
|
+
`Plugin catalog digest mismatch: expected ${signature.catalogSha256}, got ${catalogSha256}`,
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const valid = verifyBytes(
|
|
220
|
+
null,
|
|
221
|
+
canonicalPluginCatalogBytes(catalog),
|
|
222
|
+
createPublicKey(options.trustedPublicKeyPem),
|
|
223
|
+
Buffer.from(signature.signature, "base64"),
|
|
224
|
+
);
|
|
225
|
+
if (!valid) {
|
|
226
|
+
throw new PluginCatalogSignatureError(
|
|
227
|
+
"Plugin catalog signature is invalid",
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Per-version payload digests are the install-time pins; verify each so a
|
|
232
|
+
// mismatch surfaces here rather than at install.
|
|
233
|
+
for (const plugin of catalog.plugins) {
|
|
234
|
+
for (const entry of plugin.versions) {
|
|
235
|
+
const actual = pluginVersionSha256(entry.payload);
|
|
236
|
+
if (actual !== entry.payloadSha256) {
|
|
237
|
+
throw new PluginCatalogDigestError(
|
|
238
|
+
`Plugin ${plugin.pluginKey}@${entry.version} payload digest mismatch: expected ${entry.payloadSha256}, got ${actual}`,
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return catalog;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Structural validation of the catalog half of the document. Re-runs full
|
|
249
|
+
* manifest validation on the embedded payloads so a verified catalog is
|
|
250
|
+
* also a structurally valid one.
|
|
251
|
+
*/
|
|
252
|
+
export function validatePluginCatalog(value: unknown): PluginCatalog {
|
|
253
|
+
const catalog = value as Partial<PluginCatalog>;
|
|
254
|
+
if (!catalog || typeof catalog !== "object" || Array.isArray(catalog)) {
|
|
255
|
+
throw new PluginCatalogError("Plugin catalog must be an object");
|
|
256
|
+
}
|
|
257
|
+
if (catalog.schemaVersion !== PLUGIN_CATALOG_SCHEMA_VERSION) {
|
|
258
|
+
throw new PluginCatalogSchemaError(
|
|
259
|
+
`Plugin catalog schemaVersion must be ${PLUGIN_CATALOG_SCHEMA_VERSION} (got ${String(catalog.schemaVersion)})`,
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
requireIsoDate(catalog.generatedAt, "catalog.generatedAt");
|
|
263
|
+
if (catalog.source !== undefined) {
|
|
264
|
+
validateSourceProvenance(catalog.source);
|
|
265
|
+
}
|
|
266
|
+
if (!Array.isArray(catalog.plugins)) {
|
|
267
|
+
throw new PluginCatalogError("catalog.plugins must be an array");
|
|
268
|
+
}
|
|
269
|
+
const seenKeys = new Set<string>();
|
|
270
|
+
for (const plugin of catalog.plugins as Partial<PluginCatalogEntry>[]) {
|
|
271
|
+
if (!plugin || typeof plugin !== "object" || Array.isArray(plugin)) {
|
|
272
|
+
throw new PluginCatalogError("catalog plugin entries must be objects");
|
|
273
|
+
}
|
|
274
|
+
if (!Array.isArray(plugin.versions) || plugin.versions.length === 0) {
|
|
275
|
+
throw new PluginCatalogError(
|
|
276
|
+
`catalog plugin ${String(plugin.pluginKey)}: versions must be a non-empty array`,
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
for (const entry of plugin.versions as Partial<PluginCatalogVersionEntry>[]) {
|
|
280
|
+
if (!entry || typeof entry !== "object") {
|
|
281
|
+
throw new PluginCatalogError(
|
|
282
|
+
`catalog plugin ${String(plugin.pluginKey)}: version entries must be objects`,
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
requireSha256(
|
|
286
|
+
entry.payloadSha256,
|
|
287
|
+
`catalog plugin ${String(plugin.pluginKey)}@${String(entry.version)}.payloadSha256`,
|
|
288
|
+
);
|
|
289
|
+
if (
|
|
290
|
+
entry.version !== (entry.payload as Partial<PluginVersion>)?.version
|
|
291
|
+
) {
|
|
292
|
+
throw new PluginCatalogError(
|
|
293
|
+
`catalog plugin ${String(plugin.pluginKey)}: version entry "${String(entry.version)}" does not match its payload version`,
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
// Reconstruct the manifest shape and reuse contract validation.
|
|
298
|
+
validatePluginManifest({
|
|
299
|
+
pluginKey: plugin.pluginKey,
|
|
300
|
+
displayName: plugin.displayName,
|
|
301
|
+
description: plugin.description,
|
|
302
|
+
premium: plugin.premium,
|
|
303
|
+
versions: plugin.versions.map((entry) => entry.payload),
|
|
304
|
+
});
|
|
305
|
+
if (seenKeys.has(plugin.pluginKey as string)) {
|
|
306
|
+
throw new PluginCatalogError(
|
|
307
|
+
`Duplicate plugin key in catalog: ${plugin.pluginKey}`,
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
seenKeys.add(plugin.pluginKey as string);
|
|
311
|
+
}
|
|
312
|
+
return catalog as PluginCatalog;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function validateSourceProvenance(
|
|
316
|
+
value: unknown,
|
|
317
|
+
): PluginCatalogSourceProvenance {
|
|
318
|
+
const source = value as Partial<PluginCatalogSourceProvenance>;
|
|
319
|
+
if (!source || typeof source !== "object" || Array.isArray(source)) {
|
|
320
|
+
throw new PluginCatalogError("catalog.source must be an object");
|
|
321
|
+
}
|
|
322
|
+
requireRepository(source.repository, "catalog.source.repository");
|
|
323
|
+
requireRef(source.ref, "catalog.source.ref");
|
|
324
|
+
requireCommitSha(source.commitSha, "catalog.source.commitSha");
|
|
325
|
+
return {
|
|
326
|
+
repository: source.repository,
|
|
327
|
+
ref: source.ref,
|
|
328
|
+
commitSha: source.commitSha,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function validateSignatureShape(value: unknown): PluginCatalogSignature {
|
|
333
|
+
const signature = value as Partial<PluginCatalogSignature>;
|
|
334
|
+
if (!signature || typeof signature !== "object" || Array.isArray(signature)) {
|
|
335
|
+
throw new PluginCatalogSignatureError(
|
|
336
|
+
"Plugin catalog signature must be an object",
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
if (signature.algorithm !== PLUGIN_CATALOG_SIGNATURE_ALGORITHM) {
|
|
340
|
+
throw new PluginCatalogSignatureError(
|
|
341
|
+
`Plugin catalog signature algorithm must be ${PLUGIN_CATALOG_SIGNATURE_ALGORITHM}`,
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
requireSha256(signature.catalogSha256, "signature.catalogSha256");
|
|
345
|
+
requireIsoDate(signature.signedAt, "signature.signedAt");
|
|
346
|
+
if (
|
|
347
|
+
typeof signature.signature !== "string" ||
|
|
348
|
+
signature.signature.length === 0
|
|
349
|
+
) {
|
|
350
|
+
throw new PluginCatalogSignatureError("signature.signature is required");
|
|
351
|
+
}
|
|
352
|
+
return signature as PluginCatalogSignature;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function requireSha256(value: unknown, path: string): asserts value is string {
|
|
356
|
+
if (typeof value !== "string" || !/^[a-f0-9]{64}$/.test(value)) {
|
|
357
|
+
throw new PluginCatalogError(
|
|
358
|
+
`${path} must be a lowercase SHA-256 hex digest`,
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function requireIsoDate(value: unknown, path: string): asserts value is string {
|
|
364
|
+
if (
|
|
365
|
+
typeof value !== "string" ||
|
|
366
|
+
value.length === 0 ||
|
|
367
|
+
Number.isNaN(Date.parse(value))
|
|
368
|
+
) {
|
|
369
|
+
throw new PluginCatalogError(`${path} must be an ISO timestamp`);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function requireRepository(
|
|
374
|
+
value: unknown,
|
|
375
|
+
path: string,
|
|
376
|
+
): asserts value is string {
|
|
377
|
+
if (
|
|
378
|
+
typeof value !== "string" ||
|
|
379
|
+
!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(value)
|
|
380
|
+
) {
|
|
381
|
+
throw new PluginCatalogError(
|
|
382
|
+
`${path} must be a GitHub repository in owner/name form`,
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function requireRef(value: unknown, path: string): asserts value is string {
|
|
388
|
+
if (
|
|
389
|
+
typeof value !== "string" ||
|
|
390
|
+
value.length === 0 ||
|
|
391
|
+
value.length > 255 ||
|
|
392
|
+
/\s/.test(value)
|
|
393
|
+
) {
|
|
394
|
+
throw new PluginCatalogError(
|
|
395
|
+
`${path} must be a non-empty Git ref without whitespace`,
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function requireCommitSha(
|
|
401
|
+
value: unknown,
|
|
402
|
+
path: string,
|
|
403
|
+
): asserts value is string {
|
|
404
|
+
if (typeof value !== "string" || !/^[a-f0-9]{40}$/.test(value)) {
|
|
405
|
+
throw new PluginCatalogError(
|
|
406
|
+
`${path} must be a lowercase 40-character Git commit SHA`,
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function toIso(value: Date | string): string {
|
|
412
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
413
|
+
if (Number.isNaN(date.valueOf())) {
|
|
414
|
+
throw new PluginCatalogError(`Invalid date: ${String(value)}`);
|
|
415
|
+
}
|
|
416
|
+
return date.toISOString();
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Same canonical form as release-manifest: sorted keys, no whitespace —
|
|
420
|
+
// signature stability depends on this never changing.
|
|
421
|
+
function stableStringify(value: unknown): string {
|
|
422
|
+
if (value === null || typeof value !== "object") {
|
|
423
|
+
return JSON.stringify(value);
|
|
424
|
+
}
|
|
425
|
+
if (Array.isArray(value)) {
|
|
426
|
+
return `[${value.map((entry) => stableStringify(entry)).join(",")}]`;
|
|
427
|
+
}
|
|
428
|
+
const entries = Object.entries(value as Record<string, unknown>)
|
|
429
|
+
.filter(([, entry]) => entry !== undefined)
|
|
430
|
+
.sort(([a], [b]) => a.localeCompare(b));
|
|
431
|
+
return `{${entries
|
|
432
|
+
.map(([key, entry]) => `${JSON.stringify(key)}:${stableStringify(entry)}`)
|
|
433
|
+
.join(",")}}`;
|
|
434
|
+
}
|