neotoma 0.3.0 → 0.3.2
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 +9 -9
- package/dist/actions.js +2 -2
- package/dist/actions.js.map +1 -1
- package/dist/cli/bootstrap.js +0 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +158 -5
- package/dist/cli/index.js.map +1 -1
- package/dist/crypto/auth.js +1 -1
- package/dist/crypto/auth.js.map +1 -1
- package/dist/mcp_ws_bridge.js +1 -1
- package/dist/mcp_ws_bridge.js.map +1 -1
- package/dist/server.js +2 -2
- package/dist/server.js.map +1 -1
- package/dist/services/field_converters.js +3 -3
- package/dist/services/field_converters.js.map +1 -1
- package/dist/services/field_validation.js +1 -1
- package/dist/services/field_validation.js.map +1 -1
- package/dist/services/mcp_auth.js +1 -1
- package/dist/services/mcp_auth.js.map +1 -1
- package/dist/services/mcp_oauth.js +1 -1
- package/dist/services/mcp_oauth.js.map +1 -1
- package/dist/services/parquet_reader.js +1 -1
- package/dist/services/parquet_reader.js.map +1 -1
- package/dist/services/public_key_registry.js +2 -2
- package/dist/services/public_key_registry.js.map +1 -1
- package/package.json +27 -7
- package/.claude/CLAUDE.md +0 -81
- package/.claude/rules/agent_constraints.md +0 -201
- package/.claude/rules/agent_instructions_sync_rules.md +0 -100
- package/.claude/rules/agent_test_execution.md +0 -358
- package/.claude/rules/autonomous_execution.md +0 -119
- package/.claude/rules/bug_fix_detection.md +0 -145
- package/.claude/rules/bug_learning.md +0 -154
- package/.claude/rules/checkpoint_management.md +0 -194
- package/.claude/rules/configuration_management.md +0 -159
- package/.claude/rules/content_style_enforcement.md +0 -141
- package/.claude/rules/dependency_installation.md +0 -148
- package/.claude/rules/document_loading_order.md +0 -197
- package/.claude/rules/documentation_rules.md +0 -169
- package/.claude/rules/downstream_doc_updates.md +0 -269
- package/.claude/rules/env_check.md +0 -55
- package/.claude/rules/environment_variables_1password.md +0 -315
- package/.claude/rules/environment_variables_env_file.md +0 -124
- package/.claude/rules/feature_unit_detection.md +0 -132
- package/.claude/rules/file_naming.md +0 -34
- package/.claude/rules/git_remote_sync.md +0 -242
- package/.claude/rules/instruction_documentation.md +0 -287
- package/.claude/rules/native_browser_debugging.md +0 -68
- package/.claude/rules/plan_execution_testing.md +0 -101
- package/.claude/rules/plan_format.md +0 -38
- package/.claude/rules/post_build_testing.md +0 -162
- package/.claude/rules/prefer_cli_tools.md +0 -233
- package/.claude/rules/readme_maintenance.md +0 -156
- package/.claude/rules/release_detection.md +0 -95
- package/.claude/rules/release_status_readme_update.md +0 -62
- package/.claude/rules/risk_management.md +0 -170
- package/.claude/rules/security.md +0 -168
- package/.claude/rules/social_media_conventions.md +0 -42
- package/.claude/rules/test_first_workflow.md +0 -344
- package/.claude/rules/worktree_env.md +0 -56
- package/.claude/settings.json +0 -5
- package/.claude/skills/commit.md +0 -762
- package/.claude/skills/create_feature_unit.md +0 -176
- package/.claude/skills/create_release.md +0 -428
- package/.claude/skills/fix_feature_bug.md +0 -93
- package/.claude/skills/pull.md +0 -314
- package/.claude/skills/setup_symlinks.md +0 -228
- package/.codex/config.toml +0 -92
- package/.cursor/commands/analyze.md +0 -574
- package/.cursor/commands/commit.md +0 -768
- package/.cursor/commands/create_feature_unit.md +0 -182
- package/.cursor/commands/create_prototype.md +0 -95
- package/.cursor/commands/create_release.md +0 -434
- package/.cursor/commands/create_rule.md +0 -296
- package/.cursor/commands/debug.md +0 -985
- package/.cursor/commands/final_review.md +0 -97
- package/.cursor/commands/fix_feature_bug.md +0 -99
- package/.cursor/commands/manage_error_debugging.md +0 -186
- package/.cursor/commands/migrate_plans.md +0 -499
- package/.cursor/commands/publish.md +0 -559
- package/.cursor/commands/pull.md +0 -320
- package/.cursor/commands/push.md +0 -259
- package/.cursor/commands/report.md +0 -1506
- package/.cursor/commands/report_error.md +0 -1505
- package/.cursor/commands/run_feature_workflow.md +0 -83
- package/.cursor/commands/setup_commands.md +0 -87
- package/.cursor/commands/setup_symlinks.md +0 -234
- package/.cursor/commands/sync_env_from_1password.md +0 -321
- package/.cursor/commands.json +0 -100
- package/.cursor/environment.json +0 -5
- package/.cursor/mcp.json.template +0 -16
- package/.cursor/plans/agent_skills_analysis_aac0f1b6.plan.md +0 -949
- package/.cursor/plans/agent_trust_framework_eb38ca7d.plan.md +0 -634
- package/.cursor/plans/all-storage-unstructured_provenance_df0b7a8f.plan.md +0 -112
- package/.cursor/plans/automated_error_debugging_cloud_agents.plan.md +0 -526
- package/.cursor/plans/automated_error_debugging_cursor_cli.plan.md +0 -599
- package/.cursor/plans/automated_error_debugging_localhost_clarified.plan.md +0 -510
- package/.cursor/plans/bug_report_and_contribution_client_guidance.plan.md +0 -143
- package/.cursor/plans/convert_1password_sync_to_mcp_343ca61c.plan.md +0 -282
- package/.cursor/plans/debug_pending_errors_command.plan.md +0 -531
- package/.cursor/plans/error_reporting_command_8868f27b.plan.md +0 -255
- package/.cursor/plans/mcp-cli-action-item-01-openapi-core.plan.md +0 -30
- package/.cursor/plans/mcp-cli-action-item-02-openapi-mcp-cli-mapping.plan.md +0 -18
- package/.cursor/plans/mcp-cli-action-item-03-cli-equivalent-logging.plan.md +0 -18
- package/.cursor/plans/mcp-cli-action-item-06-error-taxonomy.plan.md +0 -18
- package/.cursor/plans/mcp-cli-action-item-07-fixture-replay-graphs.plan.md +0 -18
- package/.cursor/plans/mcp-cli-action-item-08-cli-reference-executor.plan.md +0 -18
- package/.cursor/plans/mcp-cli-action-item-09-mcp-tools-atomic.plan.md +0 -18
- package/.cursor/plans/mcp-cli-action-item-10-canonical-walkthrough.plan.md +0 -18
- package/.cursor/plans/mcp-cli-action-item-11-fixtures-expected-outputs.plan.md +0 -18
- package/.cursor/plans/mcp-cli-action-item-12-failure-gallery.plan.md +0 -18
- package/.cursor/plans/mcp_source-path_documentation_5ce48609.plan.md +0 -83
- package/.cursor/plans/me_and_get_authenticated_user_response_updates.plan.md +0 -125
- package/.cursor/plans/multi-llm_provider_support_3fdeb361.plan.md +0 -120
- package/.cursor/plans/plan_migration_command_integrated.plan.md +0 -393
- package/.cursor/plans/schema_icon_enhancements_plan.md +0 -533
- package/.cursor/plans/site-react-tailwind-parity.plan.md +0 -82
- package/.cursor/plans/sources-first-ingestion-v8.plan.md +0 -0
- package/.cursor/plans/sources-first_ingestion_v10_baef2ba2.plan.md +0 -363
- package/.cursor/plans/test_coverage_gap_analysis_9cb70022.plan.md +0 -39
- package/.cursor/port_to_foundation_analysis.md +0 -217
- package/.cursor/rules/agent_constraints.mdc +0 -210
- package/.cursor/rules/autonomous_execution.mdc +0 -128
- package/.cursor/rules/configuration_management.mdc +0 -168
- package/.cursor/rules/cursor_rules_editing.mdc +0 -99
- package/.cursor/rules/cursor_rules_sync.mdc +0 -74
- package/.cursor/rules/cursor_rules_sync_requirement.mdc +0 -156
- package/.cursor/rules/developer_agent_instructions_sync_rules.mdc +0 -109
- package/.cursor/rules/document_loading_order.mdc +0 -206
- package/.cursor/rules/instruction_documentation.mdc +0 -296
- package/.cursor/rules/risk_management.mdc +0 -179
- package/.cursor/rules/security.mdc +0 -177
- package/.cursor/skipped_records/companies_skipped.json +0 -27
- package/.cursor/skipped_records/contacts_skipped.json +0 -23
- package/.cursor/skipped_records/goals_skipped.json +0 -7
- package/.cursor/skipped_records/people_skipped.json +0 -14
- package/.cursor/skipped_records/projects_skipped.json +0 -39
- package/.cursor/worktrees.json +0 -12
- package/.dockerignore +0 -9
- package/.env.example +0 -84
- package/.eslintrc.json +0 -11
- package/.github/workflows/deploy-pages-site.yml +0 -55
- package/.gitmodules +0 -12
- package/.husky/pre-commit +0 -42
- package/.mcp.json +0 -13
- package/.nvmrc +0 -1
- package/.prettierrc.json +0 -7
- package/.vscode/extensions.json +0 -5
- package/.vscode/settings.json +0 -47
- package/CONTRIBUTING.md +0 -23
- package/Dockerfile +0 -28
- package/SECURITY.md +0 -41
- package/components.json +0 -21
- package/cursor_rules_manifest.json +0 -28
- package/dist/__fixtures__/helpers.d.ts +0 -91
- package/dist/__fixtures__/helpers.d.ts.map +0 -1
- package/dist/__fixtures__/helpers.js +0 -384
- package/dist/__fixtures__/helpers.js.map +0 -1
- package/dist/__fixtures__/types.d.ts +0 -285
- package/dist/__fixtures__/types.d.ts.map +0 -1
- package/dist/__fixtures__/types.js +0 -7
- package/dist/__fixtures__/types.js.map +0 -1
- package/dist/__fixtures__/validation.d.ts +0 -45
- package/dist/__fixtures__/validation.d.ts.map +0 -1
- package/dist/__fixtures__/validation.js +0 -311
- package/dist/__fixtures__/validation.js.map +0 -1
- package/dist/config/plaid.d.ts +0 -21
- package/dist/config/plaid.d.ts.map +0 -1
- package/dist/config/plaid.js +0 -38
- package/dist/config/plaid.js.map +0 -1
- package/dist/events/event_emitter.d.ts +0 -24
- package/dist/events/event_emitter.d.ts.map +0 -1
- package/dist/events/event_emitter.js +0 -85
- package/dist/events/event_emitter.js.map +0 -1
- package/dist/events/event_log.d.ts +0 -23
- package/dist/events/event_log.d.ts.map +0 -1
- package/dist/events/event_log.js +0 -77
- package/dist/events/event_log.js.map +0 -1
- package/dist/events/event_schema.d.ts +0 -58
- package/dist/events/event_schema.d.ts.map +0 -1
- package/dist/events/event_schema.js +0 -43
- package/dist/events/event_schema.js.map +0 -1
- package/dist/events/event_validator.d.ts +0 -23
- package/dist/events/event_validator.d.ts.map +0 -1
- package/dist/events/event_validator.js +0 -90
- package/dist/events/event_validator.js.map +0 -1
- package/dist/events/index.d.ts +0 -9
- package/dist/events/index.d.ts.map +0 -1
- package/dist/events/index.js +0 -9
- package/dist/events/index.js.map +0 -1
- package/dist/events/replay.d.ts +0 -27
- package/dist/events/replay.d.ts.map +0 -1
- package/dist/events/replay.js +0 -62
- package/dist/events/replay.js.map +0 -1
- package/dist/hashing/hash_chain.d.ts +0 -31
- package/dist/hashing/hash_chain.d.ts.map +0 -1
- package/dist/hashing/hash_chain.js +0 -52
- package/dist/hashing/hash_chain.js.map +0 -1
- package/dist/integrations/plaid/client.d.ts +0 -53
- package/dist/integrations/plaid/client.d.ts.map +0 -1
- package/dist/integrations/plaid/client.js +0 -224
- package/dist/integrations/plaid/client.js.map +0 -1
- package/dist/integrations/plaid/normalizers.d.ts +0 -26
- package/dist/integrations/plaid/normalizers.d.ts.map +0 -1
- package/dist/integrations/plaid/normalizers.js +0 -166
- package/dist/integrations/plaid/normalizers.js.map +0 -1
- package/dist/integrations/providers/asana.d.ts +0 -9
- package/dist/integrations/providers/asana.d.ts.map +0 -1
- package/dist/integrations/providers/asana.js +0 -62
- package/dist/integrations/providers/asana.js.map +0 -1
- package/dist/integrations/providers/base.d.ts +0 -17
- package/dist/integrations/providers/base.d.ts.map +0 -1
- package/dist/integrations/providers/base.js +0 -64
- package/dist/integrations/providers/base.js.map +0 -1
- package/dist/integrations/providers/facebook.d.ts +0 -9
- package/dist/integrations/providers/facebook.d.ts.map +0 -1
- package/dist/integrations/providers/facebook.js +0 -58
- package/dist/integrations/providers/facebook.js.map +0 -1
- package/dist/integrations/providers/gmail.d.ts +0 -10
- package/dist/integrations/providers/gmail.d.ts.map +0 -1
- package/dist/integrations/providers/gmail.js +0 -67
- package/dist/integrations/providers/gmail.js.map +0 -1
- package/dist/integrations/providers/index.d.ts +0 -6
- package/dist/integrations/providers/index.d.ts.map +0 -1
- package/dist/integrations/providers/index.js +0 -25
- package/dist/integrations/providers/index.js.map +0 -1
- package/dist/integrations/providers/instagram.d.ts +0 -9
- package/dist/integrations/providers/instagram.d.ts.map +0 -1
- package/dist/integrations/providers/instagram.js +0 -47
- package/dist/integrations/providers/instagram.js.map +0 -1
- package/dist/integrations/providers/metadata.d.ts +0 -25
- package/dist/integrations/providers/metadata.d.ts.map +0 -1
- package/dist/integrations/providers/metadata.js +0 -407
- package/dist/integrations/providers/metadata.js.map +0 -1
- package/dist/integrations/providers/types.d.ts +0 -39
- package/dist/integrations/providers/types.d.ts.map +0 -1
- package/dist/integrations/providers/types.js +0 -2
- package/dist/integrations/providers/types.js.map +0 -1
- package/dist/integrations/providers/x.d.ts +0 -9
- package/dist/integrations/providers/x.d.ts.map +0 -1
- package/dist/integrations/providers/x.js +0 -78
- package/dist/integrations/providers/x.js.map +0 -1
- package/dist/reducers/record_reducer.d.ts +0 -25
- package/dist/reducers/record_reducer.d.ts.map +0 -1
- package/dist/reducers/record_reducer.js +0 -93
- package/dist/reducers/record_reducer.js.map +0 -1
- package/dist/reducers/reducer_registry.d.ts +0 -47
- package/dist/reducers/reducer_registry.d.ts.map +0 -1
- package/dist/reducers/reducer_registry.js +0 -82
- package/dist/reducers/reducer_registry.js.map +0 -1
- package/dist/repositories/db/event_repo.d.ts +0 -14
- package/dist/repositories/db/event_repo.d.ts.map +0 -1
- package/dist/repositories/db/event_repo.js +0 -67
- package/dist/repositories/db/event_repo.js.map +0 -1
- package/dist/repositories/db/state_repo.d.ts +0 -14
- package/dist/repositories/db/state_repo.d.ts.map +0 -1
- package/dist/repositories/db/state_repo.js +0 -61
- package/dist/repositories/db/state_repo.js.map +0 -1
- package/dist/repositories/file/event_repo.d.ts +0 -18
- package/dist/repositories/file/event_repo.d.ts.map +0 -1
- package/dist/repositories/file/event_repo.js +0 -148
- package/dist/repositories/file/event_repo.js.map +0 -1
- package/dist/repositories/file/state_repo.d.ts +0 -21
- package/dist/repositories/file/state_repo.d.ts.map +0 -1
- package/dist/repositories/file/state_repo.js +0 -89
- package/dist/repositories/file/state_repo.js.map +0 -1
- package/dist/repositories/sqlite/supabase_adapter.d.ts +0 -135
- package/dist/repositories/sqlite/supabase_adapter.d.ts.map +0 -1
- package/dist/repositories/sqlite/supabase_adapter.js +0 -810
- package/dist/repositories/sqlite/supabase_adapter.js.map +0 -1
- package/dist/services/connectors.d.ts +0 -89
- package/dist/services/connectors.d.ts.map +0 -1
- package/dist/services/connectors.js +0 -248
- package/dist/services/connectors.js.map +0 -1
- package/dist/services/event_generation.d.ts +0 -37
- package/dist/services/event_generation.d.ts.map +0 -1
- package/dist/services/event_generation.js +0 -236
- package/dist/services/event_generation.js.map +0 -1
- package/dist/services/extraction/rules.d.ts +0 -26
- package/dist/services/extraction/rules.d.ts.map +0 -1
- package/dist/services/extraction/rules.js +0 -691
- package/dist/services/extraction/rules.js.map +0 -1
- package/dist/services/file_analysis.d.ts +0 -38
- package/dist/services/file_analysis.d.ts.map +0 -1
- package/dist/services/file_analysis.js +0 -325
- package/dist/services/file_analysis.js.map +0 -1
- package/dist/services/graph_builder.d.ts +0 -51
- package/dist/services/graph_builder.d.ts.map +0 -1
- package/dist/services/graph_builder.js +0 -279
- package/dist/services/graph_builder.js.map +0 -1
- package/dist/services/importers.d.ts +0 -28
- package/dist/services/importers.d.ts.map +0 -1
- package/dist/services/importers.js +0 -174
- package/dist/services/importers.js.map +0 -1
- package/dist/services/observation_ingestion.d.ts +0 -26
- package/dist/services/observation_ingestion.d.ts.map +0 -1
- package/dist/services/observation_ingestion.js +0 -364
- package/dist/services/observation_ingestion.js.map +0 -1
- package/dist/services/payload_compiler.d.ts +0 -36
- package/dist/services/payload_compiler.d.ts.map +0 -1
- package/dist/services/payload_compiler.js +0 -121
- package/dist/services/payload_compiler.js.map +0 -1
- package/dist/services/payload_identity.d.ts +0 -35
- package/dist/services/payload_identity.d.ts.map +0 -1
- package/dist/services/payload_identity.js +0 -101
- package/dist/services/payload_identity.js.map +0 -1
- package/dist/services/payload_schema.d.ts +0 -104
- package/dist/services/payload_schema.d.ts.map +0 -1
- package/dist/services/payload_schema.js +0 -46
- package/dist/services/payload_schema.js.map +0 -1
- package/dist/services/plaid_sync.d.ts +0 -77
- package/dist/services/plaid_sync.d.ts.map +0 -1
- package/dist/services/plaid_sync.js +0 -273
- package/dist/services/plaid_sync.js.map +0 -1
- package/dist/services/record_comparison.d.ts +0 -29
- package/dist/services/record_comparison.d.ts.map +0 -1
- package/dist/services/record_comparison.js +0 -84
- package/dist/services/record_comparison.js.map +0 -1
- package/dist/services/records.d.ts +0 -31
- package/dist/services/records.d.ts.map +0 -1
- package/dist/services/records.js +0 -184
- package/dist/services/records.js.map +0 -1
- package/dist/services/search.d.ts +0 -15
- package/dist/services/search.d.ts.map +0 -1
- package/dist/services/search.js +0 -81
- package/dist/services/search.js.map +0 -1
- package/dist/services/summary.d.ts +0 -5
- package/dist/services/summary.d.ts.map +0 -1
- package/dist/services/summary.js +0 -118
- package/dist/services/summary.js.map +0 -1
- package/docs/NEOTOMA_MANIFEST.md +0 -152
- package/docs/api/rest_api.md +0 -466
- package/docs/architecture/agentic_portfolio_overview.md +0 -99
- package/docs/architecture/agentic_wallet_overview.md +0 -93
- package/docs/architecture/architectural_decisions.md +0 -462
- package/docs/architecture/architecture.md +0 -842
- package/docs/architecture/blockchain_readiness_assessment.md +0 -248
- package/docs/architecture/consistency.md +0 -539
- package/docs/architecture/conversational_ux_architecture.md +0 -240
- package/docs/architecture/determinism.md +0 -762
- package/docs/architecture/idempotence_pattern.md +0 -139
- package/docs/architecture/mcp_actions_assessment.md +0 -182
- package/docs/architecture/ner_vs_schema_first_extraction.md +0 -280
- package/docs/architecture/schema_expansion.md +0 -260
- package/docs/architecture/schema_handling.md +0 -209
- package/docs/architecture/source_material_model.md +0 -281
- package/docs/assets/neotoma_banner.png +0 -0
- package/docs/conventions/TEST_EXECUTION_IMPROVEMENTS.md +0 -205
- package/docs/conventions/always_on_rules_review.md +0 -66
- package/docs/conventions/code_conventions_implementation.md +0 -111
- package/docs/conventions/code_conventions_plan.md +0 -124
- package/docs/conventions/code_review_summary.md +0 -96
- package/docs/conventions/documentation_standards.md +0 -104
- package/docs/conventions/estimation_methodology.md +0 -146
- package/docs/conventions/file_naming_migration_status.md +0 -94
- package/docs/conventions/readme_generation_framework.md +0 -230
- package/docs/conventions/time_tracking_template.md +0 -60
- package/docs/conventions/writing_style_guide.md +0 -252
- package/docs/developer/agent_cli_configuration.md +0 -140
- package/docs/developer/agent_instructions_sync_rules.mdc +0 -97
- package/docs/developer/canonical_walkthrough.md +0 -137
- package/docs/developer/cli_agent_instructions.md +0 -141
- package/docs/developer/cli_overview.md +0 -207
- package/docs/developer/cli_reference.md +0 -518
- package/docs/developer/developer_preview_launch_checklist.md +0 -128
- package/docs/developer/developer_preview_storage.md +0 -35
- package/docs/developer/developer_release_manual_test_checklist.md +0 -209
- package/docs/developer/development_workflow.md +0 -308
- package/docs/developer/environment/CONNECTOR_SECRET_KEY_SETUP.md +0 -96
- package/docs/developer/environment/DEV_CONNECTOR_SECRET_KEY_EXPLANATION.md +0 -80
- package/docs/developer/environment/ENV_MAPPINGS_STATUS.md +0 -86
- package/docs/developer/environment/ENV_VAR_NAMING_STRATEGY.md +0 -145
- package/docs/developer/environment/MIGRATION_TO_SINGLE_VAR_NAMES.md +0 -89
- package/docs/developer/environment/QUICK_START_1PASSWORD.md +0 -126
- package/docs/developer/environment/README.md +0 -27
- package/docs/developer/environment/SETUP_ENV_MAPPINGS.md +0 -256
- package/docs/developer/environment_distinction_audit.md +0 -43
- package/docs/developer/fix_doc_inconsistencies_plan.md +0 -63
- package/docs/developer/getting_started.md +0 -100
- package/docs/developer/import_interpretation_debug.md +0 -69
- package/docs/developer/launchd_dev_servers.md +0 -51
- package/docs/developer/launchd_watch_build.md +0 -37
- package/docs/developer/mcp/instructions.md +0 -71
- package/docs/developer/mcp/invalid_connection_id_handling.md +0 -181
- package/docs/developer/mcp/tool_descriptions.yaml +0 -41
- package/docs/developer/mcp/unauthenticated.md +0 -30
- package/docs/developer/mcp_chatgpt_setup.md +0 -159
- package/docs/developer/mcp_claude_code_setup.md +0 -519
- package/docs/developer/mcp_https_testing.md +0 -178
- package/docs/developer/mcp_https_testing_status.md +0 -130
- package/docs/developer/mcp_https_tunnel_status.md +0 -147
- package/docs/developer/mcp_oauth_migration_guide.md +0 -196
- package/docs/developer/mcp_overview.md +0 -145
- package/docs/developer/mcp_server_examples_from_cursor_docs.md +0 -485
- package/docs/developer/observation_architecture_documentation_integration.md +0 -368
- package/docs/developer/release_orchestrator.md +0 -197
- package/docs/developer/repository_cleanup_plan.md +0 -87
- package/docs/developer/resolved_blockers.md +0 -49
- package/docs/developer/schema_initialization.md +0 -182
- package/docs/developer/secrets/README.md +0 -15
- package/docs/developer/secrets/secrets_architecture.md +0 -95
- package/docs/developer/secrets/secrets_manager_value.md +0 -53
- package/docs/developer/secrets/secrets_secure_approach.md +0 -103
- package/docs/developer/secrets/secrets_setup.md +0 -116
- package/docs/developer/shared_components_submodule_setup.md +0 -273
- package/docs/developer/spec_compliance_validation_system.md +0 -261
- package/docs/developer/test_coverage_gap_analysis_plan.md +0 -36
- package/docs/developer/tunnels.md +0 -327
- package/docs/developer/vite_troubleshooting.md +0 -111
- package/docs/doc_dependencies.yaml +0 -497
- package/docs/examples/schema_breaking_change_reconciliation.md +0 -515
- package/docs/feature_units/completed/FU-050/FU-050_spec.md +0 -341
- package/docs/feature_units/completed/FU-050/manifest.yaml +0 -330
- package/docs/feature_units/completed/FU-051/FU-051_spec.md +0 -173
- package/docs/feature_units/completed/FU-051/manifest.yaml +0 -229
- package/docs/feature_units/completed/FU-052/FU-052_spec.md +0 -158
- package/docs/feature_units/completed/FU-052/manifest.yaml +0 -188
- package/docs/feature_units/completed/FU-053/FU-053_spec.md +0 -144
- package/docs/feature_units/completed/FU-053/manifest.yaml +0 -145
- package/docs/feature_units/completed/FU-054/FU-054_spec.md +0 -144
- package/docs/feature_units/completed/FU-054/manifest.yaml +0 -139
- package/docs/feature_units/completed/FU-702/billing_spec.md +0 -413
- package/docs/feature_units/completed/FU-702/manifest.yaml +0 -418
- package/docs/feature_units/standards/DISCOVERY_CAGAN_ALIGNMENT.md +0 -214
- package/docs/feature_units/standards/creating_feature_units.md +0 -385
- package/docs/feature_units/standards/discovery_process.md +0 -821
- package/docs/feature_units/standards/error_protocol.md +0 -60
- package/docs/feature_units/standards/execution_instructions.md +0 -87
- package/docs/feature_units/standards/feature_unit_spec.md +0 -325
- package/docs/feature_units/standards/integration_test_execution.md +0 -127
- package/docs/feature_units/standards/manifest_template.yaml +0 -370
- package/docs/feature_units/standards/model_selection.md +0 -207
- package/docs/feature_units/standards/multi_agent_orchestration.md +0 -411
- package/docs/feature_units/standards/release_report_generation.md +0 -448
- package/docs/feature_units/standards/release_report_spec.md +0 -444
- package/docs/feature_units/standards/release_report_template.md +0 -76
- package/docs/feature_units/standards/spec_compliance_validation.md +0 -147
- package/docs/feature_units/standards/worker_agent_template.md +0 -191
- package/docs/foundation/ai_safety.md +0 -10
- package/docs/foundation/composability_analysis.md +0 -308
- package/docs/foundation/core_identity.md +0 -50
- package/docs/foundation/documentation_design_system.md +0 -505
- package/docs/foundation/entity_resolution.md +0 -34
- package/docs/foundation/layered_architecture.md +0 -99
- package/docs/foundation/mmry_comparison.md +0 -176
- package/docs/foundation/philosophy.md +0 -103
- package/docs/foundation/problem_statement.md +0 -41
- package/docs/foundation/product_positioning.md +0 -69
- package/docs/foundation/product_principles.md +0 -18
- package/docs/foundation/quality_requirements.md +0 -68
- package/docs/foundation/timeline_events.md +0 -19
- package/docs/foundation/user_workflows.md +0 -38
- package/docs/implementation/PARQUET_MCP_SERVER_FIXES.md +0 -373
- package/docs/implementation/PARQUET_SUPPORT_IMPLEMENTATION.md +0 -207
- package/docs/infrastructure/deployment.md +0 -372
- package/docs/legal/README.md +0 -51
- package/docs/legal/compliance.md +0 -854
- package/docs/legal/privacy_policy.md +0 -114
- package/docs/legal/privacy_policy_changelog.md +0 -49
- package/docs/legal/terms_of_service.md +0 -185
- package/docs/legal/terms_of_service_changelog.md +0 -62
- package/docs/migration/llm_extraction_removal.md +0 -387
- package/docs/migration/llm_interpretation_docs_update.md +0 -130
- package/docs/observability/logging.md +0 -50
- package/docs/observability/metrics_standard.md +0 -38
- package/docs/observability/tracing.md +0 -41
- package/docs/operations/debugging/README.md +0 -11
- package/docs/operations/debugging/error_debug_instructions.md +0 -55
- package/docs/operations/health_check.md +0 -73
- package/docs/operations/runbook.md +0 -88
- package/docs/prompts/llm_extraction_system_prompt.md +0 -31
- package/docs/proposals/IMPLEMENTATION_SUMMARY.md +0 -198
- package/docs/proposals/MIGRATION_REPORT.md +0 -65
- package/docs/proposals/PROPOSAL_TEMPLATE.md +0 -58
- package/docs/proposals/README.md +0 -57
- package/docs/proposals/agent-trust-framework.md +0 -621
- package/docs/proposals/conversation_turn_identity_reverts_forks.md +0 -144
- package/docs/proposals/document-v023-release.md +0 -898
- package/docs/proposals/iterative_chat_store_mcp_instructions.md +0 -155
- package/docs/proposals/llm_sampling_parameters_interpretation_config.md +0 -196
- package/docs/proposals/mcp-cli-action-items.md +0 -207
- package/docs/proposals/plan-migration-command-integrated-with-commit.md +0 -386
- package/docs/proposals/sources-first-ingestion-v10.md +0 -348
- package/docs/proposals/test-coverage-gap-analysis.md +0 -26
- package/docs/prototypes/PROTOTYPE_QUICKSTART.md +0 -56
- package/docs/prototypes/README.md +0 -74
- package/docs/reference/error_codes.md +0 -223
- package/docs/releases/archived/aspirational/README.md +0 -57
- package/docs/releases/archived/aspirational/v0.4.0/manifest.yaml +0 -94
- package/docs/releases/archived/aspirational/v0.4.0/release_plan.md +0 -51
- package/docs/releases/archived/aspirational/v0.5.0/manifest.yaml +0 -152
- package/docs/releases/archived/aspirational/v0.5.0/release_plan.md +0 -236
- package/docs/releases/archived/aspirational/v0.9.0/README.md +0 -78
- package/docs/releases/archived/aspirational/v0.9.0/manifest.yaml +0 -111
- package/docs/releases/archived/aspirational/v0.9.0/release_plan.md +0 -238
- package/docs/releases/archived/aspirational/v0.9.0/status.md +0 -89
- package/docs/releases/archived/aspirational/v1.0.0/UI_REFACTORING_PLAN.md +0 -239
- package/docs/releases/archived/aspirational/v1.0.0/acceptance_criteria.md +0 -88
- package/docs/releases/archived/aspirational/v1.0.0/approach_comparison.md +0 -558
- package/docs/releases/archived/aspirational/v1.0.0/architectural_impact_chat_ui.md +0 -195
- package/docs/releases/archived/aspirational/v1.0.0/blog_post_update_directives.md +0 -491
- package/docs/releases/archived/aspirational/v1.0.0/business_viability_discovery_plan.md +0 -135
- package/docs/releases/archived/aspirational/v1.0.0/compliance_reports/FU-100_compliance.md +0 -23
- package/docs/releases/archived/aspirational/v1.0.0/compliance_reports/FU-101_compliance.md +0 -23
- package/docs/releases/archived/aspirational/v1.0.0/compliance_reports/FU-102_compliance.md +0 -23
- package/docs/releases/archived/aspirational/v1.0.0/compliance_reports/FU-103_compliance.md +0 -23
- package/docs/releases/archived/aspirational/v1.0.0/compliance_reports/FU-105_compliance.md +0 -23
- package/docs/releases/archived/aspirational/v1.0.0/continuous_discovery_log.md +0 -77
- package/docs/releases/archived/aspirational/v1.0.0/continuous_discovery_plan.md +0 -145
- package/docs/releases/archived/aspirational/v1.0.0/cross_platform_signal_detection.md +0 -181
- package/docs/releases/archived/aspirational/v1.0.0/deployment_strategy.md +0 -115
- package/docs/releases/archived/aspirational/v1.0.0/discovery_checklist.md +0 -195
- package/docs/releases/archived/aspirational/v1.0.0/discovery_filtering_criteria.md +0 -197
- package/docs/releases/archived/aspirational/v1.0.0/discovery_filtering_keywords.md +0 -143
- package/docs/releases/archived/aspirational/v1.0.0/discovery_lead_sourcing_tools.md +0 -438
- package/docs/releases/archived/aspirational/v1.0.0/discovery_plan.md +0 -185
- package/docs/releases/archived/aspirational/v1.0.0/execution_schedule.md +0 -130
- package/docs/releases/archived/aspirational/v1.0.0/feasibility_validation_plan.md +0 -116
- package/docs/releases/archived/aspirational/v1.0.0/handle_registration.md +0 -263
- package/docs/releases/archived/aspirational/v1.0.0/implementation_logs/FU-100_implementation_log.md +0 -51
- package/docs/releases/archived/aspirational/v1.0.0/implementation_logs/FU-101_implementation_log.md +0 -51
- package/docs/releases/archived/aspirational/v1.0.0/implementation_logs/FU-102_implementation_log.md +0 -51
- package/docs/releases/archived/aspirational/v1.0.0/implementation_logs/FU-103_implementation_log.md +0 -51
- package/docs/releases/archived/aspirational/v1.0.0/implementation_logs/FU-105_implementation_log.md +0 -51
- package/docs/releases/archived/aspirational/v1.0.0/manifest.yaml +0 -160
- package/docs/releases/archived/aspirational/v1.0.0/marketing_agent_instructions.md +0 -209
- package/docs/releases/archived/aspirational/v1.0.0/marketing_automation_implementation.md +0 -996
- package/docs/releases/archived/aspirational/v1.0.0/marketing_automation_plan.md +0 -291
- package/docs/releases/archived/aspirational/v1.0.0/marketing_dogfooding_analysis.md +0 -295
- package/docs/releases/archived/aspirational/v1.0.0/marketing_metrics_plan.md +0 -120
- package/docs/releases/archived/aspirational/v1.0.0/marketing_plan.md +0 -280
- package/docs/releases/archived/aspirational/v1.0.0/marketing_segments_plan.md +0 -128
- package/docs/releases/archived/aspirational/v1.0.0/participant_recruitment_log.md +0 -392
- package/docs/releases/archived/aspirational/v1.0.0/post_launch_marketing_plan.md +0 -252
- package/docs/releases/archived/aspirational/v1.0.0/pre_launch_marketing_plan.md +0 -748
- package/docs/releases/archived/aspirational/v1.0.0/pre_mortem.md +0 -122
- package/docs/releases/archived/aspirational/v1.0.0/subscription_detection_strategy.md +0 -160
- package/docs/releases/archived/aspirational/v1.0.0/usability_discovery_plan.md +0 -115
- package/docs/releases/archived/aspirational/v1.0.0/value_discovery_plan.md +0 -279
- package/docs/releases/archived/aspirational/v1.0.0/visibility_timing_analysis.md +0 -402
- package/docs/releases/archived/aspirational/v2.0.0/status.md +0 -120
- package/docs/releases/archived/aspirational/v2.1.0/manifest.yaml +0 -469
- package/docs/releases/archived/aspirational/v2.2.0/manifest.yaml +0 -165
- package/docs/releases/archived/aspirational/v2.2.0/status.md +0 -283
- package/docs/releases/compliance_reports/all_fus_compliance_summary.md +0 -112
- package/docs/releases/in_progress/v0.2.0/agent_status.json +0 -420
- package/docs/releases/in_progress/v0.2.0/status.md +0 -90
- package/docs/releases/v0.1.0/agent_status.json +0 -242
- package/docs/releases/v0.1.0/agent_status.json.example +0 -76
- package/docs/releases/v0.1.0/compliance_reports/FU-000_compliance.md +0 -23
- package/docs/releases/v0.1.0/compliance_reports/FU-002_compliance.md +0 -23
- package/docs/releases/v0.1.0/compliance_reports/FU-050_compliance.md +0 -59
- package/docs/releases/v0.1.0/compliance_reports/FU-051_compliance.md +0 -39
- package/docs/releases/v0.1.0/compliance_reports/FU-052_compliance.md +0 -44
- package/docs/releases/v0.1.0/compliance_reports/FU-053_compliance.md +0 -25
- package/docs/releases/v0.1.0/compliance_reports/FU-054_compliance.md +0 -31
- package/docs/releases/v0.1.0/compliance_reports/FU-055_compliance.md +0 -23
- package/docs/releases/v0.1.0/compliance_reports/FU-056_compliance.md +0 -23
- package/docs/releases/v0.1.0/compliance_reports/FU-057_compliance.md +0 -23
- package/docs/releases/v0.1.0/compliance_reports/FU-058_compliance.md +0 -23
- package/docs/releases/v0.1.0/compliance_reports/FU-059_compliance.md +0 -23
- package/docs/releases/v0.1.0/compliance_reports/FU-061_compliance.md +0 -23
- package/docs/releases/v0.1.0/compliance_reports/FU-100_compliance.md +0 -97
- package/docs/releases/v0.1.0/compliance_reports/FU-104_compliance.md +0 -23
- package/docs/releases/v0.1.0/compliance_reports/FU-200_compliance.md +0 -23
- package/docs/releases/v0.1.0/compliance_reports/FU-201_compliance.md +0 -23
- package/docs/releases/v0.1.0/compliance_reports/FU-202_compliance.md +0 -23
- package/docs/releases/v0.1.0/compliance_reports/FU-203_compliance.md +0 -23
- package/docs/releases/v0.1.0/compliance_reports/FU-204_compliance.md +0 -23
- package/docs/releases/v0.1.0/compliance_reports/FU-205_compliance.md +0 -23
- package/docs/releases/v0.1.0/compliance_reports/FU-206_compliance.md +0 -23
- package/docs/releases/v0.1.0/execution_schedule.md +0 -174
- package/docs/releases/v0.1.0/fu_spec_status.md +0 -83
- package/docs/releases/v0.1.0/implementation_logs/FU-000_implementation_log.md +0 -51
- package/docs/releases/v0.1.0/implementation_logs/FU-002_implementation_log.md +0 -51
- package/docs/releases/v0.1.0/implementation_logs/FU-050_implementation_log.md +0 -111
- package/docs/releases/v0.1.0/implementation_logs/FU-051_implementation_log.md +0 -91
- package/docs/releases/v0.1.0/implementation_logs/FU-052_implementation_log.md +0 -81
- package/docs/releases/v0.1.0/implementation_logs/FU-053_implementation_log.md +0 -71
- package/docs/releases/v0.1.0/implementation_logs/FU-054_implementation_log.md +0 -71
- package/docs/releases/v0.1.0/implementation_logs/FU-055_implementation_log.md +0 -51
- package/docs/releases/v0.1.0/implementation_logs/FU-056_implementation_log.md +0 -51
- package/docs/releases/v0.1.0/implementation_logs/FU-057_implementation_log.md +0 -51
- package/docs/releases/v0.1.0/implementation_logs/FU-058_implementation_log.md +0 -51
- package/docs/releases/v0.1.0/implementation_logs/FU-059_implementation_log.md +0 -51
- package/docs/releases/v0.1.0/implementation_logs/FU-061_implementation_log.md +0 -51
- package/docs/releases/v0.1.0/implementation_logs/FU-100_implementation_log.md +0 -251
- package/docs/releases/v0.1.0/implementation_logs/FU-104_implementation_log.md +0 -51
- package/docs/releases/v0.1.0/implementation_logs/FU-200_implementation_log.md +0 -51
- package/docs/releases/v0.1.0/implementation_logs/FU-201_implementation_log.md +0 -51
- package/docs/releases/v0.1.0/implementation_logs/FU-202_implementation_log.md +0 -51
- package/docs/releases/v0.1.0/implementation_logs/FU-203_implementation_log.md +0 -51
- package/docs/releases/v0.1.0/implementation_logs/FU-204_implementation_log.md +0 -51
- package/docs/releases/v0.1.0/implementation_logs/FU-205_implementation_log.md +0 -51
- package/docs/releases/v0.1.0/implementation_logs/FU-206_implementation_log.md +0 -51
- package/docs/releases/v0.1.0/integration_tests.md +0 -316
- package/docs/releases/v0.1.0/manifest.yaml +0 -291
- package/docs/releases/v0.1.0/mcp_actions_status.md +0 -101
- package/docs/releases/v0.1.0/release_plan.md +0 -196
- package/docs/releases/v0.1.0/status.md +0 -148
- package/docs/releases/v0.1.0/test_coverage_setup_notes.md +0 -39
- package/docs/releases/v0.1.1/manifest.yaml +0 -153
- package/docs/releases/v0.2.0/status.md +0 -167
- package/docs/releases/v0.2.1/acceptance_criteria.md +0 -200
- package/docs/releases/v0.2.1/execution_schedule.md +0 -114
- package/docs/releases/v0.2.1/feature_unit_overview.md +0 -246
- package/docs/releases/v0.2.1/integration_tests.md +0 -281
- package/docs/releases/v0.2.1/manifest.yaml +0 -141
- package/docs/releases/v0.2.1/release_plan.md +0 -211
- package/docs/releases/v0.2.1/status.md +0 -106
- package/docs/releases/v0.2.15/README.md +0 -79
- package/docs/releases/v0.2.15/migrations/01_add_source_graph_edges.sql +0 -109
- package/docs/releases/v0.2.15/migrations/02_drop_legacy_tables.sql +0 -130
- package/docs/releases/v0.2.2/execution_schedule.md +0 -123
- package/docs/releases/v0.2.2/integration_tests.md +0 -258
- package/docs/releases/v0.2.2/manifest.yaml +0 -108
- package/docs/releases/v0.2.2/release_plan.md +0 -246
- package/docs/releases/v0.2.2/status.md +0 -113
- package/docs/releases/v0.2.3/agent_skills_alignment.md +0 -738
- package/docs/releases/v0.2.3/enhancements_summary.md +0 -309
- package/docs/releases/v0.2.3/integration_guide.md +0 -3400
- package/docs/releases/v0.2.3/manifest.yaml +0 -212
- package/docs/releases/v0.2.3/mcp_actions.md +0 -491
- package/docs/releases/v0.2.3/plan_completion_review.md +0 -410
- package/docs/releases/v0.2.3/release_overview.md +0 -13
- package/docs/releases/v0.2.3/schema_extensions/agent_decision.md +0 -311
- package/docs/releases/v0.2.3/schema_extensions/agent_session.md +0 -464
- package/docs/releases/v0.2.3/schema_extensions/architectural_decision.md +0 -390
- package/docs/releases/v0.2.3/schema_extensions/codebase_entity.md +0 -404
- package/docs/releases/v0.2.3/schema_extensions/feature_unit.md +0 -307
- package/docs/releases/v0.2.3/schema_extensions/release.md +0 -338
- package/docs/releases/v0.2.3/schema_extensions/validation_result.md +0 -377
- package/docs/releases/v0.2.3/spec.md +0 -569
- package/docs/releases/v0.3.0/manifest.yaml +0 -77
- package/docs/releases/v0.3.0/migrations/01_add_source_graph_edges.sql +0 -110
- package/docs/releases/v0.3.0/migrations/02_drop_legacy_tables.sql +0 -131
- package/docs/releases/v0.3.0/status.md +0 -193
- package/docs/reports/AUTO_ENHANCEMENT_TEST_RESULTS.md +0 -51
- package/docs/reports/AUTO_ENHANCEMENT_TEST_SUMMARY.md +0 -64
- package/docs/reports/BIGINT_SERIALIZATION_DEBUG.md +0 -75
- package/docs/reports/BUG_LEARNING_PROTOCOL_ESTABLISHED.md +0 -179
- package/docs/reports/CONSOLE_LOG_MCP_PROTOCOL_FIX.md +0 -58
- package/docs/reports/CRYPTO_TRANSACTION_DISCREPANCY_RESOLUTION.md +0 -108
- package/docs/reports/CRYPTO_TRANSACTION_SIDE_BY_SIDE_COMPARISON.md +0 -165
- package/docs/reports/FAILING_TESTS_ANALYSIS.md +0 -140
- package/docs/reports/FAILING_TESTS_SUMMARY.md +0 -106
- package/docs/reports/FINAL_PARQUET_COMPARISON.md +0 -226
- package/docs/reports/FINAL_TEST_STATUS.md +0 -145
- package/docs/reports/LEGACY_REMOVAL_SUMMARY.md +0 -108
- package/docs/reports/MCP_ACTIONS_SUMMARY.md +0 -329
- package/docs/reports/MCP_ACTION_TEST_COVERAGE.md +0 -132
- package/docs/reports/MCP_SPEC_COMPARISON.md +0 -306
- package/docs/reports/MCP_SPEC_COMPARISON_UPDATED.md +0 -247
- package/docs/reports/MCP_SPEC_VOCABULARY_CONFORMANCE.md +0 -220
- package/docs/reports/MCP_TEST_SUITE_IMPLEMENTATION.md +0 -180
- package/docs/reports/MCP_TEST_SUITE_RESULTS.md +0 -182
- package/docs/reports/PARQUET_MCP_BUG_REPORT.md +0 -70
- package/docs/reports/PARQUET_MCP_STORAGE_COMPLETE.md +0 -214
- package/docs/reports/PARQUET_SAMPLE_FILES_CREATED.md +0 -49
- package/docs/reports/PARQUET_STORAGE_SUCCESS.md +0 -263
- package/docs/reports/PARQUET_VS_NEOTOMA_COMPARISON.md +0 -241
- package/docs/reports/REIMPORT_TASKS_BLOCKER.md +0 -50
- package/docs/reports/SAMPLE_FILE_STRUCTURE_UPDATE.md +0 -68
- package/docs/reports/SAMPLE_PARQUET_IMPORT_TEST_RESULTS.md +0 -91
- package/docs/reports/SCHEMA_ENHANCEMENT_STATUS.md +0 -84
- package/docs/reports/SCHEMA_ENHANCEMENT_TEST_RESULTS.md +0 -78
- package/docs/reports/TASK_RELOAD_BLOCKER.md +0 -40
- package/docs/reports/TEST_FIXES_SUMMARY.md +0 -96
- package/docs/reports/VOCABULARY_CONFORMANCE_REVIEW.md +0 -177
- package/docs/reports/WHY_TESTS_MISSED_BIGINT_ISSUE.md +0 -144
- package/docs/reports/beyond_rag_agent_memory_neotoma_architecture_report.md +0 -103
- package/docs/reports/dhh_clankers_with_claws_neotoma_report.md +0 -102
- package/docs/reports/dr_cintas_pageindex_structure_over_embeddings_report.md +0 -77
- package/docs/reports/failure_gallery.md +0 -97
- package/docs/reports/fixture_expansion_summary.md +0 -204
- package/docs/reports/mcp_action_names_review.md +0 -131
- package/docs/reports/mcp_cli_action_items_fidelity_report.md +0 -189
- package/docs/reports/neotoma_pdf_interpreted_as_note_investigation_2026_02_23.md +0 -89
- package/docs/reports/plan_execution_changes_report.md +0 -122
- package/docs/reports/schema_distinctions.md +0 -210
- package/docs/reports/schema_redundancy_analysis.md +0 -304
- package/docs/specs/FUNCTIONAL_REQUIREMENTS.md +0 -160
- package/docs/specs/GENERAL_REQUIREMENTS.md +0 -30
- package/docs/specs/ICP_PRIORITY_TIERS.md +0 -132
- package/docs/specs/ICP_PROFILES.md +0 -2345
- package/docs/specs/INTERNAL_MCP_RELEASE.md +0 -300
- package/docs/specs/MANIFEST_ALIGNMENT_SUMMARY.md +0 -162
- package/docs/specs/MCP_SPEC.md +0 -2275
- package/docs/specs/METRICS_REQUIREMENTS.md +0 -386
- package/docs/specs/MVP_EXECUTION_PLAN.md +0 -956
- package/docs/specs/MVP_FEATURE_UNITS.md +0 -1821
- package/docs/specs/MVP_OVERVIEW.md +0 -184
- package/docs/specs/NONFUNCTIONAL_REQUIREMENTS.md +0 -98
- package/docs/specs/ONBOARDING_SPEC.md +0 -591
- package/docs/specs/PARQUET_MCP_RESOURCES_SPEC.md +0 -328
- package/docs/specs/TEST_PLAN.md +0 -116
- package/docs/specs/UI_SPEC.md +0 -93
- package/docs/subsystems/accessibility.md +0 -48
- package/docs/subsystems/auth.md +0 -170
- package/docs/subsystems/deletion.md +0 -494
- package/docs/subsystems/entity_merge.md +0 -300
- package/docs/subsystems/errors.md +0 -82
- package/docs/subsystems/events.md +0 -50
- package/docs/subsystems/i18n.md +0 -42
- package/docs/subsystems/ingestion/ingestion.md +0 -772
- package/docs/subsystems/ingestion/state_machines.md +0 -49
- package/docs/subsystems/observation_architecture.md +0 -379
- package/docs/subsystems/privacy.md +0 -71
- package/docs/subsystems/record_types.md +0 -753
- package/docs/subsystems/reducer.md +0 -436
- package/docs/subsystems/relationships.md +0 -455
- package/docs/subsystems/schema.md +0 -1213
- package/docs/subsystems/schema_registry.md +0 -588
- package/docs/subsystems/schema_snapshots/.gitkeep +0 -6
- package/docs/subsystems/schema_snapshots/README.md +0 -209
- package/docs/subsystems/schema_snapshots/account/v1.0.json +0 -52
- package/docs/subsystems/schema_snapshots/address/v1.0.json +0 -79
- package/docs/subsystems/schema_snapshots/agent_message/v1.0.json +0 -96
- package/docs/subsystems/schema_snapshots/balance/v1.0.json +0 -64
- package/docs/subsystems/schema_snapshots/company/v1.0.json +0 -86
- package/docs/subsystems/schema_snapshots/contact/v1.0.json +0 -68
- package/docs/subsystems/schema_snapshots/contact/v1.1.json +0 -112
- package/docs/subsystems/schema_snapshots/contract/v1.0.json +0 -48
- package/docs/subsystems/schema_snapshots/conversation/v1.0.json +0 -102
- package/docs/subsystems/schema_snapshots/crypto_transaction/v1.0.json +0 -76
- package/docs/subsystems/schema_snapshots/email/v1.0.json +0 -87
- package/docs/subsystems/schema_snapshots/event/v1.0.json +0 -79
- package/docs/subsystems/schema_snapshots/exercise/v1.0.json +0 -72
- package/docs/subsystems/schema_snapshots/fixed_cost/v1.0.json +0 -112
- package/docs/subsystems/schema_snapshots/flow/v1.0.json +0 -84
- package/docs/subsystems/schema_snapshots/goal/v1.0.json +0 -71
- package/docs/subsystems/schema_snapshots/holding/v1.0.json +0 -75
- package/docs/subsystems/schema_snapshots/income/v1.0.json +0 -68
- package/docs/subsystems/schema_snapshots/liability/v1.0.json +0 -60
- package/docs/subsystems/schema_snapshots/location/v1.0.json +0 -79
- package/docs/subsystems/schema_snapshots/meal/v1.0.json +0 -68
- package/docs/subsystems/schema_snapshots/message/v1.0.json +0 -72
- package/docs/subsystems/schema_snapshots/note/v1.0.json +0 -64
- package/docs/subsystems/schema_snapshots/order/v1.0.json +0 -72
- package/docs/subsystems/schema_snapshots/person/v1.0.json +0 -86
- package/docs/subsystems/schema_snapshots/project/v1.0.json +0 -75
- package/docs/subsystems/schema_snapshots/property/v1.0.json +0 -64
- package/docs/subsystems/schema_snapshots/purchase/v1.0.json +0 -75
- package/docs/subsystems/schema_snapshots/relationship/v1.0.json +0 -63
- package/docs/subsystems/schema_snapshots/task/v1.0.json +0 -91
- package/docs/subsystems/schema_snapshots/tax_event/v1.0.json +0 -72
- package/docs/subsystems/schema_snapshots/tax_filing/v1.0.json +0 -83
- package/docs/subsystems/schema_snapshots/transaction/v1.0.json +0 -40
- package/docs/subsystems/schema_snapshots/transfer/v1.0.json +0 -76
- package/docs/subsystems/schema_snapshots/wallet/v1.0.json +0 -68
- package/docs/subsystems/search/search.md +0 -63
- package/docs/subsystems/sources.md +0 -493
- package/docs/subsystems/vector_ops.md +0 -72
- package/docs/templates/issue_template.md +0 -36
- package/docs/templates/pr_template.md +0 -74
- package/docs/testing/ROUTE_COVERAGE_MATRIX.md +0 -104
- package/docs/testing/WHY_PARTIAL_ROUTE_COVERAGE.md +0 -183
- package/docs/testing/automated_test_catalog.md +0 -353
- package/docs/testing/cli_command_coverage_review.md +0 -130
- package/docs/testing/fixtures_standard.md +0 -202
- package/docs/testing/test_coverage_audit_summary.md +0 -571
- package/docs/testing/testing_standard.md +0 -146
- package/docs/ui/DESIGN_SYSTEM_UPDATE_SUMMARY.md +0 -152
- package/docs/ui/SHADCN_INTEGRATION_COMPLETE.md +0 -255
- package/docs/ui/design_constraints_template.yaml +0 -228
- package/docs/ui/design_system/accessibility.md +0 -38
- package/docs/ui/design_system/authentication_components.md +0 -38
- package/docs/ui/design_system/billing_components.md +0 -36
- package/docs/ui/design_system/brand_alignment.md +0 -35
- package/docs/ui/design_system/color_palette.md +0 -95
- package/docs/ui/design_system/component_styles.md +0 -241
- package/docs/ui/design_system/dark_mode.md +0 -30
- package/docs/ui/design_system/empty_states.md +0 -47
- package/docs/ui/design_system/error_states.md +0 -35
- package/docs/ui/design_system/file_upload_components.md +0 -60
- package/docs/ui/design_system/future_considerations.md +0 -19
- package/docs/ui/design_system/iconography.md +0 -21
- package/docs/ui/design_system/icp_aesthetic_analysis.md +0 -35
- package/docs/ui/design_system/icp_preference_alignment.md +0 -33
- package/docs/ui/design_system/implementation_notes.md +0 -119
- package/docs/ui/design_system/loading_states.md +0 -40
- package/docs/ui/design_system/motion_and_animation.md +0 -45
- package/docs/ui/design_system/navigation_components.md +0 -63
- package/docs/ui/design_system/onboarding_components.md +0 -48
- package/docs/ui/design_system/responsive_design.md +0 -26
- package/docs/ui/design_system/search_components.md +0 -32
- package/docs/ui/design_system/settings_components.md +0 -29
- package/docs/ui/design_system/spacing_and_layout.md +0 -67
- package/docs/ui/design_system/style_guide.md +0 -395
- package/docs/ui/design_system/typography.md +0 -88
- package/docs/ui/design_system/visual_hierarchy.md +0 -64
- package/docs/ui/design_system.md +0 -124
- package/docs/ui/dsl_spec.md +0 -97
- package/docs/ui/patterns/dashboard.md +0 -58
- package/docs/ui/patterns/detail.md +0 -68
- package/docs/ui/patterns/list.md +0 -72
- package/docs/ui/patterns/navigation.md +0 -314
- package/docs/ui/patterns/settings.md +0 -52
- package/docs/ui/patterns/wizard.md +0 -53
- package/docs/ui/prototype_guide.md +0 -300
- package/docs/ui/shadcn_components.md +0 -189
- package/docs/ui/style_guide_updates.md +0 -74
- package/docs/vocabulary/canonical_terms.md +0 -318
- package/fly.toml +0 -29
- package/foundation/.cursor/rules/agent_constraints.mdc +0 -198
- package/foundation/.cursor/rules/agent_test_execution.mdc +0 -356
- package/foundation/.cursor/rules/autonomous_execution.mdc +0 -117
- package/foundation/.cursor/rules/behavioral_self_adaptation.mdc +0 -531
- package/foundation/.cursor/rules/bug_fix_detection.mdc +0 -142
- package/foundation/.cursor/rules/checkpoint_management.mdc +0 -191
- package/foundation/.cursor/rules/configuration_management.mdc +0 -156
- package/foundation/.cursor/rules/content_style_enforcement.mdc +0 -138
- package/foundation/.cursor/rules/dependency_installation.mdc +0 -146
- package/foundation/.cursor/rules/document_loading_order.mdc +0 -194
- package/foundation/.cursor/rules/documentation_rules.mdc +0 -166
- package/foundation/.cursor/rules/downstream_doc_updates.mdc +0 -266
- package/foundation/.cursor/rules/environment_variables_1password.mdc +0 -307
- package/foundation/.cursor/rules/environment_variables_env_file.mdc +0 -121
- package/foundation/.cursor/rules/feature_unit_detection.mdc +0 -129
- package/foundation/.cursor/rules/file_naming.mdc +0 -31
- package/foundation/.cursor/rules/git_remote_sync.mdc +0 -240
- package/foundation/.cursor/rules/instruction_documentation.mdc +0 -302
- package/foundation/.cursor/rules/native_browser_debugging.mdc +0 -65
- package/foundation/.cursor/rules/plan_execution_testing.mdc +0 -98
- package/foundation/.cursor/rules/post_build_testing.mdc +0 -159
- package/foundation/.cursor/rules/prefer_cli_tools.mdc +0 -234
- package/foundation/.cursor/rules/readme_maintenance.mdc +0 -153
- package/foundation/.cursor/rules/release_detection.mdc +0 -92
- package/foundation/.cursor/rules/release_status_readme_update.mdc +0 -64
- package/foundation/.cursor/rules/risk_management.mdc +0 -167
- package/foundation/.cursor/rules/security.mdc +0 -158
- package/foundation/.cursor/rules/test_first_workflow.mdc +0 -341
- package/foundation/.cursor/rules/worktree_env.mdc +0 -53
- package/foundation/.cursor/skills/analyze/SKILL.md +0 -566
- package/foundation/.cursor/skills/commit/SKILL.md +0 -782
- package/foundation/.cursor/skills/create-feature-unit/SKILL.md +0 -180
- package/foundation/.cursor/skills/create-prototype/SKILL.md +0 -91
- package/foundation/.cursor/skills/create-release/SKILL.md +0 -432
- package/foundation/.cursor/skills/create-rule/SKILL.md +0 -292
- package/foundation/.cursor/skills/debug/SKILL.md +0 -980
- package/foundation/.cursor/skills/final-review/SKILL.md +0 -93
- package/foundation/.cursor/skills/fix-feature-bug/SKILL.md +0 -100
- package/foundation/.cursor/skills/manage-error-debugging/SKILL.md +0 -182
- package/foundation/.cursor/skills/publish/SKILL.md +0 -554
- package/foundation/.cursor/skills/pull/SKILL.md +0 -315
- package/foundation/.cursor/skills/push/SKILL.md +0 -254
- package/foundation/.cursor/skills/report/SKILL.md +0 -1501
- package/foundation/.cursor/skills/report-error/SKILL.md +0 -1501
- package/foundation/.cursor/skills/run-feature-workflow/SKILL.md +0 -79
- package/foundation/.cursor/skills/setup-commands/SKILL.md +0 -28
- package/foundation/.cursor/skills/setup-cursor-copies/SKILL.md +0 -233
- package/foundation/.cursor/skills/sync-env-from-1password/SKILL.md +0 -302
- package/foundation/CONTRIBUTING.md +0 -202
- package/foundation/LICENSE +0 -22
- package/foundation/MIGRATION.md +0 -396
- package/foundation/README.md +0 -391
- package/foundation/agent_instructions/README.md +0 -559
- package/foundation/config/foundation-config.yaml +0 -498
- package/foundation/config/foundation_config.yaml +0 -475
- package/foundation/config/repo_adapters/personal.yaml +0 -40
- package/foundation/config/repo_adapters/template.yaml +0 -45
- package/foundation/conventions/code_conventions.md +0 -908
- package/foundation/conventions/documentation_standards.md +0 -459
- package/foundation/conventions/naming_patterns.yaml +0 -139
- package/foundation/conventions/testing_conventions.md +0 -374
- package/foundation/conventions/writing_style_guide.md +0 -221
- package/foundation/development/branch_strategy.md +0 -266
- package/foundation/development/feature_unit_workflow.md +0 -423
- package/foundation/development/pr_process.md +0 -385
- package/foundation/development/release_workflow.md +0 -2296
- package/foundation/development/templates/feature_unit_spec_template.md +0 -312
- package/foundation/development/templates/manifest_template_extended.yaml +0 -303
- package/foundation/development/templates/manifest_template_simple.yaml +0 -148
- package/foundation/development/workflow.md +0 -509
- package/foundation/development/worktree_setup.sh +0 -207
- package/foundation/scripts/README.md +0 -230
- package/foundation/scripts/__pycache__/op_sync_env_from_1password.cpython-314.pyc +0 -0
- package/foundation/scripts/install-foundation.sh +0 -232
- package/foundation/scripts/install_foundation.sh +0 -232
- package/foundation/scripts/op_sync_env_from_1password.py +0 -1007
- package/foundation/scripts/report_error_create_dirs.sh +0 -14
- package/foundation/scripts/report_error_generate_id.sh +0 -10
- package/foundation/scripts/report_error_update_queue.sh +0 -24
- package/foundation/scripts/report_error_validate_target.sh +0 -34
- package/foundation/scripts/report_error_wait_resolution.sh +0 -69
- package/foundation/scripts/validate_setup.sh +0 -147
- package/foundation/security/credential_management.md +0 -263
- package/foundation/security/pre_commit_audit.sh +0 -78
- package/foundation/security/security_rules.md +0 -163
- package/foundation/strategy/README.md +0 -279
- package/foundation/strategy/competitive_analysis_template.md +0 -271
- package/foundation/strategy/partnership_analysis_template.md +0 -423
- package/foundation/strategy/project_assessment_framework.md +0 -485
- package/foundation/strategy/relevance_analysis_template.md +0 -297
- package/foundation/tooling/README.md +0 -198
- package/foundation/validation/README.md +0 -120
- package/foundation-config.yaml +0 -543
- package/frontend/docs.html +0 -43
- package/frontend/frontend/.vite-prototype/deps/@noble_curves_ed25519__js.js +0 -2859
- package/frontend/frontend/.vite-prototype/deps/@noble_curves_ed25519__js.js.map +0 -7
- package/frontend/frontend/.vite-prototype/deps/@noble_hashes_utils__js.js +0 -62
- package/frontend/frontend/.vite-prototype/deps/@noble_hashes_utils__js.js.map +0 -7
- package/frontend/frontend/.vite-prototype/deps/@radix-ui_react-dialog.js +0 -464
- package/frontend/frontend/.vite-prototype/deps/@radix-ui_react-dialog.js.map +0 -7
- package/frontend/frontend/.vite-prototype/deps/@radix-ui_react-dropdown-menu.js +0 -1488
- package/frontend/frontend/.vite-prototype/deps/@radix-ui_react-dropdown-menu.js.map +0 -7
- package/frontend/frontend/.vite-prototype/deps/@radix-ui_react-label.js +0 -84
- package/frontend/frontend/.vite-prototype/deps/@radix-ui_react-label.js.map +0 -7
- package/frontend/frontend/.vite-prototype/deps/@radix-ui_react-scroll-area.js +0 -757
- package/frontend/frontend/.vite-prototype/deps/@radix-ui_react-scroll-area.js.map +0 -7
- package/frontend/frontend/.vite-prototype/deps/@radix-ui_react-select.js +0 -1351
- package/frontend/frontend/.vite-prototype/deps/@radix-ui_react-select.js.map +0 -7
- package/frontend/frontend/.vite-prototype/deps/@radix-ui_react-slot.js +0 -18
- package/frontend/frontend/.vite-prototype/deps/@radix-ui_react-slot.js.map +0 -7
- package/frontend/frontend/.vite-prototype/deps/@radix-ui_react-toast.js +0 -675
- package/frontend/frontend/.vite-prototype/deps/@radix-ui_react-toast.js.map +0 -7
- package/frontend/frontend/.vite-prototype/deps/@tanstack_react-table.js +0 -3254
- package/frontend/frontend/.vite-prototype/deps/@tanstack_react-table.js.map +0 -7
- package/frontend/frontend/.vite-prototype/deps/_metadata.json +0 -187
- package/frontend/frontend/.vite-prototype/deps/chunk-557GXNYU.js +0 -23
- package/frontend/frontend/.vite-prototype/deps/chunk-557GXNYU.js.map +0 -7
- package/frontend/frontend/.vite-prototype/deps/chunk-6FYWT56J.js.map +0 -7
- package/frontend/frontend/.vite-prototype/deps/chunk-C7HFWHWH.js +0 -271
- package/frontend/frontend/.vite-prototype/deps/chunk-C7HFWHWH.js.map +0 -7
- package/frontend/frontend/.vite-prototype/deps/chunk-CJDTXCAB.js +0 -49
- package/frontend/frontend/.vite-prototype/deps/chunk-CJDTXCAB.js.map +0 -7
- package/frontend/frontend/.vite-prototype/deps/chunk-DC5AMYBS.js +0 -39
- package/frontend/frontend/.vite-prototype/deps/chunk-DC5AMYBS.js.map +0 -7
- package/frontend/frontend/.vite-prototype/deps/chunk-F4FBYZZO.js +0 -2258
- package/frontend/frontend/.vite-prototype/deps/chunk-F4FBYZZO.js.map +0 -7
- package/frontend/frontend/.vite-prototype/deps/chunk-GIBJ3MQP.js +0 -129
- package/frontend/frontend/.vite-prototype/deps/chunk-GIBJ3MQP.js.map +0 -7
- package/frontend/frontend/.vite-prototype/deps/chunk-IMIZXM4S.js +0 -218
- package/frontend/frontend/.vite-prototype/deps/chunk-IMIZXM4S.js.map +0 -7
- package/frontend/frontend/.vite-prototype/deps/chunk-K236IVZX.js +0 -21628
- package/frontend/frontend/.vite-prototype/deps/chunk-K236IVZX.js.map +0 -7
- package/frontend/frontend/.vite-prototype/deps/chunk-LPO6FEV6.js +0 -21
- package/frontend/frontend/.vite-prototype/deps/chunk-LPO6FEV6.js.map +0 -7
- package/frontend/frontend/.vite-prototype/deps/chunk-PZJDTVNM.js +0 -144
- package/frontend/frontend/.vite-prototype/deps/chunk-PZJDTVNM.js.map +0 -7
- package/frontend/frontend/.vite-prototype/deps/chunk-RGYP7AMI.js +0 -1116
- package/frontend/frontend/.vite-prototype/deps/chunk-RGYP7AMI.js.map +0 -7
- package/frontend/frontend/.vite-prototype/deps/chunk-SMVRXQSC.js +0 -928
- package/frontend/frontend/.vite-prototype/deps/chunk-SMVRXQSC.js.map +0 -7
- package/frontend/frontend/.vite-prototype/deps/chunk-SPBFJIQZ.js +0 -49
- package/frontend/frontend/.vite-prototype/deps/chunk-SPBFJIQZ.js.map +0 -7
- package/frontend/frontend/.vite-prototype/deps/chunk-UQF5EARV.js +0 -9
- package/frontend/frontend/.vite-prototype/deps/chunk-UQF5EARV.js.map +0 -7
- package/frontend/frontend/.vite-prototype/deps/chunk-VEFHXNY4.js +0 -186
- package/frontend/frontend/.vite-prototype/deps/chunk-VEFHXNY4.js.map +0 -7
- package/frontend/frontend/.vite-prototype/deps/chunk-Z55WIHBM.js +0 -349
- package/frontend/frontend/.vite-prototype/deps/chunk-Z55WIHBM.js.map +0 -7
- package/frontend/frontend/.vite-prototype/deps/class-variance-authority.js +0 -51
- package/frontend/frontend/.vite-prototype/deps/class-variance-authority.js.map +0 -7
- package/frontend/frontend/.vite-prototype/deps/clsx.js +0 -10
- package/frontend/frontend/.vite-prototype/deps/clsx.js.map +0 -7
- package/frontend/frontend/.vite-prototype/deps/lucide-react.js +0 -31586
- package/frontend/frontend/.vite-prototype/deps/lucide-react.js.map +0 -7
- package/frontend/frontend/.vite-prototype/deps/package.json +0 -3
- package/frontend/frontend/.vite-prototype/deps/papaparse.js +0 -431
- package/frontend/frontend/.vite-prototype/deps/papaparse.js.map +0 -7
- package/frontend/frontend/.vite-prototype/deps/react-dom.js +0 -7
- package/frontend/frontend/.vite-prototype/deps/react-dom.js.map +0 -7
- package/frontend/frontend/.vite-prototype/deps/react-dom_client.js +0 -39
- package/frontend/frontend/.vite-prototype/deps/react-dom_client.js.map +0 -7
- package/frontend/frontend/.vite-prototype/deps/react.js +0 -6
- package/frontend/frontend/.vite-prototype/deps/react.js.map +0 -7
- package/frontend/frontend/.vite-prototype/deps/react_jsx-dev-runtime.js +0 -913
- package/frontend/frontend/.vite-prototype/deps/react_jsx-dev-runtime.js.map +0 -7
- package/frontend/frontend/.vite-prototype/deps/react_jsx-runtime.js +0 -7
- package/frontend/frontend/.vite-prototype/deps/react_jsx-runtime.js.map +0 -7
- package/frontend/frontend/.vite-prototype/deps/sonner.js +0 -1139
- package/frontend/frontend/.vite-prototype/deps/sonner.js.map +0 -7
- package/frontend/frontend/.vite-prototype/deps/tailwind-merge.js +0 -2534
- package/frontend/frontend/.vite-prototype/deps/tailwind-merge.js.map +0 -7
- package/frontend/index.html +0 -38
- package/frontend/src/App.tsx +0 -15
- package/frontend/src/bridge/discovery.ts +0 -126
- package/frontend/src/bridge/index.ts +0 -9
- package/frontend/src/bridge/mcp.ts +0 -85
- package/frontend/src/bridge/types.ts +0 -24
- package/frontend/src/bridge/websocket.ts +0 -349
- package/frontend/src/components/AIAgentBridge.tsx +0 -75
- package/frontend/src/components/AboutPage.tsx +0 -102
- package/frontend/src/components/AppNavigationSidebar.tsx +0 -181
- package/frontend/src/components/AppSidebar.tsx +0 -206
- package/frontend/src/components/AuthErrorHandler.tsx +0 -23
- package/frontend/src/components/Dashboard.tsx +0 -303
- package/frontend/src/components/DashboardPage.tsx +0 -78
- package/frontend/src/components/EmptyPlaceholder.tsx +0 -58
- package/frontend/src/components/EntityDetail.tsx +0 -801
- package/frontend/src/components/EntityList.tsx +0 -409
- package/frontend/src/components/ErrorBoundary.tsx +0 -131
- package/frontend/src/components/FileUploadView.tsx +0 -246
- package/frontend/src/components/FloatingSettingsButton.tsx +0 -46
- package/frontend/src/components/Header.tsx +0 -43
- package/frontend/src/components/IntegrationsPage.tsx +0 -46
- package/frontend/src/components/InterpretationList.tsx +0 -246
- package/frontend/src/components/KeyManagementDialog.tsx +0 -227
- package/frontend/src/components/Layout.tsx +0 -67
- package/frontend/src/components/MCPChatGPTPage.tsx +0 -128
- package/frontend/src/components/MCPClaudePage.tsx +0 -112
- package/frontend/src/components/MCPCodeiumPage.tsx +0 -100
- package/frontend/src/components/MCPConfigurationPage.tsx +0 -624
- package/frontend/src/components/MCPConnectionDialog.tsx +0 -177
- package/frontend/src/components/MCPConnectionsList.tsx +0 -197
- package/frontend/src/components/MCPContinuePage.tsx +0 -90
- package/frontend/src/components/MCPCursorPage.tsx +0 -379
- package/frontend/src/components/MCPGeminiPage.tsx +0 -102
- package/frontend/src/components/MCPGitHubCopilotPage.tsx +0 -100
- package/frontend/src/components/MCPGrokPage.tsx +0 -99
- package/frontend/src/components/MCPJetBrainsPage.tsx +0 -107
- package/frontend/src/components/MCPManusPage.tsx +0 -104
- package/frontend/src/components/MCPSetupDialog.tsx +0 -268
- package/frontend/src/components/MCPVSCodePage.tsx +0 -100
- package/frontend/src/components/MCPWindsurfPage.tsx +0 -99
- package/frontend/src/components/MainApp.tsx +0 -19
- package/frontend/src/components/NotFound.tsx +0 -46
- package/frontend/src/components/OAuthPage.tsx +0 -426
- package/frontend/src/components/ObservationList.tsx +0 -277
- package/frontend/src/components/ProtectedRoute.tsx +0 -38
- package/frontend/src/components/QuickEntryPanel.tsx +0 -99
- package/frontend/src/components/RealtimeStatusIndicator.tsx +0 -32
- package/frontend/src/components/RecentActivityFeed.tsx +0 -198
- package/frontend/src/components/RelationshipDetail.tsx +0 -232
- package/frontend/src/components/RelationshipList.tsx +0 -269
- package/frontend/src/components/SchemaDetail.test.tsx +0 -53
- package/frontend/src/components/SchemaDetail.tsx +0 -230
- package/frontend/src/components/SchemaList.tsx +0 -228
- package/frontend/src/components/SearchResults.tsx +0 -318
- package/frontend/src/components/SeoHead.tsx +0 -40
- package/frontend/src/components/SitePage.tsx +0 -654
- package/frontend/src/components/SourceDetail.tsx +0 -382
- package/frontend/src/components/SourceTable.tsx +0 -243
- package/frontend/src/components/StyleGuide.md +0 -48
- package/frontend/src/components/StyleGuide.tsx +0 -2316
- package/frontend/src/components/TimelineView.tsx +0 -321
- package/frontend/src/components/UniversalSearch.tsx +0 -518
- package/frontend/src/components/design-system/BadgesPage.tsx +0 -47
- package/frontend/src/components/design-system/ButtonsPage.tsx +0 -67
- package/frontend/src/components/design-system/CardsPage.tsx +0 -31
- package/frontend/src/components/design-system/CollapsiblePage.tsx +0 -84
- package/frontend/src/components/design-system/ColorsPage.tsx +0 -125
- package/frontend/src/components/design-system/DesignSystemIndex.tsx +0 -73
- package/frontend/src/components/design-system/DesignSystemLayout.tsx +0 -22
- package/frontend/src/components/design-system/DesignSystemRouter.tsx +0 -43
- package/frontend/src/components/design-system/InputsPage.tsx +0 -40
- package/frontend/src/components/design-system/PageFormatsPage.tsx +0 -263
- package/frontend/src/components/design-system/ProgressPage.tsx +0 -54
- package/frontend/src/components/design-system/SkeletonPage.tsx +0 -68
- package/frontend/src/components/design-system/SpacingPage.tsx +0 -45
- package/frontend/src/components/design-system/StyleGuidePage.tsx +0 -313
- package/frontend/src/components/design-system/SwitchPage.tsx +0 -60
- package/frontend/src/components/design-system/TablesPage.tsx +0 -206
- package/frontend/src/components/design-system/TabsPage.tsx +0 -88
- package/frontend/src/components/design-system/TooltipPage.tsx +0 -94
- package/frontend/src/components/design-system/TypographyPage.tsx +0 -81
- package/frontend/src/components/icons/CursorIcon.tsx +0 -26
- package/frontend/src/components/quick-entry/ContactDialog.tsx +0 -177
- package/frontend/src/components/quick-entry/EventDialog.tsx +0 -204
- package/frontend/src/components/quick-entry/TaskDialog.tsx +0 -204
- package/frontend/src/components/quick-entry/TransactionDialog.tsx +0 -188
- package/frontend/src/components/ui/alert.tsx +0 -59
- package/frontend/src/components/ui/badge.tsx +0 -36
- package/frontend/src/components/ui/breadcrumb.tsx +0 -120
- package/frontend/src/components/ui/button.tsx +0 -56
- package/frontend/src/components/ui/card.tsx +0 -68
- package/frontend/src/components/ui/collapsible.tsx +0 -9
- package/frontend/src/components/ui/dialog.tsx +0 -81
- package/frontend/src/components/ui/dropdown-menu.tsx +0 -195
- package/frontend/src/components/ui/input-group.tsx +0 -26
- package/frontend/src/components/ui/input.tsx +0 -24
- package/frontend/src/components/ui/label.tsx +0 -19
- package/frontend/src/components/ui/progress.tsx +0 -26
- package/frontend/src/components/ui/scroll-area.tsx +0 -35
- package/frontend/src/components/ui/select.tsx +0 -173
- package/frontend/src/components/ui/separator.tsx +0 -31
- package/frontend/src/components/ui/sheet.tsx +0 -139
- package/frontend/src/components/ui/sidebar.tsx +0 -851
- package/frontend/src/components/ui/skeleton.tsx +0 -15
- package/frontend/src/components/ui/switch.tsx +0 -35
- package/frontend/src/components/ui/table-scroll-wrapper.tsx +0 -96
- package/frontend/src/components/ui/table.tsx +0 -127
- package/frontend/src/components/ui/tabs.tsx +0 -53
- package/frontend/src/components/ui/textarea.tsx +0 -23
- package/frontend/src/components/ui/toast.tsx +0 -122
- package/frontend/src/components/ui/toaster.tsx +0 -32
- package/frontend/src/components/ui/tooltip.tsx +0 -28
- package/frontend/src/components/ui/use-toast.ts +0 -194
- package/frontend/src/constants/integrations.ts +0 -51
- package/frontend/src/contexts/AuthContext.tsx +0 -205
- package/frontend/src/contexts/RealtimeContext.tsx +0 -119
- package/frontend/src/docs/DocumentationApp.tsx +0 -104
- package/frontend/src/docs/DocumentationDirectory.tsx +0 -159
- package/frontend/src/docs/DocumentationLayout.tsx +0 -31
- package/frontend/src/docs/DocumentationPage.tsx +0 -38
- package/frontend/src/docs/DocumentationSidebar.tsx +0 -15
- package/frontend/src/docs/MarkdownContent.tsx +0 -212
- package/frontend/src/docs/README.md +0 -198
- package/frontend/src/docs/main.tsx +0 -12
- package/frontend/src/hooks/use-mobile.tsx +0 -19
- package/frontend/src/hooks/useKeys.ts +0 -202
- package/frontend/src/hooks/useMCPConfig.ts +0 -175
- package/frontend/src/hooks/useRealtimeEntities.ts +0 -56
- package/frontend/src/hooks/useRealtimeObservations.ts +0 -56
- package/frontend/src/hooks/useRealtimeRelationships.ts +0 -56
- package/frontend/src/hooks/useRealtimeSources.ts +0 -53
- package/frontend/src/hooks/useRealtimeTimeline.ts +0 -53
- package/frontend/src/hooks/useSettings.ts +0 -95
- package/frontend/src/hooks/useStorageQuota.ts +0 -65
- package/frontend/src/hooks/useTheme.tsx +0 -42
- package/frontend/src/index.css +0 -247
- package/frontend/src/lib/api_client.ts +0 -8
- package/frontend/src/lib/auth.ts +0 -100
- package/frontend/src/lib/idempotency.ts +0 -25
- package/frontend/src/lib/utils.ts +0 -7
- package/frontend/src/main.tsx +0 -40
- package/frontend/src/sample-data/sets-medium.csv +0 -55
- package/frontend/src/sample-data/sets-medium.ts +0 -45
- package/frontend/src/shared/.eslintrc.json +0 -35
- package/frontend/src/shared/.github/README.md +0 -55
- package/frontend/src/shared/.github/workflows/ci.yml +0 -88
- package/frontend/src/shared/.github/workflows/release.yml +0 -34
- package/frontend/src/shared/.github/workflows/setup.yml +0 -56
- package/frontend/src/shared/README.md +0 -163
- package/frontend/src/shared/SETUP.md +0 -121
- package/frontend/src/shared/components.json +0 -20
- package/frontend/src/shared/package-lock.json +0 -4508
- package/frontend/src/shared/package.json +0 -78
- package/frontend/src/shared/src/components/AppSidebar.tsx +0 -77
- package/frontend/src/shared/src/components/ErrorBoundary.tsx +0 -87
- package/frontend/src/shared/src/components/Layout.tsx +0 -262
- package/frontend/src/shared/src/components/ui/breadcrumb.tsx +0 -115
- package/frontend/src/shared/src/components/ui/button.tsx +0 -56
- package/frontend/src/shared/src/components/ui/calendar.tsx +0 -211
- package/frontend/src/shared/src/components/ui/input.tsx +0 -22
- package/frontend/src/shared/src/components/ui/popover.tsx +0 -29
- package/frontend/src/shared/src/components/ui/separator.tsx +0 -29
- package/frontend/src/shared/src/components/ui/sheet.tsx +0 -140
- package/frontend/src/shared/src/components/ui/sidebar.tsx +0 -519
- package/frontend/src/shared/src/components/ui/skeleton.tsx +0 -15
- package/frontend/src/shared/src/components/ui/tooltip.tsx +0 -28
- package/frontend/src/shared/src/hooks/use-mobile.tsx +0 -19
- package/frontend/src/shared/src/index.css +0 -75
- package/frontend/src/shared/src/index.ts +0 -34
- package/frontend/src/shared/src/lib/utils.ts +0 -6
- package/frontend/src/shared/tailwind.config.mjs +0 -67
- package/frontend/src/shared/tsconfig.json +0 -29
- package/frontend/src/site/seo_metadata.ts +0 -133
- package/frontend/src/site/site_data.test.ts +0 -36
- package/frontend/src/site/site_data.ts +0 -575
- package/frontend/src/store/README.md +0 -10
- package/frontend/src/utils/api_access.ts +0 -24
- package/frontend/src/utils/csv.test.ts +0 -42
- package/frontend/src/utils/csv.ts +0 -45
- package/frontend/src/utils/csv_summary.ts +0 -139
- package/frontend/src/utils/debounce.ts +0 -24
- package/frontend/src/utils/entity_display.test.ts +0 -334
- package/frontend/src/utils/entity_display.ts +0 -23
- package/frontend/src/utils/entity_type_formatter.ts +0 -56
- package/frontend/src/utils/error_utils.ts +0 -137
- package/frontend/src/utils/property_keys.ts +0 -36
- package/frontend/src/utils/schema_icons.test.ts +0 -59
- package/frontend/src/utils/schema_icons.ts +0 -229
- package/frontend/src/utils/time.ts +0 -37
- package/frontend/src/vite-env.d.ts +0 -14
- package/frontend/tsconfig.json +0 -27
- package/frontend/tsconfig.node.json +0 -11
- package/mcp/web-scraper/README.md +0 -475
- package/mcp/web-scraper/SETUP.md +0 -150
- package/mcp/web-scraper/__init__.py +0 -3
- package/mcp/web-scraper/chatgpt_scraper_mcp_server.py +0 -435
- package/mcp/web-scraper/create_repo.sh +0 -50
- package/mcp/web-scraper/imports/spotify/playlist_3i0FY58V7n8XIUyWDm3AYA.json +0 -1052
- package/mcp/web-scraper/nytimes_article.json +0 -7
- package/mcp/web-scraper/plugins/__init__.py +0 -3
- package/mcp/web-scraper/plugins/chatgpt_scraper.py +0 -105
- package/mcp/web-scraper/plugins/metacast_scraper.py +0 -828
- package/mcp/web-scraper/plugins/nyt_podcast_scraper.py +0 -538
- package/mcp/web-scraper/plugins/spotify_scraper.py +0 -618
- package/mcp/web-scraper/plugins/twitter_scraper.py +0 -576
- package/mcp/web-scraper/plugins/url_body_fetcher.py +0 -172
- package/mcp/web-scraper/pytest.ini +0 -11
- package/mcp/web-scraper/requirements-test.txt +0 -6
- package/mcp/web-scraper/requirements.txt +0 -6
- package/mcp/web-scraper/run-web-scraper-mcp.sh +0 -38
- package/mcp/web-scraper/scrape_spotify_playlist.py +0 -77
- package/mcp/web-scraper/scraper.py +0 -819
- package/mcp/web-scraper/scraper_base.py +0 -88
- package/mcp/web-scraper/scraper_registry.py +0 -32
- package/mcp/web-scraper/scripts/scrape_tweet_and_verify_referenced.py +0 -65
- package/mcp/web-scraper/store_songs_to_parquet.py +0 -120
- package/mcp/web-scraper/tests/__init__.py +0 -0
- package/mcp/web-scraper/tests/conftest.py +0 -40
- package/mcp/web-scraper/tests/integration/__init__.py +0 -0
- package/mcp/web-scraper/tests/unit/__init__.py +0 -0
- package/mcp/web-scraper/tests/unit/test_source_detection.py +0 -60
- package/mcp/web-scraper/web_scraper_mcp_server.py +0 -639
- package/openapi.yaml +0 -2071
- package/playwright/fixtures/servers.ts +0 -180
- package/playwright/global_setup.ts +0 -3
- package/playwright/tests/auto-enhancement.spec.ts +0 -370
- package/playwright/tests/coverage-map.json +0 -135
- package/playwright/tests/design-system.spec.ts +0 -216
- package/playwright/tests/entity-detail.spec.ts +0 -268
- package/playwright/tests/entity-list.spec.ts +0 -266
- package/playwright/tests/floating-settings-button.spec.ts +0 -95
- package/playwright/tests/graph-integrity.spec.ts +0 -344
- package/playwright/tests/helpers.ts +0 -560
- package/playwright/tests/interpretations.spec.ts +0 -285
- package/playwright/tests/mcp-configuration.spec.ts +0 -208
- package/playwright/tests/mcp-relationships.spec.ts +0 -631
- package/playwright/tests/mcp-store-retrieve.spec.ts +0 -288
- package/playwright/tests/not-found.spec.ts +0 -30
- package/playwright/tests/oauth-flow.spec.ts +0 -790
- package/playwright/tests/observations.spec.ts +0 -295
- package/playwright/tests/relationship-detail.spec.ts +0 -305
- package/playwright/tests/relationships-list.spec.ts +0 -275
- package/playwright/tests/schema-detail.spec.ts +0 -300
- package/playwright/tests/schemas-list.spec.ts +0 -262
- package/playwright/tests/search-flow.spec.ts +0 -216
- package/playwright/tests/site-page.spec.ts +0 -23
- package/playwright/tests/source-detail.spec.ts +0 -300
- package/playwright/tests/sources-list.spec.ts +0 -254
- package/playwright/tests/upload-flow.spec.ts +0 -279
- package/playwright/tsconfig.json +0 -17
- package/playwright/types.d.ts +0 -14
- package/playwright/utils/servers.ts +0 -435
- package/playwright.config.ts +0 -77
- package/postcss.config.mjs +0 -7
- package/scripts/add_ngrok_mapping.py +0 -99
- package/scripts/add_required_observation_fields.js +0 -96
- package/scripts/add_required_source_fields.js +0 -95
- package/scripts/analyze-data-dir-schemas.js +0 -256
- package/scripts/analyze_data_dir_for_schemas.ts +0 -378
- package/scripts/analyze_low_confidence_fields.ts +0 -235
- package/scripts/analyze_schema_redundancies.ts +0 -228
- package/scripts/apply_documentation_rules.sh +0 -81
- package/scripts/apply_file_naming_convention.sh +0 -75
- package/scripts/backfill_entity_embeddings.ts +0 -95
- package/scripts/branch.js +0 -47
- package/scripts/build_github_pages_site.tsx +0 -65
- package/scripts/check-playwright-coverage.ts +0 -159
- package/scripts/check_agent_conversations.js +0 -72
- package/scripts/check_agent_status.js +0 -156
- package/scripts/check_auto_enhancement_status.ts +0 -215
- package/scripts/check_auto_test_schemas.ts +0 -43
- package/scripts/check_claude_sync.sh +0 -35
- package/scripts/check_raw_fragments.ts +0 -46
- package/scripts/check_remaining_eligibility.ts +0 -46
- package/scripts/check_remaining_fields.ts +0 -58
- package/scripts/check_sendgrid_credits.sh +0 -122
- package/scripts/check_sendgrid_email_logs.sh +0 -150
- package/scripts/check_snapshot_schema.ts +0 -18
- package/scripts/check_sources.ts +0 -34
- package/scripts/check_stale_snapshots.ts +0 -80
- package/scripts/cleanup-auto-test-schemas.ts +0 -80
- package/scripts/cleanup_test_schemas.ts +0 -275
- package/scripts/com.neotoma.dev-servers.plist.template +0 -27
- package/scripts/com.neotoma.watch-build.plist.template +0 -27
- package/scripts/complete_submodule_setup.sh +0 -101
- package/scripts/configure_sendgrid_dns.sh +0 -162
- package/scripts/copy-env-to-worktree.js +0 -109
- package/scripts/copy_pdf_worker_wrapper.js +0 -19
- package/scripts/create_sample_parquet_files.py +0 -120
- package/scripts/create_sample_parquet_files.ts +0 -146
- package/scripts/create_sample_parquet_files_mcp.ts +0 -196
- package/scripts/create_sample_parquet_files_simple.sh +0 -132
- package/scripts/cursor-worktree-init.sh +0 -24
- package/scripts/debug-ssl.sh +0 -86
- package/scripts/debug_pdf_extraction.ts +0 -69
- package/scripts/debug_vec_knn.ts +0 -69
- package/scripts/dev-proxy.js +0 -406
- package/scripts/dev-serve.js +0 -349
- package/scripts/disable-dns.sh +0 -47
- package/scripts/doctor.ts +0 -97
- package/scripts/export_schema_snapshots.ts +0 -253
- package/scripts/fix-ssl-cert.sh +0 -72
- package/scripts/fix_mcp_phase1_tests.sh +0 -51
- package/scripts/fix_observation_column_names.js +0 -51
- package/scripts/fix_observation_priority_ordering.js +0 -45
- package/scripts/fix_observation_properties_to_fields.js +0 -48
- package/scripts/fix_sqlite_schema_mismatch.js +0 -75
- package/scripts/generate-dev-cert.sh +0 -69
- package/scripts/generate_pdf_fixtures.ts +0 -330
- package/scripts/generate_schema_icons.ts +0 -216
- package/scripts/get_branch_ports.js +0 -56
- package/scripts/get_mcp_token.sh +0 -47
- package/scripts/get_secrets_for_agents.js +0 -45
- package/scripts/ingest_data_dir.ts +0 -215
- package/scripts/ingest_finances_data.ts +0 -255
- package/scripts/initialize-schemas.ts +0 -350
- package/scripts/install_launchd_dev_servers.js +0 -54
- package/scripts/install_launchd_watch_build.js +0 -53
- package/scripts/instruct_agents_credential_setup.js +0 -130
- package/scripts/instruct_agents_env_check.js +0 -99
- package/scripts/instruct_agents_run_tests_until_passing.js +0 -128
- package/scripts/kill_port.js +0 -174
- package/scripts/linters/sync_agent_instructions_pre_commit.sh +0 -34
- package/scripts/manage_error_debugging.sh +0 -228
- package/scripts/manually_queue_enhancements.ts +0 -156
- package/scripts/merge-dev.js +0 -319
- package/scripts/migrate_auto_enhanced_fields.ts +0 -83
- package/scripts/migrate_env_to_secrets.js +0 -69
- package/scripts/migrate_plans.ts +0 -870
- package/scripts/monitor_and_fix_snapshots.ts +0 -165
- package/scripts/move_docs_rules_to_cursor.js +0 -54
- package/scripts/move_docs_rules_to_cursor.sh +0 -24
- package/scripts/neotoma-npm-reserve/README.md +0 -8
- package/scripts/neotoma-npm-reserve/package.json +0 -10
- package/scripts/notify_agents_env_vars.js +0 -109
- package/scripts/notify_agents_setup_script.js +0 -148
- package/scripts/notify_running_agents_credentials.js +0 -177
- package/scripts/pdf_worker_polyfill.mjs +0 -27
- package/scripts/pick-port.js +0 -108
- package/scripts/postinstall.js +0 -84
- package/scripts/process_remaining_eligible.ts +0 -70
- package/scripts/prototypes/run-prototype.sh +0 -40
- package/scripts/prune-merged-branches.js +0 -115
- package/scripts/recompute_bob.ts +0 -64
- package/scripts/recompute_snapshots.ts +0 -72
- package/scripts/release_orchestrator.js +0 -1995
- package/scripts/remove_duplicate_source_fields.js +0 -61
- package/scripts/rename-branch-from-commit.js +0 -111
- package/scripts/respawn_agents_with_credentials.js +0 -358
- package/scripts/respawn_single_agent.js +0 -324
- package/scripts/run-dev-server-with-tunnel-url.sh +0 -16
- package/scripts/run-dev-task.js +0 -160
- package/scripts/run_dev_servers_launchd.sh +0 -14
- package/scripts/run_integration_tests.js +0 -523
- package/scripts/run_migrations.js +0 -55
- package/scripts/run_neotoma_mcp_stdio.sh +0 -11
- package/scripts/run_neotoma_mcp_stdio_dev_watch.sh +0 -10
- package/scripts/run_neotoma_mcp_stdio_prod.sh +0 -11
- package/scripts/run_neotoma_mcp_stdio_prod_watch.sh +0 -12
- package/scripts/run_watch_build_launchd.sh +0 -18
- package/scripts/scrape_nytimes_article.py +0 -182
- package/scripts/scrape_nytimes_simple.py +0 -152
- package/scripts/secrets_manager.js +0 -232
- package/scripts/send_agent_followup.js +0 -136
- package/scripts/setup-data-symlink.js +0 -88
- package/scripts/setup-dns.sh +0 -54
- package/scripts/setup-env-copy-hook.sh +0 -67
- package/scripts/setup-foundation-symlink.js +0 -45
- package/scripts/setup-https-tunnel.sh +0 -409
- package/scripts/setup-test-env.sh +0 -18
- package/scripts/setup_agent_credentials.sh +0 -27
- package/scripts/setup_agent_environment.sh +0 -39
- package/scripts/setup_claude_instructions.sh +0 -437
- package/scripts/setup_cloudflare_email_routing.sh +0 -158
- package/scripts/setup_shared_submodule.sh +0 -100
- package/scripts/simulate_npm_install.js +0 -70
- package/scripts/spec_compliance_patterns.js +0 -260
- package/scripts/sync-env-from-1password.sh +0 -100
- package/scripts/sync_mcp_configs.js +0 -106
- package/scripts/test-base64-store.ts +0 -53
- package/scripts/test-error-handling.ts +0 -44
- package/scripts/test-file-path-store.ts +0 -142
- package/scripts/test-raw-fragments-idempotence.ts +0 -157
- package/scripts/test-raw-fragments-insert.ts +0 -88
- package/scripts/test-store-response-structure.ts +0 -125
- package/scripts/test-store-structured-response.ts +0 -126
- package/scripts/test_agent_env_spawn.js +0 -82
- package/scripts/test_confidence.ts +0 -36
- package/scripts/test_dsnp_parquet_reading.ts +0 -72
- package/scripts/test_mcp_semantic_search.ts +0 -36
- package/scripts/test_parquet_ingestion.ts +0 -52
- package/scripts/test_raw_fragments_in_response.ts +0 -31
- package/scripts/test_sendgrid_smtp.sh +0 -45
- package/scripts/test_store_parquet_via_mcp.ts +0 -66
- package/scripts/trigger_error_debug_cli.js +0 -271
- package/scripts/unblock_agents.js +0 -208
- package/scripts/validate-coverage-map.ts +0 -187
- package/scripts/validate-doc-dependencies.js +0 -410
- package/scripts/validate_all_fu_compliance.js +0 -752
- package/scripts/validate_schema_sync.ts +0 -135
- package/scripts/validate_spec_compliance.js +0 -669
- package/scripts/watch_site.js +0 -68
- package/scripts/wipe-local-database.js +0 -191
- package/scripts/with_branch_ports.js +0 -496
- package/site_pages/.nojekyll +0 -0
- package/site_pages/FAVICON_CANDIDATES.md +0 -22
- package/site_pages/favicon-1-diamond.svg +0 -3
- package/site_pages/favicon-2-n-box.svg +0 -10
- package/site_pages/favicon-3-waves.svg +0 -3
- package/site_pages/favicon-4-n-minimal.svg +0 -3
- package/site_pages/favicon-5-bars.svg +0 -5
- package/site_pages/favicon.svg +0 -10
- package/site_pages/index.html +0 -40
- package/site_pages/robots.txt +0 -4
- package/site_pages/sitemap.xml +0 -5
- package/src/actions.ts +0 -4468
- package/src/cli/agent_instructions_scan.ts +0 -793
- package/src/cli/bootstrap.ts +0 -35
- package/src/cli/config.ts +0 -338
- package/src/cli/format.ts +0 -337
- package/src/cli/index.ts +0 -11417
- package/src/cli/init_abort.ts +0 -7
- package/src/cli/mcp_config_scan.ts +0 -1359
- package/src/cli/pack_rat.ts +0 -134
- package/src/config/record_types.ts +0 -562
- package/src/config.ts +0 -164
- package/src/crypto/agent_identity.ts +0 -46
- package/src/crypto/auth.ts +0 -110
- package/src/crypto/column_encryption.ts +0 -109
- package/src/crypto/crypto.test.ts +0 -311
- package/src/crypto/envelope.ts +0 -240
- package/src/crypto/export.ts +0 -131
- package/src/crypto/index.ts +0 -13
- package/src/crypto/key_derivation.ts +0 -176
- package/src/crypto/keys.ts +0 -85
- package/src/crypto/mcp_auth_token.ts +0 -35
- package/src/crypto/signature.ts +0 -50
- package/src/crypto/types.ts +0 -48
- package/src/db.ts +0 -66
- package/src/embeddings.ts +0 -76
- package/src/index.ts +0 -25
- package/src/mcp_ws_bridge.ts +0 -222
- package/src/middleware/encrypt_response.ts +0 -39
- package/src/normalize.ts +0 -351
- package/src/record_types.test.ts +0 -36
- package/src/reducers/index.ts +0 -5
- package/src/reducers/observation_reducer.ts +0 -458
- package/src/reducers/relationship_reducer.ts +0 -291
- package/src/repositories/db/capability_repo.ts +0 -29
- package/src/repositories/file/capability_repo.ts +0 -29
- package/src/repositories/index.ts +0 -7
- package/src/repositories/interfaces.ts +0 -112
- package/src/repositories/sqlite/__tests__/local_db_adapter.test.ts +0 -138
- package/src/repositories/sqlite/local_db_adapter.ts +0 -961
- package/src/repositories/sqlite/sqlite_client.ts +0 -378
- package/src/server.ts +0 -6157
- package/src/services/__tests__/csv_chunking.test.ts +0 -125
- package/src/services/__tests__/deletion.test.ts +0 -358
- package/src/services/__tests__/entity_semantic_search.test.ts +0 -45
- package/src/services/__tests__/local_auth.test.ts +0 -52
- package/src/services/__tests__/local_entity_embedding.test.ts +0 -108
- package/src/services/__tests__/mcp_oauth.test.ts +0 -407
- package/src/services/__tests__/oauth_key_gate.test.ts +0 -50
- package/src/services/__tests__/oauth_state.test.ts +0 -40
- package/src/services/__tests__/schema_icon_service.test.ts +0 -151
- package/src/services/auto_enhancement_processor.ts +0 -237
- package/src/services/capability_registry.ts +0 -202
- package/src/services/correction.ts +0 -87
- package/src/services/csv_chunking.ts +0 -128
- package/src/services/csv_row_extraction.ts +0 -124
- package/src/services/dashboard_stats.ts +0 -115
- package/src/services/deletion.ts +0 -482
- package/src/services/deletion_monitor.ts +0 -314
- package/src/services/encryption_service.ts +0 -110
- package/src/services/entity_merge.ts +0 -100
- package/src/services/entity_queries.ts +0 -518
- package/src/services/entity_resolution.ts +0 -367
- package/src/services/entity_semantic_search.ts +0 -64
- package/src/services/entity_snapshot_embedding.ts +0 -115
- package/src/services/field_canonicalization.ts +0 -393
- package/src/services/field_converters.ts +0 -173
- package/src/services/field_validation.ts +0 -173
- package/src/services/file_text_extraction.ts +0 -213
- package/src/services/finances_field_mapping.ts +0 -473
- package/src/services/gdpr_deletion.ts +0 -497
- package/src/services/interpretation.ts +0 -939
- package/src/services/llm_extraction.ts +0 -548
- package/src/services/local_auth.ts +0 -154
- package/src/services/local_entity_embedding.ts +0 -196
- package/src/services/mcp_auth.ts +0 -75
- package/src/services/mcp_oauth.ts +0 -1625
- package/src/services/mcp_oauth_errors.ts +0 -139
- package/src/services/oauth_key_gate.ts +0 -116
- package/src/services/oauth_state.ts +0 -70
- package/src/services/observation_identity.ts +0 -161
- package/src/services/observation_storage.ts +0 -162
- package/src/services/parquet_reader.ts +0 -375
- package/src/services/public_key_registry.ts +0 -103
- package/src/services/raw_fragments.ts +0 -217
- package/src/services/raw_storage.ts +0 -267
- package/src/services/relationships.ts +0 -495
- package/src/services/schema_definitions.ts +0 -2041
- package/src/services/schema_icon_service.ts +0 -260
- package/src/services/schema_inference.ts +0 -401
- package/src/services/schema_recommendation.ts +0 -1475
- package/src/services/schema_registry.ts +0 -1403
- package/src/services/snapshot_computation.ts +0 -92
- package/src/services/source_identity.ts +0 -22
- package/src/services/timeline_events.ts +0 -198
- package/src/shared/action_handlers/entity_handlers.ts +0 -115
- package/src/shared/action_schemas.test.ts +0 -59
- package/src/shared/action_schemas.ts +0 -317
- package/src/shared/api_client.ts +0 -86
- package/src/shared/contract_mappings.ts +0 -646
- package/src/shared/entity_display_name.ts +0 -64
- package/src/shared/local_transport.ts +0 -126
- package/src/shared/openapi_schema.ts +0 -175
- package/src/shared/openapi_types.ts +0 -2810
- package/src/shared/record_display_summary.ts +0 -133
- package/src/utils/__tests__/csv_summary.test.ts +0 -53
- package/src/utils/__tests__/lucide_icons.test.ts +0 -133
- package/src/utils/chat.test.ts +0 -42
- package/src/utils/chat.ts +0 -35
- package/src/utils/csv.test.ts +0 -41
- package/src/utils/csv.ts +0 -77
- package/src/utils/csv_summary.ts +0 -195
- package/src/utils/log_encrypt.ts +0 -144
- package/src/utils/logger.ts +0 -94
- package/src/utils/lucide_icons.ts +0 -279
- package/src/utils/property_keys.test.ts +0 -109
- package/src/utils/property_keys.ts +0 -66
- package/src/utils/property_sanitizer.test.ts +0 -105
- package/src/utils/property_sanitizer.ts +0 -135
- package/src/version_check.test.ts +0 -107
- package/src/version_check.ts +0 -58
- package/supabase/.temp/cli-latest +0 -0
- package/tailwind.config.mjs +0 -134
- package/tests/TEST_ACTION_MATRIX.md +0 -387
- package/tests/agent/mcp_instruction_behavior.test.ts +0 -72
- package/tests/agent/runner/agent_runner.ts +0 -27
- package/tests/agent/runner/claude_code_runner.ts +0 -128
- package/tests/agent/runner/cursor_cli_runner.ts +0 -7
- package/tests/cli/api_client_offline_fallback.test.ts +0 -89
- package/tests/cli/cli_admin_commands.test.ts +0 -268
- package/tests/cli/cli_api_commands.test.ts +0 -75
- package/tests/cli/cli_auth_commands.test.ts +0 -67
- package/tests/cli/cli_command_coverage_guard.test.ts +0 -50
- package/tests/cli/cli_correction_commands.test.ts +0 -277
- package/tests/cli/cli_direct_invocation_parity.test.ts +0 -46
- package/tests/cli/cli_entity_commands.test.ts +0 -324
- package/tests/cli/cli_entity_subcommands.test.ts +0 -422
- package/tests/cli/cli_infra_commands.test.ts +0 -475
- package/tests/cli/cli_mcp_commands.test.ts +0 -51
- package/tests/cli/cli_observation_commands.test.ts +0 -232
- package/tests/cli/cli_query_commands.test.ts +0 -369
- package/tests/cli/cli_relationship_commands.test.ts +0 -410
- package/tests/cli/cli_schema_commands.test.ts +0 -296
- package/tests/cli/cli_session_startup_ux.test.ts +0 -112
- package/tests/cli/cli_smoke.test.ts +0 -456
- package/tests/cli/cli_source_commands.test.ts +0 -214
- package/tests/cli/cli_stats_commands.test.ts +0 -51
- package/tests/cli/cli_store_commands.test.ts +0 -339
- package/tests/cli/cli_timeline_commands.test.ts +0 -294
- package/tests/cli/config.test.ts +0 -36
- package/tests/cli/config_api_discovery.test.ts +0 -91
- package/tests/cli/test_command_detection.test.ts +0 -128
- package/tests/cli/test_debug_tty.test.ts +0 -32
- package/tests/contract/contract_mapping.test.ts +0 -131
- package/tests/contract/contract_mcp_cli_parity.test.ts +0 -223
- package/tests/contract/openapi_schema.test.ts +0 -15
- package/tests/fixtures/README.md +0 -22
- package/tests/fixtures/agent_mcp/insurance_policy.txt +0 -5
- package/tests/fixtures/agent_mcp_cases.json +0 -137
- package/tests/fixtures/csv/sample_balances.csv +0 -7
- package/tests/fixtures/csv/sample_contacts.csv +0 -6
- package/tests/fixtures/csv/sample_crypto_transactions.csv +0 -6
- package/tests/fixtures/csv/sample_flows.csv +0 -7
- package/tests/fixtures/csv/sample_holdings.csv +0 -7
- package/tests/fixtures/csv/sample_income.csv +0 -7
- package/tests/fixtures/csv/sample_purchases.csv +0 -7
- package/tests/fixtures/csv/sample_tax_events.csv +0 -7
- package/tests/fixtures/csv/sample_transactions.csv +0 -7
- package/tests/fixtures/csv/sample_transfers.csv +0 -6
- package/tests/fixtures/expected/contact_snapshot.json +0 -19
- package/tests/fixtures/expected/transaction_snapshot.json +0 -21
- package/tests/fixtures/helpers.ts +0 -522
- package/tests/fixtures/json/account.json +0 -63
- package/tests/fixtures/json/argument.json +0 -39
- package/tests/fixtures/json/balance.json +0 -56
- package/tests/fixtures/json/belief.json +0 -44
- package/tests/fixtures/json/contact.json +0 -83
- package/tests/fixtures/json/contract.json +0 -62
- package/tests/fixtures/json/crypto_transaction.json +0 -67
- package/tests/fixtures/json/domain.json +0 -41
- package/tests/fixtures/json/emotion.json +0 -31
- package/tests/fixtures/json/fixed_cost.json +0 -75
- package/tests/fixtures/json/flow.json +0 -71
- package/tests/fixtures/json/habit.json +0 -54
- package/tests/fixtures/json/habit_completion.json +0 -31
- package/tests/fixtures/json/habit_objective.json +0 -40
- package/tests/fixtures/json/holding.json +0 -65
- package/tests/fixtures/json/income.json +0 -59
- package/tests/fixtures/json/liability.json +0 -50
- package/tests/fixtures/json/order.json +0 -62
- package/tests/fixtures/json/outcome.json +0 -56
- package/tests/fixtures/json/process.json +0 -43
- package/tests/fixtures/json/property.json +0 -52
- package/tests/fixtures/json/purchase.json +0 -66
- package/tests/fixtures/json/research.json +0 -53
- package/tests/fixtures/json/strategy.json +0 -68
- package/tests/fixtures/json/task_attachment.json +0 -62
- package/tests/fixtures/json/task_comment.json +0 -54
- package/tests/fixtures/json/task_dependency.json +0 -51
- package/tests/fixtures/json/task_story.json +0 -53
- package/tests/fixtures/json/tax_event.json +0 -60
- package/tests/fixtures/json/tax_filing.json +0 -61
- package/tests/fixtures/json/transaction.json +0 -62
- package/tests/fixtures/json/transfer.json +0 -60
- package/tests/fixtures/json/wallet.json +0 -51
- package/tests/fixtures/json/workout.json +0 -45
- package/tests/fixtures/pdf/sample-upload.txt +0 -2
- package/tests/fixtures/pdf/sample_bank_statement.md +0 -32
- package/tests/fixtures/pdf/sample_bank_statement.pdf +0 -0
- package/tests/fixtures/pdf/sample_contract.md +0 -27
- package/tests/fixtures/pdf/sample_contract.pdf +0 -0
- package/tests/fixtures/pdf/sample_exercise.md +0 -31
- package/tests/fixtures/pdf/sample_exercise.pdf +0 -0
- package/tests/fixtures/pdf/sample_holding_statement.md +0 -29
- package/tests/fixtures/pdf/sample_holding_statement.pdf +0 -0
- package/tests/fixtures/pdf/sample_invoice.md +0 -27
- package/tests/fixtures/pdf/sample_invoice.pdf +0 -108
- package/tests/fixtures/pdf/sample_meal.md +0 -28
- package/tests/fixtures/pdf/sample_meal.pdf +0 -111
- package/tests/fixtures/pdf/sample_note.md +0 -28
- package/tests/fixtures/pdf/sample_note.pdf +0 -0
- package/tests/fixtures/pdf/sample_receipt.md +0 -28
- package/tests/fixtures/pdf/sample_receipt.pdf +0 -0
- package/tests/fixtures/pdf/sample_research.md +0 -31
- package/tests/fixtures/pdf/sample_research.pdf +0 -0
- package/tests/fixtures/pdf/sample_tax_form.md +0 -38
- package/tests/fixtures/pdf/sample_tax_form.pdf +0 -0
- package/tests/fixtures/pdf/sample_transaction_receipt.md +0 -28
- package/tests/fixtures/pdf/sample_transaction_receipt.pdf +0 -0
- package/tests/fixtures/replay_graph.test.ts +0 -125
- package/tests/fixtures/sample_invoice.pdf +0 -1
- package/tests/fixtures/types.ts +0 -321
- package/tests/fixtures/validation.ts +0 -333
- package/tests/helpers/cleanup_helpers.ts +0 -550
- package/tests/helpers/create_test_parquet.ts +0 -187
- package/tests/helpers/cross_layer_helpers.ts +0 -130
- package/tests/helpers/mcp_action_helper.ts +0 -55
- package/tests/helpers/mcp_spec_validators.ts +0 -433
- package/tests/helpers/test_data_helpers.ts +0 -226
- package/tests/helpers/test_schema_helpers.ts +0 -264
- package/tests/integration/cli_to_mcp_entities.test.ts +0 -211
- package/tests/integration/cli_to_mcp_relationships.test.ts +0 -196
- package/tests/integration/cli_to_mcp_schemas.test.ts +0 -232
- package/tests/integration/cli_to_mcp_stats_snapshots.test.ts +0 -67
- package/tests/integration/cli_to_mcp_store.test.ts +0 -249
- package/tests/integration/dashboard_stats.test.ts +0 -180
- package/tests/integration/entity_queries.test.ts +0 -666
- package/tests/integration/field_converters.test.ts +0 -266
- package/tests/integration/fixture_mcp_store_replay.test.ts +0 -255
- package/tests/integration/gdpr_deletion.test.ts +0 -433
- package/tests/integration/llm_extraction.test.ts +0 -554
- package/tests/integration/mcp_actions_matrix.test.ts +0 -969
- package/tests/integration/mcp_auto_enhancement.test.ts +0 -659
- package/tests/integration/mcp_auto_schema_creation.test.ts +0 -658
- package/tests/integration/mcp_correction_variations.test.ts +0 -641
- package/tests/integration/mcp_entity_creation.test.ts +0 -333
- package/tests/integration/mcp_entity_variations.test.ts +0 -474
- package/tests/integration/mcp_graph_variations.test.ts +0 -380
- package/tests/integration/mcp_npm_check_update.test.ts +0 -54
- package/tests/integration/mcp_query_variations.test.ts +0 -392
- package/tests/integration/mcp_relationship_variations.test.ts +0 -433
- package/tests/integration/mcp_resource_variations.test.ts +0 -423
- package/tests/integration/mcp_resources.test.ts +0 -452
- package/tests/integration/mcp_schema_actions.test.ts +0 -673
- package/tests/integration/mcp_schema_variations.test.ts +0 -428
- package/tests/integration/mcp_store_parquet.test.ts +0 -466
- package/tests/integration/mcp_store_unstructured.test.ts +0 -413
- package/tests/integration/mcp_store_variations.test.ts +0 -452
- package/tests/integration/nonjson_csv_store_behavior.test.ts +0 -348
- package/tests/integration/nonjson_fixtures_mcp_replay.test.ts +0 -253
- package/tests/integration/observation_ingestion.test.ts +0 -362
- package/tests/integration/payload/payload_submission.test.ts +0 -296
- package/tests/integration/payload_compiler.test.ts +0 -347
- package/tests/integration/public_key_registry.test.ts +0 -204
- package/tests/integration/relationship_snapshots.test.ts +0 -235
- package/tests/integration/schema_recommendation_integration.test.ts +0 -418
- package/tests/integration/setup_v0.2.0_schemas.ts +0 -61
- package/tests/integration/v0.2.0_ingestion.test.ts +0 -527
- package/tests/services/auto_enhancement_converter_detection.test.ts +0 -663
- package/tests/services/auto_enhancement_processor.test.ts +0 -445
- package/tests/services/capability_registry.test.ts +0 -254
- package/tests/services/converter_detection_unit.test.ts +0 -168
- package/tests/services/encryption_service.test.ts +0 -147
- package/tests/services/entity_resolution.test.ts +0 -285
- package/tests/services/field_canonicalization.test.ts +0 -215
- package/tests/services/field_converters.test.ts +0 -211
- package/tests/services/field_validation.test.ts +0 -318
- package/tests/services/finances_field_mapping.test.ts +0 -245
- package/tests/services/interpretation.test.ts +0 -144
- package/tests/services/mcp_auth.test.ts +0 -34
- package/tests/services/observation_identity.test.ts +0 -154
- package/tests/services/payload_identity.test.ts +0 -325
- package/tests/services/payload_schema.test.ts +0 -282
- package/tests/services/raw_storage.test.ts +0 -546
- package/tests/services/schema_definitions.test.ts +0 -119
- package/tests/services/schema_recommendation.test.ts +0 -590
- package/tests/services/schema_registry_incremental.test.ts +0 -882
- package/tests/services/summary.test.ts +0 -26
- package/tests/unit/bigint_serialization.test.ts +0 -220
- package/tests/unit/observation_reducer_converters.test.ts +0 -430
- package/tests/unit/parquet_reader.test.ts +0 -155
- package/tests/unit/relationship_reducer.test.ts +0 -208
- package/tests/unit/schema_inference.test.ts +0 -358
- package/tests/unit/seo_metadata.test.ts +0 -52
- package/tsconfig.json +0 -22
- package/vite.config.ts +0 -165
- package/vitest.config.ts +0 -111
- package/vitest.global_setup.ts +0 -42
- package/vitest.setup.ts +0 -89
package/src/actions.ts
DELETED
|
@@ -1,4468 +0,0 @@
|
|
|
1
|
-
import express from "express";
|
|
2
|
-
import cors from "cors";
|
|
3
|
-
import helmet from "helmet";
|
|
4
|
-
import morgan from "morgan";
|
|
5
|
-
import rateLimit from "express-rate-limit";
|
|
6
|
-
import { z } from "zod";
|
|
7
|
-
import { randomUUID } from "node:crypto";
|
|
8
|
-
import { db } from "./db.js";
|
|
9
|
-
import { config } from "./config.js";
|
|
10
|
-
import fs from "fs";
|
|
11
|
-
import path from "path";
|
|
12
|
-
import yaml from "js-yaml";
|
|
13
|
-
import {
|
|
14
|
-
ensurePublicKeyRegistered,
|
|
15
|
-
getPublicKey,
|
|
16
|
-
getUserIdFromBearerToken,
|
|
17
|
-
isBearerTokenValid,
|
|
18
|
-
} from "./services/public_key_registry.js";
|
|
19
|
-
import { verifyRequest, parseAuthHeader } from "./crypto/auth.js";
|
|
20
|
-
import { encryptResponseMiddleware } from "./middleware/encrypt_response.js";
|
|
21
|
-
import { initServerKeys } from "./services/encryption_service.js";
|
|
22
|
-
import { storeRawContent } from "./services/raw_storage.js";
|
|
23
|
-
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
24
|
-
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
25
|
-
import { NeotomaServer } from "./server.js";
|
|
26
|
-
import { logger } from "./utils/logger.js";
|
|
27
|
-
import { OAuthError } from "./services/mcp_oauth_errors.js";
|
|
28
|
-
import { ensureLocalDevUser, LOCAL_DEV_USER_ID } from "./services/local_auth.js";
|
|
29
|
-
import { getSqliteDb } from "./repositories/sqlite/sqlite_client.js";
|
|
30
|
-
import { getMcpAuthToken } from "./crypto/mcp_auth_token.js";
|
|
31
|
-
import {
|
|
32
|
-
isOauthKeyCredentialValid,
|
|
33
|
-
normalizeOauthNextPath,
|
|
34
|
-
OAuthKeySessionStore,
|
|
35
|
-
} from "./services/oauth_key_gate.js";
|
|
36
|
-
import {
|
|
37
|
-
AnalyzeSchemaCandidatesRequestSchema,
|
|
38
|
-
CorrectEntityRequestSchema,
|
|
39
|
-
CreateRelationshipRequestSchema,
|
|
40
|
-
DeleteEntityRequestSchema,
|
|
41
|
-
DeleteRelationshipRequestSchema,
|
|
42
|
-
EntitiesQueryRequestSchema,
|
|
43
|
-
EntitySnapshotRequestSchema,
|
|
44
|
-
FieldProvenanceRequestSchema,
|
|
45
|
-
GetSchemaRecommendationsRequestSchema,
|
|
46
|
-
ListObservationsRequestSchema,
|
|
47
|
-
ListRelationshipsRequestSchema,
|
|
48
|
-
MergeEntitiesRequestSchema,
|
|
49
|
-
ObservationsQueryRequestSchema,
|
|
50
|
-
RegisterSchemaRequestSchema,
|
|
51
|
-
RelationshipSnapshotRequestSchema,
|
|
52
|
-
ReinterpretRequestSchema,
|
|
53
|
-
InterpretUninterpretedRequestSchema,
|
|
54
|
-
RestoreEntityRequestSchema,
|
|
55
|
-
RestoreRelationshipRequestSchema,
|
|
56
|
-
RetrieveEntityByIdentifierSchema,
|
|
57
|
-
RetrieveGraphNeighborhoodSchema,
|
|
58
|
-
RetrieveRelatedEntitiesSchema,
|
|
59
|
-
StoreRequestSchema,
|
|
60
|
-
StoreUnstructuredRequestSchema,
|
|
61
|
-
UpdateSchemaIncrementalRequestSchema,
|
|
62
|
-
} from "./shared/action_schemas.js";
|
|
63
|
-
import { queryEntitiesWithCount } from "./shared/action_handlers/entity_handlers.js";
|
|
64
|
-
import {
|
|
65
|
-
prepareEntitySnapshotWithEmbedding,
|
|
66
|
-
upsertEntitySnapshotWithEmbedding,
|
|
67
|
-
} from "./services/entity_snapshot_embedding.js";
|
|
68
|
-
// import { setupDocumentationRoutes } from "./routes/documentation.js";
|
|
69
|
-
|
|
70
|
-
type ErrorEnvelope = {
|
|
71
|
-
error_code: string;
|
|
72
|
-
message: string;
|
|
73
|
-
details?: Record<string, unknown>;
|
|
74
|
-
trace_id?: string;
|
|
75
|
-
timestamp: string;
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
export const app = express();
|
|
79
|
-
// Trust proxy headers (required for express-rate-limit when X-Forwarded-For is present)
|
|
80
|
-
app.set("trust proxy", true);
|
|
81
|
-
// Configure CSP to allow CDN scripts for the uploader and API connects
|
|
82
|
-
app.use(
|
|
83
|
-
helmet({
|
|
84
|
-
contentSecurityPolicy: {
|
|
85
|
-
useDefaults: true,
|
|
86
|
-
directives: {
|
|
87
|
-
defaultSrc: ["'self'"],
|
|
88
|
-
scriptSrc: ["'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net", "https://unpkg.com"],
|
|
89
|
-
connectSrc: ["'self'", "http:", "https:"],
|
|
90
|
-
imgSrc: ["'self'", "data:"],
|
|
91
|
-
styleSrc: ["'self'", "'unsafe-inline'", "https://unpkg.com"],
|
|
92
|
-
objectSrc: ["'none'"],
|
|
93
|
-
frameSrc: ["'self'"],
|
|
94
|
-
},
|
|
95
|
-
},
|
|
96
|
-
})
|
|
97
|
-
);
|
|
98
|
-
// Configure CORS to allow frontend origin
|
|
99
|
-
const corsOptions = {
|
|
100
|
-
origin:
|
|
101
|
-
process.env.NEOTOMA_FRONTEND_URL ||
|
|
102
|
-
process.env.FRONTEND_URL ||
|
|
103
|
-
"http://localhost:5195",
|
|
104
|
-
credentials: true,
|
|
105
|
-
methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"],
|
|
106
|
-
allowedHeaders: [
|
|
107
|
-
"Content-Type",
|
|
108
|
-
"Authorization",
|
|
109
|
-
"X-Requested-With",
|
|
110
|
-
"X-Connection-Id",
|
|
111
|
-
"mcp-session-id",
|
|
112
|
-
],
|
|
113
|
-
exposedHeaders: ["Content-Type"],
|
|
114
|
-
optionsSuccessStatus: 200, // Some legacy browsers (IE11, various SmartTVs) choke on 204
|
|
115
|
-
};
|
|
116
|
-
app.use(cors(corsOptions));
|
|
117
|
-
app.use(express.json({ limit: "10mb" }));
|
|
118
|
-
app.use(morgan("dev"));
|
|
119
|
-
|
|
120
|
-
// Rate limiters for OAuth endpoints
|
|
121
|
-
// validate.trustProxy: false — we use trust proxy behind one proxy; skip strict IP check
|
|
122
|
-
const rateLimitOptions = {
|
|
123
|
-
standardHeaders: true,
|
|
124
|
-
legacyHeaders: false,
|
|
125
|
-
validate: { trustProxy: false } as const,
|
|
126
|
-
};
|
|
127
|
-
const oauthInitiateLimit = rateLimit({
|
|
128
|
-
windowMs: 60 * 1000, // 1 minute
|
|
129
|
-
max: 5,
|
|
130
|
-
message: "Too many OAuth initiation requests, please try again later",
|
|
131
|
-
...rateLimitOptions,
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
const oauthCallbackLimit = rateLimit({
|
|
135
|
-
windowMs: 60 * 1000,
|
|
136
|
-
max: 10,
|
|
137
|
-
message: "Too many OAuth callback requests, please try again later",
|
|
138
|
-
...rateLimitOptions,
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
const oauthTokenLimit = rateLimit({
|
|
142
|
-
windowMs: 60 * 1000,
|
|
143
|
-
max: 20,
|
|
144
|
-
message: "Too many token requests, please try again later",
|
|
145
|
-
...rateLimitOptions,
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
const oauthRegisterLimit = rateLimit({
|
|
149
|
-
windowMs: 60 * 1000,
|
|
150
|
-
max: 10,
|
|
151
|
-
message: "Too many registration requests, please try again later",
|
|
152
|
-
...rateLimitOptions,
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
// Favicon (no-auth) to avoid 401 noise when not present on disk
|
|
156
|
-
app.get("/favicon.ico", (_req, res) => res.status(204).end());
|
|
157
|
-
|
|
158
|
-
// ============================================================================
|
|
159
|
-
// OAuth discovery (RFC 8414 / MCP Authorization) for Cursor and other clients
|
|
160
|
-
// ============================================================================
|
|
161
|
-
|
|
162
|
-
app.get("/.well-known/oauth-authorization-server", (req, res) => {
|
|
163
|
-
const base = config.apiBase;
|
|
164
|
-
res.setHeader("Content-Type", "application/json");
|
|
165
|
-
res.json({
|
|
166
|
-
issuer: base,
|
|
167
|
-
authorization_endpoint: `${base}/mcp/oauth/authorize`,
|
|
168
|
-
token_endpoint: `${base}/mcp/oauth/token`,
|
|
169
|
-
registration_endpoint: `${base}/mcp/oauth/register`,
|
|
170
|
-
scopes_supported: ["openid", "email"],
|
|
171
|
-
response_types_supported: ["code"],
|
|
172
|
-
code_challenge_methods_supported: ["S256"],
|
|
173
|
-
grant_types_supported: ["authorization_code", "refresh_token"],
|
|
174
|
-
token_endpoint_auth_methods_supported: ["none"],
|
|
175
|
-
});
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
app.get("/.well-known/oauth-protected-resource", async (req, res) => {
|
|
179
|
-
// oauth-repro pattern: return 401 on protected resource endpoint when unauthenticated
|
|
180
|
-
// Validate X-Connection-Id here so Cursor gets consistent invalid_token signal (helps trigger Connect prompt)
|
|
181
|
-
const authHeader = (req.headers["authorization"] || req.headers["Authorization"]) as
|
|
182
|
-
| string
|
|
183
|
-
| undefined;
|
|
184
|
-
const connectionIdHeader = req.headers["x-connection-id"] || req.headers["X-Connection-Id"];
|
|
185
|
-
|
|
186
|
-
// If X-Connection-Id is sent, validate it. Invalid/expired connection → 401 with error=invalid_token
|
|
187
|
-
// so Cursor may clear stored credentials and show Connect button.
|
|
188
|
-
if (connectionIdHeader && !authHeader?.startsWith("Bearer ")) {
|
|
189
|
-
try {
|
|
190
|
-
const { getAccessTokenForConnection } = await import("./services/mcp_oauth.js");
|
|
191
|
-
await getAccessTokenForConnection(connectionIdHeader as string);
|
|
192
|
-
} catch {
|
|
193
|
-
const wwwAuth = `Bearer resource_metadata="${config.apiBase}/.well-known/oauth-protected-resource", error="invalid_token", error_description="Connection invalid or expired. Remove X-Connection-Id from mcp.json and click Connect to re-authenticate."`;
|
|
194
|
-
res.setHeader("WWW-Authenticate", wwwAuth);
|
|
195
|
-
return res.status(401).json({
|
|
196
|
-
error: "invalid_token",
|
|
197
|
-
error_description:
|
|
198
|
-
"Connection invalid or expired. Remove X-Connection-Id from mcp.json and click Connect to re-authenticate.",
|
|
199
|
-
});
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
if (!authHeader?.startsWith("Bearer ") && !connectionIdHeader) {
|
|
204
|
-
res.setHeader(
|
|
205
|
-
"WWW-Authenticate",
|
|
206
|
-
`Bearer resource_metadata="${config.apiBase}/.well-known/oauth-protected-resource"`
|
|
207
|
-
);
|
|
208
|
-
return res.status(401).json({
|
|
209
|
-
error: "Unauthorized: Authentication required",
|
|
210
|
-
});
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
res.setHeader("Content-Type", "application/json");
|
|
214
|
-
res.json({
|
|
215
|
-
authorization_servers: [config.apiBase],
|
|
216
|
-
});
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
// Server info endpoint (no-auth) - exposes server port for MCP configuration
|
|
220
|
-
// When NEOTOMA_MCP_PROXY_URL or MCP_PROXY_URL is set (e.g. ngrok tunnel), mcpUrl uses it so "Add to Cursor" uses the proxy.
|
|
221
|
-
app.get("/server-info", (_req, res) => {
|
|
222
|
-
const httpPortEnv =
|
|
223
|
-
process.env.NEOTOMA_HTTP_PORT || process.env.HTTP_PORT;
|
|
224
|
-
const httpPort = httpPortEnv
|
|
225
|
-
? parseInt(httpPortEnv, 10)
|
|
226
|
-
: config.httpPort || 8080;
|
|
227
|
-
const mcpBase =
|
|
228
|
-
process.env.NEOTOMA_MCP_PROXY_URL ||
|
|
229
|
-
process.env.MCP_PROXY_URL ||
|
|
230
|
-
config.apiBase;
|
|
231
|
-
const base = mcpBase.replace(/\/$/, "");
|
|
232
|
-
const mcpUrl = base.endsWith("/mcp") ? base : `${base}/mcp`;
|
|
233
|
-
res.json({
|
|
234
|
-
httpPort,
|
|
235
|
-
apiBase: config.apiBase,
|
|
236
|
-
mcpUrl,
|
|
237
|
-
});
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
// ============================================================================
|
|
241
|
-
// MCP StreamableHTTP Endpoint (OAuth-enabled MCP transport)
|
|
242
|
-
// ============================================================================
|
|
243
|
-
|
|
244
|
-
// Store MCP transports by session ID
|
|
245
|
-
const mcpTransports = new Map<string, StreamableHTTPServerTransport>();
|
|
246
|
-
// Store server instances by session ID to preserve authentication state
|
|
247
|
-
const mcpServerInstances = new Map<string, NeotomaServer>();
|
|
248
|
-
|
|
249
|
-
/** True when request is to localhost/127.0.0.1 (local access). Tunnel requests have a non-local Host. */
|
|
250
|
-
function isLocalRequest(req: express.Request): boolean {
|
|
251
|
-
const host = (((req.headers["host"] || req.headers["Host"]) as string) || "")
|
|
252
|
-
.split(":")[0]
|
|
253
|
-
.toLowerCase();
|
|
254
|
-
return host === "localhost" || host === "127.0.0.1" || host === "[::1]" || host === "::1";
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
const OAUTH_KEY_SESSION_COOKIE = "neotoma_oauth_key_session";
|
|
258
|
-
const oauthKeySessions = new OAuthKeySessionStore();
|
|
259
|
-
|
|
260
|
-
function readCookie(req: express.Request, name: string): string | undefined {
|
|
261
|
-
const header = (req.headers["cookie"] || req.headers["Cookie"] || "") as string;
|
|
262
|
-
if (!header) return undefined;
|
|
263
|
-
const parts = header.split(";").map((part) => part.trim());
|
|
264
|
-
for (const part of parts) {
|
|
265
|
-
const eq = part.indexOf("=");
|
|
266
|
-
if (eq <= 0) continue;
|
|
267
|
-
const key = part.slice(0, eq);
|
|
268
|
-
if (key !== name) continue;
|
|
269
|
-
return decodeURIComponent(part.slice(eq + 1));
|
|
270
|
-
}
|
|
271
|
-
return undefined;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
function hasValidOAuthKeySession(req: express.Request): boolean {
|
|
275
|
-
const token = readCookie(req, OAUTH_KEY_SESSION_COOKIE);
|
|
276
|
-
return oauthKeySessions.isValid(token);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
function setOAuthKeySessionCookie(req: express.Request, res: express.Response): void {
|
|
280
|
-
const token = oauthKeySessions.create();
|
|
281
|
-
const forwardedProto =
|
|
282
|
-
((req.headers["x-forwarded-proto"] || req.headers["X-Forwarded-Proto"]) as string | undefined)
|
|
283
|
-
?.split(",")[0]
|
|
284
|
-
?.trim()
|
|
285
|
-
?.toLowerCase() || "";
|
|
286
|
-
const secure = req.secure || req.protocol === "https" || forwardedProto === "https";
|
|
287
|
-
res.cookie(OAUTH_KEY_SESSION_COOKIE, token, {
|
|
288
|
-
httpOnly: true,
|
|
289
|
-
sameSite: "lax",
|
|
290
|
-
secure,
|
|
291
|
-
path: "/mcp/oauth",
|
|
292
|
-
maxAge: 15 * 60 * 1000,
|
|
293
|
-
});
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
// MCP StreamableHTTP endpoint (GET, POST, DELETE)
|
|
297
|
-
// This endpoint enables Cursor's "Connect" button for OAuth authentication
|
|
298
|
-
app.all("/mcp", async (req, res) => {
|
|
299
|
-
try {
|
|
300
|
-
const sessionId = req.headers["mcp-session-id"] as string | undefined;
|
|
301
|
-
|
|
302
|
-
// Check for authentication BEFORE processing MCP requests
|
|
303
|
-
// When encryption off: no auth by default; optional NEOTOMA_BEARER_TOKEN (or OAuth)
|
|
304
|
-
// When encryption on: require Bearer token derived from private key (same key as data encryption)
|
|
305
|
-
const authHeader = (req.headers["authorization"] || req.headers["Authorization"]) as
|
|
306
|
-
| string
|
|
307
|
-
| undefined;
|
|
308
|
-
let connectionIdHeader = (req.headers["x-connection-id"] || req.headers["X-Connection-Id"]) as
|
|
309
|
-
| string
|
|
310
|
-
| undefined;
|
|
311
|
-
|
|
312
|
-
if (config.encryption.enabled) {
|
|
313
|
-
// Encryption on: require static token derived from user's private key
|
|
314
|
-
const expectedToken = getMcpAuthToken();
|
|
315
|
-
if (authHeader?.startsWith("Bearer ") && expectedToken) {
|
|
316
|
-
const token = authHeader.slice(7).trim();
|
|
317
|
-
if (token === expectedToken) {
|
|
318
|
-
req.headers["x-connection-id"] = "dev-local";
|
|
319
|
-
connectionIdHeader = "dev-local";
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
} else {
|
|
323
|
-
// Encryption off: local requests can use no-auth. HTTP (insecure) defaults to anonymous 000... user; HTTPS/localhost can use dev-local.
|
|
324
|
-
if (!authHeader?.startsWith("Bearer ") && !connectionIdHeader) {
|
|
325
|
-
if (isLocalRequest(req)) {
|
|
326
|
-
const isInsecure = req.protocol === "http" || !(req as any).secure;
|
|
327
|
-
if (isInsecure) {
|
|
328
|
-
req.headers["x-connection-id"] = "dev-local-http";
|
|
329
|
-
connectionIdHeader = "dev-local-http";
|
|
330
|
-
} else {
|
|
331
|
-
req.headers["x-connection-id"] = "dev-local";
|
|
332
|
-
connectionIdHeader = "dev-local";
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
if (authHeader?.startsWith("Bearer ") && process.env.NEOTOMA_BEARER_TOKEN) {
|
|
337
|
-
const token = authHeader.slice(7).trim();
|
|
338
|
-
if (token === process.env.NEOTOMA_BEARER_TOKEN) {
|
|
339
|
-
req.headers["x-connection-id"] = "dev-local";
|
|
340
|
-
connectionIdHeader = "dev-local";
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
const hasAuth = !!(authHeader?.startsWith("Bearer ") || connectionIdHeader);
|
|
346
|
-
|
|
347
|
-
// When encryption is on, only the key-derived token is accepted
|
|
348
|
-
if (config.encryption.enabled && connectionIdHeader !== "dev-local") {
|
|
349
|
-
const wwwAuthHeader = `Bearer resource_metadata="${config.apiBase}/.well-known/oauth-protected-resource"`;
|
|
350
|
-
res.setHeader("WWW-Authenticate", wwwAuthHeader);
|
|
351
|
-
const msg =
|
|
352
|
-
"Encryption is enabled. Use MCP token from your private key: run 'neotoma auth mcp-token' and add Authorization: Bearer <token> to mcp.json.";
|
|
353
|
-
if (req.method === "POST" && req.body && typeof req.body === "object") {
|
|
354
|
-
return res.status(401).json({
|
|
355
|
-
jsonrpc: "2.0",
|
|
356
|
-
error: { code: -32001, message: msg },
|
|
357
|
-
id: req.body?.id ?? null,
|
|
358
|
-
});
|
|
359
|
-
}
|
|
360
|
-
return res.status(401).json({ error: "Unauthorized", error_description: msg });
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
// Return 401 if: no auth (regardless of session existence)
|
|
364
|
-
// This matches oauth-repro's app.all("/mcp*", ...) pattern
|
|
365
|
-
// After OAuth, Cursor will send Bearer token or connection_id, so we allow through
|
|
366
|
-
// Fix: Changed from (!sessionId && !hasAuth) to (!hasAuth) to ensure 401 on first protected call
|
|
367
|
-
// even if Cursor establishes a session first (via GET for SSE)
|
|
368
|
-
if (!hasAuth) {
|
|
369
|
-
const wwwAuthHeader = `Bearer resource_metadata="${config.apiBase}/.well-known/oauth-protected-resource"`;
|
|
370
|
-
res.setHeader("WWW-Authenticate", wwwAuthHeader);
|
|
371
|
-
const unauthMessage =
|
|
372
|
-
config.requireKeyForOauth
|
|
373
|
-
? "Unauthorized: Authentication required (key-authenticated OAuth or Bearer token)."
|
|
374
|
-
: "Unauthorized: Authentication required";
|
|
375
|
-
|
|
376
|
-
// For POST requests with JSON-RPC body, return JSON-RPC error format
|
|
377
|
-
if (req.method === "POST" && req.body && typeof req.body === "object") {
|
|
378
|
-
return res.status(401).json({
|
|
379
|
-
jsonrpc: "2.0",
|
|
380
|
-
error: {
|
|
381
|
-
code: -32001,
|
|
382
|
-
message: unauthMessage,
|
|
383
|
-
},
|
|
384
|
-
id: req.body?.id ?? null,
|
|
385
|
-
});
|
|
386
|
-
}
|
|
387
|
-
// For GET/DELETE requests, return simple 401
|
|
388
|
-
return res.status(401).json({
|
|
389
|
-
error: unauthMessage,
|
|
390
|
-
});
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
// Validate X-Connection-Id when that is the auth method (no Bearer). Invalid IDs return 401
|
|
394
|
-
// so Cursor shows Connect button instead of blocking on "Loading tools".
|
|
395
|
-
// Skip validation for dev-local and dev-local-http (no-auth defaults).
|
|
396
|
-
if (
|
|
397
|
-
connectionIdHeader &&
|
|
398
|
-
connectionIdHeader !== "dev-local" &&
|
|
399
|
-
connectionIdHeader !== "dev-local-http" &&
|
|
400
|
-
!authHeader?.startsWith("Bearer ")
|
|
401
|
-
) {
|
|
402
|
-
try {
|
|
403
|
-
const { getAccessTokenForConnection } = await import("./services/mcp_oauth.js");
|
|
404
|
-
await getAccessTokenForConnection(connectionIdHeader as string);
|
|
405
|
-
} catch {
|
|
406
|
-
logger.info(
|
|
407
|
-
`[MCP HTTP] Invalid or expired X-Connection-Id: ${connectionIdHeader}. Returning 401 to show Connect button.`
|
|
408
|
-
);
|
|
409
|
-
// RFC 6750: error=invalid_token signals client to clear credentials and re-authenticate.
|
|
410
|
-
// Use consistent error format so Cursor may show Connect prompt.
|
|
411
|
-
const desc =
|
|
412
|
-
"Connection invalid or expired. Remove X-Connection-Id from mcp.json and click Connect to re-authenticate.";
|
|
413
|
-
const wwwAuthHeader = `Bearer resource_metadata="${config.apiBase}/.well-known/oauth-protected-resource", error="invalid_token", error_description="${desc}"`;
|
|
414
|
-
res.setHeader("WWW-Authenticate", wwwAuthHeader);
|
|
415
|
-
if (req.method === "POST" && req.body && typeof req.body === "object") {
|
|
416
|
-
return res.status(401).json({
|
|
417
|
-
jsonrpc: "2.0",
|
|
418
|
-
error: {
|
|
419
|
-
code: -32001,
|
|
420
|
-
message: desc,
|
|
421
|
-
},
|
|
422
|
-
id: req.body?.id ?? null,
|
|
423
|
-
});
|
|
424
|
-
}
|
|
425
|
-
return res.status(401).json({
|
|
426
|
-
error: "invalid_token",
|
|
427
|
-
error_description: desc,
|
|
428
|
-
});
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
// Get or create transport
|
|
433
|
-
let transport = sessionId ? mcpTransports.get(sessionId) : undefined;
|
|
434
|
-
|
|
435
|
-
if (!transport && req.method === "POST" && isInitializeRequest(req.body)) {
|
|
436
|
-
// Create new server instance for each session to ensure clean auth state
|
|
437
|
-
// This ensures OAuth flow is required for each new connection
|
|
438
|
-
const serverInstance = new NeotomaServer();
|
|
439
|
-
const connectionIdFromReq = (req.headers["x-connection-id"] ||
|
|
440
|
-
req.headers["X-Connection-Id"]) as string | undefined;
|
|
441
|
-
if (connectionIdFromReq) {
|
|
442
|
-
serverInstance.setSessionConnectionId(connectionIdFromReq);
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
// Create new transport for initialization
|
|
446
|
-
transport = new StreamableHTTPServerTransport({
|
|
447
|
-
sessionIdGenerator: () => randomUUID(),
|
|
448
|
-
onsessioninitialized: (sid) => {
|
|
449
|
-
if (transport) {
|
|
450
|
-
mcpTransports.set(sid, transport);
|
|
451
|
-
// Store server instance by session ID to preserve authentication state
|
|
452
|
-
mcpServerInstances.set(sid, serverInstance);
|
|
453
|
-
logger.info(`[MCP HTTP] Session initialized: ${sid}, server instance stored`);
|
|
454
|
-
}
|
|
455
|
-
},
|
|
456
|
-
});
|
|
457
|
-
|
|
458
|
-
transport.onclose = () => {
|
|
459
|
-
if (transport?.sessionId) {
|
|
460
|
-
mcpTransports.delete(transport.sessionId);
|
|
461
|
-
mcpServerInstances.delete(transport.sessionId);
|
|
462
|
-
logger.error(`[MCP HTTP] Session closed: ${transport.sessionId}`);
|
|
463
|
-
}
|
|
464
|
-
};
|
|
465
|
-
|
|
466
|
-
// Connect transport to server
|
|
467
|
-
await serverInstance.runHTTP(transport);
|
|
468
|
-
} else if (!transport) {
|
|
469
|
-
return res.status(400).json({
|
|
470
|
-
jsonrpc: "2.0",
|
|
471
|
-
error: { code: -32000, message: "Bad Request: No valid session ID provided" },
|
|
472
|
-
id: null,
|
|
473
|
-
});
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
// Handle request with the transport
|
|
477
|
-
await transport.handleRequest(req, res, req.body);
|
|
478
|
-
} catch (error: any) {
|
|
479
|
-
logger.error("[MCP HTTP] Request error:", error);
|
|
480
|
-
if (!res.headersSent) {
|
|
481
|
-
res.status(500).json({
|
|
482
|
-
jsonrpc: "2.0",
|
|
483
|
-
error: { code: -32603, message: "Internal server error" },
|
|
484
|
-
id: null,
|
|
485
|
-
});
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
});
|
|
489
|
-
|
|
490
|
-
// Basic redaction helpers for safer debug logs
|
|
491
|
-
const SENSITIVE_FIELDS = new Set([
|
|
492
|
-
"token",
|
|
493
|
-
"access_token",
|
|
494
|
-
"accessToken",
|
|
495
|
-
"public_token",
|
|
496
|
-
"publicToken",
|
|
497
|
-
"bearer_token",
|
|
498
|
-
"bearerToken",
|
|
499
|
-
"password",
|
|
500
|
-
"secret",
|
|
501
|
-
"api_key",
|
|
502
|
-
"apiKey",
|
|
503
|
-
"client_secret",
|
|
504
|
-
"clientSecret",
|
|
505
|
-
"authorization",
|
|
506
|
-
"Authorization",
|
|
507
|
-
]);
|
|
508
|
-
|
|
509
|
-
function redactHeaders(headers: Record<string, unknown>): Record<string, unknown> {
|
|
510
|
-
const clone = { ...headers } as Record<string, unknown>;
|
|
511
|
-
if (clone.authorization) clone.authorization = "[REDACTED]";
|
|
512
|
-
if (clone.Authorization) clone.Authorization = "[REDACTED]";
|
|
513
|
-
return clone;
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
function redactSensitiveFields(obj: unknown): unknown {
|
|
517
|
-
if (!obj || typeof obj !== "object") {
|
|
518
|
-
return obj;
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
if (Array.isArray(obj)) {
|
|
522
|
-
return obj.map(redactSensitiveFields);
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
const redacted = { ...(obj as Record<string, unknown>) };
|
|
526
|
-
for (const key in redacted) {
|
|
527
|
-
const lowerKey = key.toLowerCase();
|
|
528
|
-
if (SENSITIVE_FIELDS.has(key) || SENSITIVE_FIELDS.has(lowerKey)) {
|
|
529
|
-
redacted[key] = "[REDACTED]";
|
|
530
|
-
} else if (typeof redacted[key] === "object" && redacted[key] !== null) {
|
|
531
|
-
redacted[key] = redactSensitiveFields(redacted[key]);
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
return redacted;
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
function logDebug(event: string, req: express.Request, extra?: Record<string, unknown>): void {
|
|
538
|
-
const safe = {
|
|
539
|
-
method: req.method,
|
|
540
|
-
path: req.path,
|
|
541
|
-
query: redactSensitiveFields(req.query),
|
|
542
|
-
headers: redactHeaders(req.headers as Record<string, unknown>),
|
|
543
|
-
body: redactSensitiveFields(req.body),
|
|
544
|
-
...(extra ? (redactSensitiveFields(extra) as Record<string, unknown>) : {}),
|
|
545
|
-
};
|
|
546
|
-
// eslint-disable-next-line no-console
|
|
547
|
-
console.debug(`[DEBUG] ${event}`, safe);
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
function logWarn(event: string, req: express.Request, extra?: Record<string, unknown>): void {
|
|
551
|
-
const safe = {
|
|
552
|
-
method: req.method,
|
|
553
|
-
path: req.path,
|
|
554
|
-
query: redactSensitiveFields(req.query),
|
|
555
|
-
headers: redactHeaders(req.headers as Record<string, unknown>),
|
|
556
|
-
body: redactSensitiveFields(req.body),
|
|
557
|
-
...(extra ? (redactSensitiveFields(extra) as Record<string, unknown>) : {}),
|
|
558
|
-
};
|
|
559
|
-
// eslint-disable-next-line no-console
|
|
560
|
-
console.warn(`[WARN] ${event}`, safe);
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
function logError(
|
|
564
|
-
event: string,
|
|
565
|
-
req: express.Request,
|
|
566
|
-
error: unknown,
|
|
567
|
-
extra?: Record<string, unknown>
|
|
568
|
-
): void {
|
|
569
|
-
const payload = {
|
|
570
|
-
method: req.method,
|
|
571
|
-
path: req.path,
|
|
572
|
-
query: redactSensitiveFields(req.query),
|
|
573
|
-
headers: redactHeaders(req.headers as Record<string, unknown>),
|
|
574
|
-
body: redactSensitiveFields(req.body),
|
|
575
|
-
error:
|
|
576
|
-
error instanceof Error
|
|
577
|
-
? { name: error.name, message: error.message, stack: error.stack }
|
|
578
|
-
: redactSensitiveFields(error),
|
|
579
|
-
...(extra ? (redactSensitiveFields(extra) as Record<string, unknown>) : {}),
|
|
580
|
-
};
|
|
581
|
-
// eslint-disable-next-line no-console
|
|
582
|
-
console.error(`[ERROR] ${event}`, payload);
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
function buildErrorEnvelope(
|
|
586
|
-
errorCode: string,
|
|
587
|
-
message: string,
|
|
588
|
-
details?: Record<string, unknown>,
|
|
589
|
-
traceId?: string
|
|
590
|
-
): ErrorEnvelope {
|
|
591
|
-
return {
|
|
592
|
-
error_code: errorCode,
|
|
593
|
-
message,
|
|
594
|
-
details,
|
|
595
|
-
trace_id: traceId,
|
|
596
|
-
timestamp: new Date().toISOString(),
|
|
597
|
-
};
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
function sendError(
|
|
601
|
-
res: express.Response,
|
|
602
|
-
status: number,
|
|
603
|
-
errorCode: string,
|
|
604
|
-
message: string,
|
|
605
|
-
details?: Record<string, unknown>
|
|
606
|
-
): express.Response {
|
|
607
|
-
return res.status(status).json(buildErrorEnvelope(errorCode, message, details));
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
function sendValidationError(res: express.Response, issues: unknown): express.Response {
|
|
611
|
-
return res.status(400).json(
|
|
612
|
-
buildErrorEnvelope("VALIDATION_INVALID_FORMAT", "Invalid request payload.", {
|
|
613
|
-
issues,
|
|
614
|
-
})
|
|
615
|
-
);
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
// Public health endpoint (no auth)
|
|
619
|
-
app.get("/health", (_req, res) => {
|
|
620
|
-
return res.json({ ok: true });
|
|
621
|
-
});
|
|
622
|
-
|
|
623
|
-
// ============================================================================
|
|
624
|
-
// MCP OAuth Endpoints
|
|
625
|
-
// ============================================================================
|
|
626
|
-
|
|
627
|
-
// Initiate MCP OAuth flow
|
|
628
|
-
app.post("/mcp/oauth/initiate", oauthInitiateLimit, async (req, res) => {
|
|
629
|
-
try {
|
|
630
|
-
const { connection_id, client_name, redirect_uri } = req.body;
|
|
631
|
-
|
|
632
|
-
if (!connection_id || typeof connection_id !== "string") {
|
|
633
|
-
return sendError(res, 400, "VALIDATION_MISSING_FIELD", "connection_id is required");
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
const { initiateOAuthFlow } = await import("./services/mcp_oauth.js");
|
|
637
|
-
const frontendBase =
|
|
638
|
-
process.env.NEOTOMA_FRONTEND_URL ||
|
|
639
|
-
process.env.FRONTEND_URL ||
|
|
640
|
-
"http://localhost:5195";
|
|
641
|
-
const finalRedirectUri =
|
|
642
|
-
typeof redirect_uri === "string" && redirect_uri.length > 0
|
|
643
|
-
? redirect_uri
|
|
644
|
-
: config.storageBackend === "local"
|
|
645
|
-
? `${frontendBase}/oauth`
|
|
646
|
-
: `${config.apiBase}/mcp/oauth/callback`;
|
|
647
|
-
const result = await initiateOAuthFlow(connection_id, client_name, finalRedirectUri);
|
|
648
|
-
|
|
649
|
-
return res.json(result);
|
|
650
|
-
} catch (error: any) {
|
|
651
|
-
logError("MCPOAuthInitiate", req, error);
|
|
652
|
-
|
|
653
|
-
// Check if it's a structured OAuthError
|
|
654
|
-
if (error instanceof OAuthError) {
|
|
655
|
-
const oauthError = error as {
|
|
656
|
-
code: string;
|
|
657
|
-
message: string;
|
|
658
|
-
statusCode: number;
|
|
659
|
-
retryable?: boolean;
|
|
660
|
-
details?: Record<string, any>;
|
|
661
|
-
};
|
|
662
|
-
|
|
663
|
-
return res.status(oauthError.statusCode).json({
|
|
664
|
-
error_code: oauthError.code,
|
|
665
|
-
error: oauthError.message,
|
|
666
|
-
message: oauthError.message,
|
|
667
|
-
retryable: oauthError.retryable || false,
|
|
668
|
-
details: oauthError.details,
|
|
669
|
-
timestamp: new Date().toISOString(),
|
|
670
|
-
...(oauthError.code === "OAUTH_CLIENT_REGISTRATION_FAILED" && {
|
|
671
|
-
hint: "Enable 'Allow Dynamic OAuth Apps' in auth server, or set NEOTOMA_OAUTH_CLIENT_ID in .env file",
|
|
672
|
-
}),
|
|
673
|
-
});
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
// Fallback for non-OAuth errors
|
|
677
|
-
const isConfigError =
|
|
678
|
-
error.message?.includes("client_id not configured") ||
|
|
679
|
-
error.message?.includes("OAUTH_CLIENT_ID");
|
|
680
|
-
const statusCode = isConfigError ? 400 : 500;
|
|
681
|
-
|
|
682
|
-
return res.status(statusCode).json({
|
|
683
|
-
error_code: isConfigError ? "OAUTH_CLIENT_REGISTRATION_FAILED" : "INTERNAL_ERROR",
|
|
684
|
-
error: error.message,
|
|
685
|
-
message: error.message,
|
|
686
|
-
timestamp: new Date().toISOString(),
|
|
687
|
-
...(isConfigError && {
|
|
688
|
-
hint: "Enable 'Allow Dynamic OAuth Apps' in auth server, or set NEOTOMA_OAUTH_CLIENT_ID in .env file",
|
|
689
|
-
}),
|
|
690
|
-
});
|
|
691
|
-
}
|
|
692
|
-
});
|
|
693
|
-
|
|
694
|
-
// OAuth callback endpoint
|
|
695
|
-
app.get("/mcp/oauth/callback", oauthCallbackLimit, async (req, res) => {
|
|
696
|
-
try {
|
|
697
|
-
if (config.storageBackend === "local") {
|
|
698
|
-
return res
|
|
699
|
-
.status(400)
|
|
700
|
-
.send("Local OAuth callback is disabled. Use /mcp/oauth/local-login.");
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
const { code, state } = req.query;
|
|
704
|
-
|
|
705
|
-
if (!code || !state || typeof code !== "string" || typeof state !== "string") {
|
|
706
|
-
return res.status(400).send("Missing code or state parameter");
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
const { handleOAuthCallback } = await import("./services/mcp_oauth.js");
|
|
710
|
-
const { connectionId, redirectUri, clientState } = await handleOAuthCallback(code, state);
|
|
711
|
-
|
|
712
|
-
// If client provided a redirect_uri (Cursor, CLI, or other), send code+state there
|
|
713
|
-
const isLocalCallback =
|
|
714
|
-
redirectUri &&
|
|
715
|
-
(redirectUri.startsWith("cursor://") ||
|
|
716
|
-
redirectUri.startsWith("http://127.0.0.1") ||
|
|
717
|
-
redirectUri.startsWith("http://localhost"));
|
|
718
|
-
if (isLocalCallback) {
|
|
719
|
-
const params = new URLSearchParams({ code: connectionId, state: clientState ?? state });
|
|
720
|
-
const redirectUrl = `${redirectUri}?${params.toString()}`;
|
|
721
|
-
return res.redirect(redirectUrl);
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
// Default: redirect to frontend success page
|
|
725
|
-
const frontendBase =
|
|
726
|
-
process.env.NEOTOMA_FRONTEND_URL ||
|
|
727
|
-
process.env.FRONTEND_URL ||
|
|
728
|
-
"http://localhost:5195";
|
|
729
|
-
const successUrl = `${frontendBase}/oauth?connection_id=${encodeURIComponent(connectionId)}&status=success`;
|
|
730
|
-
return res.redirect(successUrl);
|
|
731
|
-
} catch (error: any) {
|
|
732
|
-
logError("MCPOAuthCallback", req, error);
|
|
733
|
-
|
|
734
|
-
// Extract structured error information
|
|
735
|
-
let errorCode = "OAUTH_TOKEN_EXCHANGE_FAILED";
|
|
736
|
-
let errorMessage = error.message || "OAuth callback failed";
|
|
737
|
-
let errorDetails: string | undefined;
|
|
738
|
-
|
|
739
|
-
if (error instanceof OAuthError) {
|
|
740
|
-
errorCode = error.code;
|
|
741
|
-
errorMessage = error.message;
|
|
742
|
-
if (error.details) {
|
|
743
|
-
errorDetails = JSON.stringify(error.details);
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
// Build error URL with structured error information
|
|
748
|
-
const params = new URLSearchParams({
|
|
749
|
-
status: "error",
|
|
750
|
-
error_code: errorCode,
|
|
751
|
-
error: errorMessage,
|
|
752
|
-
});
|
|
753
|
-
if (errorDetails) {
|
|
754
|
-
params.set("error_details", errorDetails);
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
const frontendBaseForError =
|
|
758
|
-
process.env.NEOTOMA_FRONTEND_URL ||
|
|
759
|
-
process.env.FRONTEND_URL ||
|
|
760
|
-
"http://localhost:5195";
|
|
761
|
-
const errorUrl = `${frontendBaseForError}/oauth?${params.toString()}`;
|
|
762
|
-
return res.redirect(errorUrl);
|
|
763
|
-
}
|
|
764
|
-
});
|
|
765
|
-
|
|
766
|
-
// RFC 8414 authorization endpoint (GET) for Cursor and other OAuth clients
|
|
767
|
-
app.get("/mcp/oauth/key-auth", async (req, res) => {
|
|
768
|
-
if (!config.requireKeyForOauth) {
|
|
769
|
-
const nextPath = normalizeOauthNextPath((req.query.next as string | undefined) || undefined);
|
|
770
|
-
return res.redirect(nextPath);
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
const nextPath = normalizeOauthNextPath((req.query.next as string | undefined) || undefined);
|
|
774
|
-
if (hasValidOAuthKeySession(req)) {
|
|
775
|
-
return res.redirect(nextPath);
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
779
|
-
return res.send(`<!DOCTYPE html>
|
|
780
|
-
<html>
|
|
781
|
-
<head>
|
|
782
|
-
<meta charset="utf-8" />
|
|
783
|
-
<title>Neotoma key authentication</title>
|
|
784
|
-
</head>
|
|
785
|
-
<body>
|
|
786
|
-
<h1>Authenticate with your key</h1>
|
|
787
|
-
<p>OAuth requires key authentication before continuing. Provide your private key hex or mnemonic.</p>
|
|
788
|
-
<form method="post" action="/mcp/oauth/key-auth">
|
|
789
|
-
<input type="hidden" name="next" value="${nextPath}" />
|
|
790
|
-
<div>
|
|
791
|
-
<label for="private_key_hex">Private key hex (optional)</label><br />
|
|
792
|
-
<input id="private_key_hex" name="private_key_hex" type="password" autocomplete="off" />
|
|
793
|
-
</div>
|
|
794
|
-
<div style="margin-top: 12px;">
|
|
795
|
-
<label for="mnemonic">Mnemonic (optional)</label><br />
|
|
796
|
-
<textarea id="mnemonic" name="mnemonic" rows="3" cols="80"></textarea>
|
|
797
|
-
</div>
|
|
798
|
-
<div style="margin-top: 12px;">
|
|
799
|
-
<label for="mnemonic_passphrase">Mnemonic passphrase (optional)</label><br />
|
|
800
|
-
<input id="mnemonic_passphrase" name="mnemonic_passphrase" type="password" autocomplete="off" />
|
|
801
|
-
</div>
|
|
802
|
-
<div style="margin-top: 14px;">
|
|
803
|
-
<button type="submit">Authenticate and continue</button>
|
|
804
|
-
</div>
|
|
805
|
-
</form>
|
|
806
|
-
<p style="margin-top: 16px;">
|
|
807
|
-
If you do not have key credentials configured for this server, OAuth is unavailable. Use
|
|
808
|
-
<code>NEOTOMA_BEARER_TOKEN</code> and configure MCP with <code>Authorization: Bearer <token></code>.
|
|
809
|
-
</p>
|
|
810
|
-
</body>
|
|
811
|
-
</html>`);
|
|
812
|
-
});
|
|
813
|
-
|
|
814
|
-
app.post("/mcp/oauth/key-auth", express.urlencoded({ extended: true }), async (req, res) => {
|
|
815
|
-
const nextPath = normalizeOauthNextPath((req.body?.next as string | undefined) || undefined);
|
|
816
|
-
|
|
817
|
-
if (!config.requireKeyForOauth) {
|
|
818
|
-
return res.redirect(nextPath);
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
const result = isOauthKeyCredentialValid({
|
|
822
|
-
privateKeyHex: req.body?.private_key_hex,
|
|
823
|
-
mnemonic: req.body?.mnemonic,
|
|
824
|
-
mnemonicPassphrase: req.body?.mnemonic_passphrase,
|
|
825
|
-
});
|
|
826
|
-
|
|
827
|
-
if (!result.ok) {
|
|
828
|
-
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
829
|
-
return res.status(401).send(`<!DOCTYPE html>
|
|
830
|
-
<html>
|
|
831
|
-
<head>
|
|
832
|
-
<meta charset="utf-8" />
|
|
833
|
-
<title>Key authentication failed</title>
|
|
834
|
-
</head>
|
|
835
|
-
<body>
|
|
836
|
-
<h1>Key authentication failed</h1>
|
|
837
|
-
<p>${result.reason || "Unable to validate key credentials."}</p>
|
|
838
|
-
<p><a href="/mcp/oauth/key-auth?next=${encodeURIComponent(nextPath)}">Try again</a></p>
|
|
839
|
-
</body>
|
|
840
|
-
</html>`);
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
setOAuthKeySessionCookie(req, res);
|
|
844
|
-
return res.redirect(nextPath);
|
|
845
|
-
});
|
|
846
|
-
|
|
847
|
-
// RFC 8414 authorization endpoint (GET) for Cursor and other OAuth clients
|
|
848
|
-
app.get("/mcp/oauth/authorize", async (req, res) => {
|
|
849
|
-
try {
|
|
850
|
-
const redirect_uri = req.query.redirect_uri as string | undefined;
|
|
851
|
-
const state = req.query.state as string | undefined;
|
|
852
|
-
const code_challenge = req.query.code_challenge as string | undefined;
|
|
853
|
-
const code_challenge_method = req.query.code_challenge_method as string | undefined;
|
|
854
|
-
const client_id = req.query.client_id as string | undefined;
|
|
855
|
-
const dev_stub = req.query.dev_stub as string | undefined;
|
|
856
|
-
|
|
857
|
-
if (!redirect_uri) {
|
|
858
|
-
return res.status(400).send("redirect_uri is required");
|
|
859
|
-
}
|
|
860
|
-
if (!state) {
|
|
861
|
-
return res.status(400).send("state is required");
|
|
862
|
-
}
|
|
863
|
-
if (!code_challenge || code_challenge_method !== "S256") {
|
|
864
|
-
return res.status(400).send("code_challenge and code_challenge_method=S256 are required");
|
|
865
|
-
}
|
|
866
|
-
if (config.requireKeyForOauth && !hasValidOAuthKeySession(req)) {
|
|
867
|
-
const nextPath = normalizeOauthNextPath(req.originalUrl);
|
|
868
|
-
return res.redirect(`/mcp/oauth/key-auth?next=${encodeURIComponent(nextPath)}`);
|
|
869
|
-
}
|
|
870
|
-
if (dev_stub === "1" || dev_stub === "true") {
|
|
871
|
-
return res
|
|
872
|
-
.status(400)
|
|
873
|
-
.send("dev_stub is disabled. OAuth requires key authentication via /mcp/oauth/key-auth.");
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
if (config.storageBackend === "local") {
|
|
877
|
-
// When reached via tunnel, only allow redirect_uri to localhost or known app schemes
|
|
878
|
-
if (!isLocalRequest(req)) {
|
|
879
|
-
const { isRedirectUriAllowedForTunnel } = await import("./services/mcp_oauth.js");
|
|
880
|
-
if (!isRedirectUriAllowedForTunnel(redirect_uri)) {
|
|
881
|
-
return res.status(400).send(
|
|
882
|
-
"redirect_uri is not allowed when connecting via a tunnel. Use cursor://, http://localhost, or http://127.0.0.1."
|
|
883
|
-
);
|
|
884
|
-
}
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
const { randomUUID } = await import("node:crypto");
|
|
888
|
-
const connectionId = randomUUID();
|
|
889
|
-
const { createLocalAuthorizationRequest } = await import("./services/mcp_oauth.js");
|
|
890
|
-
|
|
891
|
-
const authRequest = await createLocalAuthorizationRequest({
|
|
892
|
-
connectionId,
|
|
893
|
-
redirectUri: redirect_uri,
|
|
894
|
-
clientState: state,
|
|
895
|
-
codeChallenge: code_challenge,
|
|
896
|
-
});
|
|
897
|
-
// Keep local OAuth redirects on the current origin (tunnel or localhost) even if
|
|
898
|
-
// authRequest.authUrl was built from a different absolute base URL.
|
|
899
|
-
try {
|
|
900
|
-
const parsed = new URL(authRequest.authUrl);
|
|
901
|
-
return res.redirect(`${parsed.pathname}${parsed.search}`);
|
|
902
|
-
} catch {
|
|
903
|
-
return res.redirect(authRequest.authUrl);
|
|
904
|
-
}
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
const { randomUUID } = await import("node:crypto");
|
|
908
|
-
const connectionId = randomUUID();
|
|
909
|
-
const { initiateOAuthFlow } = await import("./services/mcp_oauth.js");
|
|
910
|
-
const result = await initiateOAuthFlow(
|
|
911
|
-
connectionId,
|
|
912
|
-
client_id ?? undefined,
|
|
913
|
-
redirect_uri,
|
|
914
|
-
state
|
|
915
|
-
);
|
|
916
|
-
|
|
917
|
-
return res.redirect(result.authUrl);
|
|
918
|
-
} catch (error: any) {
|
|
919
|
-
logError("MCPOAuthAuthorize", req, error);
|
|
920
|
-
return res.status(500).send(error.message ?? "Authorization failed");
|
|
921
|
-
}
|
|
922
|
-
});
|
|
923
|
-
|
|
924
|
-
// Local OAuth login page (local backend only)
|
|
925
|
-
// With MCP-style auth: encryption off = auto dev-local, encryption on = Bearer token only (no OAuth)
|
|
926
|
-
app.get("/mcp/oauth/local-login", async (req, res) => {
|
|
927
|
-
if (config.storageBackend !== "local") {
|
|
928
|
-
return res.status(404).send("Not found");
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
const state = (req.query.state as string | undefined)?.trim();
|
|
932
|
-
if (!state) {
|
|
933
|
-
return res.status(400).send("state is required");
|
|
934
|
-
}
|
|
935
|
-
if (config.requireKeyForOauth && !hasValidOAuthKeySession(req)) {
|
|
936
|
-
const nextPath = normalizeOauthNextPath(req.originalUrl);
|
|
937
|
-
return res.redirect(`/mcp/oauth/key-auth?next=${encodeURIComponent(nextPath)}`);
|
|
938
|
-
}
|
|
939
|
-
|
|
940
|
-
// When encryption is enabled: OAuth is not supported (MCP handler requires key-derived token)
|
|
941
|
-
if (config.encryption.enabled) {
|
|
942
|
-
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
943
|
-
return res.send(`<!DOCTYPE html>
|
|
944
|
-
<html>
|
|
945
|
-
<head>
|
|
946
|
-
<meta charset="utf-8" />
|
|
947
|
-
<title>OAuth not supported with encryption</title>
|
|
948
|
-
</head>
|
|
949
|
-
<body>
|
|
950
|
-
<h1>OAuth not supported when encryption is enabled</h1>
|
|
951
|
-
<p>
|
|
952
|
-
When encryption is enabled (NEOTOMA_ENCRYPTION_ENABLED=true), MCP authentication
|
|
953
|
-
uses a key-derived Bearer token instead of OAuth.
|
|
954
|
-
</p>
|
|
955
|
-
<h2>How to configure:</h2>
|
|
956
|
-
<ol>
|
|
957
|
-
<li>Run <code>neotoma auth mcp-token</code> to get your token</li>
|
|
958
|
-
<li>Add to .cursor/mcp.json under neotoma server:
|
|
959
|
-
<pre>"headers": { "Authorization": "Bearer <your-token>" }</pre>
|
|
960
|
-
</li>
|
|
961
|
-
<li>Remove <code>X-Connection-Id</code> if present</li>
|
|
962
|
-
<li>Restart Cursor</li>
|
|
963
|
-
</ol>
|
|
964
|
-
<p>See <a href="https://github.com/neotoma/neotoma/blob/main/docs/developer/mcp_oauth_troubleshooting.md">MCP OAuth Troubleshooting</a> for details.</p>
|
|
965
|
-
</body>
|
|
966
|
-
</html>`);
|
|
967
|
-
}
|
|
968
|
-
|
|
969
|
-
// When encryption is off: local requests auto-approve; tunnel requests require explicit approval
|
|
970
|
-
const fromTunnel = !isLocalRequest(req);
|
|
971
|
-
if (fromTunnel && req.query.approve !== "1") {
|
|
972
|
-
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
973
|
-
const base = `${req.protocol}://${req.get("host") || ""}`;
|
|
974
|
-
const approveUrl = `${base}${req.path}?${new URLSearchParams({ state, approve: "1" }).toString()}`;
|
|
975
|
-
return res.send(`<!DOCTYPE html>
|
|
976
|
-
<html>
|
|
977
|
-
<head>
|
|
978
|
-
<meta charset="utf-8" />
|
|
979
|
-
<title>Approve MCP connection</title>
|
|
980
|
-
</head>
|
|
981
|
-
<body>
|
|
982
|
-
<h1>Approve MCP connection</h1>
|
|
983
|
-
<p>A client is requesting access to Neotoma. Only approve if you started this connection (e.g. by clicking Connect in Cursor).</p>
|
|
984
|
-
<p><a href="${approveUrl}">Approve this connection</a></p>
|
|
985
|
-
<p><small>If you did not expect this, close the tab. No access will be granted.</small></p>
|
|
986
|
-
</body>
|
|
987
|
-
</html>`);
|
|
988
|
-
}
|
|
989
|
-
|
|
990
|
-
try {
|
|
991
|
-
const devUser = ensureLocalDevUser();
|
|
992
|
-
const { completeLocalAuthorization } = await import("./services/mcp_oauth.js");
|
|
993
|
-
const { connectionId, redirectUri, clientState } = await completeLocalAuthorization(
|
|
994
|
-
state,
|
|
995
|
-
devUser.id
|
|
996
|
-
);
|
|
997
|
-
const frontendBase =
|
|
998
|
-
process.env.NEOTOMA_FRONTEND_URL ||
|
|
999
|
-
process.env.FRONTEND_URL ||
|
|
1000
|
-
"http://localhost:5195";
|
|
1001
|
-
const frontendOauth = `${frontendBase}/oauth`;
|
|
1002
|
-
if (redirectUri) {
|
|
1003
|
-
if (!clientState && redirectUri.startsWith(frontendOauth)) {
|
|
1004
|
-
const successUrl = `${frontendOauth}?connection_id=${encodeURIComponent(connectionId)}&status=success`;
|
|
1005
|
-
return res.redirect(successUrl);
|
|
1006
|
-
}
|
|
1007
|
-
const params = new URLSearchParams({
|
|
1008
|
-
code: connectionId,
|
|
1009
|
-
state: clientState ?? "",
|
|
1010
|
-
});
|
|
1011
|
-
return res.redirect(`${redirectUri}?${params.toString()}`);
|
|
1012
|
-
}
|
|
1013
|
-
return res.redirect(
|
|
1014
|
-
`${frontendOauth}?connection_id=${encodeURIComponent(connectionId)}&status=success`
|
|
1015
|
-
);
|
|
1016
|
-
} catch (error: any) {
|
|
1017
|
-
logError("MCPLocalLoginDevStub", req, error);
|
|
1018
|
-
const status = error?.code === "OAUTH_STATE_INVALID" || error?.statusCode === 400 ? 400 : 401;
|
|
1019
|
-
return res.status(status).send(error.message ?? "Dev account authorization failed");
|
|
1020
|
-
}
|
|
1021
|
-
});
|
|
1022
|
-
|
|
1023
|
-
// POST handler removed: local-login now auto-uses dev account (no email/password form)
|
|
1024
|
-
|
|
1025
|
-
// RFC 8414 token endpoint (POST) for Cursor and other OAuth clients
|
|
1026
|
-
app.post(
|
|
1027
|
-
"/mcp/oauth/token",
|
|
1028
|
-
oauthTokenLimit,
|
|
1029
|
-
express.urlencoded({ extended: true }),
|
|
1030
|
-
async (req, res) => {
|
|
1031
|
-
try {
|
|
1032
|
-
const grant_type = req.body?.grant_type;
|
|
1033
|
-
const code = req.body?.code;
|
|
1034
|
-
|
|
1035
|
-
if (grant_type !== "authorization_code") {
|
|
1036
|
-
return res.status(400).json({
|
|
1037
|
-
error: "unsupported_grant_type",
|
|
1038
|
-
error_description: "Only authorization_code is supported",
|
|
1039
|
-
});
|
|
1040
|
-
}
|
|
1041
|
-
if (!code || typeof code !== "string") {
|
|
1042
|
-
return res
|
|
1043
|
-
.status(400)
|
|
1044
|
-
.json({ error: "invalid_request", error_description: "code is required" });
|
|
1045
|
-
}
|
|
1046
|
-
|
|
1047
|
-
const { getTokenResponseForConnection } = await import("./services/mcp_oauth.js");
|
|
1048
|
-
const token = await getTokenResponseForConnection(code);
|
|
1049
|
-
|
|
1050
|
-
res.setHeader("Content-Type", "application/json");
|
|
1051
|
-
return res.json(token);
|
|
1052
|
-
} catch (error: any) {
|
|
1053
|
-
logError("MCPOAuthToken", req, error);
|
|
1054
|
-
return res.status(400).json({
|
|
1055
|
-
error: "invalid_grant",
|
|
1056
|
-
error_description: error.message ?? "Token exchange failed",
|
|
1057
|
-
});
|
|
1058
|
-
}
|
|
1059
|
-
}
|
|
1060
|
-
);
|
|
1061
|
-
|
|
1062
|
-
// RFC 7591 Dynamic Client Registration (for Cursor and other MCP clients)
|
|
1063
|
-
app.post("/mcp/oauth/register", oauthRegisterLimit, async (req, res) => {
|
|
1064
|
-
try {
|
|
1065
|
-
const body = req.body as { redirect_uris?: string[]; client_name?: string; scope?: string };
|
|
1066
|
-
if (!body || typeof body !== "object") {
|
|
1067
|
-
return res.status(400).json({
|
|
1068
|
-
error: "invalid_request",
|
|
1069
|
-
error_description: "Request body must be JSON object with redirect_uris",
|
|
1070
|
-
});
|
|
1071
|
-
}
|
|
1072
|
-
const { handleDynamicRegistration } = await import("./services/mcp_oauth.js");
|
|
1073
|
-
const reg = await handleDynamicRegistration({
|
|
1074
|
-
redirect_uris: body.redirect_uris ?? [],
|
|
1075
|
-
client_name: body.client_name,
|
|
1076
|
-
scope: body.scope,
|
|
1077
|
-
});
|
|
1078
|
-
res.setHeader("Content-Type", "application/json");
|
|
1079
|
-
return res.status(201).json(reg);
|
|
1080
|
-
} catch (error: any) {
|
|
1081
|
-
logError("MCPOAuthRegister", req, error);
|
|
1082
|
-
if (error instanceof OAuthError) {
|
|
1083
|
-
const oauth = error as { code?: string; message?: string; statusCode?: number };
|
|
1084
|
-
const status = oauth.statusCode ?? 400;
|
|
1085
|
-
const code =
|
|
1086
|
-
oauth.code === "OAUTH_INVALID_REDIRECT_URI"
|
|
1087
|
-
? "invalid_redirect_uri"
|
|
1088
|
-
: status >= 500
|
|
1089
|
-
? "server_error"
|
|
1090
|
-
: "invalid_client_metadata";
|
|
1091
|
-
return res.status(status).json({
|
|
1092
|
-
error: code,
|
|
1093
|
-
error_description: oauth.message ?? "Registration failed",
|
|
1094
|
-
});
|
|
1095
|
-
}
|
|
1096
|
-
return res.status(400).json({
|
|
1097
|
-
error: "invalid_client_metadata",
|
|
1098
|
-
error_description: error.message ?? "Dynamic client registration failed",
|
|
1099
|
-
});
|
|
1100
|
-
}
|
|
1101
|
-
});
|
|
1102
|
-
|
|
1103
|
-
// Get connection status
|
|
1104
|
-
app.get("/mcp/oauth/status", async (req, res) => {
|
|
1105
|
-
try {
|
|
1106
|
-
const { connection_id } = req.query;
|
|
1107
|
-
|
|
1108
|
-
if (!connection_id || typeof connection_id !== "string") {
|
|
1109
|
-
return sendError(res, 400, "VALIDATION_MISSING_FIELD", "connection_id is required");
|
|
1110
|
-
}
|
|
1111
|
-
|
|
1112
|
-
const { getConnectionStatus } = await import("./services/mcp_oauth.js");
|
|
1113
|
-
const status = await getConnectionStatus(connection_id);
|
|
1114
|
-
|
|
1115
|
-
return res.json({ status, connection_id });
|
|
1116
|
-
} catch (error: any) {
|
|
1117
|
-
logError("MCPOAuthStatus", req, error);
|
|
1118
|
-
return sendError(res, 500, "DB_QUERY_FAILED", error.message);
|
|
1119
|
-
}
|
|
1120
|
-
});
|
|
1121
|
-
|
|
1122
|
-
// List user's MCP connections (authenticated)
|
|
1123
|
-
app.get("/mcp/oauth/connections", async (req, res) => {
|
|
1124
|
-
try {
|
|
1125
|
-
// Extract bearer token
|
|
1126
|
-
const authHeader = req.headers.authorization;
|
|
1127
|
-
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
|
1128
|
-
return sendError(res, 401, "AUTH_REQUIRED", "Authorization header required");
|
|
1129
|
-
}
|
|
1130
|
-
|
|
1131
|
-
const token = authHeader.substring(7);
|
|
1132
|
-
|
|
1133
|
-
// Validate token and get user
|
|
1134
|
-
const { validateSessionToken } = await import("./services/mcp_auth.js");
|
|
1135
|
-
const { userId } = await validateSessionToken(token);
|
|
1136
|
-
|
|
1137
|
-
// List connections
|
|
1138
|
-
const { listConnections } = await import("./services/mcp_oauth.js");
|
|
1139
|
-
const connections = await listConnections(userId);
|
|
1140
|
-
|
|
1141
|
-
return res.json({ connections });
|
|
1142
|
-
} catch (error: any) {
|
|
1143
|
-
logError("MCPOAuthListConnections", req, error);
|
|
1144
|
-
return sendError(res, 401, "AUTH_INVALID", "Invalid or expired token");
|
|
1145
|
-
}
|
|
1146
|
-
});
|
|
1147
|
-
|
|
1148
|
-
// Revoke MCP connection (authenticated)
|
|
1149
|
-
app.delete("/mcp/oauth/connections/:connection_id", async (req, res) => {
|
|
1150
|
-
try {
|
|
1151
|
-
// Extract bearer token
|
|
1152
|
-
const authHeader = req.headers.authorization;
|
|
1153
|
-
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
|
1154
|
-
return sendError(res, 401, "AUTH_REQUIRED", "Authorization header required");
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
|
-
const token = authHeader.substring(7);
|
|
1158
|
-
const { connection_id } = req.params;
|
|
1159
|
-
|
|
1160
|
-
// Validate token and get user
|
|
1161
|
-
const { validateSessionToken } = await import("./services/mcp_auth.js");
|
|
1162
|
-
const { userId } = await validateSessionToken(token);
|
|
1163
|
-
|
|
1164
|
-
// Revoke connection
|
|
1165
|
-
const { revokeConnection } = await import("./services/mcp_oauth.js");
|
|
1166
|
-
await revokeConnection(connection_id, userId);
|
|
1167
|
-
|
|
1168
|
-
return res.json({ success: true });
|
|
1169
|
-
} catch (error: any) {
|
|
1170
|
-
logError("MCPOAuthRevokeConnection", req, error);
|
|
1171
|
-
return sendError(res, 500, "DB_QUERY_FAILED", error.message);
|
|
1172
|
-
}
|
|
1173
|
-
});
|
|
1174
|
-
|
|
1175
|
-
// Get OAuth authorization details (proxy to avoid CORS)
|
|
1176
|
-
// This endpoint proxies requests to OAuth 2.1 Server to avoid CORS issues
|
|
1177
|
-
app.get("/mcp/oauth/authorization-details", async (req, res) => {
|
|
1178
|
-
logger.info(
|
|
1179
|
-
`[MCP OAuth] Authorization details request: authorization_id=${req.query.authorization_id}`
|
|
1180
|
-
);
|
|
1181
|
-
try {
|
|
1182
|
-
// Extract bearer token
|
|
1183
|
-
const authHeader = req.headers.authorization;
|
|
1184
|
-
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
|
1185
|
-
return sendError(res, 401, "AUTH_REQUIRED", "Authorization header required");
|
|
1186
|
-
}
|
|
1187
|
-
|
|
1188
|
-
const token = authHeader.substring(7);
|
|
1189
|
-
const { authorization_id } = req.query;
|
|
1190
|
-
|
|
1191
|
-
if (!authorization_id || typeof authorization_id !== "string") {
|
|
1192
|
-
return sendError(res, 400, "VALIDATION_MISSING_FIELD", "authorization_id is required");
|
|
1193
|
-
}
|
|
1194
|
-
|
|
1195
|
-
// Validate token and get user (ensures user is authenticated)
|
|
1196
|
-
const { validateSessionToken } = await import("./services/mcp_auth.js");
|
|
1197
|
-
await validateSessionToken(token);
|
|
1198
|
-
|
|
1199
|
-
const db = getSqliteDb();
|
|
1200
|
-
const stateData = db
|
|
1201
|
-
.prepare("SELECT redirect_uri FROM mcp_oauth_state WHERE state = ?")
|
|
1202
|
-
.get(authorization_id) as { redirect_uri?: string } | undefined;
|
|
1203
|
-
|
|
1204
|
-
if (!stateData?.redirect_uri) {
|
|
1205
|
-
return res.status(404).json({
|
|
1206
|
-
error: "Authorization not found",
|
|
1207
|
-
error_description:
|
|
1208
|
-
"The authorization_id has expired or is invalid. Please try the OAuth flow again.",
|
|
1209
|
-
});
|
|
1210
|
-
}
|
|
1211
|
-
|
|
1212
|
-
return res.json({
|
|
1213
|
-
id: authorization_id,
|
|
1214
|
-
status: "approved",
|
|
1215
|
-
client_id: "local_mcp_oauth_client",
|
|
1216
|
-
redirect_uri: stateData.redirect_uri,
|
|
1217
|
-
});
|
|
1218
|
-
} catch (error: any) {
|
|
1219
|
-
logError("MCPOAuthAuthorizationDetails", req, error);
|
|
1220
|
-
return sendError(res, 500, "DB_QUERY_FAILED", error.message);
|
|
1221
|
-
}
|
|
1222
|
-
});
|
|
1223
|
-
|
|
1224
|
-
// Dev-only endpoint to sign in as a specific user (for development/testing)
|
|
1225
|
-
app.post("/auth/dev-signin", async (req, res) => {
|
|
1226
|
-
// Only allow in development mode
|
|
1227
|
-
if (config.environment !== "development") {
|
|
1228
|
-
return sendError(res, 403, "FORBIDDEN", "Dev signin only available in development mode");
|
|
1229
|
-
}
|
|
1230
|
-
return sendError(res, 403, "FORBIDDEN", "Dev signin endpoint is disabled in local-only mode");
|
|
1231
|
-
});
|
|
1232
|
-
|
|
1233
|
-
// Public key-based authentication middleware
|
|
1234
|
-
// MCP-style auth (same patterns as /mcp): encryption off = no auth or dev token; encryption on = key-derived token
|
|
1235
|
-
app.use(async (req, res, next) => {
|
|
1236
|
-
// Bypass auth only for truly public endpoints (no user data)
|
|
1237
|
-
if (
|
|
1238
|
-
req.method === "OPTIONS" ||
|
|
1239
|
-
(req.method === "GET" && (req.path === "/openapi.yaml" || req.path === "/health")) ||
|
|
1240
|
-
(req.method === "POST" && req.path === "/auth/dev-signin")
|
|
1241
|
-
) {
|
|
1242
|
-
return next();
|
|
1243
|
-
}
|
|
1244
|
-
|
|
1245
|
-
const headerAuth = (req.headers.authorization || req.headers.Authorization || "") as string;
|
|
1246
|
-
|
|
1247
|
-
// Local mode: when storage is local and request is from localhost, no Bearer → default user (00..) by default
|
|
1248
|
-
if (
|
|
1249
|
-
config.storageBackend === "local" &&
|
|
1250
|
-
isLocalRequest(req) &&
|
|
1251
|
-
!headerAuth.startsWith("Bearer ")
|
|
1252
|
-
) {
|
|
1253
|
-
const devUser = ensureLocalDevUser();
|
|
1254
|
-
(req as any).authenticatedUserId = devUser.id;
|
|
1255
|
-
return next();
|
|
1256
|
-
}
|
|
1257
|
-
|
|
1258
|
-
// MCP-style auth (aligns CLI and REST API with MCP). Local requests can skip Bearer; tunnel requires Bearer or OAuth.
|
|
1259
|
-
if (config.encryption.enabled) {
|
|
1260
|
-
const expectedToken = getMcpAuthToken();
|
|
1261
|
-
if (headerAuth.startsWith("Bearer ") && expectedToken) {
|
|
1262
|
-
const token = headerAuth.slice(7).trim();
|
|
1263
|
-
if (token === expectedToken) {
|
|
1264
|
-
const devUser = ensureLocalDevUser();
|
|
1265
|
-
(req as any).authenticatedUserId = devUser.id;
|
|
1266
|
-
return next();
|
|
1267
|
-
}
|
|
1268
|
-
}
|
|
1269
|
-
} else {
|
|
1270
|
-
if (!headerAuth.startsWith("Bearer ")) {
|
|
1271
|
-
if (isLocalRequest(req)) {
|
|
1272
|
-
const devUser = ensureLocalDevUser();
|
|
1273
|
-
(req as any).authenticatedUserId = devUser.id;
|
|
1274
|
-
return next();
|
|
1275
|
-
}
|
|
1276
|
-
}
|
|
1277
|
-
if (process.env.NEOTOMA_BEARER_TOKEN) {
|
|
1278
|
-
const token = headerAuth.slice(7).trim();
|
|
1279
|
-
if (token === process.env.NEOTOMA_BEARER_TOKEN) {
|
|
1280
|
-
const devUser = ensureLocalDevUser();
|
|
1281
|
-
(req as any).authenticatedUserId = devUser.id;
|
|
1282
|
-
return next();
|
|
1283
|
-
}
|
|
1284
|
-
}
|
|
1285
|
-
}
|
|
1286
|
-
|
|
1287
|
-
// Bearer required for remaining paths (Ed25519, OAuth)
|
|
1288
|
-
if (!headerAuth.startsWith("Bearer ")) {
|
|
1289
|
-
logWarn("AuthMissingBearer", req);
|
|
1290
|
-
return sendError(res, 401, "AUTH_REQUIRED", "Missing Bearer token");
|
|
1291
|
-
}
|
|
1292
|
-
|
|
1293
|
-
const bearerToken = headerAuth.slice("Bearer ".length).trim();
|
|
1294
|
-
|
|
1295
|
-
// Try to validate as Ed25519 bearer token first
|
|
1296
|
-
const registered = ensurePublicKeyRegistered(bearerToken);
|
|
1297
|
-
|
|
1298
|
-
if (registered && isBearerTokenValid(bearerToken)) {
|
|
1299
|
-
// Optional: Verify signature if provided
|
|
1300
|
-
const { signature } = parseAuthHeader(headerAuth);
|
|
1301
|
-
if (signature && req.body) {
|
|
1302
|
-
const bodyString = typeof req.body === "string" ? req.body : JSON.stringify(req.body);
|
|
1303
|
-
const isValid = verifyRequest(bodyString, signature, bearerToken);
|
|
1304
|
-
if (!isValid) {
|
|
1305
|
-
logWarn("AuthInvalidSignature", req);
|
|
1306
|
-
return sendError(res, 403, "AUTH_INVALID", "Invalid request signature");
|
|
1307
|
-
}
|
|
1308
|
-
}
|
|
1309
|
-
|
|
1310
|
-
// Attach public key to request for encryption service
|
|
1311
|
-
(req as any).publicKey = getPublicKey(bearerToken);
|
|
1312
|
-
(req as any).bearerToken = bearerToken;
|
|
1313
|
-
// If this token was registered with a userId, set it so getAuthenticatedUserId works without query param
|
|
1314
|
-
const registeredUserId = getUserIdFromBearerToken(bearerToken);
|
|
1315
|
-
if (registeredUserId) {
|
|
1316
|
-
(req as any).authenticatedUserId = registeredUserId;
|
|
1317
|
-
}
|
|
1318
|
-
} else {
|
|
1319
|
-
// Try to validate as session token
|
|
1320
|
-
try {
|
|
1321
|
-
const { validateSessionToken } = await import("./services/mcp_auth.js");
|
|
1322
|
-
const validated = await validateSessionToken(bearerToken);
|
|
1323
|
-
// Attach user_id and email to request for user-scoped queries and /me
|
|
1324
|
-
(req as any).authenticatedUserId = validated.userId;
|
|
1325
|
-
(req as any).authenticatedUserEmail = validated.email;
|
|
1326
|
-
(req as any).bearerToken = bearerToken;
|
|
1327
|
-
} catch (authError) {
|
|
1328
|
-
// Not a valid token
|
|
1329
|
-
logWarn("AuthInvalidToken", req, {
|
|
1330
|
-
error: authError instanceof Error ? authError.message : String(authError),
|
|
1331
|
-
});
|
|
1332
|
-
return sendError(res, 401, "AUTH_INVALID", "Unauthorized - invalid authentication token");
|
|
1333
|
-
}
|
|
1334
|
-
}
|
|
1335
|
-
|
|
1336
|
-
return next();
|
|
1337
|
-
});
|
|
1338
|
-
|
|
1339
|
-
// Response encryption middleware (applies to all authenticated routes)
|
|
1340
|
-
app.use(encryptResponseMiddleware);
|
|
1341
|
-
|
|
1342
|
-
// Current session (authenticated user details)
|
|
1343
|
-
app.get("/me", async (req, res) => {
|
|
1344
|
-
try {
|
|
1345
|
-
const userId = (req as any).authenticatedUserId;
|
|
1346
|
-
const email = (req as any).authenticatedUserEmail;
|
|
1347
|
-
if (!userId) {
|
|
1348
|
-
return sendError(res, 401, "AUTH_REQUIRED", "Not authenticated");
|
|
1349
|
-
}
|
|
1350
|
-
const storage =
|
|
1351
|
-
config.storageBackend === "local"
|
|
1352
|
-
? {
|
|
1353
|
-
storage_backend: "local" as const,
|
|
1354
|
-
data_dir: config.dataDir,
|
|
1355
|
-
sqlite_db: config.sqlitePath,
|
|
1356
|
-
}
|
|
1357
|
-
: undefined;
|
|
1358
|
-
return res.json({ user_id: userId, email: email ?? undefined, storage });
|
|
1359
|
-
} catch (error: any) {
|
|
1360
|
-
logError("GetMe", req, error);
|
|
1361
|
-
return sendError(res, 401, "AUTH_REQUIRED", error.message ?? "Not authenticated");
|
|
1362
|
-
}
|
|
1363
|
-
});
|
|
1364
|
-
|
|
1365
|
-
/**
|
|
1366
|
-
* Helper to extract authenticated user_id from request
|
|
1367
|
-
* Supports: middleware-set user (e.g. dev-local when encryption off), session token, Ed25519 bearer
|
|
1368
|
-
* @param req - Express request object
|
|
1369
|
-
* @param providedUserId - Optional user_id from request body/query
|
|
1370
|
-
* @returns Authenticated user_id
|
|
1371
|
-
* @throws Error if not authenticated or user_id mismatch
|
|
1372
|
-
*/
|
|
1373
|
-
async function getAuthenticatedUserId(
|
|
1374
|
-
req: express.Request,
|
|
1375
|
-
providedUserId?: string
|
|
1376
|
-
): Promise<string> {
|
|
1377
|
-
// Use user already set by auth middleware (e.g. dev-local when encryption off and no Bearer)
|
|
1378
|
-
const authenticatedUserId = (req as any).authenticatedUserId;
|
|
1379
|
-
if (authenticatedUserId) {
|
|
1380
|
-
if (providedUserId && providedUserId !== authenticatedUserId) {
|
|
1381
|
-
// When authenticated as local dev user, allow body/query user_id override for CLI tests and dev flows.
|
|
1382
|
-
if (authenticatedUserId === LOCAL_DEV_USER_ID) {
|
|
1383
|
-
return providedUserId;
|
|
1384
|
-
}
|
|
1385
|
-
throw new Error(
|
|
1386
|
-
`user_id parameter (${providedUserId}) does not match authenticated user (${authenticatedUserId})`
|
|
1387
|
-
);
|
|
1388
|
-
}
|
|
1389
|
-
return authenticatedUserId;
|
|
1390
|
-
}
|
|
1391
|
-
|
|
1392
|
-
const headerAuth = req.headers.authorization || "";
|
|
1393
|
-
if (!headerAuth.startsWith("Bearer ")) {
|
|
1394
|
-
throw new Error("Not authenticated - missing Bearer token");
|
|
1395
|
-
}
|
|
1396
|
-
|
|
1397
|
-
// Ed25519 bearer token - user_id must be provided
|
|
1398
|
-
if (!providedUserId) {
|
|
1399
|
-
throw new Error("user_id required when using Ed25519 bearer token");
|
|
1400
|
-
}
|
|
1401
|
-
|
|
1402
|
-
// For Ed25519 tokens, we trust the provided user_id (token validation happens in middleware)
|
|
1403
|
-
return providedUserId;
|
|
1404
|
-
}
|
|
1405
|
-
|
|
1406
|
-
/**
|
|
1407
|
-
* Helper to handle errors in API endpoints, including authentication errors
|
|
1408
|
-
*/
|
|
1409
|
-
function handleApiError(
|
|
1410
|
-
req: express.Request,
|
|
1411
|
-
res: express.Response,
|
|
1412
|
-
error: unknown,
|
|
1413
|
-
defaultMessage: string,
|
|
1414
|
-
errorCode: string = "DB_QUERY_FAILED",
|
|
1415
|
-
logContext?: string
|
|
1416
|
-
): express.Response {
|
|
1417
|
-
if (error instanceof Error && error.message.includes("Not authenticated")) {
|
|
1418
|
-
return res.status(401).json(
|
|
1419
|
-
buildErrorEnvelope("AUTH_REQUIRED", "Authentication required.", {
|
|
1420
|
-
detail: error.message,
|
|
1421
|
-
})
|
|
1422
|
-
);
|
|
1423
|
-
}
|
|
1424
|
-
if (error instanceof Error && error.message.includes("user_id parameter")) {
|
|
1425
|
-
return res.status(403).json(
|
|
1426
|
-
buildErrorEnvelope("FORBIDDEN", "user_id does not match authenticated user.", {
|
|
1427
|
-
detail: error.message,
|
|
1428
|
-
})
|
|
1429
|
-
);
|
|
1430
|
-
}
|
|
1431
|
-
logError(logContext || "APIError", req, error);
|
|
1432
|
-
const message = error instanceof Error ? error.message : defaultMessage;
|
|
1433
|
-
return res.status(500).json(buildErrorEnvelope(errorCode, message));
|
|
1434
|
-
}
|
|
1435
|
-
|
|
1436
|
-
// Schemas
|
|
1437
|
-
|
|
1438
|
-
// ============================================================================
|
|
1439
|
-
// v0.2.15 Entity-Based HTTP API Endpoints
|
|
1440
|
-
// ============================================================================
|
|
1441
|
-
|
|
1442
|
-
// POST /api/entities/query - Query entities with filters
|
|
1443
|
-
// REQUIRES AUTHENTICATION - all queries filtered by authenticated user_id
|
|
1444
|
-
app.post("/entities/query", async (req, res) => {
|
|
1445
|
-
const parsed = EntitiesQueryRequestSchema.safeParse(req.body);
|
|
1446
|
-
if (!parsed.success) {
|
|
1447
|
-
logWarn("ValidationError:entities_query", req, {
|
|
1448
|
-
issues: parsed.error.issues,
|
|
1449
|
-
});
|
|
1450
|
-
return sendValidationError(res, parsed.error.issues);
|
|
1451
|
-
}
|
|
1452
|
-
|
|
1453
|
-
try {
|
|
1454
|
-
// Get authenticated user_id (REQUIRED)
|
|
1455
|
-
const userId = await getAuthenticatedUserId(req, parsed.data.user_id);
|
|
1456
|
-
|
|
1457
|
-
const { entity_type, search, limit, offset, include_merged } = parsed.data;
|
|
1458
|
-
const { entities, total } = await queryEntitiesWithCount({
|
|
1459
|
-
userId,
|
|
1460
|
-
entityType: entity_type,
|
|
1461
|
-
includeMerged: include_merged,
|
|
1462
|
-
search,
|
|
1463
|
-
limit,
|
|
1464
|
-
offset,
|
|
1465
|
-
});
|
|
1466
|
-
|
|
1467
|
-
return res.json({
|
|
1468
|
-
entities,
|
|
1469
|
-
total,
|
|
1470
|
-
limit,
|
|
1471
|
-
offset,
|
|
1472
|
-
});
|
|
1473
|
-
} catch (error) {
|
|
1474
|
-
return handleApiError(
|
|
1475
|
-
req,
|
|
1476
|
-
res,
|
|
1477
|
-
error,
|
|
1478
|
-
"Failed to query entities",
|
|
1479
|
-
"DB_QUERY_FAILED",
|
|
1480
|
-
"APIError:entities_query"
|
|
1481
|
-
);
|
|
1482
|
-
}
|
|
1483
|
-
});
|
|
1484
|
-
|
|
1485
|
-
// GET /api/entities/:id - Get entity detail with snapshot and provenance (FU-601)
|
|
1486
|
-
// REQUIRES AUTHENTICATION - verifies entity belongs to authenticated user
|
|
1487
|
-
app.get("/entities/:id", async (req, res) => {
|
|
1488
|
-
try {
|
|
1489
|
-
const entityId = req.params.id;
|
|
1490
|
-
|
|
1491
|
-
// Get authenticated user_id (REQUIRED)
|
|
1492
|
-
const userId = await getAuthenticatedUserId(req, req.query.user_id as string | undefined);
|
|
1493
|
-
|
|
1494
|
-
// Verify entity exists and belongs to authenticated user
|
|
1495
|
-
const { data: entity, error: entityError } = await db
|
|
1496
|
-
.from("entities")
|
|
1497
|
-
.select("id, user_id")
|
|
1498
|
-
.eq("id", entityId)
|
|
1499
|
-
.eq("user_id", userId) // SECURITY: Only return if belongs to authenticated user
|
|
1500
|
-
.single();
|
|
1501
|
-
|
|
1502
|
-
if (entityError || !entity) {
|
|
1503
|
-
return sendError(res, 404, "RESOURCE_NOT_FOUND", "Entity not found");
|
|
1504
|
-
}
|
|
1505
|
-
|
|
1506
|
-
const { getEntityWithProvenance } = await import("./services/entity_queries.js");
|
|
1507
|
-
const entityWithProvenance = await getEntityWithProvenance(entityId);
|
|
1508
|
-
|
|
1509
|
-
if (!entityWithProvenance) {
|
|
1510
|
-
return sendError(res, 404, "RESOURCE_NOT_FOUND", "Entity not found");
|
|
1511
|
-
}
|
|
1512
|
-
|
|
1513
|
-
return res.json(entityWithProvenance);
|
|
1514
|
-
} catch (error) {
|
|
1515
|
-
if (error instanceof Error && error.message.includes("Not authenticated")) {
|
|
1516
|
-
return sendError(res, 401, "AUTH_REQUIRED", error.message);
|
|
1517
|
-
}
|
|
1518
|
-
logError("APIError:entity_detail", req, error);
|
|
1519
|
-
const message = error instanceof Error ? error.message : "Failed to get entity";
|
|
1520
|
-
return sendError(res, 500, "DB_QUERY_FAILED", message);
|
|
1521
|
-
}
|
|
1522
|
-
});
|
|
1523
|
-
|
|
1524
|
-
// GET /api/entities/:id/observations - Get observations for entity (FU-601)
|
|
1525
|
-
// REQUIRES AUTHENTICATION - verifies entity belongs to authenticated user
|
|
1526
|
-
app.get("/entities/:id/observations", async (req, res) => {
|
|
1527
|
-
try {
|
|
1528
|
-
const entityId = req.params.id;
|
|
1529
|
-
|
|
1530
|
-
// Get authenticated user_id (REQUIRED)
|
|
1531
|
-
const userId = await getAuthenticatedUserId(req, req.query.user_id as string | undefined);
|
|
1532
|
-
|
|
1533
|
-
// Verify entity exists and belongs to authenticated user
|
|
1534
|
-
const { data: entity, error: entityError } = await db
|
|
1535
|
-
.from("entities")
|
|
1536
|
-
.select("id")
|
|
1537
|
-
.eq("id", entityId)
|
|
1538
|
-
.eq("user_id", userId) // SECURITY: Only return if belongs to authenticated user
|
|
1539
|
-
.single();
|
|
1540
|
-
|
|
1541
|
-
if (entityError || !entity) {
|
|
1542
|
-
return sendError(res, 404, "RESOURCE_NOT_FOUND", "Entity not found");
|
|
1543
|
-
}
|
|
1544
|
-
|
|
1545
|
-
const limit = parseInt(req.query.limit as string) || 100;
|
|
1546
|
-
const offset = parseInt(req.query.offset as string) || 0;
|
|
1547
|
-
|
|
1548
|
-
// Get observations for this entity - filter by user_id for security
|
|
1549
|
-
const { data, error, count } = await db
|
|
1550
|
-
.from("observations")
|
|
1551
|
-
.select("*", { count: "exact" })
|
|
1552
|
-
.eq("entity_id", entityId)
|
|
1553
|
-
.eq("user_id", userId) // SECURITY: Only return observations for authenticated user
|
|
1554
|
-
.order("observed_at", { ascending: false })
|
|
1555
|
-
.range(offset, offset + limit - 1);
|
|
1556
|
-
|
|
1557
|
-
if (error) throw error;
|
|
1558
|
-
|
|
1559
|
-
return res.json({
|
|
1560
|
-
observations: data || [],
|
|
1561
|
-
total: count || 0,
|
|
1562
|
-
limit,
|
|
1563
|
-
offset,
|
|
1564
|
-
});
|
|
1565
|
-
} catch (error) {
|
|
1566
|
-
if (error instanceof Error && error.message.includes("Not authenticated")) {
|
|
1567
|
-
return sendError(res, 401, "AUTH_REQUIRED", error.message);
|
|
1568
|
-
}
|
|
1569
|
-
logError("APIError:entity_observations", req, error);
|
|
1570
|
-
const message = error instanceof Error ? error.message : "Failed to get observations";
|
|
1571
|
-
return sendError(res, 500, "DB_QUERY_FAILED", message);
|
|
1572
|
-
}
|
|
1573
|
-
});
|
|
1574
|
-
|
|
1575
|
-
// GET /api/entities/:id/relationships - Get relationships for entity (FU-601)
|
|
1576
|
-
// REQUIRES AUTHENTICATION - verifies entity belongs to authenticated user
|
|
1577
|
-
app.get("/entities/:id/relationships", async (req, res) => {
|
|
1578
|
-
try {
|
|
1579
|
-
const entityId = req.params.id;
|
|
1580
|
-
|
|
1581
|
-
// Get authenticated user_id (REQUIRED)
|
|
1582
|
-
const userId = await getAuthenticatedUserId(req, req.query.user_id as string | undefined);
|
|
1583
|
-
|
|
1584
|
-
// Verify entity exists and belongs to authenticated user
|
|
1585
|
-
const { data: entity, error: entityError } = await db
|
|
1586
|
-
.from("entities")
|
|
1587
|
-
.select("id")
|
|
1588
|
-
.eq("id", entityId)
|
|
1589
|
-
.eq("user_id", userId) // SECURITY: Only return if belongs to authenticated user
|
|
1590
|
-
.single();
|
|
1591
|
-
|
|
1592
|
-
if (entityError || !entity) {
|
|
1593
|
-
return sendError(res, 404, "RESOURCE_NOT_FOUND", "Entity not found");
|
|
1594
|
-
}
|
|
1595
|
-
|
|
1596
|
-
// Get relationships where this entity is the source - filter by user_id
|
|
1597
|
-
const { data: outgoing, error: outgoingError } = await db
|
|
1598
|
-
.from("relationship_snapshots")
|
|
1599
|
-
.select("*")
|
|
1600
|
-
.eq("source_entity_id", entityId)
|
|
1601
|
-
.eq("user_id", userId); // SECURITY: Only return relationships for authenticated user
|
|
1602
|
-
|
|
1603
|
-
if (outgoingError) throw outgoingError;
|
|
1604
|
-
|
|
1605
|
-
// Get relationships where this entity is the target - filter by user_id
|
|
1606
|
-
const { data: incoming, error: incomingError } = await db
|
|
1607
|
-
.from("relationship_snapshots")
|
|
1608
|
-
.select("*")
|
|
1609
|
-
.eq("target_entity_id", entityId)
|
|
1610
|
-
.eq("user_id", userId); // SECURITY: Only return relationships for authenticated user
|
|
1611
|
-
|
|
1612
|
-
if (incomingError) throw incomingError;
|
|
1613
|
-
|
|
1614
|
-
// Add id field for frontend compatibility (use relationship_key)
|
|
1615
|
-
const formatRelationships = (rels: any[]) =>
|
|
1616
|
-
(rels || []).map((rel: any) => ({
|
|
1617
|
-
...rel,
|
|
1618
|
-
id: rel.relationship_key,
|
|
1619
|
-
}));
|
|
1620
|
-
|
|
1621
|
-
return res.json({
|
|
1622
|
-
outgoing: formatRelationships(outgoing),
|
|
1623
|
-
incoming: formatRelationships(incoming),
|
|
1624
|
-
});
|
|
1625
|
-
} catch (error) {
|
|
1626
|
-
logError("APIError:entity_relationships", req, error);
|
|
1627
|
-
const message = error instanceof Error ? error.message : "Failed to get relationships";
|
|
1628
|
-
return sendError(res, 500, "DB_QUERY_FAILED", message);
|
|
1629
|
-
}
|
|
1630
|
-
});
|
|
1631
|
-
|
|
1632
|
-
// GET /api/schemas - List all schemas (FU-XXX)
|
|
1633
|
-
// REQUIRES AUTHENTICATION - returns global schemas + user-specific schemas for authenticated user
|
|
1634
|
-
app.get("/schemas", async (req, res) => {
|
|
1635
|
-
try {
|
|
1636
|
-
// Get authenticated user_id (REQUIRED)
|
|
1637
|
-
// For Ed25519 tokens, user_id may be in query params; for session tokens, it's extracted from token
|
|
1638
|
-
const userId = await getAuthenticatedUserId(req, req.query.user_id as string | undefined);
|
|
1639
|
-
|
|
1640
|
-
const keyword = req.query.keyword as string | undefined;
|
|
1641
|
-
const filterEntityType = req.query.entity_type as string | undefined;
|
|
1642
|
-
const limitRaw = req.query.limit as string | undefined;
|
|
1643
|
-
const offsetRaw = req.query.offset as string | undefined;
|
|
1644
|
-
const limit =
|
|
1645
|
-
limitRaw && /^\d+$/.test(limitRaw) ? Math.max(0, parseInt(limitRaw, 10)) : 100;
|
|
1646
|
-
const offset =
|
|
1647
|
-
offsetRaw && /^\d+$/.test(offsetRaw) ? Math.max(0, parseInt(offsetRaw, 10)) : 0;
|
|
1648
|
-
|
|
1649
|
-
const { SchemaRegistryService } = await import("./services/schema_registry.js");
|
|
1650
|
-
const schemaRegistry = new SchemaRegistryService();
|
|
1651
|
-
|
|
1652
|
-
// Get schemas - listEntityTypes will return global + user-specific schemas
|
|
1653
|
-
// The service should filter by user_id, but for now we'll filter in the endpoint
|
|
1654
|
-
const allSchemas = await schemaRegistry.listEntityTypes(keyword);
|
|
1655
|
-
|
|
1656
|
-
// Filter to only show global schemas (user_id is null) or user-specific schemas for this user
|
|
1657
|
-
// Note: listEntityTypes doesn't currently filter by user_id, so we need to query directly
|
|
1658
|
-
// Also fetch metadata (including icons) for each schema
|
|
1659
|
-
const { data: dbSchemas, error: dbError } = await db
|
|
1660
|
-
.from("schema_registry")
|
|
1661
|
-
.select("entity_type, metadata")
|
|
1662
|
-
.eq("active", true)
|
|
1663
|
-
.or(`user_id.is.null,user_id.eq.${userId}`); // SECURITY: Only global or user's schemas
|
|
1664
|
-
|
|
1665
|
-
if (dbError) throw dbError;
|
|
1666
|
-
|
|
1667
|
-
const allowedEntityTypes = new Set((dbSchemas || []).map((s: any) => s.entity_type));
|
|
1668
|
-
let filteredSchemas = allSchemas.filter((s) => allowedEntityTypes.has(s.entity_type));
|
|
1669
|
-
|
|
1670
|
-
// Filter by entity_type if provided
|
|
1671
|
-
if (filterEntityType) {
|
|
1672
|
-
filteredSchemas = filteredSchemas.filter((s) => s.entity_type === filterEntityType);
|
|
1673
|
-
}
|
|
1674
|
-
|
|
1675
|
-
// Enrich schemas with metadata (including icons)
|
|
1676
|
-
const schemaMetadataMap = new Map(
|
|
1677
|
-
(dbSchemas || []).map((s: any) => [s.entity_type, s.metadata || {}])
|
|
1678
|
-
);
|
|
1679
|
-
|
|
1680
|
-
const enrichedSchemas = filteredSchemas.map((schema) => ({
|
|
1681
|
-
...schema,
|
|
1682
|
-
metadata: schemaMetadataMap.get(schema.entity_type) || {},
|
|
1683
|
-
}));
|
|
1684
|
-
|
|
1685
|
-
// Filter out test schemas (marked with metadata.test === true)
|
|
1686
|
-
const productionSchemas = enrichedSchemas.filter((schema) => {
|
|
1687
|
-
const metadata = (schema.metadata || {}) as Record<string, unknown>;
|
|
1688
|
-
return metadata["test"] !== true;
|
|
1689
|
-
});
|
|
1690
|
-
|
|
1691
|
-
const sortedSchemas = productionSchemas.sort((a, b) =>
|
|
1692
|
-
String(a.entity_type).localeCompare(String(b.entity_type))
|
|
1693
|
-
);
|
|
1694
|
-
const paginatedSchemas = sortedSchemas.slice(offset, offset + limit);
|
|
1695
|
-
|
|
1696
|
-
return res.json({
|
|
1697
|
-
schemas: paginatedSchemas,
|
|
1698
|
-
total: productionSchemas.length,
|
|
1699
|
-
limit,
|
|
1700
|
-
offset,
|
|
1701
|
-
});
|
|
1702
|
-
} catch (error) {
|
|
1703
|
-
return handleApiError(
|
|
1704
|
-
req,
|
|
1705
|
-
res,
|
|
1706
|
-
error,
|
|
1707
|
-
"Failed to list schemas",
|
|
1708
|
-
"DB_QUERY_FAILED",
|
|
1709
|
-
"APIError:schemas_list"
|
|
1710
|
-
);
|
|
1711
|
-
}
|
|
1712
|
-
});
|
|
1713
|
-
|
|
1714
|
-
// GET /api/schemas/:entity_type - Get specific schema (FU-XXX)
|
|
1715
|
-
app.get("/schemas/:entity_type", async (req, res) => {
|
|
1716
|
-
try {
|
|
1717
|
-
const entityType = decodeURIComponent(req.params.entity_type);
|
|
1718
|
-
const { SchemaRegistryService } = await import("./services/schema_registry.js");
|
|
1719
|
-
const schemaRegistry = new SchemaRegistryService();
|
|
1720
|
-
|
|
1721
|
-
// Handle authentication - support both Ed25519 and session tokens
|
|
1722
|
-
const headerAuth = req.headers.authorization || "";
|
|
1723
|
-
let userId: string | undefined = req.query.user_id as string | undefined;
|
|
1724
|
-
|
|
1725
|
-
if (headerAuth.startsWith("Bearer ")) {
|
|
1726
|
-
const token = headerAuth.slice("Bearer ".length).trim();
|
|
1727
|
-
|
|
1728
|
-
// Try to validate as Ed25519 bearer token first
|
|
1729
|
-
const registered = ensurePublicKeyRegistered(token);
|
|
1730
|
-
if (!registered || !isBearerTokenValid(token)) {
|
|
1731
|
-
// Try to validate as session token
|
|
1732
|
-
try {
|
|
1733
|
-
const { validateSessionToken } = await import("./services/mcp_auth.js");
|
|
1734
|
-
const validated = await validateSessionToken(token);
|
|
1735
|
-
// Use validated user ID if not provided in query
|
|
1736
|
-
userId = userId || validated.userId;
|
|
1737
|
-
} catch (authError) {
|
|
1738
|
-
// Not a valid token - continue without user_id (will try global schema)
|
|
1739
|
-
}
|
|
1740
|
-
}
|
|
1741
|
-
// If Ed25519 token is valid, use user_id from query (Ed25519 tokens don't contain user info)
|
|
1742
|
-
}
|
|
1743
|
-
|
|
1744
|
-
// Try to load active schema (global or user-specific), then fallback to code-defined schemas
|
|
1745
|
-
let schema = await schemaRegistry.loadActiveSchema(entityType, userId);
|
|
1746
|
-
|
|
1747
|
-
if (!schema) {
|
|
1748
|
-
const { ENTITY_SCHEMAS } = await import("./services/schema_definitions.js");
|
|
1749
|
-
const normalized = entityType.toLowerCase().trim().replace(/\s+/g, "_");
|
|
1750
|
-
const codeSchema = ENTITY_SCHEMAS[entityType] ?? ENTITY_SCHEMAS[normalized];
|
|
1751
|
-
if (codeSchema) {
|
|
1752
|
-
schema = {
|
|
1753
|
-
id: "",
|
|
1754
|
-
entity_type: codeSchema.entity_type,
|
|
1755
|
-
schema_version: codeSchema.schema_version,
|
|
1756
|
-
schema_definition: codeSchema.schema_definition,
|
|
1757
|
-
reducer_config: codeSchema.reducer_config,
|
|
1758
|
-
active: true,
|
|
1759
|
-
created_at: new Date().toISOString(),
|
|
1760
|
-
};
|
|
1761
|
-
}
|
|
1762
|
-
}
|
|
1763
|
-
|
|
1764
|
-
if (!schema) {
|
|
1765
|
-
return sendError(res, 404, "RESOURCE_NOT_FOUND", `Schema not found: ${entityType}`);
|
|
1766
|
-
}
|
|
1767
|
-
|
|
1768
|
-
return res.json(schema);
|
|
1769
|
-
} catch (error) {
|
|
1770
|
-
logError("APIError:schema_detail", req, error);
|
|
1771
|
-
const message = error instanceof Error ? error.message : "Failed to get schema";
|
|
1772
|
-
return sendError(res, 500, "DB_QUERY_FAILED", message);
|
|
1773
|
-
}
|
|
1774
|
-
});
|
|
1775
|
-
|
|
1776
|
-
// GET /api/relationships - List all relationships with filtering (FU-XXX)
|
|
1777
|
-
// REQUIRES AUTHENTICATION - all queries filtered by authenticated user_id
|
|
1778
|
-
app.get("/relationships", async (req, res) => {
|
|
1779
|
-
try {
|
|
1780
|
-
// Get authenticated user_id (REQUIRED)
|
|
1781
|
-
const userId = await getAuthenticatedUserId(req, req.query.user_id as string | undefined);
|
|
1782
|
-
|
|
1783
|
-
const relationshipType = req.query.relationship_type as string | undefined;
|
|
1784
|
-
const sourceEntityId = req.query.source_entity_id as string | undefined;
|
|
1785
|
-
const targetEntityId = req.query.target_entity_id as string | undefined;
|
|
1786
|
-
const limit = parseInt(req.query.limit as string) || 100;
|
|
1787
|
-
const offset = parseInt(req.query.offset as string) || 0;
|
|
1788
|
-
|
|
1789
|
-
// Build query - ALWAYS filter by authenticated user_id
|
|
1790
|
-
let query = db
|
|
1791
|
-
.from("relationship_snapshots")
|
|
1792
|
-
.select("*", { count: "exact" })
|
|
1793
|
-
.eq("user_id", userId); // SECURITY: Always filter by authenticated user
|
|
1794
|
-
|
|
1795
|
-
if (relationshipType) {
|
|
1796
|
-
query = query.eq("relationship_type", relationshipType);
|
|
1797
|
-
}
|
|
1798
|
-
if (sourceEntityId) {
|
|
1799
|
-
query = query.eq("source_entity_id", sourceEntityId);
|
|
1800
|
-
}
|
|
1801
|
-
if (targetEntityId) {
|
|
1802
|
-
query = query.eq("target_entity_id", targetEntityId);
|
|
1803
|
-
}
|
|
1804
|
-
|
|
1805
|
-
const { data, error, count } = await query
|
|
1806
|
-
.order("last_observation_at", { ascending: false })
|
|
1807
|
-
.range(offset, offset + limit - 1);
|
|
1808
|
-
|
|
1809
|
-
if (error) throw error;
|
|
1810
|
-
|
|
1811
|
-
// Get unique entity IDs to fetch entity details
|
|
1812
|
-
const entityIds = new Set<string>();
|
|
1813
|
-
(data || []).forEach((rel: any) => {
|
|
1814
|
-
entityIds.add(rel.source_entity_id);
|
|
1815
|
-
entityIds.add(rel.target_entity_id);
|
|
1816
|
-
});
|
|
1817
|
-
|
|
1818
|
-
// Fetch entity details - filter by user_id for security
|
|
1819
|
-
let entityMap = new Map<string, { canonical_name: string; entity_type: string }>();
|
|
1820
|
-
if (entityIds.size > 0) {
|
|
1821
|
-
const { data: entities, error: entityError } = await db
|
|
1822
|
-
.from("entities")
|
|
1823
|
-
.select("id, canonical_name, entity_type")
|
|
1824
|
-
.in("id", Array.from(entityIds))
|
|
1825
|
-
.eq("user_id", userId); // SECURITY: Only return entities for authenticated user
|
|
1826
|
-
|
|
1827
|
-
if (!entityError && entities) {
|
|
1828
|
-
entityMap = new Map(
|
|
1829
|
-
entities.map((e: any) => [
|
|
1830
|
-
e.id,
|
|
1831
|
-
{ canonical_name: e.canonical_name, entity_type: e.entity_type },
|
|
1832
|
-
])
|
|
1833
|
-
);
|
|
1834
|
-
}
|
|
1835
|
-
}
|
|
1836
|
-
|
|
1837
|
-
// Add id field and entity information for frontend compatibility
|
|
1838
|
-
const relationships = (data || []).map((rel: any) => {
|
|
1839
|
-
const sourceEntity = entityMap.get(rel.source_entity_id);
|
|
1840
|
-
const targetEntity = entityMap.get(rel.target_entity_id);
|
|
1841
|
-
|
|
1842
|
-
return {
|
|
1843
|
-
...rel,
|
|
1844
|
-
id: rel.relationship_key,
|
|
1845
|
-
source_canonical_name: sourceEntity?.canonical_name,
|
|
1846
|
-
source_entity_type: sourceEntity?.entity_type,
|
|
1847
|
-
target_canonical_name: targetEntity?.canonical_name,
|
|
1848
|
-
target_entity_type: targetEntity?.entity_type,
|
|
1849
|
-
};
|
|
1850
|
-
});
|
|
1851
|
-
|
|
1852
|
-
return res.json({
|
|
1853
|
-
relationships,
|
|
1854
|
-
total: count || 0,
|
|
1855
|
-
limit,
|
|
1856
|
-
offset,
|
|
1857
|
-
});
|
|
1858
|
-
} catch (error) {
|
|
1859
|
-
return handleApiError(
|
|
1860
|
-
req,
|
|
1861
|
-
res,
|
|
1862
|
-
error,
|
|
1863
|
-
"Failed to list relationships",
|
|
1864
|
-
"DB_QUERY_FAILED",
|
|
1865
|
-
"APIError:relationships_list"
|
|
1866
|
-
);
|
|
1867
|
-
}
|
|
1868
|
-
});
|
|
1869
|
-
|
|
1870
|
-
// GET /api/relationships/:id - Get specific relationship (FU-XXX)
|
|
1871
|
-
// REQUIRES AUTHENTICATION - verifies relationship belongs to authenticated user
|
|
1872
|
-
// Note: id is actually relationship_key (composite key)
|
|
1873
|
-
app.get("/relationships/:id", async (req, res) => {
|
|
1874
|
-
try {
|
|
1875
|
-
// Get authenticated user_id (REQUIRED)
|
|
1876
|
-
const userId = await getAuthenticatedUserId(req, req.query.user_id as string | undefined);
|
|
1877
|
-
|
|
1878
|
-
const relationshipKey = decodeURIComponent(req.params.id);
|
|
1879
|
-
|
|
1880
|
-
// Verify relationship exists and belongs to authenticated user
|
|
1881
|
-
const { data, error } = await db
|
|
1882
|
-
.from("relationship_snapshots")
|
|
1883
|
-
.select("*")
|
|
1884
|
-
.eq("relationship_key", relationshipKey)
|
|
1885
|
-
.eq("user_id", userId) // SECURITY: Only return if belongs to authenticated user
|
|
1886
|
-
.single();
|
|
1887
|
-
|
|
1888
|
-
if (error) {
|
|
1889
|
-
if (error.code === "PGRST116") {
|
|
1890
|
-
return sendError(
|
|
1891
|
-
res,
|
|
1892
|
-
404,
|
|
1893
|
-
"RESOURCE_NOT_FOUND",
|
|
1894
|
-
`Relationship not found: ${relationshipKey}`
|
|
1895
|
-
);
|
|
1896
|
-
}
|
|
1897
|
-
throw error;
|
|
1898
|
-
}
|
|
1899
|
-
|
|
1900
|
-
// Fetch entity details - filter by user_id for security
|
|
1901
|
-
const { data: sourceEntity } = await db
|
|
1902
|
-
.from("entities")
|
|
1903
|
-
.select("canonical_name, entity_type")
|
|
1904
|
-
.eq("id", data.source_entity_id)
|
|
1905
|
-
.eq("user_id", userId) // SECURITY: Only return if belongs to authenticated user
|
|
1906
|
-
.single();
|
|
1907
|
-
|
|
1908
|
-
const { data: targetEntity } = await db
|
|
1909
|
-
.from("entities")
|
|
1910
|
-
.select("canonical_name, entity_type")
|
|
1911
|
-
.eq("id", data.target_entity_id)
|
|
1912
|
-
.eq("user_id", userId) // SECURITY: Only return if belongs to authenticated user
|
|
1913
|
-
.single();
|
|
1914
|
-
|
|
1915
|
-
// Add id field and entity information for frontend compatibility
|
|
1916
|
-
return res.json({
|
|
1917
|
-
...data,
|
|
1918
|
-
id: data.relationship_key,
|
|
1919
|
-
source_canonical_name: sourceEntity?.canonical_name,
|
|
1920
|
-
source_entity_type: sourceEntity?.entity_type,
|
|
1921
|
-
target_canonical_name: targetEntity?.canonical_name,
|
|
1922
|
-
target_entity_type: targetEntity?.entity_type,
|
|
1923
|
-
});
|
|
1924
|
-
} catch (error) {
|
|
1925
|
-
return handleApiError(
|
|
1926
|
-
req,
|
|
1927
|
-
res,
|
|
1928
|
-
error,
|
|
1929
|
-
"Failed to get relationship",
|
|
1930
|
-
"DB_QUERY_FAILED",
|
|
1931
|
-
"APIError:relationship_detail"
|
|
1932
|
-
);
|
|
1933
|
-
}
|
|
1934
|
-
});
|
|
1935
|
-
|
|
1936
|
-
// POST /api/relationships/snapshot - Get relationship snapshot with provenance
|
|
1937
|
-
app.post("/relationships/snapshot", async (req, res) => {
|
|
1938
|
-
const parsed = RelationshipSnapshotRequestSchema.safeParse(req.body);
|
|
1939
|
-
if (!parsed.success) {
|
|
1940
|
-
logWarn("ValidationError:get_relationship_snapshot", req, { issues: parsed.error.issues });
|
|
1941
|
-
return sendValidationError(res, parsed.error.issues);
|
|
1942
|
-
}
|
|
1943
|
-
|
|
1944
|
-
try {
|
|
1945
|
-
const userId = await getAuthenticatedUserId(req, undefined);
|
|
1946
|
-
const { relationship_type, source_entity_id, target_entity_id } = parsed.data;
|
|
1947
|
-
const relationshipKey = `${relationship_type}:${source_entity_id}:${target_entity_id}`;
|
|
1948
|
-
|
|
1949
|
-
const { data: snapshot, error: snapshotError } = await db
|
|
1950
|
-
.from("relationship_snapshots")
|
|
1951
|
-
.select("*")
|
|
1952
|
-
.eq("relationship_key", relationshipKey)
|
|
1953
|
-
.eq("user_id", userId)
|
|
1954
|
-
.maybeSingle();
|
|
1955
|
-
|
|
1956
|
-
if (snapshotError) throw snapshotError;
|
|
1957
|
-
if (!snapshot) {
|
|
1958
|
-
return sendError(
|
|
1959
|
-
res,
|
|
1960
|
-
404,
|
|
1961
|
-
"RESOURCE_NOT_FOUND",
|
|
1962
|
-
`Relationship not found: ${relationshipKey}`
|
|
1963
|
-
);
|
|
1964
|
-
}
|
|
1965
|
-
|
|
1966
|
-
const { data: observations, error: obsError } = await db
|
|
1967
|
-
.from("relationship_observations")
|
|
1968
|
-
.select("id, source_id, observed_at, specificity_score, source_priority, metadata")
|
|
1969
|
-
.eq("relationship_key", relationshipKey)
|
|
1970
|
-
.eq("user_id", userId)
|
|
1971
|
-
.order("observed_at", { ascending: false });
|
|
1972
|
-
|
|
1973
|
-
if (obsError) throw obsError;
|
|
1974
|
-
|
|
1975
|
-
return res.json({ snapshot, observations: observations ?? [] });
|
|
1976
|
-
} catch (error) {
|
|
1977
|
-
return handleApiError(
|
|
1978
|
-
req,
|
|
1979
|
-
res,
|
|
1980
|
-
error,
|
|
1981
|
-
"Failed to get relationship snapshot",
|
|
1982
|
-
"DB_QUERY_FAILED",
|
|
1983
|
-
"APIError:relationship_snapshot"
|
|
1984
|
-
);
|
|
1985
|
-
}
|
|
1986
|
-
});
|
|
1987
|
-
|
|
1988
|
-
// GET /api/timeline - Get timeline events with filtering (FU-303)
|
|
1989
|
-
// REQUIRES AUTHENTICATION - filters events through sources.user_id
|
|
1990
|
-
app.get("/timeline", async (req, res) => {
|
|
1991
|
-
try {
|
|
1992
|
-
// Get authenticated user_id (REQUIRED)
|
|
1993
|
-
const userId = await getAuthenticatedUserId(req, req.query.user_id as string | undefined);
|
|
1994
|
-
|
|
1995
|
-
const startDate = req.query.start_date as string | undefined;
|
|
1996
|
-
const endDate = req.query.end_date as string | undefined;
|
|
1997
|
-
const eventType = req.query.event_type as string | undefined;
|
|
1998
|
-
const entityId = req.query.entity_id as string | undefined;
|
|
1999
|
-
const limit = parseInt(req.query.limit as string) || 100;
|
|
2000
|
-
const offset = parseInt(req.query.offset as string) || 0;
|
|
2001
|
-
|
|
2002
|
-
// Get source IDs for this user first (timeline_events doesn't have user_id)
|
|
2003
|
-
const { data: userSources, error: sourcesError } = await db
|
|
2004
|
-
.from("sources")
|
|
2005
|
-
.select("id")
|
|
2006
|
-
.eq("user_id", userId);
|
|
2007
|
-
|
|
2008
|
-
if (sourcesError) throw sourcesError;
|
|
2009
|
-
|
|
2010
|
-
const sourceIds = (userSources || []).map((s: any) => s.id);
|
|
2011
|
-
|
|
2012
|
-
// Build query - filter by source_ids that belong to authenticated user
|
|
2013
|
-
let query = db.from("timeline_events").select("*", { count: "exact" });
|
|
2014
|
-
|
|
2015
|
-
// SECURITY: Only return events from sources that belong to authenticated user
|
|
2016
|
-
if (sourceIds.length > 0) {
|
|
2017
|
-
query = query.in("source_id", sourceIds);
|
|
2018
|
-
} else {
|
|
2019
|
-
// User has no sources - return empty result
|
|
2020
|
-
return res.json({
|
|
2021
|
-
events: [],
|
|
2022
|
-
total: 0,
|
|
2023
|
-
limit,
|
|
2024
|
-
offset,
|
|
2025
|
-
});
|
|
2026
|
-
}
|
|
2027
|
-
|
|
2028
|
-
// Filter by date range
|
|
2029
|
-
if (startDate) {
|
|
2030
|
-
query = query.gte("event_timestamp", startDate);
|
|
2031
|
-
}
|
|
2032
|
-
if (endDate) {
|
|
2033
|
-
query = query.lte("event_timestamp", endDate);
|
|
2034
|
-
}
|
|
2035
|
-
|
|
2036
|
-
// Filter by event type
|
|
2037
|
-
if (eventType) {
|
|
2038
|
-
query = query.eq("event_type", eventType);
|
|
2039
|
-
}
|
|
2040
|
-
|
|
2041
|
-
// Filter by entity_id
|
|
2042
|
-
if (entityId) {
|
|
2043
|
-
query = query.eq("entity_id", entityId);
|
|
2044
|
-
}
|
|
2045
|
-
|
|
2046
|
-
// Sort by event timestamp (chronological)
|
|
2047
|
-
query = query.order("event_timestamp", { ascending: false });
|
|
2048
|
-
|
|
2049
|
-
// Pagination
|
|
2050
|
-
query = query.range(offset, offset + limit - 1);
|
|
2051
|
-
|
|
2052
|
-
const { data, error, count } = await query;
|
|
2053
|
-
|
|
2054
|
-
if (error) {
|
|
2055
|
-
const err = error as { code?: string; message?: string };
|
|
2056
|
-
const errorDetails =
|
|
2057
|
-
typeof error === "object" && error && "details" in error
|
|
2058
|
-
? (error as { details?: string }).details
|
|
2059
|
-
: undefined;
|
|
2060
|
-
const errorHint =
|
|
2061
|
-
typeof error === "object" && error && "hint" in error
|
|
2062
|
-
? (error as { hint?: string }).hint
|
|
2063
|
-
: undefined;
|
|
2064
|
-
|
|
2065
|
-
logError("APIError:timeline_query", req, error, {
|
|
2066
|
-
errorCode: err.code,
|
|
2067
|
-
errorMessage: err.message,
|
|
2068
|
-
errorDetails,
|
|
2069
|
-
errorHint,
|
|
2070
|
-
userId: userId || "none",
|
|
2071
|
-
});
|
|
2072
|
-
|
|
2073
|
-
return sendError(res, 500, "DB_QUERY_FAILED", "Failed to query timeline events", {
|
|
2074
|
-
code: err.code,
|
|
2075
|
-
message: err.message,
|
|
2076
|
-
hint: errorHint,
|
|
2077
|
-
});
|
|
2078
|
-
}
|
|
2079
|
-
|
|
2080
|
-
return res.json({
|
|
2081
|
-
events: data || [],
|
|
2082
|
-
total: count || 0,
|
|
2083
|
-
limit,
|
|
2084
|
-
offset,
|
|
2085
|
-
});
|
|
2086
|
-
} catch (error) {
|
|
2087
|
-
if (error instanceof Error && error.message.includes("Not authenticated")) {
|
|
2088
|
-
return sendError(res, 401, "AUTH_REQUIRED", error.message);
|
|
2089
|
-
}
|
|
2090
|
-
logError("APIError:timeline", req, error);
|
|
2091
|
-
const message = error instanceof Error ? error.message : "Failed to get timeline";
|
|
2092
|
-
// Include error code if available (for db errors)
|
|
2093
|
-
const errorCode = (error as any)?.code;
|
|
2094
|
-
const errorDetails = errorCode ? { code: errorCode, message } : { message };
|
|
2095
|
-
return sendError(res, 500, "DB_QUERY_FAILED", message, errorDetails);
|
|
2096
|
-
}
|
|
2097
|
-
});
|
|
2098
|
-
|
|
2099
|
-
// GET /api/timeline/:id - Get one timeline event by ID (FU-303)
|
|
2100
|
-
// REQUIRES AUTHENTICATION - verifies event source belongs to authenticated user
|
|
2101
|
-
app.get("/timeline/:id", async (req, res) => {
|
|
2102
|
-
try {
|
|
2103
|
-
const eventId = decodeURIComponent(req.params.id);
|
|
2104
|
-
const userId = await getAuthenticatedUserId(req, req.query.user_id as string | undefined);
|
|
2105
|
-
|
|
2106
|
-
// Get source IDs for this user first (timeline_events doesn't have user_id)
|
|
2107
|
-
const { data: userSources, error: sourcesError } = await db
|
|
2108
|
-
.from("sources")
|
|
2109
|
-
.select("id")
|
|
2110
|
-
.eq("user_id", userId);
|
|
2111
|
-
|
|
2112
|
-
if (sourcesError) throw sourcesError;
|
|
2113
|
-
const sourceIds = (userSources || []).map((source: { id: string }) => source.id);
|
|
2114
|
-
|
|
2115
|
-
if (sourceIds.length === 0) {
|
|
2116
|
-
return sendError(res, 404, "RESOURCE_NOT_FOUND", "Timeline event not found");
|
|
2117
|
-
}
|
|
2118
|
-
|
|
2119
|
-
const { data: event, error } = await db
|
|
2120
|
-
.from("timeline_events")
|
|
2121
|
-
.select("*")
|
|
2122
|
-
.eq("id", eventId)
|
|
2123
|
-
.in("source_id", sourceIds)
|
|
2124
|
-
.maybeSingle();
|
|
2125
|
-
|
|
2126
|
-
if (error) {
|
|
2127
|
-
const err = error as { code?: string; message?: string };
|
|
2128
|
-
if (err.code === "PGRST116") {
|
|
2129
|
-
return sendError(res, 404, "RESOURCE_NOT_FOUND", "Timeline event not found");
|
|
2130
|
-
}
|
|
2131
|
-
throw error;
|
|
2132
|
-
}
|
|
2133
|
-
|
|
2134
|
-
if (!event) {
|
|
2135
|
-
return sendError(res, 404, "RESOURCE_NOT_FOUND", "Timeline event not found");
|
|
2136
|
-
}
|
|
2137
|
-
|
|
2138
|
-
return res.json({ event });
|
|
2139
|
-
} catch (error) {
|
|
2140
|
-
if (error instanceof Error && error.message.includes("Not authenticated")) {
|
|
2141
|
-
return sendError(res, 401, "AUTH_REQUIRED", error.message);
|
|
2142
|
-
}
|
|
2143
|
-
logError("APIError:timeline_detail", req, error);
|
|
2144
|
-
const message = error instanceof Error ? error.message : "Failed to get timeline event";
|
|
2145
|
-
const errorCode = (error as { code?: string })?.code;
|
|
2146
|
-
const errorDetails = errorCode ? { code: errorCode, message } : { message };
|
|
2147
|
-
return sendError(res, 500, "DB_QUERY_FAILED", message, errorDetails);
|
|
2148
|
-
}
|
|
2149
|
-
});
|
|
2150
|
-
|
|
2151
|
-
// GET /api/sources - Get source list (FU-301)
|
|
2152
|
-
app.get("/sources", async (req, res) => {
|
|
2153
|
-
try {
|
|
2154
|
-
// Get authenticated user_id (REQUIRED)
|
|
2155
|
-
const userId = await getAuthenticatedUserId(req, req.query.user_id as string | undefined);
|
|
2156
|
-
|
|
2157
|
-
const search = req.query.search as string | undefined;
|
|
2158
|
-
const mimeType = req.query.mime_type as string | undefined;
|
|
2159
|
-
const sourceType = req.query.source_type as string | undefined;
|
|
2160
|
-
const limit = parseInt(req.query.limit as string) || 100;
|
|
2161
|
-
const offset = parseInt(req.query.offset as string) || 0;
|
|
2162
|
-
|
|
2163
|
-
// Build query - ALWAYS filter by authenticated user_id
|
|
2164
|
-
let query = db.from("sources").select("*", { count: "exact" }).eq("user_id", userId); // SECURITY: Always filter by authenticated user
|
|
2165
|
-
|
|
2166
|
-
// Filter by MIME type
|
|
2167
|
-
if (mimeType) {
|
|
2168
|
-
query = query.eq("mime_type", mimeType);
|
|
2169
|
-
}
|
|
2170
|
-
|
|
2171
|
-
// Filter by source type
|
|
2172
|
-
if (sourceType) {
|
|
2173
|
-
query = query.eq("source_type", sourceType);
|
|
2174
|
-
}
|
|
2175
|
-
|
|
2176
|
-
// Search in file names and raw text
|
|
2177
|
-
if (search) {
|
|
2178
|
-
query = query.or(`file_name.ilike.%${search}%,original_filename.ilike.%${search}%`);
|
|
2179
|
-
}
|
|
2180
|
-
|
|
2181
|
-
// Sort by created_at descending (most recent first)
|
|
2182
|
-
query = query.order("created_at", { ascending: false });
|
|
2183
|
-
|
|
2184
|
-
// Pagination
|
|
2185
|
-
query = query.range(offset, offset + limit - 1);
|
|
2186
|
-
|
|
2187
|
-
const { data, error, count } = await query;
|
|
2188
|
-
|
|
2189
|
-
if (error) throw error;
|
|
2190
|
-
|
|
2191
|
-
return res.json({
|
|
2192
|
-
sources: data || [],
|
|
2193
|
-
total: count || 0,
|
|
2194
|
-
limit,
|
|
2195
|
-
offset,
|
|
2196
|
-
});
|
|
2197
|
-
} catch (error) {
|
|
2198
|
-
return handleApiError(
|
|
2199
|
-
req,
|
|
2200
|
-
res,
|
|
2201
|
-
error,
|
|
2202
|
-
"Failed to get sources",
|
|
2203
|
-
"DB_QUERY_FAILED",
|
|
2204
|
-
"APIError:sources_list"
|
|
2205
|
-
);
|
|
2206
|
-
}
|
|
2207
|
-
});
|
|
2208
|
-
|
|
2209
|
-
// GET /api/sources/:id - Get source detail (FU-302)
|
|
2210
|
-
// REQUIRES AUTHENTICATION - verifies source belongs to authenticated user
|
|
2211
|
-
app.get("/sources/:id", async (req, res) => {
|
|
2212
|
-
try {
|
|
2213
|
-
// Get authenticated user_id (REQUIRED)
|
|
2214
|
-
const userId = await getAuthenticatedUserId(req, req.query.user_id as string | undefined);
|
|
2215
|
-
|
|
2216
|
-
const sourceId = req.params.id;
|
|
2217
|
-
|
|
2218
|
-
// Verify source exists and belongs to authenticated user
|
|
2219
|
-
const { data: source, error } = await db
|
|
2220
|
-
.from("sources")
|
|
2221
|
-
.select("*")
|
|
2222
|
-
.eq("id", sourceId)
|
|
2223
|
-
.eq("user_id", userId) // SECURITY: Only return if belongs to authenticated user
|
|
2224
|
-
.single();
|
|
2225
|
-
|
|
2226
|
-
if (error) {
|
|
2227
|
-
if (error.code === "PGRST116") {
|
|
2228
|
-
return sendError(res, 404, "RESOURCE_NOT_FOUND", "Source not found");
|
|
2229
|
-
}
|
|
2230
|
-
throw error;
|
|
2231
|
-
}
|
|
2232
|
-
|
|
2233
|
-
if (!source) {
|
|
2234
|
-
return sendError(res, 404, "RESOURCE_NOT_FOUND", "Source not found");
|
|
2235
|
-
}
|
|
2236
|
-
|
|
2237
|
-
return res.json(source);
|
|
2238
|
-
} catch (error) {
|
|
2239
|
-
return handleApiError(
|
|
2240
|
-
req,
|
|
2241
|
-
res,
|
|
2242
|
-
error,
|
|
2243
|
-
"Failed to get source",
|
|
2244
|
-
"DB_QUERY_FAILED",
|
|
2245
|
-
"APIError:source_detail"
|
|
2246
|
-
);
|
|
2247
|
-
}
|
|
2248
|
-
});
|
|
2249
|
-
|
|
2250
|
-
// GET /api/interpretations - Get interpretations with filters (FU-302)
|
|
2251
|
-
// REQUIRES AUTHENTICATION - all queries filtered by authenticated user_id
|
|
2252
|
-
app.get("/interpretations", async (req, res) => {
|
|
2253
|
-
try {
|
|
2254
|
-
// Get authenticated user_id (REQUIRED)
|
|
2255
|
-
const userId = await getAuthenticatedUserId(req, req.query.user_id as string | undefined);
|
|
2256
|
-
|
|
2257
|
-
const sourceId = req.query.source_id as string | undefined;
|
|
2258
|
-
const limit = parseInt(req.query.limit as string) || 100;
|
|
2259
|
-
const offset = parseInt(req.query.offset as string) || 0;
|
|
2260
|
-
|
|
2261
|
-
// Build query - ALWAYS filter by authenticated user_id
|
|
2262
|
-
let query = db
|
|
2263
|
-
.from("interpretations")
|
|
2264
|
-
.select("*", { count: "exact" })
|
|
2265
|
-
.eq("user_id", userId); // SECURITY: Always filter by authenticated user
|
|
2266
|
-
|
|
2267
|
-
if (sourceId) {
|
|
2268
|
-
query = query.eq("source_id", sourceId);
|
|
2269
|
-
}
|
|
2270
|
-
|
|
2271
|
-
query = query.order("started_at", { ascending: false });
|
|
2272
|
-
query = query.range(offset, offset + limit - 1);
|
|
2273
|
-
|
|
2274
|
-
const { data, error, count } = await query;
|
|
2275
|
-
|
|
2276
|
-
if (error) throw error;
|
|
2277
|
-
|
|
2278
|
-
return res.json({
|
|
2279
|
-
interpretations: data || [],
|
|
2280
|
-
total: count || 0,
|
|
2281
|
-
limit,
|
|
2282
|
-
offset,
|
|
2283
|
-
});
|
|
2284
|
-
} catch (error) {
|
|
2285
|
-
logError("APIError:interpretations_list", req, error);
|
|
2286
|
-
const message = error instanceof Error ? error.message : "Failed to get interpretations";
|
|
2287
|
-
return sendError(res, 500, "DB_QUERY_FAILED", message);
|
|
2288
|
-
}
|
|
2289
|
-
});
|
|
2290
|
-
|
|
2291
|
-
// GET /api/observations - Get observations with filters (FU-302, FU-601)
|
|
2292
|
-
// REQUIRES AUTHENTICATION - all queries filtered by authenticated user_id
|
|
2293
|
-
app.get("/observations", async (req, res) => {
|
|
2294
|
-
try {
|
|
2295
|
-
// Get authenticated user_id (REQUIRED)
|
|
2296
|
-
const userId = await getAuthenticatedUserId(req, req.query.user_id as string | undefined);
|
|
2297
|
-
|
|
2298
|
-
const sourceId = req.query.source_id as string | undefined;
|
|
2299
|
-
const entityId = req.query.entity_id as string | undefined;
|
|
2300
|
-
const limit = parseInt(req.query.limit as string) || 100;
|
|
2301
|
-
const offset = parseInt(req.query.offset as string) || 0;
|
|
2302
|
-
|
|
2303
|
-
// Build query - ALWAYS filter by authenticated user_id
|
|
2304
|
-
let query = db.from("observations").select("*", { count: "exact" }).eq("user_id", userId); // SECURITY: Always filter by authenticated user
|
|
2305
|
-
|
|
2306
|
-
if (sourceId) {
|
|
2307
|
-
query = query.eq("source_id", sourceId);
|
|
2308
|
-
}
|
|
2309
|
-
|
|
2310
|
-
if (entityId) {
|
|
2311
|
-
query = query.eq("entity_id", entityId);
|
|
2312
|
-
}
|
|
2313
|
-
|
|
2314
|
-
query = query.order("observed_at", { ascending: false });
|
|
2315
|
-
query = query.range(offset, offset + limit - 1);
|
|
2316
|
-
|
|
2317
|
-
const { data, error, count } = await query;
|
|
2318
|
-
|
|
2319
|
-
if (error) throw error;
|
|
2320
|
-
|
|
2321
|
-
return res.json({
|
|
2322
|
-
observations: data || [],
|
|
2323
|
-
total: count || 0,
|
|
2324
|
-
limit,
|
|
2325
|
-
offset,
|
|
2326
|
-
});
|
|
2327
|
-
} catch (error) {
|
|
2328
|
-
return handleApiError(
|
|
2329
|
-
req,
|
|
2330
|
-
res,
|
|
2331
|
-
error,
|
|
2332
|
-
"Failed to get observations",
|
|
2333
|
-
"DB_QUERY_FAILED",
|
|
2334
|
-
"APIError:observations_list"
|
|
2335
|
-
);
|
|
2336
|
-
}
|
|
2337
|
-
});
|
|
2338
|
-
|
|
2339
|
-
// GET /api/stats - Get dashboard statistics (FU-305)
|
|
2340
|
-
// REQUIRES AUTHENTICATION - all stats filtered by authenticated user_id
|
|
2341
|
-
app.get("/stats", async (req, res) => {
|
|
2342
|
-
try {
|
|
2343
|
-
// Get authenticated user_id (REQUIRED)
|
|
2344
|
-
const userId = await getAuthenticatedUserId(req, req.query.user_id as string | undefined);
|
|
2345
|
-
|
|
2346
|
-
const { getDashboardStats } = await import("./services/dashboard_stats.js");
|
|
2347
|
-
const stats = await getDashboardStats(userId); // SECURITY: Stats filtered by authenticated user
|
|
2348
|
-
|
|
2349
|
-
return res.json(stats);
|
|
2350
|
-
} catch (error) {
|
|
2351
|
-
return handleApiError(
|
|
2352
|
-
req,
|
|
2353
|
-
res,
|
|
2354
|
-
error,
|
|
2355
|
-
"Failed to get dashboard stats",
|
|
2356
|
-
"DB_QUERY_FAILED",
|
|
2357
|
-
"APIError:dashboard_stats"
|
|
2358
|
-
);
|
|
2359
|
-
}
|
|
2360
|
-
});
|
|
2361
|
-
|
|
2362
|
-
// POST /api/observations/create - Create observation for entity
|
|
2363
|
-
// REQUIRES AUTHENTICATION - validates user_id matches authenticated user
|
|
2364
|
-
app.post("/observations/create", async (req, res) => {
|
|
2365
|
-
const schema = z.object({
|
|
2366
|
-
entity_type: z.string(),
|
|
2367
|
-
entity_identifier: z.string(),
|
|
2368
|
-
fields: z.record(z.unknown()),
|
|
2369
|
-
source_priority: z.number().optional().default(100),
|
|
2370
|
-
user_id: z.string().uuid(),
|
|
2371
|
-
});
|
|
2372
|
-
|
|
2373
|
-
const parsed = schema.safeParse(req.body);
|
|
2374
|
-
if (!parsed.success) {
|
|
2375
|
-
logWarn("ValidationError:observations_create", req, {
|
|
2376
|
-
issues: parsed.error.issues,
|
|
2377
|
-
});
|
|
2378
|
-
return sendValidationError(res, parsed.error.issues);
|
|
2379
|
-
}
|
|
2380
|
-
|
|
2381
|
-
try {
|
|
2382
|
-
// Get authenticated user_id and validate it matches provided user_id
|
|
2383
|
-
const authenticatedUserId = await getAuthenticatedUserId(req, parsed.data.user_id);
|
|
2384
|
-
|
|
2385
|
-
const { entity_type, entity_identifier, fields, source_priority, user_id } = parsed.data;
|
|
2386
|
-
|
|
2387
|
-
// SECURITY: Ensure provided user_id matches authenticated user
|
|
2388
|
-
if (user_id !== authenticatedUserId) {
|
|
2389
|
-
return res
|
|
2390
|
-
.status(403)
|
|
2391
|
-
.json(buildErrorEnvelope("FORBIDDEN", "user_id does not match authenticated user."));
|
|
2392
|
-
}
|
|
2393
|
-
|
|
2394
|
-
const { resolveEntity } = await import("./services/entity_resolution.js");
|
|
2395
|
-
const { createObservation } = await import("./services/observation_storage.js");
|
|
2396
|
-
const { getSnapshot } = await import("./services/snapshot_computation.js");
|
|
2397
|
-
|
|
2398
|
-
const entity_id = await resolveEntity({
|
|
2399
|
-
entityType: entity_type,
|
|
2400
|
-
fields: { name: entity_identifier },
|
|
2401
|
-
userId: user_id,
|
|
2402
|
-
});
|
|
2403
|
-
|
|
2404
|
-
const obsData = await createObservation({
|
|
2405
|
-
entity_id,
|
|
2406
|
-
entity_type,
|
|
2407
|
-
schema_version: "1.0",
|
|
2408
|
-
source_id: null,
|
|
2409
|
-
interpretation_id: null,
|
|
2410
|
-
observed_at: new Date().toISOString(),
|
|
2411
|
-
specificity_score: 1.0,
|
|
2412
|
-
source_priority,
|
|
2413
|
-
fields,
|
|
2414
|
-
user_id,
|
|
2415
|
-
});
|
|
2416
|
-
|
|
2417
|
-
const snapshot = await getSnapshot(entity_id, user_id);
|
|
2418
|
-
|
|
2419
|
-
return res.json({
|
|
2420
|
-
observation_id: obsData.id,
|
|
2421
|
-
entity_id,
|
|
2422
|
-
snapshot: snapshot?.snapshot || {},
|
|
2423
|
-
});
|
|
2424
|
-
} catch (error) {
|
|
2425
|
-
logError("APIError:observations_create", req, error);
|
|
2426
|
-
const message = error instanceof Error ? error.message : "Failed to create observation";
|
|
2427
|
-
return res.status(500).json(buildErrorEnvelope("DB_QUERY_FAILED", message));
|
|
2428
|
-
}
|
|
2429
|
-
});
|
|
2430
|
-
|
|
2431
|
-
async function storeStructuredForApi(params: {
|
|
2432
|
-
userId: string;
|
|
2433
|
-
entities: Record<string, unknown>[];
|
|
2434
|
-
sourcePriority: number;
|
|
2435
|
-
idempotencyKey: string;
|
|
2436
|
-
originalFilename?: string;
|
|
2437
|
-
}) {
|
|
2438
|
-
const { userId, entities, sourcePriority, idempotencyKey, originalFilename } = params;
|
|
2439
|
-
const { resolveEntity } = await import("./services/entity_resolution.js");
|
|
2440
|
-
|
|
2441
|
-
const { data: existingSource, error: existingSourceError } = await db
|
|
2442
|
-
.from("sources")
|
|
2443
|
-
.select("id")
|
|
2444
|
-
.eq("user_id", userId)
|
|
2445
|
-
.eq("idempotency_key", idempotencyKey)
|
|
2446
|
-
.maybeSingle();
|
|
2447
|
-
|
|
2448
|
-
if (existingSourceError) {
|
|
2449
|
-
throw existingSourceError;
|
|
2450
|
-
}
|
|
2451
|
-
|
|
2452
|
-
if (existingSource) {
|
|
2453
|
-
const { listObservationsForSource } = await import("./services/observation_storage.js");
|
|
2454
|
-
const existingObs = await listObservationsForSource(existingSource.id, userId);
|
|
2455
|
-
|
|
2456
|
-
const existingEntities = existingObs.map((obs) => ({
|
|
2457
|
-
entity_id: obs.entity_id,
|
|
2458
|
-
entity_type: obs.entity_type,
|
|
2459
|
-
observation_id: obs.id,
|
|
2460
|
-
}));
|
|
2461
|
-
|
|
2462
|
-
return {
|
|
2463
|
-
success: true,
|
|
2464
|
-
source_id: existingSource.id,
|
|
2465
|
-
entities_created: existingEntities.length,
|
|
2466
|
-
observations_created: existingEntities.length,
|
|
2467
|
-
entities: existingEntities,
|
|
2468
|
-
};
|
|
2469
|
-
}
|
|
2470
|
-
|
|
2471
|
-
const { createObservation } = await import("./services/observation_storage.js");
|
|
2472
|
-
|
|
2473
|
-
const jsonContent = JSON.stringify(entities, (key, value) => {
|
|
2474
|
-
if (typeof value === "bigint") {
|
|
2475
|
-
return Number(value);
|
|
2476
|
-
}
|
|
2477
|
-
return value;
|
|
2478
|
-
});
|
|
2479
|
-
const filenameForStorage = originalFilename?.trim() || undefined;
|
|
2480
|
-
const storageResult = await storeRawContent({
|
|
2481
|
-
userId,
|
|
2482
|
-
fileBuffer: Buffer.from(jsonContent, "utf-8"),
|
|
2483
|
-
mimeType: "application/json",
|
|
2484
|
-
originalFilename: filenameForStorage,
|
|
2485
|
-
idempotencyKey,
|
|
2486
|
-
provenance: {
|
|
2487
|
-
upload_method: "api_store",
|
|
2488
|
-
client: "api",
|
|
2489
|
-
source_priority: sourcePriority,
|
|
2490
|
-
},
|
|
2491
|
-
});
|
|
2492
|
-
|
|
2493
|
-
const createdEntities = [];
|
|
2494
|
-
|
|
2495
|
-
for (const entityData of entities) {
|
|
2496
|
-
const entity_type = entityData.entity_type as string;
|
|
2497
|
-
if (!entity_type) {
|
|
2498
|
-
throw new Error("entity_type is required for each entity");
|
|
2499
|
-
}
|
|
2500
|
-
|
|
2501
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- intentional omit
|
|
2502
|
-
const { entity_type: _removed, ...fields } = entityData;
|
|
2503
|
-
const entity_id = await resolveEntity({
|
|
2504
|
-
entityType: entity_type,
|
|
2505
|
-
fields,
|
|
2506
|
-
userId,
|
|
2507
|
-
});
|
|
2508
|
-
|
|
2509
|
-
const obsData = await createObservation({
|
|
2510
|
-
entity_id,
|
|
2511
|
-
entity_type,
|
|
2512
|
-
schema_version: "1.0",
|
|
2513
|
-
source_id: storageResult.sourceId,
|
|
2514
|
-
interpretation_id: null,
|
|
2515
|
-
observed_at: new Date().toISOString(),
|
|
2516
|
-
specificity_score: 1.0,
|
|
2517
|
-
source_priority: sourcePriority,
|
|
2518
|
-
fields,
|
|
2519
|
-
user_id: userId,
|
|
2520
|
-
});
|
|
2521
|
-
|
|
2522
|
-
createdEntities.push({
|
|
2523
|
-
entity_id,
|
|
2524
|
-
entity_type,
|
|
2525
|
-
observation_id: obsData.id,
|
|
2526
|
-
});
|
|
2527
|
-
}
|
|
2528
|
-
|
|
2529
|
-
return {
|
|
2530
|
-
success: true,
|
|
2531
|
-
source_id: storageResult.sourceId,
|
|
2532
|
-
entities_created: createdEntities.length,
|
|
2533
|
-
observations_created: createdEntities.length,
|
|
2534
|
-
entities: createdEntities,
|
|
2535
|
-
};
|
|
2536
|
-
}
|
|
2537
|
-
|
|
2538
|
-
async function storeUnstructuredForApi(params: {
|
|
2539
|
-
userId: string;
|
|
2540
|
-
fileContent: string;
|
|
2541
|
-
mimeType: string;
|
|
2542
|
-
idempotencyKey?: string;
|
|
2543
|
-
originalFilename?: string;
|
|
2544
|
-
interpret: boolean;
|
|
2545
|
-
interpretationConfig?: Record<string, unknown>;
|
|
2546
|
-
}) {
|
|
2547
|
-
const { runInterpretation } = await import("./services/interpretation.js");
|
|
2548
|
-
const {
|
|
2549
|
-
fileContent,
|
|
2550
|
-
mimeType,
|
|
2551
|
-
idempotencyKey,
|
|
2552
|
-
originalFilename,
|
|
2553
|
-
interpret,
|
|
2554
|
-
userId,
|
|
2555
|
-
interpretationConfig,
|
|
2556
|
-
} = params;
|
|
2557
|
-
const fileBuffer = Buffer.from(fileContent, "base64");
|
|
2558
|
-
const resolvedIdempotencyKey =
|
|
2559
|
-
idempotencyKey ??
|
|
2560
|
-
(await import("node:crypto")).createHash("sha256").update(fileBuffer).digest("hex");
|
|
2561
|
-
|
|
2562
|
-
const storageResult = await storeRawContent({
|
|
2563
|
-
userId,
|
|
2564
|
-
fileBuffer,
|
|
2565
|
-
mimeType,
|
|
2566
|
-
originalFilename: originalFilename?.trim() || undefined,
|
|
2567
|
-
idempotencyKey: resolvedIdempotencyKey,
|
|
2568
|
-
provenance: { upload_method: "api_store_unstructured", client: "api" },
|
|
2569
|
-
});
|
|
2570
|
-
|
|
2571
|
-
const response: {
|
|
2572
|
-
source_id: string;
|
|
2573
|
-
content_hash: string;
|
|
2574
|
-
file_size: number;
|
|
2575
|
-
deduplicated?: boolean;
|
|
2576
|
-
entities_created?: number;
|
|
2577
|
-
observations_created?: number;
|
|
2578
|
-
interpretation_run_id?: string;
|
|
2579
|
-
interpretation?: unknown;
|
|
2580
|
-
interpretation_debug?: Record<string, unknown>;
|
|
2581
|
-
entity_ids?: string[];
|
|
2582
|
-
} = {
|
|
2583
|
-
source_id: storageResult.sourceId,
|
|
2584
|
-
content_hash: storageResult.contentHash,
|
|
2585
|
-
file_size: storageResult.fileSize,
|
|
2586
|
-
deduplicated: storageResult.deduplicated,
|
|
2587
|
-
entities_created: 0,
|
|
2588
|
-
observations_created: 0,
|
|
2589
|
-
};
|
|
2590
|
-
|
|
2591
|
-
if (interpret && storageResult.sourceId) {
|
|
2592
|
-
const { extractTextFromBuffer, getPdfFirstPageImageDataUrl, getPdfWorkerDebug } = await import(
|
|
2593
|
-
"./services/file_text_extraction.js"
|
|
2594
|
-
);
|
|
2595
|
-
const {
|
|
2596
|
-
extractWithLLM,
|
|
2597
|
-
extractWithLLMFromImage,
|
|
2598
|
-
extractFromCSVWithChunking,
|
|
2599
|
-
isLLMExtractionAvailable,
|
|
2600
|
-
} = await import("./services/llm_extraction.js");
|
|
2601
|
-
|
|
2602
|
-
const rawText = await extractTextFromBuffer(fileBuffer, mimeType, originalFilename || "file");
|
|
2603
|
-
|
|
2604
|
-
const isCsv = mimeType?.toLowerCase() === "text/csv";
|
|
2605
|
-
|
|
2606
|
-
if (!isCsv && !isLLMExtractionAvailable()) {
|
|
2607
|
-
response.interpretation = {
|
|
2608
|
-
skipped: true,
|
|
2609
|
-
reason: "openai_not_configured",
|
|
2610
|
-
message: "Set OPENAI_API_KEY in .env to enable AI interpretation",
|
|
2611
|
-
};
|
|
2612
|
-
return response;
|
|
2613
|
-
}
|
|
2614
|
-
|
|
2615
|
-
const isPdf =
|
|
2616
|
-
(mimeType || "").toLowerCase().includes("pdf") ||
|
|
2617
|
-
(originalFilename || "").toLowerCase().endsWith(".pdf");
|
|
2618
|
-
const rawTextLength = typeof rawText === "string" ? rawText.length : 0;
|
|
2619
|
-
|
|
2620
|
-
// Avoid nondeterministic LLM errors for empty non-PDF files; store the source and skip interpretation.
|
|
2621
|
-
if (rawTextLength === 0 && !isPdf && !isCsv) {
|
|
2622
|
-
response.interpretation = {
|
|
2623
|
-
skipped: true,
|
|
2624
|
-
reason: "empty_content",
|
|
2625
|
-
message: "No extractable text found; interpretation skipped",
|
|
2626
|
-
};
|
|
2627
|
-
response.interpretation_debug = {
|
|
2628
|
-
raw_text_length: 0,
|
|
2629
|
-
};
|
|
2630
|
-
return response;
|
|
2631
|
-
}
|
|
2632
|
-
|
|
2633
|
-
let extractionResult:
|
|
2634
|
-
| Awaited<ReturnType<typeof extractWithLLM>>
|
|
2635
|
-
| Awaited<ReturnType<typeof extractFromCSVWithChunking>>
|
|
2636
|
-
| undefined = undefined;
|
|
2637
|
-
let extractedData: Array<Record<string, unknown>> = [];
|
|
2638
|
-
const pdfDebug = getPdfWorkerDebug();
|
|
2639
|
-
const interpretationDebug: Record<string, unknown> = {
|
|
2640
|
-
raw_text_length: rawTextLength,
|
|
2641
|
-
pdf_worker_wrapper_used: pdfDebug.configured,
|
|
2642
|
-
pdf_worker_wrapper_path_tried: pdfDebug.wrapper_path_tried,
|
|
2643
|
-
pdf_worker_set_worker_error: pdfDebug.set_worker_error,
|
|
2644
|
-
};
|
|
2645
|
-
if (rawTextLength === 0 && isPdf) {
|
|
2646
|
-
interpretationDebug.vision_fallback_attempted = true;
|
|
2647
|
-
const imageResult = await getPdfFirstPageImageDataUrl(fileBuffer, mimeType, originalFilename || "file", {
|
|
2648
|
-
returnError: true,
|
|
2649
|
-
});
|
|
2650
|
-
const imageDataUrl = typeof imageResult === "object" ? imageResult.dataUrl : imageResult;
|
|
2651
|
-
if (typeof imageResult === "object" && imageResult.error) {
|
|
2652
|
-
interpretationDebug.vision_fallback_image_error = imageResult.error;
|
|
2653
|
-
}
|
|
2654
|
-
interpretationDebug.vision_fallback_image_got = Boolean(imageDataUrl);
|
|
2655
|
-
if (imageDataUrl) {
|
|
2656
|
-
try {
|
|
2657
|
-
extractionResult = await extractWithLLMFromImage(
|
|
2658
|
-
imageDataUrl,
|
|
2659
|
-
originalFilename || "file",
|
|
2660
|
-
mimeType,
|
|
2661
|
-
"gpt-4o"
|
|
2662
|
-
);
|
|
2663
|
-
interpretationDebug.used_vision_fallback = true;
|
|
2664
|
-
} catch (visionErr) {
|
|
2665
|
-
interpretationDebug.vision_fallback_error =
|
|
2666
|
-
visionErr instanceof Error ? visionErr.message : String(visionErr);
|
|
2667
|
-
extractionResult = await extractWithLLM(rawText, originalFilename || "file", mimeType, "gpt-4o");
|
|
2668
|
-
}
|
|
2669
|
-
} else {
|
|
2670
|
-
extractionResult = await extractWithLLM(rawText, originalFilename || "file", mimeType, "gpt-4o");
|
|
2671
|
-
}
|
|
2672
|
-
} else if (isCsv) {
|
|
2673
|
-
const { extractEntitiesFromCsvRows } = await import("./services/csv_row_extraction.js");
|
|
2674
|
-
extractedData = extractEntitiesFromCsvRows(fileBuffer, originalFilename || "file");
|
|
2675
|
-
} else {
|
|
2676
|
-
extractionResult = await extractWithLLM(rawText, originalFilename || "file", mimeType, "gpt-4o");
|
|
2677
|
-
}
|
|
2678
|
-
|
|
2679
|
-
if (extractedData.length === 0 && extractionResult) {
|
|
2680
|
-
if ("entities" in extractionResult) {
|
|
2681
|
-
const multiResult = extractionResult as {
|
|
2682
|
-
entities: Array<{ entity_type: string; fields: Record<string, unknown> }>;
|
|
2683
|
-
};
|
|
2684
|
-
extractedData = multiResult.entities.map((e) => ({
|
|
2685
|
-
entity_type: e.entity_type,
|
|
2686
|
-
...e.fields,
|
|
2687
|
-
}));
|
|
2688
|
-
} else {
|
|
2689
|
-
const { entity_type, fields } = extractionResult;
|
|
2690
|
-
extractedData = [{ entity_type, ...fields }];
|
|
2691
|
-
}
|
|
2692
|
-
}
|
|
2693
|
-
|
|
2694
|
-
const defaultConfig = {
|
|
2695
|
-
provider: "openai",
|
|
2696
|
-
model_id: "gpt-4o",
|
|
2697
|
-
temperature: 0,
|
|
2698
|
-
prompt_hash: "llm_extraction_v2_idempotent",
|
|
2699
|
-
code_version: "v0.2.0",
|
|
2700
|
-
};
|
|
2701
|
-
interpretationDebug.extraction_field_keys = extractedData.flatMap((d) =>
|
|
2702
|
-
Object.keys(d).filter((k) => k !== "entity_type" && k !== "type")
|
|
2703
|
-
);
|
|
2704
|
-
response.interpretation_debug = interpretationDebug;
|
|
2705
|
-
try {
|
|
2706
|
-
const interpretationResult = await runInterpretation({
|
|
2707
|
-
userId,
|
|
2708
|
-
sourceId: storageResult.sourceId,
|
|
2709
|
-
extractedData,
|
|
2710
|
-
config: {
|
|
2711
|
-
...defaultConfig,
|
|
2712
|
-
...(interpretationConfig ?? {}),
|
|
2713
|
-
},
|
|
2714
|
-
});
|
|
2715
|
-
response.interpretation = interpretationResult;
|
|
2716
|
-
if (interpretationResult.entities?.length) {
|
|
2717
|
-
response.entity_ids = interpretationResult.entities.map((e) => e.entityId);
|
|
2718
|
-
response.entities_created = interpretationResult.entities.length;
|
|
2719
|
-
response.observations_created = interpretationResult.entities.length;
|
|
2720
|
-
}
|
|
2721
|
-
if (interpretationResult.interpretationId) {
|
|
2722
|
-
response.interpretation_run_id = interpretationResult.interpretationId;
|
|
2723
|
-
}
|
|
2724
|
-
} catch (interpretError) {
|
|
2725
|
-
response.interpretation = {
|
|
2726
|
-
error: interpretError instanceof Error ? interpretError.message : String(interpretError),
|
|
2727
|
-
skipped: true,
|
|
2728
|
-
};
|
|
2729
|
-
}
|
|
2730
|
-
}
|
|
2731
|
-
|
|
2732
|
-
return response;
|
|
2733
|
-
}
|
|
2734
|
-
|
|
2735
|
-
// POST /api/store - Unified store for structured, unstructured, or combined payloads
|
|
2736
|
-
app.post("/store", async (req, res) => {
|
|
2737
|
-
const parsed = StoreRequestSchema.safeParse(req.body);
|
|
2738
|
-
if (!parsed.success) {
|
|
2739
|
-
logWarn("ValidationError:store", req, {
|
|
2740
|
-
issues: parsed.error.issues,
|
|
2741
|
-
});
|
|
2742
|
-
return sendValidationError(res, parsed.error.issues);
|
|
2743
|
-
}
|
|
2744
|
-
|
|
2745
|
-
try {
|
|
2746
|
-
const userId = await getAuthenticatedUserId(req, parsed.data.user_id);
|
|
2747
|
-
const hasEntities = Boolean(parsed.data.entities?.length);
|
|
2748
|
-
const hasFileContent = Boolean(parsed.data.file_content && parsed.data.mime_type);
|
|
2749
|
-
const hasFilePath = Boolean(parsed.data.file_path);
|
|
2750
|
-
const hasUnstructured = hasFileContent || hasFilePath;
|
|
2751
|
-
|
|
2752
|
-
let structuredResult: Record<string, unknown> | undefined;
|
|
2753
|
-
let unstructuredResult: Record<string, unknown> | undefined;
|
|
2754
|
-
|
|
2755
|
-
if (hasEntities) {
|
|
2756
|
-
if (!parsed.data.idempotency_key) {
|
|
2757
|
-
return sendError(res, 400, "VALIDATION_ERROR", "idempotency_key is required when entities are provided");
|
|
2758
|
-
}
|
|
2759
|
-
structuredResult = await storeStructuredForApi({
|
|
2760
|
-
userId,
|
|
2761
|
-
entities: parsed.data.entities as Record<string, unknown>[],
|
|
2762
|
-
sourcePriority: parsed.data.source_priority ?? 100,
|
|
2763
|
-
idempotencyKey: parsed.data.idempotency_key,
|
|
2764
|
-
originalFilename: parsed.data.original_filename,
|
|
2765
|
-
});
|
|
2766
|
-
}
|
|
2767
|
-
|
|
2768
|
-
if (hasUnstructured) {
|
|
2769
|
-
let fileContent = parsed.data.file_content;
|
|
2770
|
-
let mimeType = parsed.data.mime_type;
|
|
2771
|
-
let originalFilename = parsed.data.original_filename;
|
|
2772
|
-
|
|
2773
|
-
if (hasFilePath) {
|
|
2774
|
-
const resolvedPath = path.isAbsolute(parsed.data.file_path as string)
|
|
2775
|
-
? (parsed.data.file_path as string)
|
|
2776
|
-
: path.resolve(process.cwd(), parsed.data.file_path as string);
|
|
2777
|
-
const fileBuffer = fs.readFileSync(resolvedPath);
|
|
2778
|
-
fileContent = fileBuffer.toString("base64");
|
|
2779
|
-
if (!mimeType) {
|
|
2780
|
-
const ext = path.extname(resolvedPath).toLowerCase();
|
|
2781
|
-
mimeType = ext === ".pdf" ? "application/pdf" : ext === ".csv" ? "text/csv" : ext === ".json" ? "application/json" : "text/plain";
|
|
2782
|
-
}
|
|
2783
|
-
originalFilename = originalFilename || path.basename(resolvedPath);
|
|
2784
|
-
}
|
|
2785
|
-
|
|
2786
|
-
if (!fileContent || !mimeType) {
|
|
2787
|
-
return sendError(res, 400, "VALIDATION_ERROR", "Unstructured store requires file_content+mime_type or file_path");
|
|
2788
|
-
}
|
|
2789
|
-
|
|
2790
|
-
unstructuredResult = await storeUnstructuredForApi({
|
|
2791
|
-
userId,
|
|
2792
|
-
fileContent,
|
|
2793
|
-
mimeType,
|
|
2794
|
-
idempotencyKey: parsed.data.file_idempotency_key,
|
|
2795
|
-
originalFilename,
|
|
2796
|
-
interpret: parsed.data.interpret ?? true,
|
|
2797
|
-
interpretationConfig: parsed.data.interpretation_config,
|
|
2798
|
-
});
|
|
2799
|
-
}
|
|
2800
|
-
|
|
2801
|
-
if (structuredResult && unstructuredResult) {
|
|
2802
|
-
return res.json({
|
|
2803
|
-
structured: structuredResult,
|
|
2804
|
-
unstructured: unstructuredResult,
|
|
2805
|
-
});
|
|
2806
|
-
}
|
|
2807
|
-
if (structuredResult) {
|
|
2808
|
-
return res.json(structuredResult);
|
|
2809
|
-
}
|
|
2810
|
-
return res.status(200).json(unstructuredResult);
|
|
2811
|
-
} catch (error) {
|
|
2812
|
-
if (error instanceof Error && error.message.includes("Not authenticated")) {
|
|
2813
|
-
return sendError(res, 401, "AUTH_REQUIRED", error.message);
|
|
2814
|
-
}
|
|
2815
|
-
logError("APIError:store", req, error);
|
|
2816
|
-
const message = error instanceof Error ? error.message : "Failed to store payload";
|
|
2817
|
-
return sendError(res, 500, "DB_QUERY_FAILED", message);
|
|
2818
|
-
}
|
|
2819
|
-
});
|
|
2820
|
-
|
|
2821
|
-
// POST /api/store/unstructured - Store raw file (base64), optional AI interpretation
|
|
2822
|
-
app.post("/store/unstructured", async (req, res) => {
|
|
2823
|
-
const parsed = StoreUnstructuredRequestSchema.safeParse(req.body);
|
|
2824
|
-
if (!parsed.success) {
|
|
2825
|
-
logWarn("ValidationError:store_unstructured", req, { issues: parsed.error.issues });
|
|
2826
|
-
return sendValidationError(res, parsed.error.issues);
|
|
2827
|
-
}
|
|
2828
|
-
|
|
2829
|
-
try {
|
|
2830
|
-
const userId = await getAuthenticatedUserId(req, parsed.data.user_id);
|
|
2831
|
-
const response = await storeUnstructuredForApi({
|
|
2832
|
-
userId,
|
|
2833
|
-
fileContent: parsed.data.file_content,
|
|
2834
|
-
mimeType: parsed.data.mime_type,
|
|
2835
|
-
idempotencyKey: parsed.data.idempotency_key,
|
|
2836
|
-
originalFilename: parsed.data.original_filename,
|
|
2837
|
-
interpret: parsed.data.interpret ?? true,
|
|
2838
|
-
interpretationConfig: parsed.data.interpretation_config,
|
|
2839
|
-
});
|
|
2840
|
-
return res.status(200).json(response);
|
|
2841
|
-
} catch (error) {
|
|
2842
|
-
if (error instanceof Error && error.message.includes("Not authenticated")) {
|
|
2843
|
-
return sendError(res, 401, "AUTH_REQUIRED", error.message);
|
|
2844
|
-
}
|
|
2845
|
-
logError("APIError:store_unstructured", req, error);
|
|
2846
|
-
const message = error instanceof Error ? error.message : "Failed to store unstructured file";
|
|
2847
|
-
return sendError(res, 500, "DB_QUERY_FAILED", message);
|
|
2848
|
-
}
|
|
2849
|
-
});
|
|
2850
|
-
|
|
2851
|
-
// POST /api/observations/query - Query observations
|
|
2852
|
-
app.post("/observations/query", async (req, res) => {
|
|
2853
|
-
const parsed = ObservationsQueryRequestSchema.safeParse(req.body);
|
|
2854
|
-
if (!parsed.success) {
|
|
2855
|
-
logWarn("ValidationError:observations_query", req, {
|
|
2856
|
-
issues: parsed.error.issues,
|
|
2857
|
-
});
|
|
2858
|
-
return sendValidationError(res, parsed.error.issues);
|
|
2859
|
-
}
|
|
2860
|
-
|
|
2861
|
-
try {
|
|
2862
|
-
// Get authenticated user_id (REQUIRED)
|
|
2863
|
-
const userId = await getAuthenticatedUserId(req, parsed.data.user_id);
|
|
2864
|
-
|
|
2865
|
-
const { observation_id, entity_id, entity_type, source_id, limit, offset } = parsed.data;
|
|
2866
|
-
|
|
2867
|
-
// Build query - ALWAYS filter by authenticated user_id
|
|
2868
|
-
let query = db.from("observations").select("*", { count: "exact" }).eq("user_id", userId); // SECURITY: Always filter by authenticated user
|
|
2869
|
-
|
|
2870
|
-
if (observation_id) {
|
|
2871
|
-
query = query.eq("id", observation_id);
|
|
2872
|
-
}
|
|
2873
|
-
|
|
2874
|
-
if (entity_id) {
|
|
2875
|
-
query = query.eq("entity_id", entity_id);
|
|
2876
|
-
}
|
|
2877
|
-
|
|
2878
|
-
if (entity_type) {
|
|
2879
|
-
query = query.eq("entity_type", entity_type);
|
|
2880
|
-
}
|
|
2881
|
-
|
|
2882
|
-
if (source_id) {
|
|
2883
|
-
query = query.eq("source_id", source_id);
|
|
2884
|
-
}
|
|
2885
|
-
|
|
2886
|
-
query = query.order("observed_at", { ascending: false }).range(offset, offset + limit - 1);
|
|
2887
|
-
|
|
2888
|
-
const { data, error, count } = await query;
|
|
2889
|
-
|
|
2890
|
-
if (error) throw error;
|
|
2891
|
-
|
|
2892
|
-
return res.json({
|
|
2893
|
-
observations: data || [],
|
|
2894
|
-
total: count || 0,
|
|
2895
|
-
limit,
|
|
2896
|
-
offset,
|
|
2897
|
-
});
|
|
2898
|
-
} catch (error) {
|
|
2899
|
-
if (error instanceof Error && error.message.includes("Not authenticated")) {
|
|
2900
|
-
return sendError(res, 401, "AUTH_REQUIRED", error.message);
|
|
2901
|
-
}
|
|
2902
|
-
logError("APIError:observations_query", req, error);
|
|
2903
|
-
const message = error instanceof Error ? error.message : "Failed to query observations";
|
|
2904
|
-
return sendError(res, 500, "DB_QUERY_FAILED", message);
|
|
2905
|
-
}
|
|
2906
|
-
});
|
|
2907
|
-
|
|
2908
|
-
// POST /api/entities/merge - Merge duplicate entities
|
|
2909
|
-
// REQUIRES AUTHENTICATION - validates user_id matches authenticated user and entities belong to user
|
|
2910
|
-
app.post("/entities/merge", async (req, res) => {
|
|
2911
|
-
const parsed = MergeEntitiesRequestSchema.safeParse(req.body);
|
|
2912
|
-
if (!parsed.success) {
|
|
2913
|
-
logWarn("ValidationError:entities_merge", req, {
|
|
2914
|
-
issues: parsed.error.issues,
|
|
2915
|
-
});
|
|
2916
|
-
return sendValidationError(res, parsed.error.issues);
|
|
2917
|
-
}
|
|
2918
|
-
|
|
2919
|
-
try {
|
|
2920
|
-
const authenticatedUserId = await getAuthenticatedUserId(req, parsed.data.user_id);
|
|
2921
|
-
|
|
2922
|
-
const { from_entity_id, to_entity_id, merge_reason, user_id: providedUserId } = parsed.data;
|
|
2923
|
-
const user_id = providedUserId || authenticatedUserId;
|
|
2924
|
-
|
|
2925
|
-
if (providedUserId && providedUserId !== authenticatedUserId) {
|
|
2926
|
-
return sendError(res, 403, "FORBIDDEN", "user_id does not match authenticated user.");
|
|
2927
|
-
}
|
|
2928
|
-
|
|
2929
|
-
const { mergeEntities } = await import("./services/entity_merge.js");
|
|
2930
|
-
const result = await mergeEntities({
|
|
2931
|
-
fromEntityId: from_entity_id,
|
|
2932
|
-
toEntityId: to_entity_id,
|
|
2933
|
-
userId: user_id,
|
|
2934
|
-
mergeReason: merge_reason,
|
|
2935
|
-
mergedBy: "http_api",
|
|
2936
|
-
});
|
|
2937
|
-
|
|
2938
|
-
return res.json(result);
|
|
2939
|
-
} catch (error) {
|
|
2940
|
-
const { EntityNotFoundError, EntityAlreadyMergedError } = await import(
|
|
2941
|
-
"./services/entity_merge.js"
|
|
2942
|
-
);
|
|
2943
|
-
if (error instanceof EntityNotFoundError) {
|
|
2944
|
-
return sendError(res, 404, "RESOURCE_NOT_FOUND", error.message);
|
|
2945
|
-
}
|
|
2946
|
-
if (error instanceof EntityAlreadyMergedError) {
|
|
2947
|
-
return sendError(res, 400, "VALIDATION_INVALID_FORMAT", error.message);
|
|
2948
|
-
}
|
|
2949
|
-
logError("APIError:entities_merge", req, error);
|
|
2950
|
-
const message = error instanceof Error ? error.message : "Failed to merge entities";
|
|
2951
|
-
return sendError(res, 500, "DB_QUERY_FAILED", message);
|
|
2952
|
-
}
|
|
2953
|
-
});
|
|
2954
|
-
|
|
2955
|
-
// MCP Actions for Observation Architecture (FU-061)
|
|
2956
|
-
|
|
2957
|
-
// Get entity snapshot with provenance
|
|
2958
|
-
app.post("/get_entity_snapshot", async (req, res) => {
|
|
2959
|
-
const parsed = EntitySnapshotRequestSchema.safeParse(req.body);
|
|
2960
|
-
if (!parsed.success) {
|
|
2961
|
-
logWarn("ValidationError:get_entity_snapshot", req, {
|
|
2962
|
-
issues: parsed.error.issues,
|
|
2963
|
-
});
|
|
2964
|
-
return sendValidationError(res, parsed.error.issues);
|
|
2965
|
-
}
|
|
2966
|
-
|
|
2967
|
-
const { entity_id } = parsed.data;
|
|
2968
|
-
|
|
2969
|
-
const { data, error } = await db
|
|
2970
|
-
.from("entity_snapshots")
|
|
2971
|
-
.select("*")
|
|
2972
|
-
.eq("entity_id", entity_id)
|
|
2973
|
-
.single();
|
|
2974
|
-
|
|
2975
|
-
if (error) {
|
|
2976
|
-
if (error.code === "PGRST116") {
|
|
2977
|
-
return sendError(res, 404, "RESOURCE_NOT_FOUND", "Entity not found");
|
|
2978
|
-
}
|
|
2979
|
-
logError("DbError:get_entity_snapshot", req, error);
|
|
2980
|
-
return sendError(res, 500, "DB_QUERY_FAILED", error.message);
|
|
2981
|
-
}
|
|
2982
|
-
|
|
2983
|
-
logDebug("Success:get_entity_snapshot", req, { entity_id });
|
|
2984
|
-
return res.json(data);
|
|
2985
|
-
});
|
|
2986
|
-
|
|
2987
|
-
// List observations for entity
|
|
2988
|
-
app.post("/list_observations", async (req, res) => {
|
|
2989
|
-
const parsed = ListObservationsRequestSchema.safeParse(req.body);
|
|
2990
|
-
if (!parsed.success) {
|
|
2991
|
-
logWarn("ValidationError:list_observations", req, {
|
|
2992
|
-
issues: parsed.error.issues,
|
|
2993
|
-
});
|
|
2994
|
-
return sendValidationError(res, parsed.error.issues);
|
|
2995
|
-
}
|
|
2996
|
-
|
|
2997
|
-
const { entity_id, limit = 100, offset = 0 } = parsed.data;
|
|
2998
|
-
|
|
2999
|
-
const query = db
|
|
3000
|
-
.from("observations")
|
|
3001
|
-
.select("*")
|
|
3002
|
-
.eq("entity_id", entity_id)
|
|
3003
|
-
.order("observed_at", { ascending: false })
|
|
3004
|
-
.range(offset, offset + limit - 1);
|
|
3005
|
-
|
|
3006
|
-
const { data, error } = await query;
|
|
3007
|
-
|
|
3008
|
-
if (error) {
|
|
3009
|
-
logError("DbError:list_observations", req, error);
|
|
3010
|
-
return sendError(res, 500, "DB_QUERY_FAILED", error.message);
|
|
3011
|
-
}
|
|
3012
|
-
|
|
3013
|
-
logDebug("Success:list_observations", req, {
|
|
3014
|
-
entity_id,
|
|
3015
|
-
count: data?.length || 0,
|
|
3016
|
-
});
|
|
3017
|
-
return res.json({ observations: data || [] });
|
|
3018
|
-
});
|
|
3019
|
-
|
|
3020
|
-
// Get field provenance (trace field to source documents)
|
|
3021
|
-
app.post("/get_field_provenance", async (req, res) => {
|
|
3022
|
-
const parsed = FieldProvenanceRequestSchema.safeParse(req.body);
|
|
3023
|
-
if (!parsed.success) {
|
|
3024
|
-
logWarn("ValidationError:get_field_provenance", req, {
|
|
3025
|
-
issues: parsed.error.issues,
|
|
3026
|
-
});
|
|
3027
|
-
return sendValidationError(res, parsed.error.issues);
|
|
3028
|
-
}
|
|
3029
|
-
|
|
3030
|
-
const { entity_id, field } = parsed.data;
|
|
3031
|
-
|
|
3032
|
-
// Get snapshot to find observation ID for this field
|
|
3033
|
-
const { data: snapshot } = await db
|
|
3034
|
-
.from("entity_snapshots")
|
|
3035
|
-
.select("provenance")
|
|
3036
|
-
.eq("entity_id", entity_id)
|
|
3037
|
-
.single();
|
|
3038
|
-
|
|
3039
|
-
if (!snapshot || !snapshot.provenance) {
|
|
3040
|
-
return sendError(res, 404, "RESOURCE_NOT_FOUND", "Entity or field not found");
|
|
3041
|
-
}
|
|
3042
|
-
|
|
3043
|
-
const provenance = snapshot.provenance as Record<string, string>;
|
|
3044
|
-
const observationId = provenance[field];
|
|
3045
|
-
|
|
3046
|
-
if (!observationId) {
|
|
3047
|
-
return sendError(res, 404, "RESOURCE_NOT_FOUND", "Field not found in provenance");
|
|
3048
|
-
}
|
|
3049
|
-
|
|
3050
|
-
// Get observation(s) - may be comma-separated for merge_array
|
|
3051
|
-
const observationIds = observationId.split(",");
|
|
3052
|
-
|
|
3053
|
-
const { data: observations, error: obsError } = await db
|
|
3054
|
-
.from("observations")
|
|
3055
|
-
.select("*, source_id")
|
|
3056
|
-
.in("id", observationIds);
|
|
3057
|
-
|
|
3058
|
-
if (obsError) {
|
|
3059
|
-
logError("DbError:get_field_provenance", req, obsError);
|
|
3060
|
-
return sendError(res, 500, "DB_QUERY_FAILED", obsError.message);
|
|
3061
|
-
}
|
|
3062
|
-
|
|
3063
|
-
// Get sources for provenance
|
|
3064
|
-
const sourceIds = (observations || [])
|
|
3065
|
-
.map((obs: { source_id?: string }) => obs.source_id)
|
|
3066
|
-
.filter((sourceId: string | undefined): sourceId is string => Boolean(sourceId));
|
|
3067
|
-
|
|
3068
|
-
const { data: sources, error: sourceError } = await db
|
|
3069
|
-
.from("sources")
|
|
3070
|
-
.select("id, content_hash, mime_type, storage_url, file_name, created_at")
|
|
3071
|
-
.in("id", sourceIds);
|
|
3072
|
-
|
|
3073
|
-
if (sourceError) {
|
|
3074
|
-
logError("DbError:get_field_provenance:sources", req, sourceError);
|
|
3075
|
-
}
|
|
3076
|
-
|
|
3077
|
-
logDebug("Success:get_field_provenance", req, { entity_id, field });
|
|
3078
|
-
return res.json({
|
|
3079
|
-
field,
|
|
3080
|
-
entity_id,
|
|
3081
|
-
observation_ids: observationIds,
|
|
3082
|
-
observations: observations || [],
|
|
3083
|
-
sources: sources || [],
|
|
3084
|
-
});
|
|
3085
|
-
});
|
|
3086
|
-
|
|
3087
|
-
// Create relationship
|
|
3088
|
-
app.post("/create_relationship", async (req, res) => {
|
|
3089
|
-
const parsed = CreateRelationshipRequestSchema.safeParse(req.body);
|
|
3090
|
-
if (!parsed.success) {
|
|
3091
|
-
logWarn("ValidationError:create_relationship", req, {
|
|
3092
|
-
issues: parsed.error.issues,
|
|
3093
|
-
});
|
|
3094
|
-
return sendValidationError(res, parsed.error.issues);
|
|
3095
|
-
}
|
|
3096
|
-
|
|
3097
|
-
const { relationship_type, source_entity_id, target_entity_id, source_id, metadata } =
|
|
3098
|
-
parsed.data;
|
|
3099
|
-
|
|
3100
|
-
const userId = "00000000-0000-0000-0000-000000000000"; // v0.1.0 single-user
|
|
3101
|
-
|
|
3102
|
-
const { relationshipsService } = await import("./services/relationships.js");
|
|
3103
|
-
|
|
3104
|
-
try {
|
|
3105
|
-
const relationship = await relationshipsService.createRelationship({
|
|
3106
|
-
relationship_type,
|
|
3107
|
-
source_entity_id,
|
|
3108
|
-
target_entity_id,
|
|
3109
|
-
source_id: source_id || null,
|
|
3110
|
-
metadata: metadata || {},
|
|
3111
|
-
user_id: userId,
|
|
3112
|
-
});
|
|
3113
|
-
|
|
3114
|
-
logDebug("Success:create_relationship", req, {
|
|
3115
|
-
relationship_key: relationship.relationship_key,
|
|
3116
|
-
});
|
|
3117
|
-
return res.json(relationship);
|
|
3118
|
-
} catch (error) {
|
|
3119
|
-
logError("RelationshipCreationError:create_relationship", req, error);
|
|
3120
|
-
return sendError(
|
|
3121
|
-
res,
|
|
3122
|
-
500,
|
|
3123
|
-
"DB_QUERY_FAILED",
|
|
3124
|
-
error instanceof Error ? error.message : "Failed to create relationship"
|
|
3125
|
-
);
|
|
3126
|
-
}
|
|
3127
|
-
});
|
|
3128
|
-
|
|
3129
|
-
// List relationships
|
|
3130
|
-
app.post("/list_relationships", async (req, res) => {
|
|
3131
|
-
const parsed = ListRelationshipsRequestSchema.safeParse(req.body);
|
|
3132
|
-
if (!parsed.success) {
|
|
3133
|
-
logWarn("ValidationError:list_relationships", req, {
|
|
3134
|
-
issues: parsed.error.issues,
|
|
3135
|
-
});
|
|
3136
|
-
return sendValidationError(res, parsed.error.issues);
|
|
3137
|
-
}
|
|
3138
|
-
|
|
3139
|
-
const { entity_id, direction = "both", relationship_type } = parsed.data;
|
|
3140
|
-
const normalizedDirection =
|
|
3141
|
-
direction === "incoming" || direction === "inbound"
|
|
3142
|
-
? "incoming"
|
|
3143
|
-
: direction === "outgoing" || direction === "outbound"
|
|
3144
|
-
? "outgoing"
|
|
3145
|
-
: "both";
|
|
3146
|
-
|
|
3147
|
-
const { relationshipsService } = await import("./services/relationships.js");
|
|
3148
|
-
|
|
3149
|
-
try {
|
|
3150
|
-
let relationships;
|
|
3151
|
-
if (relationship_type) {
|
|
3152
|
-
relationships = await relationshipsService.getRelationshipsByType(relationship_type as any);
|
|
3153
|
-
// Filter by entity_id
|
|
3154
|
-
relationships = relationships.filter(
|
|
3155
|
-
(rel) => rel.source_entity_id === entity_id || rel.target_entity_id === entity_id
|
|
3156
|
-
);
|
|
3157
|
-
} else {
|
|
3158
|
-
relationships = await relationshipsService.getRelationshipsForEntity(
|
|
3159
|
-
entity_id,
|
|
3160
|
-
normalizedDirection
|
|
3161
|
-
);
|
|
3162
|
-
}
|
|
3163
|
-
|
|
3164
|
-
logDebug("Success:list_relationships", req, {
|
|
3165
|
-
entity_id,
|
|
3166
|
-
count: relationships.length,
|
|
3167
|
-
});
|
|
3168
|
-
return res.json({ relationships });
|
|
3169
|
-
} catch (error) {
|
|
3170
|
-
logError("RelationshipQueryError:list_relationships", req, error);
|
|
3171
|
-
return sendError(
|
|
3172
|
-
res,
|
|
3173
|
-
500,
|
|
3174
|
-
"DB_QUERY_FAILED",
|
|
3175
|
-
error instanceof Error ? error.message : "Failed to query relationships"
|
|
3176
|
-
);
|
|
3177
|
-
}
|
|
3178
|
-
});
|
|
3179
|
-
|
|
3180
|
-
app.get("/get_file_url", async (req, res) => {
|
|
3181
|
-
const schema = z.object({
|
|
3182
|
-
file_path: z.string(),
|
|
3183
|
-
expires_in: z.coerce.number().optional(),
|
|
3184
|
-
});
|
|
3185
|
-
const parsed = schema.safeParse(req.query);
|
|
3186
|
-
if (!parsed.success) {
|
|
3187
|
-
logWarn("ValidationError:get_file_url", req, {
|
|
3188
|
-
issues: parsed.error.issues,
|
|
3189
|
-
});
|
|
3190
|
-
return sendValidationError(res, parsed.error.issues);
|
|
3191
|
-
}
|
|
3192
|
-
const { file_path, expires_in } = parsed.data;
|
|
3193
|
-
|
|
3194
|
-
const parts = file_path.split("/");
|
|
3195
|
-
const bucket = parts[0];
|
|
3196
|
-
const path = parts.slice(1).join("/");
|
|
3197
|
-
|
|
3198
|
-
const { data, error } = await db.storage
|
|
3199
|
-
.from(bucket)
|
|
3200
|
-
.createSignedUrl(path, expires_in || 3600);
|
|
3201
|
-
if (error || !data?.signedUrl) {
|
|
3202
|
-
logError("StorageError:get_file_url", req, error, { bucket, path });
|
|
3203
|
-
return sendError(res, 500, "DB_QUERY_FAILED", error?.message ?? "Failed to create signed URL");
|
|
3204
|
-
}
|
|
3205
|
-
|
|
3206
|
-
logDebug("Success:get_file_url", req, { path: file_path });
|
|
3207
|
-
return res.json({ url: data.signedUrl });
|
|
3208
|
-
});
|
|
3209
|
-
|
|
3210
|
-
// ========== New HTTP REST endpoints for Phase 0 E2E test coverage ==========
|
|
3211
|
-
|
|
3212
|
-
// POST /retrieve_entity_by_identifier - Search for entity by identifier
|
|
3213
|
-
app.post("/retrieve_entity_by_identifier", async (req, res) => {
|
|
3214
|
-
const parsed = RetrieveEntityByIdentifierSchema.safeParse(req.body);
|
|
3215
|
-
if (!parsed.success) {
|
|
3216
|
-
logWarn("ValidationError:retrieve_entity_by_identifier", req, {
|
|
3217
|
-
issues: parsed.error.issues,
|
|
3218
|
-
});
|
|
3219
|
-
return sendValidationError(res, parsed.error.issues);
|
|
3220
|
-
}
|
|
3221
|
-
|
|
3222
|
-
try {
|
|
3223
|
-
const { identifier, entity_type } = parsed.data;
|
|
3224
|
-
|
|
3225
|
-
// Import normalization and ID generation functions
|
|
3226
|
-
const { normalizeEntityValue, generateEntityId } =
|
|
3227
|
-
await import("./services/entity_resolution.js");
|
|
3228
|
-
|
|
3229
|
-
// Normalize the identifier
|
|
3230
|
-
const normalized = entity_type
|
|
3231
|
-
? normalizeEntityValue(entity_type, identifier)
|
|
3232
|
-
: identifier.trim().toLowerCase();
|
|
3233
|
-
|
|
3234
|
-
// Search in entities table
|
|
3235
|
-
let query = db
|
|
3236
|
-
.from("entities")
|
|
3237
|
-
.select("*")
|
|
3238
|
-
.or(`canonical_name.ilike.%${normalized}%,aliases.cs.["${normalized}"]`);
|
|
3239
|
-
|
|
3240
|
-
if (entity_type) {
|
|
3241
|
-
query = query.eq("entity_type", entity_type);
|
|
3242
|
-
}
|
|
3243
|
-
|
|
3244
|
-
const { data: entities, error } = await query.limit(100);
|
|
3245
|
-
|
|
3246
|
-
if (error) {
|
|
3247
|
-
logError("DbError:retrieve_entity_by_identifier", req, error);
|
|
3248
|
-
return sendError(res, 500, "DB_QUERY_FAILED", error.message);
|
|
3249
|
-
}
|
|
3250
|
-
|
|
3251
|
-
// Try entity ID match if no results
|
|
3252
|
-
if ((!entities || entities.length === 0) && entity_type) {
|
|
3253
|
-
const possibleId = generateEntityId(entity_type, identifier);
|
|
3254
|
-
const { data: entityById, error: idError } = await db
|
|
3255
|
-
.from("entities")
|
|
3256
|
-
.select("*")
|
|
3257
|
-
.eq("id", possibleId)
|
|
3258
|
-
.single();
|
|
3259
|
-
|
|
3260
|
-
if (!idError && entityById) {
|
|
3261
|
-
return res.json({ entities: [entityById], total: 1 });
|
|
3262
|
-
}
|
|
3263
|
-
}
|
|
3264
|
-
|
|
3265
|
-
logDebug("Success:retrieve_entity_by_identifier", req, { identifier, count: entities?.length });
|
|
3266
|
-
return res.json({ entities: entities || [], total: entities?.length || 0 });
|
|
3267
|
-
} catch (error) {
|
|
3268
|
-
return handleApiError(
|
|
3269
|
-
req,
|
|
3270
|
-
res,
|
|
3271
|
-
error,
|
|
3272
|
-
"Failed to retrieve entity by identifier",
|
|
3273
|
-
"DB_QUERY_FAILED",
|
|
3274
|
-
"APIError:retrieve_entity_by_identifier"
|
|
3275
|
-
);
|
|
3276
|
-
}
|
|
3277
|
-
});
|
|
3278
|
-
|
|
3279
|
-
// POST /retrieve_related_entities - Get entities connected via relationships
|
|
3280
|
-
app.post("/retrieve_related_entities", async (req, res) => {
|
|
3281
|
-
const parsed = RetrieveRelatedEntitiesSchema.safeParse(req.body);
|
|
3282
|
-
if (!parsed.success) {
|
|
3283
|
-
logWarn("ValidationError:retrieve_related_entities", req, {
|
|
3284
|
-
issues: parsed.error.issues,
|
|
3285
|
-
});
|
|
3286
|
-
return sendValidationError(res, parsed.error.issues);
|
|
3287
|
-
}
|
|
3288
|
-
|
|
3289
|
-
try {
|
|
3290
|
-
const {
|
|
3291
|
-
entity_id,
|
|
3292
|
-
relationship_types,
|
|
3293
|
-
direction = "both",
|
|
3294
|
-
max_hops: _max_hops = 1,
|
|
3295
|
-
include_entities = true,
|
|
3296
|
-
} = parsed.data;
|
|
3297
|
-
|
|
3298
|
-
// Simple 1-hop query implementation
|
|
3299
|
-
const relationships: any[] = [];
|
|
3300
|
-
|
|
3301
|
-
// Get outbound relationships
|
|
3302
|
-
if (direction === "outbound" || direction === "both") {
|
|
3303
|
-
let query = db
|
|
3304
|
-
.from("relationship_snapshots")
|
|
3305
|
-
.select("*")
|
|
3306
|
-
.eq("source_entity_id", entity_id);
|
|
3307
|
-
|
|
3308
|
-
if (relationship_types && relationship_types.length > 0) {
|
|
3309
|
-
query = query.in("relationship_type", relationship_types);
|
|
3310
|
-
}
|
|
3311
|
-
|
|
3312
|
-
const { data, error } = await query;
|
|
3313
|
-
if (!error && data) {
|
|
3314
|
-
relationships.push(...data);
|
|
3315
|
-
}
|
|
3316
|
-
}
|
|
3317
|
-
|
|
3318
|
-
// Get inbound relationships
|
|
3319
|
-
if (direction === "inbound" || direction === "both") {
|
|
3320
|
-
let query = db
|
|
3321
|
-
.from("relationship_snapshots")
|
|
3322
|
-
.select("*")
|
|
3323
|
-
.eq("target_entity_id", entity_id);
|
|
3324
|
-
|
|
3325
|
-
if (relationship_types && relationship_types.length > 0) {
|
|
3326
|
-
query = query.in("relationship_type", relationship_types);
|
|
3327
|
-
}
|
|
3328
|
-
|
|
3329
|
-
const { data, error } = await query;
|
|
3330
|
-
if (!error && data) {
|
|
3331
|
-
relationships.push(...data);
|
|
3332
|
-
}
|
|
3333
|
-
}
|
|
3334
|
-
|
|
3335
|
-
// Get entities if requested
|
|
3336
|
-
let entities: any[] = [];
|
|
3337
|
-
if (include_entities && relationships.length > 0) {
|
|
3338
|
-
const relatedIds = new Set<string>();
|
|
3339
|
-
relationships.forEach((rel) => {
|
|
3340
|
-
if (rel.source_entity_id !== entity_id) relatedIds.add(rel.source_entity_id);
|
|
3341
|
-
if (rel.target_entity_id !== entity_id) relatedIds.add(rel.target_entity_id);
|
|
3342
|
-
});
|
|
3343
|
-
|
|
3344
|
-
if (relatedIds.size > 0) {
|
|
3345
|
-
const { data, error } = await db
|
|
3346
|
-
.from("entities")
|
|
3347
|
-
.select("*")
|
|
3348
|
-
.in("id", Array.from(relatedIds));
|
|
3349
|
-
|
|
3350
|
-
if (!error && data) {
|
|
3351
|
-
entities = data;
|
|
3352
|
-
}
|
|
3353
|
-
}
|
|
3354
|
-
}
|
|
3355
|
-
|
|
3356
|
-
logDebug("Success:retrieve_related_entities", req, { entity_id, count: relationships.length });
|
|
3357
|
-
return res.json({ relationships, entities });
|
|
3358
|
-
} catch (error) {
|
|
3359
|
-
return handleApiError(
|
|
3360
|
-
req,
|
|
3361
|
-
res,
|
|
3362
|
-
error,
|
|
3363
|
-
"Failed to retrieve related entities",
|
|
3364
|
-
"DB_QUERY_FAILED",
|
|
3365
|
-
"APIError:retrieve_related_entities"
|
|
3366
|
-
);
|
|
3367
|
-
}
|
|
3368
|
-
});
|
|
3369
|
-
|
|
3370
|
-
// POST /retrieve_graph_neighborhood - Get complete graph neighborhood
|
|
3371
|
-
app.post("/retrieve_graph_neighborhood", async (req, res) => {
|
|
3372
|
-
const parsed = RetrieveGraphNeighborhoodSchema.safeParse(req.body);
|
|
3373
|
-
if (!parsed.success) {
|
|
3374
|
-
logWarn("ValidationError:retrieve_graph_neighborhood", req, {
|
|
3375
|
-
issues: parsed.error.issues,
|
|
3376
|
-
});
|
|
3377
|
-
return sendValidationError(res, parsed.error.issues);
|
|
3378
|
-
}
|
|
3379
|
-
|
|
3380
|
-
try {
|
|
3381
|
-
const {
|
|
3382
|
-
node_id,
|
|
3383
|
-
node_type = "entity",
|
|
3384
|
-
include_relationships = true,
|
|
3385
|
-
include_sources = true,
|
|
3386
|
-
include_events: _include_events = true,
|
|
3387
|
-
include_observations = false,
|
|
3388
|
-
} = parsed.data;
|
|
3389
|
-
|
|
3390
|
-
const result: any = { node_id, node_type };
|
|
3391
|
-
|
|
3392
|
-
if (node_type === "entity") {
|
|
3393
|
-
// Get entity
|
|
3394
|
-
const { data: entity, error: entityError } = await db
|
|
3395
|
-
.from("entities")
|
|
3396
|
-
.select("*")
|
|
3397
|
-
.eq("id", node_id)
|
|
3398
|
-
.single();
|
|
3399
|
-
|
|
3400
|
-
if (!entityError && entity) {
|
|
3401
|
-
result.entity = entity;
|
|
3402
|
-
}
|
|
3403
|
-
|
|
3404
|
-
// Get relationships if requested
|
|
3405
|
-
if (include_relationships) {
|
|
3406
|
-
const { data: relationships, error: relError } = await db
|
|
3407
|
-
.from("relationship_snapshots")
|
|
3408
|
-
.select("*")
|
|
3409
|
-
.or(`source_entity_id.eq.${node_id},target_entity_id.eq.${node_id}`);
|
|
3410
|
-
|
|
3411
|
-
if (!relError) {
|
|
3412
|
-
result.relationships = relationships || [];
|
|
3413
|
-
}
|
|
3414
|
-
}
|
|
3415
|
-
|
|
3416
|
-
// Get observations if requested
|
|
3417
|
-
if (include_observations) {
|
|
3418
|
-
const { data: observations, error: obsError } = await db
|
|
3419
|
-
.from("observations")
|
|
3420
|
-
.select("*")
|
|
3421
|
-
.eq("entity_id", node_id);
|
|
3422
|
-
|
|
3423
|
-
if (!obsError) {
|
|
3424
|
-
result.observations = observations || [];
|
|
3425
|
-
}
|
|
3426
|
-
}
|
|
3427
|
-
|
|
3428
|
-
// Get sources if requested
|
|
3429
|
-
if (include_sources && include_observations) {
|
|
3430
|
-
const sourceIds = result.observations?.map((o: any) => o.source_id).filter(Boolean) || [];
|
|
3431
|
-
if (sourceIds.length > 0) {
|
|
3432
|
-
const { data: sources, error: srcError } = await db
|
|
3433
|
-
.from("source")
|
|
3434
|
-
.select("*")
|
|
3435
|
-
.in("id", sourceIds);
|
|
3436
|
-
|
|
3437
|
-
if (!srcError) {
|
|
3438
|
-
result.sources = sources || [];
|
|
3439
|
-
}
|
|
3440
|
-
}
|
|
3441
|
-
}
|
|
3442
|
-
} else if (node_type === "source") {
|
|
3443
|
-
// Get source
|
|
3444
|
-
const { data: source, error: srcError } = await db
|
|
3445
|
-
.from("source")
|
|
3446
|
-
.select("*")
|
|
3447
|
-
.eq("id", node_id)
|
|
3448
|
-
.single();
|
|
3449
|
-
|
|
3450
|
-
if (!srcError && source) {
|
|
3451
|
-
result.source = source;
|
|
3452
|
-
}
|
|
3453
|
-
|
|
3454
|
-
// Get observations from this source
|
|
3455
|
-
if (include_observations) {
|
|
3456
|
-
const { data: observations, error: obsError } = await db
|
|
3457
|
-
.from("observations")
|
|
3458
|
-
.select("*")
|
|
3459
|
-
.eq("source_id", node_id);
|
|
3460
|
-
|
|
3461
|
-
if (!obsError) {
|
|
3462
|
-
result.observations = observations || [];
|
|
3463
|
-
}
|
|
3464
|
-
}
|
|
3465
|
-
}
|
|
3466
|
-
|
|
3467
|
-
logDebug("Success:retrieve_graph_neighborhood", req, { node_id });
|
|
3468
|
-
return res.json(result);
|
|
3469
|
-
} catch (error) {
|
|
3470
|
-
return handleApiError(
|
|
3471
|
-
req,
|
|
3472
|
-
res,
|
|
3473
|
-
error,
|
|
3474
|
-
"Failed to retrieve graph neighborhood",
|
|
3475
|
-
"DB_QUERY_FAILED",
|
|
3476
|
-
"APIError:retrieve_graph_neighborhood"
|
|
3477
|
-
);
|
|
3478
|
-
}
|
|
3479
|
-
});
|
|
3480
|
-
|
|
3481
|
-
// POST /delete_entity - Soft delete entity
|
|
3482
|
-
app.post("/delete_entity", async (req, res) => {
|
|
3483
|
-
const parsed = DeleteEntityRequestSchema.safeParse(req.body);
|
|
3484
|
-
if (!parsed.success) {
|
|
3485
|
-
logWarn("ValidationError:delete_entity", req, {
|
|
3486
|
-
issues: parsed.error.issues,
|
|
3487
|
-
});
|
|
3488
|
-
return sendValidationError(res, parsed.error.issues);
|
|
3489
|
-
}
|
|
3490
|
-
|
|
3491
|
-
try {
|
|
3492
|
-
const userId = await getAuthenticatedUserId(req, parsed.data.user_id);
|
|
3493
|
-
const { entity_id, entity_type, reason } = parsed.data;
|
|
3494
|
-
|
|
3495
|
-
const { softDeleteEntity } = await import("./services/deletion.js");
|
|
3496
|
-
|
|
3497
|
-
const result = await softDeleteEntity(entity_id, entity_type, userId, reason);
|
|
3498
|
-
|
|
3499
|
-
if (!result.success) {
|
|
3500
|
-
const status = result.error === "Entity not found" ? 404 : 500;
|
|
3501
|
-
return sendError(res, status, "DELETE_FAILED", result.error || "Failed to delete entity");
|
|
3502
|
-
}
|
|
3503
|
-
|
|
3504
|
-
logDebug("Success:delete_entity", req, { entity_id });
|
|
3505
|
-
return res.json({ success: true, entity_id, observation_id: result.observation_id });
|
|
3506
|
-
} catch (error) {
|
|
3507
|
-
return handleApiError(
|
|
3508
|
-
req,
|
|
3509
|
-
res,
|
|
3510
|
-
error,
|
|
3511
|
-
"Failed to delete entity",
|
|
3512
|
-
"DB_QUERY_FAILED",
|
|
3513
|
-
"APIError:delete_entity"
|
|
3514
|
-
);
|
|
3515
|
-
}
|
|
3516
|
-
});
|
|
3517
|
-
|
|
3518
|
-
// POST /restore_entity - Restore soft-deleted entity
|
|
3519
|
-
app.post("/restore_entity", async (req, res) => {
|
|
3520
|
-
const parsed = RestoreEntityRequestSchema.safeParse(req.body);
|
|
3521
|
-
if (!parsed.success) {
|
|
3522
|
-
logWarn("ValidationError:restore_entity", req, {
|
|
3523
|
-
issues: parsed.error.issues,
|
|
3524
|
-
});
|
|
3525
|
-
return sendValidationError(res, parsed.error.issues);
|
|
3526
|
-
}
|
|
3527
|
-
|
|
3528
|
-
try {
|
|
3529
|
-
const userId = await getAuthenticatedUserId(req, parsed.data.user_id);
|
|
3530
|
-
const { entity_id, entity_type, reason } = parsed.data;
|
|
3531
|
-
|
|
3532
|
-
const { restoreEntity } = await import("./services/deletion.js");
|
|
3533
|
-
|
|
3534
|
-
const result = await restoreEntity(entity_id, entity_type, userId, reason);
|
|
3535
|
-
|
|
3536
|
-
if (!result.success) {
|
|
3537
|
-
return sendError(res, 500, "RESTORE_FAILED", result.error || "Failed to restore entity");
|
|
3538
|
-
}
|
|
3539
|
-
|
|
3540
|
-
logDebug("Success:restore_entity", req, { entity_id });
|
|
3541
|
-
return res.json({ success: true, entity_id, observation_id: result.observation_id });
|
|
3542
|
-
} catch (error) {
|
|
3543
|
-
return handleApiError(
|
|
3544
|
-
req,
|
|
3545
|
-
res,
|
|
3546
|
-
error,
|
|
3547
|
-
"Failed to restore entity",
|
|
3548
|
-
"DB_QUERY_FAILED",
|
|
3549
|
-
"APIError:restore_entity"
|
|
3550
|
-
);
|
|
3551
|
-
}
|
|
3552
|
-
});
|
|
3553
|
-
|
|
3554
|
-
// POST /delete_relationship - Soft delete relationship
|
|
3555
|
-
app.post("/delete_relationship", async (req, res) => {
|
|
3556
|
-
const parsed = DeleteRelationshipRequestSchema.safeParse(req.body);
|
|
3557
|
-
if (!parsed.success) {
|
|
3558
|
-
logWarn("ValidationError:delete_relationship", req, {
|
|
3559
|
-
issues: parsed.error.issues,
|
|
3560
|
-
});
|
|
3561
|
-
return sendValidationError(res, parsed.error.issues);
|
|
3562
|
-
}
|
|
3563
|
-
|
|
3564
|
-
try {
|
|
3565
|
-
const userId = await getAuthenticatedUserId(req, parsed.data.user_id);
|
|
3566
|
-
const { relationship_type, source_entity_id, target_entity_id, reason } = parsed.data;
|
|
3567
|
-
|
|
3568
|
-
const { softDeleteRelationship } = await import("./services/deletion.js");
|
|
3569
|
-
|
|
3570
|
-
// Construct relationship key
|
|
3571
|
-
const relationshipKey = `${relationship_type}:${source_entity_id}:${target_entity_id}`;
|
|
3572
|
-
|
|
3573
|
-
const result = await softDeleteRelationship(
|
|
3574
|
-
relationshipKey,
|
|
3575
|
-
relationship_type,
|
|
3576
|
-
source_entity_id,
|
|
3577
|
-
target_entity_id,
|
|
3578
|
-
userId,
|
|
3579
|
-
reason
|
|
3580
|
-
);
|
|
3581
|
-
|
|
3582
|
-
if (!result.success) {
|
|
3583
|
-
return sendError(res, 500, "DELETE_FAILED", result.error || "Failed to delete relationship");
|
|
3584
|
-
}
|
|
3585
|
-
|
|
3586
|
-
logDebug("Success:delete_relationship", req, {
|
|
3587
|
-
relationship_type,
|
|
3588
|
-
source_entity_id,
|
|
3589
|
-
target_entity_id,
|
|
3590
|
-
});
|
|
3591
|
-
return res.json({ success: true, observation_id: result.observation_id });
|
|
3592
|
-
} catch (error) {
|
|
3593
|
-
return handleApiError(
|
|
3594
|
-
req,
|
|
3595
|
-
res,
|
|
3596
|
-
error,
|
|
3597
|
-
"Failed to delete relationship",
|
|
3598
|
-
"DB_QUERY_FAILED",
|
|
3599
|
-
"APIError:delete_relationship"
|
|
3600
|
-
);
|
|
3601
|
-
}
|
|
3602
|
-
});
|
|
3603
|
-
|
|
3604
|
-
// POST /restore_relationship - Restore soft-deleted relationship
|
|
3605
|
-
app.post("/restore_relationship", async (req, res) => {
|
|
3606
|
-
const parsed = RestoreRelationshipRequestSchema.safeParse(req.body);
|
|
3607
|
-
if (!parsed.success) {
|
|
3608
|
-
logWarn("ValidationError:restore_relationship", req, {
|
|
3609
|
-
issues: parsed.error.issues,
|
|
3610
|
-
});
|
|
3611
|
-
return sendValidationError(res, parsed.error.issues);
|
|
3612
|
-
}
|
|
3613
|
-
|
|
3614
|
-
try {
|
|
3615
|
-
const userId = await getAuthenticatedUserId(req, parsed.data.user_id);
|
|
3616
|
-
const { relationship_type, source_entity_id, target_entity_id, reason } = parsed.data;
|
|
3617
|
-
|
|
3618
|
-
const { restoreRelationship } = await import("./services/deletion.js");
|
|
3619
|
-
|
|
3620
|
-
// Construct relationship key
|
|
3621
|
-
const relationshipKey = `${relationship_type}:${source_entity_id}:${target_entity_id}`;
|
|
3622
|
-
|
|
3623
|
-
const result = await restoreRelationship(
|
|
3624
|
-
relationshipKey,
|
|
3625
|
-
relationship_type,
|
|
3626
|
-
source_entity_id,
|
|
3627
|
-
target_entity_id,
|
|
3628
|
-
userId,
|
|
3629
|
-
reason
|
|
3630
|
-
);
|
|
3631
|
-
|
|
3632
|
-
if (!result.success) {
|
|
3633
|
-
return sendError(
|
|
3634
|
-
res,
|
|
3635
|
-
500,
|
|
3636
|
-
"RESTORE_FAILED",
|
|
3637
|
-
result.error || "Failed to restore relationship"
|
|
3638
|
-
);
|
|
3639
|
-
}
|
|
3640
|
-
|
|
3641
|
-
logDebug("Success:restore_relationship", req, {
|
|
3642
|
-
relationship_type,
|
|
3643
|
-
source_entity_id,
|
|
3644
|
-
target_entity_id,
|
|
3645
|
-
});
|
|
3646
|
-
return res.json({ success: true, observation_id: result.observation_id });
|
|
3647
|
-
} catch (error) {
|
|
3648
|
-
return handleApiError(
|
|
3649
|
-
req,
|
|
3650
|
-
res,
|
|
3651
|
-
error,
|
|
3652
|
-
"Failed to restore relationship",
|
|
3653
|
-
"DB_QUERY_FAILED",
|
|
3654
|
-
"APIError:restore_relationship"
|
|
3655
|
-
);
|
|
3656
|
-
}
|
|
3657
|
-
});
|
|
3658
|
-
|
|
3659
|
-
// POST /analyze_schema_candidates - Analyze raw fragments for schema promotion
|
|
3660
|
-
app.post("/analyze_schema_candidates", async (req, res) => {
|
|
3661
|
-
const parsed = AnalyzeSchemaCandidatesRequestSchema.safeParse(req.body);
|
|
3662
|
-
if (!parsed.success) {
|
|
3663
|
-
logWarn("ValidationError:analyze_schema_candidates", req, {
|
|
3664
|
-
issues: parsed.error.issues,
|
|
3665
|
-
});
|
|
3666
|
-
return sendValidationError(res, parsed.error.issues);
|
|
3667
|
-
}
|
|
3668
|
-
|
|
3669
|
-
try {
|
|
3670
|
-
const userId = await getAuthenticatedUserId(req, parsed.data.user_id);
|
|
3671
|
-
const { entity_type, min_frequency = 5, min_confidence = 0.8 } = parsed.data;
|
|
3672
|
-
|
|
3673
|
-
// Query raw_fragments for field frequency analysis
|
|
3674
|
-
let query = db
|
|
3675
|
-
.from("raw_fragments")
|
|
3676
|
-
.select("fragment_key, frequency_count, entity_type");
|
|
3677
|
-
|
|
3678
|
-
if (entity_type) {
|
|
3679
|
-
query = query.eq("entity_type", entity_type);
|
|
3680
|
-
}
|
|
3681
|
-
|
|
3682
|
-
if (userId) {
|
|
3683
|
-
query = query.eq("user_id", userId);
|
|
3684
|
-
}
|
|
3685
|
-
|
|
3686
|
-
const { data: fragments, error } = await query;
|
|
3687
|
-
|
|
3688
|
-
if (error) {
|
|
3689
|
-
logError("DbError:analyze_schema_candidates", req, error);
|
|
3690
|
-
return sendError(res, 500, "DB_QUERY_FAILED", error.message);
|
|
3691
|
-
}
|
|
3692
|
-
|
|
3693
|
-
// Analyze field frequency
|
|
3694
|
-
const fieldFrequency = new Map<string, number>();
|
|
3695
|
-
(fragments || []).forEach((frag: any) => {
|
|
3696
|
-
const key = String(frag.fragment_key ?? "");
|
|
3697
|
-
if (!key) return;
|
|
3698
|
-
const increment =
|
|
3699
|
-
typeof frag.frequency_count === "number" && Number.isFinite(frag.frequency_count)
|
|
3700
|
-
? frag.frequency_count
|
|
3701
|
-
: 1;
|
|
3702
|
-
const count = fieldFrequency.get(key) ?? 0;
|
|
3703
|
-
fieldFrequency.set(key, count + increment);
|
|
3704
|
-
});
|
|
3705
|
-
|
|
3706
|
-
// Build candidates
|
|
3707
|
-
const candidates = Array.from(fieldFrequency.entries())
|
|
3708
|
-
.filter(([, count]) => count >= min_frequency)
|
|
3709
|
-
.map(([field_name, frequency]) => {
|
|
3710
|
-
const confidence = Math.min(frequency / 100, 1);
|
|
3711
|
-
const recommendedType =
|
|
3712
|
-
/\b(date|dated|dob)\b/i.test(field_name) || /_at$/i.test(field_name)
|
|
3713
|
-
? "date"
|
|
3714
|
-
: "string";
|
|
3715
|
-
return {
|
|
3716
|
-
field_name,
|
|
3717
|
-
frequency,
|
|
3718
|
-
confidence,
|
|
3719
|
-
recommended_type: recommendedType,
|
|
3720
|
-
};
|
|
3721
|
-
})
|
|
3722
|
-
.filter((c) => c.confidence >= min_confidence)
|
|
3723
|
-
.sort((a, b) => {
|
|
3724
|
-
if (b.frequency !== a.frequency) return b.frequency - a.frequency;
|
|
3725
|
-
return a.field_name.localeCompare(b.field_name);
|
|
3726
|
-
});
|
|
3727
|
-
|
|
3728
|
-
logDebug("Success:analyze_schema_candidates", req, { entity_type, count: candidates.length });
|
|
3729
|
-
return res.json({ candidates });
|
|
3730
|
-
} catch (error) {
|
|
3731
|
-
return handleApiError(
|
|
3732
|
-
req,
|
|
3733
|
-
res,
|
|
3734
|
-
error,
|
|
3735
|
-
"Failed to analyze schema candidates",
|
|
3736
|
-
"DB_QUERY_FAILED",
|
|
3737
|
-
"APIError:analyze_schema_candidates"
|
|
3738
|
-
);
|
|
3739
|
-
}
|
|
3740
|
-
});
|
|
3741
|
-
|
|
3742
|
-
// POST /get_schema_recommendations - Get schema update recommendations
|
|
3743
|
-
app.post("/get_schema_recommendations", async (req, res) => {
|
|
3744
|
-
const parsed = GetSchemaRecommendationsRequestSchema.safeParse(req.body);
|
|
3745
|
-
if (!parsed.success) {
|
|
3746
|
-
logWarn("ValidationError:get_schema_recommendations", req, {
|
|
3747
|
-
issues: parsed.error.issues,
|
|
3748
|
-
});
|
|
3749
|
-
return sendValidationError(res, parsed.error.issues);
|
|
3750
|
-
}
|
|
3751
|
-
|
|
3752
|
-
try {
|
|
3753
|
-
const userId = await getAuthenticatedUserId(req, parsed.data.user_id);
|
|
3754
|
-
const { entity_type, source, status } = parsed.data;
|
|
3755
|
-
|
|
3756
|
-
// Query schema_recommendations table
|
|
3757
|
-
let query = db.from("schema_recommendations").select("*").eq("entity_type", entity_type);
|
|
3758
|
-
|
|
3759
|
-
if (userId) {
|
|
3760
|
-
query = query.eq("user_id", userId);
|
|
3761
|
-
}
|
|
3762
|
-
|
|
3763
|
-
if (source && source !== "all") {
|
|
3764
|
-
query = query.eq("source", source);
|
|
3765
|
-
}
|
|
3766
|
-
|
|
3767
|
-
if (status) {
|
|
3768
|
-
query = query.eq("status", status);
|
|
3769
|
-
}
|
|
3770
|
-
|
|
3771
|
-
const { data: recommendations, error } = await query
|
|
3772
|
-
.order("confidence_score", { ascending: false })
|
|
3773
|
-
.order("created_at", { ascending: false });
|
|
3774
|
-
|
|
3775
|
-
if (error) {
|
|
3776
|
-
logError("DbError:get_schema_recommendations", req, error);
|
|
3777
|
-
return sendError(res, 500, "DB_QUERY_FAILED", error.message);
|
|
3778
|
-
}
|
|
3779
|
-
|
|
3780
|
-
logDebug("Success:get_schema_recommendations", req, {
|
|
3781
|
-
entity_type,
|
|
3782
|
-
count: recommendations?.length,
|
|
3783
|
-
});
|
|
3784
|
-
return res.json({ recommendations: recommendations || [] });
|
|
3785
|
-
} catch (error) {
|
|
3786
|
-
return handleApiError(
|
|
3787
|
-
req,
|
|
3788
|
-
res,
|
|
3789
|
-
error,
|
|
3790
|
-
"Failed to get schema recommendations",
|
|
3791
|
-
"DB_QUERY_FAILED",
|
|
3792
|
-
"APIError:get_schema_recommendations"
|
|
3793
|
-
);
|
|
3794
|
-
}
|
|
3795
|
-
});
|
|
3796
|
-
|
|
3797
|
-
// POST /update_schema_incremental - Incrementally update schema with new fields
|
|
3798
|
-
app.post("/update_schema_incremental", async (req, res) => {
|
|
3799
|
-
const parsed = UpdateSchemaIncrementalRequestSchema.safeParse(req.body);
|
|
3800
|
-
if (!parsed.success) {
|
|
3801
|
-
logWarn("ValidationError:update_schema_incremental", req, {
|
|
3802
|
-
issues: parsed.error.issues,
|
|
3803
|
-
});
|
|
3804
|
-
return sendValidationError(res, parsed.error.issues);
|
|
3805
|
-
}
|
|
3806
|
-
|
|
3807
|
-
try {
|
|
3808
|
-
const userId = await getAuthenticatedUserId(req, parsed.data.user_id);
|
|
3809
|
-
const {
|
|
3810
|
-
entity_type,
|
|
3811
|
-
fields_to_add,
|
|
3812
|
-
schema_version,
|
|
3813
|
-
user_specific = false,
|
|
3814
|
-
activate = true,
|
|
3815
|
-
} = parsed.data;
|
|
3816
|
-
|
|
3817
|
-
// Get current schema
|
|
3818
|
-
let query = db
|
|
3819
|
-
.from("schema_registry")
|
|
3820
|
-
.select("*")
|
|
3821
|
-
.eq("entity_type", entity_type)
|
|
3822
|
-
.eq("active", true);
|
|
3823
|
-
|
|
3824
|
-
if (user_specific && userId) {
|
|
3825
|
-
query = query.eq("user_id", userId);
|
|
3826
|
-
} else {
|
|
3827
|
-
query = query.is("user_id", null);
|
|
3828
|
-
}
|
|
3829
|
-
|
|
3830
|
-
const { data: currentSchema, error: fetchError } = await query.single();
|
|
3831
|
-
|
|
3832
|
-
if (fetchError && fetchError.code !== "PGRST116") {
|
|
3833
|
-
logError("DbError:update_schema_incremental", req, fetchError);
|
|
3834
|
-
return sendError(res, 500, "DB_QUERY_FAILED", fetchError.message);
|
|
3835
|
-
}
|
|
3836
|
-
|
|
3837
|
-
// Build new schema definition
|
|
3838
|
-
const baseSchema = currentSchema?.schema_definition || { fields: {} };
|
|
3839
|
-
const newFields: Record<string, any> = {};
|
|
3840
|
-
|
|
3841
|
-
fields_to_add.forEach((field: any) => {
|
|
3842
|
-
newFields[field.field_name] = {
|
|
3843
|
-
type: field.field_type,
|
|
3844
|
-
required: field.required || false,
|
|
3845
|
-
reducer_strategy: field.reducer_strategy || "highest_priority",
|
|
3846
|
-
};
|
|
3847
|
-
});
|
|
3848
|
-
|
|
3849
|
-
const updatedSchema = {
|
|
3850
|
-
...baseSchema,
|
|
3851
|
-
fields: { ...baseSchema.fields, ...newFields },
|
|
3852
|
-
};
|
|
3853
|
-
|
|
3854
|
-
// Insert new schema version
|
|
3855
|
-
const { data: newSchemaVersion, error: insertError } = await db
|
|
3856
|
-
.from("schema_registry")
|
|
3857
|
-
.insert({
|
|
3858
|
-
entity_type,
|
|
3859
|
-
schema_version: schema_version || `${parseInt(currentSchema?.schema_version || "1") + 1}`,
|
|
3860
|
-
schema_definition: updatedSchema,
|
|
3861
|
-
reducer_config: currentSchema?.reducer_config || {},
|
|
3862
|
-
user_id: user_specific ? userId : null,
|
|
3863
|
-
active: activate,
|
|
3864
|
-
created_at: new Date().toISOString(),
|
|
3865
|
-
})
|
|
3866
|
-
.select()
|
|
3867
|
-
.single();
|
|
3868
|
-
|
|
3869
|
-
if (insertError) {
|
|
3870
|
-
logError("DbError:update_schema_incremental", req, insertError);
|
|
3871
|
-
return sendError(res, 500, "DB_QUERY_FAILED", insertError.message);
|
|
3872
|
-
}
|
|
3873
|
-
|
|
3874
|
-
// Deactivate old schema if activating new one
|
|
3875
|
-
if (activate && currentSchema) {
|
|
3876
|
-
await db.from("schema_registry").update({ active: false }).eq("id", currentSchema.id);
|
|
3877
|
-
}
|
|
3878
|
-
|
|
3879
|
-
logDebug("Success:update_schema_incremental", req, {
|
|
3880
|
-
entity_type,
|
|
3881
|
-
fields_added: fields_to_add.length,
|
|
3882
|
-
});
|
|
3883
|
-
return res.json({
|
|
3884
|
-
success: true,
|
|
3885
|
-
schema: newSchemaVersion,
|
|
3886
|
-
schema_version: newSchemaVersion.schema_version,
|
|
3887
|
-
});
|
|
3888
|
-
} catch (error) {
|
|
3889
|
-
return handleApiError(
|
|
3890
|
-
req,
|
|
3891
|
-
res,
|
|
3892
|
-
error,
|
|
3893
|
-
"Failed to update schema incrementally",
|
|
3894
|
-
"DB_QUERY_FAILED",
|
|
3895
|
-
"APIError:update_schema_incremental"
|
|
3896
|
-
);
|
|
3897
|
-
}
|
|
3898
|
-
});
|
|
3899
|
-
|
|
3900
|
-
// POST /register_schema - Register new schema or schema version
|
|
3901
|
-
app.post("/register_schema", async (req, res) => {
|
|
3902
|
-
const parsed = RegisterSchemaRequestSchema.safeParse(req.body);
|
|
3903
|
-
if (!parsed.success) {
|
|
3904
|
-
logWarn("ValidationError:register_schema", req, {
|
|
3905
|
-
issues: parsed.error.issues,
|
|
3906
|
-
});
|
|
3907
|
-
return sendValidationError(res, parsed.error.issues);
|
|
3908
|
-
}
|
|
3909
|
-
|
|
3910
|
-
try {
|
|
3911
|
-
const userId = await getAuthenticatedUserId(req, parsed.data.user_id);
|
|
3912
|
-
const {
|
|
3913
|
-
entity_type,
|
|
3914
|
-
schema_definition,
|
|
3915
|
-
reducer_config,
|
|
3916
|
-
schema_version = "1.0",
|
|
3917
|
-
user_specific = false,
|
|
3918
|
-
activate = false,
|
|
3919
|
-
} = parsed.data;
|
|
3920
|
-
|
|
3921
|
-
// Insert new schema
|
|
3922
|
-
const { data: newSchema, error } = await db
|
|
3923
|
-
.from("schema_registry")
|
|
3924
|
-
.insert({
|
|
3925
|
-
entity_type,
|
|
3926
|
-
schema_version,
|
|
3927
|
-
schema_definition,
|
|
3928
|
-
reducer_config,
|
|
3929
|
-
user_id: user_specific ? userId : null,
|
|
3930
|
-
active: activate,
|
|
3931
|
-
created_at: new Date().toISOString(),
|
|
3932
|
-
})
|
|
3933
|
-
.select()
|
|
3934
|
-
.single();
|
|
3935
|
-
|
|
3936
|
-
if (error) {
|
|
3937
|
-
logError("DbError:register_schema", req, error);
|
|
3938
|
-
return sendError(res, 500, "DB_QUERY_FAILED", error.message);
|
|
3939
|
-
}
|
|
3940
|
-
|
|
3941
|
-
// If activating, deactivate other schemas for this entity type
|
|
3942
|
-
if (activate) {
|
|
3943
|
-
await db
|
|
3944
|
-
.from("schema_registry")
|
|
3945
|
-
.update({ active: false })
|
|
3946
|
-
.eq("entity_type", entity_type)
|
|
3947
|
-
.not("id", "eq", newSchema.id);
|
|
3948
|
-
|
|
3949
|
-
if (user_specific) {
|
|
3950
|
-
await db.from("schema_registry").update({ active: false }).eq("user_id", userId);
|
|
3951
|
-
}
|
|
3952
|
-
}
|
|
3953
|
-
|
|
3954
|
-
logDebug("Success:register_schema", req, { entity_type, schema_version });
|
|
3955
|
-
return res.json({
|
|
3956
|
-
success: true,
|
|
3957
|
-
schema: newSchema,
|
|
3958
|
-
schema_version: newSchema.schema_version,
|
|
3959
|
-
});
|
|
3960
|
-
} catch (error) {
|
|
3961
|
-
return handleApiError(
|
|
3962
|
-
req,
|
|
3963
|
-
res,
|
|
3964
|
-
error,
|
|
3965
|
-
"Failed to register schema",
|
|
3966
|
-
"DB_QUERY_FAILED",
|
|
3967
|
-
"APIError:register_schema"
|
|
3968
|
-
);
|
|
3969
|
-
}
|
|
3970
|
-
});
|
|
3971
|
-
|
|
3972
|
-
async function runReinterpretForSource(
|
|
3973
|
-
sourceId: string,
|
|
3974
|
-
interpretationConfig: Record<string, unknown> | undefined,
|
|
3975
|
-
userId?: string
|
|
3976
|
-
): Promise<{ interpretationId: string; observationsCreated: number; userId: string }> {
|
|
3977
|
-
// Get source
|
|
3978
|
-
let sourceQuery = db.from("sources").select("*").eq("id", sourceId);
|
|
3979
|
-
if (userId) {
|
|
3980
|
-
sourceQuery = sourceQuery.eq("user_id", userId);
|
|
3981
|
-
}
|
|
3982
|
-
const { data: source, error: srcError } = await sourceQuery.single();
|
|
3983
|
-
|
|
3984
|
-
if (srcError || !source) {
|
|
3985
|
-
throw new Error("Source not found");
|
|
3986
|
-
}
|
|
3987
|
-
|
|
3988
|
-
// Re-extract from source.raw_text based on mime_type
|
|
3989
|
-
let extractedData: Array<Record<string, unknown>> = [];
|
|
3990
|
-
if (typeof source.raw_text === "string" && source.raw_text.trim().length > 0) {
|
|
3991
|
-
const mimeType = source.mime_type || "text/plain";
|
|
3992
|
-
const fileName = source.original_filename;
|
|
3993
|
-
|
|
3994
|
-
// Import extraction functions
|
|
3995
|
-
const { extractWithLLM } = await import("./services/llm_extraction.js");
|
|
3996
|
-
|
|
3997
|
-
// Get model ID from config or use default
|
|
3998
|
-
const modelId =
|
|
3999
|
-
interpretationConfig && typeof interpretationConfig.model_id === "string"
|
|
4000
|
-
? interpretationConfig.model_id
|
|
4001
|
-
: "gpt-4o";
|
|
4002
|
-
|
|
4003
|
-
// Handle CSV files with chunking support
|
|
4004
|
-
if (mimeType === "text/csv" || fileName?.toLowerCase().endsWith(".csv")) {
|
|
4005
|
-
const { extractEntitiesFromCsvRows } = await import("./services/csv_row_extraction.js");
|
|
4006
|
-
extractedData = extractEntitiesFromCsvRows(Buffer.from(source.raw_text, "utf8"), fileName);
|
|
4007
|
-
} else {
|
|
4008
|
-
// Non-CSV files - use standard extraction
|
|
4009
|
-
const extractionResult = await extractWithLLM(source.raw_text, fileName, mimeType, modelId);
|
|
4010
|
-
const { entity_type, fields } = extractionResult;
|
|
4011
|
-
extractedData = [
|
|
4012
|
-
{
|
|
4013
|
-
entity_type,
|
|
4014
|
-
...fields,
|
|
4015
|
-
},
|
|
4016
|
-
];
|
|
4017
|
-
}
|
|
4018
|
-
}
|
|
4019
|
-
|
|
4020
|
-
const { runInterpretation } = await import("./services/interpretation.js");
|
|
4021
|
-
const result = await runInterpretation({
|
|
4022
|
-
userId: source.user_id,
|
|
4023
|
-
sourceId,
|
|
4024
|
-
extractedData,
|
|
4025
|
-
config: (interpretationConfig || {}) as any,
|
|
4026
|
-
});
|
|
4027
|
-
|
|
4028
|
-
return {
|
|
4029
|
-
interpretationId: result.interpretationId,
|
|
4030
|
-
observationsCreated: result.observationsCreated,
|
|
4031
|
-
userId: source.user_id,
|
|
4032
|
-
};
|
|
4033
|
-
}
|
|
4034
|
-
|
|
4035
|
-
async function listUninterpretedSourceIds(userId: string, limit: number): Promise<string[]> {
|
|
4036
|
-
const interpretedSet = new Set<string>();
|
|
4037
|
-
const { data: interpretedData, error: interpretedError } = await db
|
|
4038
|
-
.from("interpretations")
|
|
4039
|
-
.select("source_id")
|
|
4040
|
-
.eq("user_id", userId);
|
|
4041
|
-
if (interpretedError) {
|
|
4042
|
-
throw interpretedError;
|
|
4043
|
-
}
|
|
4044
|
-
for (const row of interpretedData || []) {
|
|
4045
|
-
if (row.source_id) interpretedSet.add(row.source_id);
|
|
4046
|
-
}
|
|
4047
|
-
|
|
4048
|
-
const pageSize = Math.max(100, limit * 3);
|
|
4049
|
-
const sourceIds: string[] = [];
|
|
4050
|
-
let offset = 0;
|
|
4051
|
-
|
|
4052
|
-
while (sourceIds.length < limit) {
|
|
4053
|
-
const { data: sourcePage, error: sourceError } = await db
|
|
4054
|
-
.from("sources")
|
|
4055
|
-
.select("id")
|
|
4056
|
-
.eq("user_id", userId)
|
|
4057
|
-
.order("created_at", { ascending: true })
|
|
4058
|
-
.range(offset, offset + pageSize - 1);
|
|
4059
|
-
|
|
4060
|
-
if (sourceError) {
|
|
4061
|
-
throw sourceError;
|
|
4062
|
-
}
|
|
4063
|
-
if (!sourcePage || sourcePage.length === 0) {
|
|
4064
|
-
break;
|
|
4065
|
-
}
|
|
4066
|
-
|
|
4067
|
-
for (const source of sourcePage) {
|
|
4068
|
-
if (sourceIds.length >= limit) break;
|
|
4069
|
-
if (interpretedSet.has(source.id)) continue;
|
|
4070
|
-
sourceIds.push(source.id);
|
|
4071
|
-
}
|
|
4072
|
-
|
|
4073
|
-
if (sourcePage.length < pageSize) {
|
|
4074
|
-
break;
|
|
4075
|
-
}
|
|
4076
|
-
offset += sourcePage.length;
|
|
4077
|
-
}
|
|
4078
|
-
|
|
4079
|
-
return sourceIds;
|
|
4080
|
-
}
|
|
4081
|
-
|
|
4082
|
-
// POST /reinterpret - Re-run AI interpretation on existing source
|
|
4083
|
-
app.post("/reinterpret", async (req, res) => {
|
|
4084
|
-
const parsed = ReinterpretRequestSchema.safeParse(req.body);
|
|
4085
|
-
if (!parsed.success) {
|
|
4086
|
-
logWarn("ValidationError:reinterpret", req, {
|
|
4087
|
-
issues: parsed.error.issues,
|
|
4088
|
-
});
|
|
4089
|
-
return sendValidationError(res, parsed.error.issues);
|
|
4090
|
-
}
|
|
4091
|
-
|
|
4092
|
-
try {
|
|
4093
|
-
let { source_id } = parsed.data;
|
|
4094
|
-
const { interpretation_config, interpretation_id } = parsed.data;
|
|
4095
|
-
|
|
4096
|
-
// Look up source_id from interpretation if not provided directly
|
|
4097
|
-
if (!source_id && interpretation_id) {
|
|
4098
|
-
const { data: interp, error: interpError } = await db
|
|
4099
|
-
.from("interpretations")
|
|
4100
|
-
.select("source_id")
|
|
4101
|
-
.eq("id", interpretation_id)
|
|
4102
|
-
.maybeSingle();
|
|
4103
|
-
if (interpError || !interp) {
|
|
4104
|
-
return sendError(res, 404, "RESOURCE_NOT_FOUND", "Interpretation not found");
|
|
4105
|
-
}
|
|
4106
|
-
source_id = interp.source_id;
|
|
4107
|
-
}
|
|
4108
|
-
|
|
4109
|
-
if (!source_id) {
|
|
4110
|
-
return sendError(res, 400, "VALIDATION_ERROR", "source_id is required");
|
|
4111
|
-
}
|
|
4112
|
-
|
|
4113
|
-
const result = await runReinterpretForSource(source_id, interpretation_config);
|
|
4114
|
-
logDebug("Success:reinterpret", req, { source_id });
|
|
4115
|
-
return res.json({
|
|
4116
|
-
success: true,
|
|
4117
|
-
interpretation_id: result.interpretationId,
|
|
4118
|
-
observations_created: result.observationsCreated,
|
|
4119
|
-
});
|
|
4120
|
-
} catch (error) {
|
|
4121
|
-
if (error instanceof Error && error.message === "Source not found") {
|
|
4122
|
-
return sendError(res, 404, "RESOURCE_NOT_FOUND", "Source not found");
|
|
4123
|
-
}
|
|
4124
|
-
return handleApiError(
|
|
4125
|
-
req,
|
|
4126
|
-
res,
|
|
4127
|
-
error,
|
|
4128
|
-
"Failed to reinterpret source",
|
|
4129
|
-
"DB_QUERY_FAILED",
|
|
4130
|
-
"APIError:reinterpret"
|
|
4131
|
-
);
|
|
4132
|
-
}
|
|
4133
|
-
});
|
|
4134
|
-
|
|
4135
|
-
// POST /interpret-uninterpreted - Re-run interpretation for sources without prior interpretations
|
|
4136
|
-
app.post("/interpret-uninterpreted", async (req, res) => {
|
|
4137
|
-
const parsed = InterpretUninterpretedRequestSchema.safeParse(req.body);
|
|
4138
|
-
if (!parsed.success) {
|
|
4139
|
-
logWarn("ValidationError:interpret_uninterpreted", req, {
|
|
4140
|
-
issues: parsed.error.issues,
|
|
4141
|
-
});
|
|
4142
|
-
return sendValidationError(res, parsed.error.issues);
|
|
4143
|
-
}
|
|
4144
|
-
|
|
4145
|
-
try {
|
|
4146
|
-
const userId = await getAuthenticatedUserId(req, parsed.data.user_id);
|
|
4147
|
-
const limit = parsed.data.limit ?? 50;
|
|
4148
|
-
const dryRun = parsed.data.dry_run ?? false;
|
|
4149
|
-
const interpretationConfig = parsed.data.interpretation_config;
|
|
4150
|
-
const sourceIds = await listUninterpretedSourceIds(userId, limit);
|
|
4151
|
-
|
|
4152
|
-
if (dryRun) {
|
|
4153
|
-
return res.json({
|
|
4154
|
-
dry_run: true,
|
|
4155
|
-
count: sourceIds.length,
|
|
4156
|
-
would_interpret: sourceIds,
|
|
4157
|
-
});
|
|
4158
|
-
}
|
|
4159
|
-
|
|
4160
|
-
const interpreted: Array<{
|
|
4161
|
-
source_id: string;
|
|
4162
|
-
interpretation_id: string;
|
|
4163
|
-
observations_created: number;
|
|
4164
|
-
}> = [];
|
|
4165
|
-
const errors: Array<{ source_id: string; error: string }> = [];
|
|
4166
|
-
|
|
4167
|
-
for (const sourceId of sourceIds) {
|
|
4168
|
-
try {
|
|
4169
|
-
const result = await runReinterpretForSource(sourceId, interpretationConfig, userId);
|
|
4170
|
-
interpreted.push({
|
|
4171
|
-
source_id: sourceId,
|
|
4172
|
-
interpretation_id: result.interpretationId,
|
|
4173
|
-
observations_created: result.observationsCreated,
|
|
4174
|
-
});
|
|
4175
|
-
} catch (error) {
|
|
4176
|
-
const message =
|
|
4177
|
-
error instanceof Error && error.message.trim().length > 0
|
|
4178
|
-
? error.message
|
|
4179
|
-
: "Failed to reinterpret source";
|
|
4180
|
-
errors.push({ source_id: sourceId, error: message });
|
|
4181
|
-
}
|
|
4182
|
-
}
|
|
4183
|
-
|
|
4184
|
-
logDebug("Success:interpret_uninterpreted", req, {
|
|
4185
|
-
user_id: userId,
|
|
4186
|
-
processed: interpreted.length,
|
|
4187
|
-
errors: errors.length,
|
|
4188
|
-
limit,
|
|
4189
|
-
});
|
|
4190
|
-
return res.json({
|
|
4191
|
-
dry_run: false,
|
|
4192
|
-
count: interpreted.length,
|
|
4193
|
-
interpreted,
|
|
4194
|
-
errors,
|
|
4195
|
-
});
|
|
4196
|
-
} catch (error) {
|
|
4197
|
-
return handleApiError(
|
|
4198
|
-
req,
|
|
4199
|
-
res,
|
|
4200
|
-
error,
|
|
4201
|
-
"Failed to interpret uninterpreted sources",
|
|
4202
|
-
"DB_QUERY_FAILED",
|
|
4203
|
-
"APIError:interpret_uninterpreted"
|
|
4204
|
-
);
|
|
4205
|
-
}
|
|
4206
|
-
});
|
|
4207
|
-
|
|
4208
|
-
// POST /correct - Create correction observation to override field value
|
|
4209
|
-
app.post("/correct", async (req, res) => {
|
|
4210
|
-
const parsed = CorrectEntityRequestSchema.safeParse(req.body);
|
|
4211
|
-
if (!parsed.success) {
|
|
4212
|
-
logWarn("ValidationError:correct", req, {
|
|
4213
|
-
issues: parsed.error.issues,
|
|
4214
|
-
});
|
|
4215
|
-
return sendValidationError(res, parsed.error.issues);
|
|
4216
|
-
}
|
|
4217
|
-
|
|
4218
|
-
try {
|
|
4219
|
-
const userId = await getAuthenticatedUserId(req, parsed.data.user_id);
|
|
4220
|
-
const { entity_id, entity_type, field, value, idempotency_key } = parsed.data;
|
|
4221
|
-
|
|
4222
|
-
const { createCorrection } = await import("./services/correction.js");
|
|
4223
|
-
const result = await createCorrection({
|
|
4224
|
-
entity_id,
|
|
4225
|
-
entity_type,
|
|
4226
|
-
field,
|
|
4227
|
-
value,
|
|
4228
|
-
schema_version: "1.0",
|
|
4229
|
-
user_id: userId,
|
|
4230
|
-
idempotency_key,
|
|
4231
|
-
});
|
|
4232
|
-
|
|
4233
|
-
logDebug("Success:correct", req, { entity_id, field });
|
|
4234
|
-
return res.json({ success: true, observation_id: result.observation_id, snapshot: result.snapshot });
|
|
4235
|
-
} catch (error) {
|
|
4236
|
-
return handleApiError(
|
|
4237
|
-
req,
|
|
4238
|
-
res,
|
|
4239
|
-
error,
|
|
4240
|
-
"Failed to create correction",
|
|
4241
|
-
"DB_QUERY_FAILED",
|
|
4242
|
-
"APIError:correct"
|
|
4243
|
-
);
|
|
4244
|
-
}
|
|
4245
|
-
});
|
|
4246
|
-
|
|
4247
|
-
// POST /get_authenticated_user - Get authenticated user ID
|
|
4248
|
-
app.post("/get_authenticated_user", async (req, res) => {
|
|
4249
|
-
try {
|
|
4250
|
-
const userId = await getAuthenticatedUserId(req, undefined);
|
|
4251
|
-
const storage =
|
|
4252
|
-
config.storageBackend === "local"
|
|
4253
|
-
? {
|
|
4254
|
-
storage_backend: "local" as const,
|
|
4255
|
-
data_dir: config.dataDir,
|
|
4256
|
-
sqlite_db: config.sqlitePath,
|
|
4257
|
-
}
|
|
4258
|
-
: undefined;
|
|
4259
|
-
|
|
4260
|
-
logDebug("Success:get_authenticated_user", req, { user_id: userId });
|
|
4261
|
-
return res.json({ user_id: userId, storage });
|
|
4262
|
-
} catch (error) {
|
|
4263
|
-
return handleApiError(
|
|
4264
|
-
req,
|
|
4265
|
-
res,
|
|
4266
|
-
error,
|
|
4267
|
-
"Failed to get authenticated user",
|
|
4268
|
-
"AUTH_REQUIRED",
|
|
4269
|
-
"APIError:get_authenticated_user"
|
|
4270
|
-
);
|
|
4271
|
-
}
|
|
4272
|
-
});
|
|
4273
|
-
|
|
4274
|
-
// POST /health_check_snapshots - Check for stale entity snapshots
|
|
4275
|
-
app.post("/health_check_snapshots", async (req, res) => {
|
|
4276
|
-
const schema = z.object({
|
|
4277
|
-
auto_fix: z.boolean().optional().default(false),
|
|
4278
|
-
});
|
|
4279
|
-
const parsed = schema.safeParse(req.body);
|
|
4280
|
-
if (!parsed.success) {
|
|
4281
|
-
logWarn("ValidationError:health_check_snapshots", req, {
|
|
4282
|
-
issues: parsed.error.issues,
|
|
4283
|
-
});
|
|
4284
|
-
return sendValidationError(res, parsed.error.issues);
|
|
4285
|
-
}
|
|
4286
|
-
|
|
4287
|
-
try {
|
|
4288
|
-
const { auto_fix } = parsed.data;
|
|
4289
|
-
|
|
4290
|
-
// Query for stale snapshots (observation_count=0 but observations exist)
|
|
4291
|
-
const { data: staleSnapshots, error } = await db
|
|
4292
|
-
.from("entity_snapshots")
|
|
4293
|
-
.select("entity_id, entity_type, observation_count")
|
|
4294
|
-
.eq("observation_count", 0);
|
|
4295
|
-
|
|
4296
|
-
if (error) {
|
|
4297
|
-
logError("DbError:health_check_snapshots", req, error);
|
|
4298
|
-
return sendError(res, 500, "DB_QUERY_FAILED", error.message);
|
|
4299
|
-
}
|
|
4300
|
-
|
|
4301
|
-
// Check if these entities actually have observations
|
|
4302
|
-
const staleEntities = [];
|
|
4303
|
-
for (const snapshot of staleSnapshots || []) {
|
|
4304
|
-
const { data: observations, error: obsError } = await db
|
|
4305
|
-
.from("observations")
|
|
4306
|
-
.select("id")
|
|
4307
|
-
.eq("entity_id", snapshot.entity_id)
|
|
4308
|
-
.limit(1);
|
|
4309
|
-
|
|
4310
|
-
if (!obsError && observations && observations.length > 0) {
|
|
4311
|
-
staleEntities.push(snapshot);
|
|
4312
|
-
}
|
|
4313
|
-
}
|
|
4314
|
-
|
|
4315
|
-
let fixedCount = 0;
|
|
4316
|
-
if (auto_fix && staleEntities.length > 0) {
|
|
4317
|
-
// Recompute snapshots for stale entities using observationReducer
|
|
4318
|
-
const { observationReducer } = await import("./reducers/observation_reducer.js");
|
|
4319
|
-
|
|
4320
|
-
for (const entity of staleEntities) {
|
|
4321
|
-
try {
|
|
4322
|
-
// Get all observations for this entity
|
|
4323
|
-
const { data: observations } = await db
|
|
4324
|
-
.from("observations")
|
|
4325
|
-
.select("*")
|
|
4326
|
-
.eq("entity_id", entity.entity_id)
|
|
4327
|
-
.order("observed_at", { ascending: false });
|
|
4328
|
-
|
|
4329
|
-
if (!observations || observations.length === 0) continue;
|
|
4330
|
-
|
|
4331
|
-
// Recompute snapshot
|
|
4332
|
-
const newSnapshot = await observationReducer.computeSnapshot(
|
|
4333
|
-
entity.entity_id,
|
|
4334
|
-
observations as any
|
|
4335
|
-
);
|
|
4336
|
-
if (!newSnapshot) continue;
|
|
4337
|
-
|
|
4338
|
-
const rowWithEmbedding = await prepareEntitySnapshotWithEmbedding({
|
|
4339
|
-
entity_id: newSnapshot.entity_id,
|
|
4340
|
-
entity_type: newSnapshot.entity_type,
|
|
4341
|
-
schema_version: newSnapshot.schema_version,
|
|
4342
|
-
snapshot: newSnapshot.snapshot,
|
|
4343
|
-
computed_at: newSnapshot.computed_at,
|
|
4344
|
-
observation_count: newSnapshot.observation_count,
|
|
4345
|
-
last_observation_at: newSnapshot.last_observation_at,
|
|
4346
|
-
provenance: newSnapshot.provenance,
|
|
4347
|
-
user_id: newSnapshot.user_id,
|
|
4348
|
-
});
|
|
4349
|
-
await upsertEntitySnapshotWithEmbedding(rowWithEmbedding);
|
|
4350
|
-
|
|
4351
|
-
fixedCount++;
|
|
4352
|
-
} catch (error) {
|
|
4353
|
-
console.error(`Failed to fix snapshot for ${entity.entity_id}:`, error);
|
|
4354
|
-
}
|
|
4355
|
-
}
|
|
4356
|
-
}
|
|
4357
|
-
|
|
4358
|
-
logDebug("Success:health_check_snapshots", req, {
|
|
4359
|
-
stale_count: staleEntities.length,
|
|
4360
|
-
auto_fix,
|
|
4361
|
-
});
|
|
4362
|
-
return res.json({
|
|
4363
|
-
healthy: staleEntities.length === 0,
|
|
4364
|
-
message:
|
|
4365
|
-
staleEntities.length === 0
|
|
4366
|
-
? "All snapshots healthy"
|
|
4367
|
-
: auto_fix
|
|
4368
|
-
? `Found ${staleEntities.length} stale snapshots, fixed ${fixedCount}`
|
|
4369
|
-
: `Found ${staleEntities.length} stale snapshots`,
|
|
4370
|
-
checked: staleSnapshots?.length || 0,
|
|
4371
|
-
stale: staleEntities.length,
|
|
4372
|
-
fixed: auto_fix ? fixedCount : undefined,
|
|
4373
|
-
stale_snapshots: staleEntities,
|
|
4374
|
-
});
|
|
4375
|
-
} catch (error) {
|
|
4376
|
-
return handleApiError(
|
|
4377
|
-
req,
|
|
4378
|
-
res,
|
|
4379
|
-
error,
|
|
4380
|
-
"Failed to check snapshot health",
|
|
4381
|
-
"DB_QUERY_FAILED",
|
|
4382
|
-
"APIError:health_check_snapshots"
|
|
4383
|
-
);
|
|
4384
|
-
}
|
|
4385
|
-
});
|
|
4386
|
-
|
|
4387
|
-
// ========== End of Phase 0 endpoints ==========
|
|
4388
|
-
|
|
4389
|
-
// Chat endpoint removed - violates Application Layer constraint "MUST NOT contain conversational logic"
|
|
4390
|
-
// Conversational interactions should be externalized to MCP-compatible agents per architecture
|
|
4391
|
-
|
|
4392
|
-
app.get("/openapi.yaml", (req, res) => {
|
|
4393
|
-
const openApiPath = path.join(process.cwd(), "openapi.yaml");
|
|
4394
|
-
const openApiContent = fs.readFileSync(openApiPath, "utf-8");
|
|
4395
|
-
const spec = yaml.load(openApiContent) as { servers?: Array<{ url: string; description?: string }> };
|
|
4396
|
-
const baseUrl = (config.apiBase || "").replace(/\/$/, "");
|
|
4397
|
-
if (spec.servers?.length && baseUrl) {
|
|
4398
|
-
spec.servers[0] = { ...spec.servers[0], url: baseUrl };
|
|
4399
|
-
}
|
|
4400
|
-
res.setHeader("Content-Type", "application/yaml");
|
|
4401
|
-
res.send(yaml.dump(spec, { lineWidth: -1 }));
|
|
4402
|
-
});
|
|
4403
|
-
|
|
4404
|
-
// Documentation routes (FU-301) - must be before SPA fallback
|
|
4405
|
-
// setupDocumentationRoutes(app); // TODO: Re-enable after implementing routes/documentation.ts
|
|
4406
|
-
|
|
4407
|
-
// SPA fallback - serve index.html for non-API routes (must be after all API routes)
|
|
4408
|
-
|
|
4409
|
-
/** Try to bind on a port; resolves with server and port, or rejects on error (e.g. EADDRINUSE). */
|
|
4410
|
-
function tryListen(
|
|
4411
|
-
port: number
|
|
4412
|
-
): Promise<{ server: ReturnType<express.Express["listen"]>; port: number }> {
|
|
4413
|
-
return new Promise((resolve, reject) => {
|
|
4414
|
-
const server = app.listen(port, () => {
|
|
4415
|
-
resolve({ server, port });
|
|
4416
|
-
});
|
|
4417
|
-
server.once("error", (err: NodeJS.ErrnoException) => {
|
|
4418
|
-
server.close();
|
|
4419
|
-
reject(err);
|
|
4420
|
-
});
|
|
4421
|
-
});
|
|
4422
|
-
}
|
|
4423
|
-
|
|
4424
|
-
// Export function to start HTTP server (called explicitly, not on import)
|
|
4425
|
-
export async function startHTTPServer() {
|
|
4426
|
-
// Initialize encryption service
|
|
4427
|
-
await initServerKeys();
|
|
4428
|
-
|
|
4429
|
-
const httpPortEnv = process.env.NEOTOMA_HTTP_PORT || process.env.HTTP_PORT;
|
|
4430
|
-
const basePort = httpPortEnv
|
|
4431
|
-
? parseInt(httpPortEnv, 10)
|
|
4432
|
-
: config.httpPort || 8080;
|
|
4433
|
-
const portFile = process.env.NEOTOMA_SESSION_PORT_FILE;
|
|
4434
|
-
const maxTries = 20;
|
|
4435
|
-
|
|
4436
|
-
for (let offset = 0; offset < maxTries; offset++) {
|
|
4437
|
-
const port = basePort + offset;
|
|
4438
|
-
try {
|
|
4439
|
-
const { server } = await tryListen(port);
|
|
4440
|
-
if (portFile) {
|
|
4441
|
-
fs.writeFileSync(portFile, String(port), "utf-8");
|
|
4442
|
-
}
|
|
4443
|
-
// eslint-disable-next-line no-console
|
|
4444
|
-
console.log(`HTTP Actions listening on :${port}`);
|
|
4445
|
-
|
|
4446
|
-
// Start background OAuth state cleanup job
|
|
4447
|
-
import("./services/mcp_oauth.js").then((oauth) => {
|
|
4448
|
-
oauth.startStateCleanupJob();
|
|
4449
|
-
});
|
|
4450
|
-
return { server, port };
|
|
4451
|
-
} catch (err: unknown) {
|
|
4452
|
-
const code = (err as NodeJS.ErrnoException)?.code;
|
|
4453
|
-
if (code === "EADDRINUSE" && offset < maxTries - 1) {
|
|
4454
|
-
continue;
|
|
4455
|
-
}
|
|
4456
|
-
throw err;
|
|
4457
|
-
}
|
|
4458
|
-
}
|
|
4459
|
-
}
|
|
4460
|
-
|
|
4461
|
-
// Only auto-start if not disabled AND if this is the main module
|
|
4462
|
-
const isMainModule = import.meta.url === `file://${process.argv[1]}`;
|
|
4463
|
-
if (process.env.NEOTOMA_ACTIONS_DISABLE_AUTOSTART !== "1" && isMainModule) {
|
|
4464
|
-
startHTTPServer().catch((err) => {
|
|
4465
|
-
console.error("Failed to start HTTP server:", err);
|
|
4466
|
-
process.exit(1);
|
|
4467
|
-
});
|
|
4468
|
-
}
|