neotoma 0.18.8 → 0.20.0
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/dist/actions.d.ts +59 -0
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +325 -15
- package/dist/actions.js.map +1 -1
- package/dist/cli/index.d.ts +14 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +87 -4
- package/dist/cli/index.js.map +1 -1
- package/dist/docs/developer/cli_agent_instructions.md +37 -0
- package/dist/docs/developer/cli_reference.md +1 -1
- package/dist/docs/developer/mcp/instructions.md +3 -2
- package/dist/docs/developer/mcp/proxy.md +6 -4
- package/dist/docs/foundation/entity_resolution.md +34 -7
- package/dist/docs/foundation/what_to_store.md +1 -1
- package/dist/docs/infrastructure/client_instance_deployment.md +151 -0
- package/dist/docs/reference/error_codes.md +75 -23
- package/dist/docs/subsystems/errors.md +3 -0
- package/dist/docs/testing/automated_test_catalog.md +30 -14
- package/dist/inspector/assets/access_policies-6-6DPI39.js +1 -0
- package/dist/inspector/assets/{agent_badge-D5c8UAE9.js → agent_badge-CXSO0sXJ.js} +1 -1
- package/dist/inspector/assets/{agent_detail-lZm2dWDR.js → agent_detail-Cr7iqdye.js} +1 -1
- package/dist/inspector/assets/{agent_filter-_s9ctnZg.js → agent_filter-DQvQyxMp.js} +1 -1
- package/dist/inspector/assets/{agent_grant_detail-jPlKCXYO.js → agent_grant_detail-DRRM7KWw.js} +1 -1
- package/dist/inspector/assets/{agent_grant_form-BUQdfq4A.js → agent_grant_form-DNfWiVBQ.js} +1 -1
- package/dist/inspector/assets/{agent_grants-D7dYlLNX.js → agent_grants-BWgOPiOe.js} +1 -1
- package/dist/inspector/assets/{agents-Bl50GiDS.js → agents-CPLAQelS.js} +1 -1
- package/dist/inspector/assets/{api_not_configured_state-GASK-mhp.js → api_not_configured_state-DHesochj.js} +1 -1
- package/dist/inspector/assets/{attribution_card-Dks_zpQx.js → attribution_card-CCINKjRY.js} +1 -1
- package/dist/inspector/assets/{attribution_summary-Cli2qL-J.js → attribution_summary-Dkwz1aNS.js} +1 -1
- package/dist/inspector/assets/{bundles-4y41Dsvz.js → bundles-f-bRlQYm.js} +1 -1
- package/dist/inspector/assets/{card-DyP1iupa.js → card-18-eZw0x.js} +1 -1
- package/dist/inspector/assets/{checkbox-CIUQh9NP.js → checkbox-DUu6AMCH.js} +1 -1
- package/dist/inspector/assets/{compliance-CZT3IVp5.js → compliance-C1iqMRYK.js} +1 -1
- package/dist/inspector/assets/{confirm-dialog-DwPqhney.js → confirm-dialog-KIPuwTQR.js} +1 -1
- package/dist/inspector/assets/{conversation_common-DqXXK2sj.js → conversation_common-aPvd5zmX.js} +1 -1
- package/dist/inspector/assets/{conversation_detail-BUqgnrpM.js → conversation_detail-nJDDMCfc.js} +1 -1
- package/dist/inspector/assets/{copy_id_button-CFvm8hKT.js → copy_id_button-BwaRGKNA.js} +1 -1
- package/dist/inspector/assets/{copyable_code_block-Ds_m6kph.js → copyable_code_block-DDi03yAz.js} +1 -1
- package/dist/inspector/assets/{corrections-CYBwDzLB.js → corrections-Dz2v9ju9.js} +1 -1
- package/dist/inspector/assets/{dashboard-Bn74vHnA.js → dashboard-CZOdnoJJ.js} +1 -1
- package/dist/inspector/assets/{data-table-BdvHAu9o.js → data-table-5i_gqEfj.js} +1 -1
- package/dist/inspector/assets/{design-CvkXmrg8.js → design-BdKBUVmX.js} +1 -1
- package/dist/inspector/assets/{dialog-ClLKFBmR.js → dialog-CNJoZoQS.js} +1 -1
- package/dist/inspector/assets/{docs-DsWM97KO.js → docs-sXzc0LTz.js} +1 -1
- package/dist/inspector/assets/{docs_page_layout-B2uZRfVC.js → docs_page_layout-CGg8ubQX.js} +1 -1
- package/dist/inspector/assets/{embed_graph-dvewK9nr.js → embed_graph-BpLLD_vX.js} +1 -1
- package/dist/inspector/assets/{empty_state-Bc-ujW2X.js → empty_state-DNk61WLW.js} +1 -1
- package/dist/inspector/assets/{entities-BOrwMORJ.js → entities-Cexhp3qr.js} +1 -1
- package/dist/inspector/assets/{entity_correct-r178klDI.js → entity_correct-CfSggXgT.js} +1 -1
- package/dist/inspector/assets/{entity_history-CKLn3lKb.js → entity_history-CGila0eu.js} +1 -1
- package/dist/inspector/assets/{entity_history_sections-FVZdr00Y.js → entity_history_sections-CXYX1gph.js} +1 -1
- package/dist/inspector/assets/{entity_link-yKDYqbhx.js → entity_link-DUdsaj-A.js} +1 -1
- package/dist/inspector/assets/{entity_relationships_by_type-SQLvwlXL.js → entity_relationships_by_type-B_PH1APK.js} +1 -1
- package/dist/inspector/assets/{entity_segment_page-BXDQbUUp.js → entity_segment_page-DRYjT1AU.js} +1 -1
- package/dist/inspector/assets/{entity_table_column_toggle-D7CwUiAH.js → entity_table_column_toggle-DwwHY7Y8.js} +1 -1
- package/dist/inspector/assets/{entity_timeline-Dg0wEuRA.js → entity_timeline-B4XYY56n.js} +1 -1
- package/dist/inspector/assets/{entity_types-DLlkd4ex.js → entity_types-ByB2PWZM.js} +1 -1
- package/dist/inspector/assets/{faq-KbswV10B.js → faq-B-PEHwkf.js} +1 -1
- package/dist/inspector/assets/{graph_auto_fit-SqyYePaZ.js → graph_auto_fit-ClZnIOoR.js} +1 -1
- package/dist/inspector/assets/{graph_explorer-DZgjc-hi.js → graph_explorer-C-Xo8VWP.js} +1 -1
- package/dist/inspector/assets/{home-CUYUSmke.js → home-D1UVMr6_.js} +1 -1
- package/dist/inspector/assets/{index-BGqcUFFW.js → index-C6fxoS_Z.js} +1582 -1582
- package/dist/inspector/assets/{index-CBJY_OOI.js → index-Cp0BQ57U.js} +1 -1
- package/dist/inspector/assets/{index-DOxiDdC1.js → index-MnCv_KTr.js} +1 -1
- package/dist/inspector/assets/interpretations-BCs9qtw0.js +1 -0
- package/dist/inspector/assets/{interpretations-CSeyL3hr.js → interpretations-DG5kUN-V.js} +1 -1
- package/dist/inspector/assets/{issue_author_attribution-Czyui6JR.js → issue_author_attribution-D7_vwrHJ.js} +1 -1
- package/dist/inspector/assets/{issue_detail-DgdqvrIo.js → issue_detail-DKYUwKt_.js} +1 -1
- package/dist/inspector/assets/{issues-dw8Q41vn.js → issues-DcSMjt-p.js} +1 -1
- package/dist/inspector/assets/{json_viewer-CEMz7ftn.js → json_viewer-BKa7wwrs.js} +1 -1
- package/dist/inspector/assets/{label-qbFxe1yC.js → label-BO-aVMjO.js} +1 -1
- package/dist/inspector/assets/{live_relative_time-CVpdUZRq.js → live_relative_time-Dx00M9I6.js} +1 -1
- package/dist/inspector/assets/mobile_filter_popover-I9fJvTGr.js +1 -0
- package/dist/inspector/assets/{not_found-DFUrM9eP.js → not_found-DuCJouBh.js} +1 -1
- package/dist/inspector/assets/oauth_callback-w0kyGAb4.js +1 -0
- package/dist/inspector/assets/oauth_signin-3LQrxsKz.js +1 -0
- package/dist/inspector/assets/{observations-BO6KImWs.js → observations-Bq6mvl5m.js} +1 -1
- package/dist/inspector/assets/{page_shell-COxcPFXG.js → page_shell-BoTb5of6.js} +1 -1
- package/dist/inspector/assets/{pagination-NLnVe1CU.js → pagination-BJtT1Wgy.js} +1 -1
- package/dist/inspector/assets/{peer_detail-DdtQsiwD.js → peer_detail-BM1jhSS2.js} +1 -1
- package/dist/inspector/assets/{peers-D0bzQeyz.js → peers-CYSx-jAx.js} +1 -1
- package/dist/inspector/assets/{pin_primitive_button-BtuY3Ig7.js → pin_primitive_button-BdAR3e5K.js} +1 -1
- package/dist/inspector/assets/{recent_activity-8t3t2hCi.js → recent_activity-Dio9UBCA.js} +1 -1
- package/dist/inspector/assets/{recent_conversations-xURWFQ1G.js → recent_conversations-Be7S63h9.js} +1 -1
- package/dist/inspector/assets/{recent_conversations-B351X_is.js → recent_conversations-iJTSJf5e.js} +1 -1
- package/dist/inspector/assets/{recent_records_feed-C0auJYwP.js → recent_records_feed-ZxHjVjQ6.js} +1 -1
- package/dist/inspector/assets/record_activity-BmMC7Q7l.js +1 -0
- package/dist/inspector/assets/{relationship_detail-B9y50i1N.js → relationship_detail-i9rsGxA6.js} +1 -1
- package/dist/inspector/assets/{relationship_panel_groups-DgC2aJin.js → relationship_panel_groups-CXFlrRHU.js} +1 -1
- package/dist/inspector/assets/{relationships-u7QTLzYr.js → relationships-DqAzfnlB.js} +1 -1
- package/dist/inspector/assets/{relationships-DvN6JCJC.js → relationships-DvWtOU7W.js} +1 -1
- package/dist/inspector/assets/{sandbox-DzEfLQAo.js → sandbox-Cn4vq2Pz.js} +1 -1
- package/dist/inspector/assets/{schema_detail-BI3lLXFK.js → schema_detail-BdORXewP.js} +1 -1
- package/dist/inspector/assets/{schemas-DpAQVcki.js → schemas-HYsBtoHT.js} +1 -1
- package/dist/inspector/assets/{search-CjE75jdm.js → search-LzJnqvTB.js} +1 -1
- package/dist/inspector/assets/{segmented_control-D9QbgEtB.js → segmented_control-CHJO5zMt.js} +1 -1
- package/dist/inspector/assets/{select-KOFjYHgz.js → select-DFzP9laV.js} +1 -1
- package/dist/inspector/assets/settings-Bm0jNa4p.js +1 -0
- package/dist/inspector/assets/{source_content-Bi_jkeg2.js → source_content-PwVSPaIU.js} +1 -1
- package/dist/inspector/assets/{source_content_open_button-B9mhDPoj.js → source_content_open_button-C7NKRG7q.js} +1 -1
- package/dist/inspector/assets/{source_detail-B3jsntel.js → source_detail-C2D885Zy.js} +1 -1
- package/dist/inspector/assets/{source_link-DrAAN7IB.js → source_link-6lMlhQnK.js} +1 -1
- package/dist/inspector/assets/{sources-B7H4Tgy7.js → sources-BvGo5OZp.js} +1 -1
- package/dist/inspector/assets/{subscriptions-B79INOqC.js → subscriptions-BGYmtTLV.js} +1 -1
- package/dist/inspector/assets/{switch-DEw5ZD9Y.js → switch-DkusEYsE.js} +1 -1
- package/dist/inspector/assets/{tabs-CZDCOqHp.js → tabs-hkbPHxT0.js} +1 -1
- package/dist/inspector/assets/{textarea-DM2MsnIS.js → textarea-BhC7xtLk.js} +1 -1
- package/dist/inspector/assets/{timeline-DtGp-dM7.js → timeline-CHi-lYcV.js} +1 -1
- package/dist/inspector/assets/{timeline-CwKOl2cK.js → timeline-DmjoODDd.js} +1 -1
- package/dist/inspector/assets/{timeline_event_detail-D-gU-DZY.js → timeline_event_detail-BfKrsQih.js} +1 -1
- package/dist/inspector/assets/{turn_detail-D20KYWIY.js → turn_detail-DGj1lT0k.js} +1 -1
- package/dist/inspector/assets/{turns-CCukFhV6.js → turns-CHfOIg9X.js} +1 -1
- package/dist/inspector/assets/{use_agents-Duqes0yE.js → use_agents-6sUtEOpZ.js} +1 -1
- package/dist/inspector/assets/{use_entities-CAYbeIfx.js → use_entities-DeIlqnzx.js} +1 -1
- package/dist/inspector/assets/use_interpretations-DvfzvGu3.js +1 -0
- package/dist/inspector/assets/{use_mutations-CaZO8lpD.js → use_mutations-CQ3_5Ppg.js} +1 -1
- package/dist/inspector/assets/{use_peers-CGLvEf5V.js → use_peers-CW3OAs41.js} +1 -1
- package/dist/inspector/assets/{use_relationships-B8fSaOmY.js → use_relationships-Dbtmx8X8.js} +1 -1
- package/dist/inspector/assets/{use_stats-DHAaJFKE.js → use_stats-OyHBzetJ.js} +1 -1
- package/dist/inspector/assets/{use_timeline-BMIPVqoi.js → use_timeline-FINsibXm.js} +1 -1
- package/dist/inspector/assets/{use_turns-DRAnBf4e.js → use_turns-D4AhGAvM.js} +1 -1
- package/dist/inspector/assets/{world_time_event_timeline-CWFM4Q3K.js → world_time_event_timeline-F0elR7Vc.js} +1 -1
- package/dist/inspector/index.html +1 -1
- package/dist/proxy/mcp_stdio_proxy.d.ts +11 -5
- package/dist/proxy/mcp_stdio_proxy.d.ts.map +1 -1
- package/dist/proxy/mcp_stdio_proxy.js +12 -6
- package/dist/proxy/mcp_stdio_proxy.js.map +1 -1
- package/dist/repositories/sqlite/local_db_adapter.d.ts.map +1 -1
- package/dist/repositories/sqlite/local_db_adapter.js +12 -0
- package/dist/repositories/sqlite/local_db_adapter.js.map +1 -1
- package/dist/repositories/sqlite/sqlite_client.d.ts.map +1 -1
- package/dist/repositories/sqlite/sqlite_client.js +7 -0
- package/dist/repositories/sqlite/sqlite_client.js.map +1 -1
- package/dist/seed_schemas_entry.d.ts +28 -0
- package/dist/seed_schemas_entry.d.ts.map +1 -0
- package/dist/seed_schemas_entry.js +142 -0
- package/dist/seed_schemas_entry.js.map +1 -0
- package/dist/server.d.ts +9 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +151 -3
- package/dist/server.js.map +1 -1
- package/dist/services/company_query.d.ts +54 -0
- package/dist/services/company_query.d.ts.map +1 -0
- package/dist/services/company_query.js +175 -0
- package/dist/services/company_query.js.map +1 -0
- package/dist/services/company_resolution.d.ts +86 -0
- package/dist/services/company_resolution.d.ts.map +1 -0
- package/dist/services/company_resolution.js +195 -0
- package/dist/services/company_resolution.js.map +1 -0
- package/dist/services/entity_cursor.d.ts +57 -0
- package/dist/services/entity_cursor.d.ts.map +1 -0
- package/dist/services/entity_cursor.js +104 -0
- package/dist/services/entity_cursor.js.map +1 -0
- package/dist/services/entity_queries.d.ts +32 -0
- package/dist/services/entity_queries.d.ts.map +1 -1
- package/dist/services/entity_queries.js +64 -2
- package/dist/services/entity_queries.js.map +1 -1
- package/dist/services/entity_query_limits.d.ts +20 -0
- package/dist/services/entity_query_limits.d.ts.map +1 -0
- package/dist/services/entity_query_limits.js +25 -0
- package/dist/services/entity_query_limits.js.map +1 -0
- package/dist/services/entity_resolution.d.ts.map +1 -1
- package/dist/services/entity_resolution.js +12 -2
- package/dist/services/entity_resolution.js.map +1 -1
- package/dist/services/google_oidc.d.ts +110 -0
- package/dist/services/google_oidc.d.ts.map +1 -0
- package/dist/services/google_oidc.js +249 -0
- package/dist/services/google_oidc.js.map +1 -0
- package/dist/services/inspector_mount.d.ts.map +1 -1
- package/dist/services/inspector_mount.js +9 -0
- package/dist/services/inspector_mount.js.map +1 -1
- package/dist/services/interpretation.d.ts.map +1 -1
- package/dist/services/interpretation.js +17 -0
- package/dist/services/interpretation.js.map +1 -1
- package/dist/services/issues/issue_operations.d.ts +4 -0
- package/dist/services/issues/issue_operations.d.ts.map +1 -1
- package/dist/services/issues/issue_operations.js +18 -0
- package/dist/services/issues/issue_operations.js.map +1 -1
- package/dist/services/issues/neotoma_client.d.ts.map +1 -1
- package/dist/services/issues/neotoma_client.js +10 -5
- package/dist/services/issues/neotoma_client.js.map +1 -1
- package/dist/services/issues/observer_import.d.ts +1 -1
- package/dist/services/issues/observer_import.d.ts.map +1 -1
- package/dist/services/issues/observer_import.js +3 -3
- package/dist/services/issues/observer_import.js.map +1 -1
- package/dist/services/mcp_oauth.d.ts +1 -1
- package/dist/services/mcp_oauth.d.ts.map +1 -1
- package/dist/services/mcp_oauth.js +11 -1
- package/dist/services/mcp_oauth.js.map +1 -1
- package/dist/services/oauth_key_gate.d.ts +15 -1
- package/dist/services/oauth_key_gate.d.ts.map +1 -1
- package/dist/services/oauth_key_gate.js +42 -3
- package/dist/services/oauth_key_gate.js.map +1 -1
- package/dist/services/schema_definitions.d.ts.map +1 -1
- package/dist/services/schema_definitions.js +27 -1
- package/dist/services/schema_definitions.js.map +1 -1
- package/dist/services/schema_reference_linking.d.ts +61 -2
- package/dist/services/schema_reference_linking.d.ts.map +1 -1
- package/dist/services/schema_reference_linking.js +264 -21
- package/dist/services/schema_reference_linking.js.map +1 -1
- package/dist/services/schema_registry.d.ts +25 -0
- package/dist/services/schema_registry.d.ts.map +1 -1
- package/dist/services/schema_registry.js +22 -0
- package/dist/services/schema_registry.js.map +1 -1
- package/dist/services/snapshot_computation.d.ts +18 -0
- package/dist/services/snapshot_computation.d.ts.map +1 -1
- package/dist/services/snapshot_computation.js +27 -3
- package/dist/services/snapshot_computation.js.map +1 -1
- package/dist/shared/action_handlers/entity_handlers.d.ts +9 -0
- package/dist/shared/action_handlers/entity_handlers.d.ts.map +1 -1
- package/dist/shared/action_handlers/entity_handlers.js +9 -2
- package/dist/shared/action_handlers/entity_handlers.js.map +1 -1
- package/dist/shared/action_handlers/entity_identifier_handler.d.ts.map +1 -1
- package/dist/shared/action_handlers/entity_identifier_handler.js +131 -4
- package/dist/shared/action_handlers/entity_identifier_handler.js.map +1 -1
- package/dist/shared/action_schemas.d.ts +132 -38
- package/dist/shared/action_schemas.d.ts.map +1 -1
- package/dist/shared/action_schemas.js +171 -1
- package/dist/shared/action_schemas.js.map +1 -1
- package/dist/shared/contract_mappings.d.ts.map +1 -1
- package/dist/shared/contract_mappings.js +9 -0
- package/dist/shared/contract_mappings.js.map +1 -1
- package/dist/shared/openapi_types.d.ts +249 -5
- package/dist/shared/openapi_types.d.ts.map +1 -1
- package/dist/tool_definitions.d.ts +1 -1
- package/dist/tool_definitions.d.ts.map +1 -1
- package/dist/tool_definitions.js +30 -4
- package/dist/tool_definitions.js.map +1 -1
- package/docs/developer/mcp/instructions.md +3 -2
- package/openapi.yaml +288 -5
- package/package.json +2 -1
- package/dist/inspector/assets/access_policies-C0QzIgOo.js +0 -1
- package/dist/inspector/assets/interpretations-B1shK6Is.js +0 -1
- package/dist/inspector/assets/mobile_filter_popover-CI_BhFTn.js +0 -1
- package/dist/inspector/assets/record_activity-DUwg1WBC.js +0 -1
- package/dist/inspector/assets/settings-CHzeuzGL.js +0 -1
- package/dist/inspector/assets/use_interpretations-D_RbNlHg.js +0 -1
package/dist/actions.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import express from "express";
|
|
|
2
2
|
import { type GuestIdentity } from "./services/access_policy.js";
|
|
3
3
|
import { type AgentIdentity } from "./crypto/agent_identity.js";
|
|
4
4
|
import { type NeotomaSandboxModeName } from "./services/sandbox_mode.js";
|
|
5
|
+
import { OAuthKeySessionStore } from "./services/oauth_key_gate.js";
|
|
5
6
|
import { type StoreInterpretationInput } from "./shared/action_schemas.js";
|
|
6
7
|
/** Exposed for the /me route and any other surface that needs the boot-time mode verdict. */
|
|
7
8
|
export declare function getResolvedServerMode(): NeotomaSandboxModeName | null;
|
|
@@ -76,6 +77,51 @@ export declare function isTrustedProxyIP(ip: string, env?: NodeJS.ProcessEnv): b
|
|
|
76
77
|
* entirely and remains valid for single-host deployments without a tunnel.
|
|
77
78
|
*/
|
|
78
79
|
export declare function isLocalRequest(req: express.Request): boolean;
|
|
80
|
+
/** Exported for tests asserting the cookie maxAge and the server-side session
|
|
81
|
+
* expiry agree — the invariant setOAuthKeySessionCookie's docstring names. */
|
|
82
|
+
export declare const oauthKeySessions: OAuthKeySessionStore;
|
|
83
|
+
/**
|
|
84
|
+
* Lifetime of a SIGN-IN session (Google). Distinct from the key-entry gate's
|
|
85
|
+
* short default: a user who signs in expects to stay signed in, whereas the
|
|
86
|
+
* key-entry gate deliberately closes fast. Reusing the 15-minute gate TTL for
|
|
87
|
+
* sign-in is what logged hosted-instance users out every quarter hour (#2005).
|
|
88
|
+
*
|
|
89
|
+
* Override with NEOTOMA_SIGN_IN_SESSION_TTL_MIN (minutes). Default 7 days.
|
|
90
|
+
*/
|
|
91
|
+
export declare const DEFAULT_SIGN_IN_SESSION_TTL_MIN: number;
|
|
92
|
+
/** Upper bound for the sign-in TTL override: 365 days in minutes. A value past
|
|
93
|
+
* this is a typo (an extra zero), not an intent — without a ceiling,
|
|
94
|
+
* "9999999999999" yields a ~19-million-year session. Mirrors the low-end
|
|
95
|
+
* guard rather than guarding only one side (#2007 qa lens). */
|
|
96
|
+
export declare const MAX_SIGN_IN_SESSION_TTL_MIN: number;
|
|
97
|
+
/**
|
|
98
|
+
* Parse NEOTOMA_SIGN_IN_SESSION_TTL_MIN into milliseconds. Exported (and pure)
|
|
99
|
+
* so the override's precedence and malformed-input behavior are directly
|
|
100
|
+
* testable — a silently-wrong TTL here reintroduces exactly the desync class
|
|
101
|
+
* this change fixes.
|
|
102
|
+
*
|
|
103
|
+
* Precedence: an integer within (0, MAX_SIGN_IN_SESSION_TTL_MIN] wins.
|
|
104
|
+
* Everything else — unset, non-numeric, zero, negative, or above the ceiling —
|
|
105
|
+
* falls back to the 7-day default rather than producing a degenerate session.
|
|
106
|
+
*
|
|
107
|
+
* Note on parseInt semantics, deliberately kept: "7.5" truncates to 7 minutes
|
|
108
|
+
* and "1e10" parses as 1 minute (parseInt stops at the 'e'). Both are accepted
|
|
109
|
+
* as-is because they are in-range integers after parsing; the fallback exists
|
|
110
|
+
* for values that are absent, unparseable, or out of range, not to second-guess
|
|
111
|
+
* a caller who wrote a decimal. Asserted in tests so the behavior is documented
|
|
112
|
+
* rather than incidental.
|
|
113
|
+
*/
|
|
114
|
+
export declare function resolveSignInSessionTtlMs(raw: string | undefined): number;
|
|
115
|
+
/**
|
|
116
|
+
* Issue an OAuth key-session cookie. `ttlMs` covers BOTH the server-side
|
|
117
|
+
* session and the cookie's own maxAge — they must agree, or the browser keeps
|
|
118
|
+
* presenting a cookie the server has already forgotten (or vice versa).
|
|
119
|
+
* Omit it for the short key-entry gate; pass SIGN_IN_SESSION_TTL_MS for
|
|
120
|
+
* sign-in (#2005).
|
|
121
|
+
*/
|
|
122
|
+
export declare function setOAuthKeySessionCookie(req: express.Request, res: express.Response, ttlMs?: number): string;
|
|
123
|
+
/** Resolve the local-auth user_id a Google-verified session (if any) mapped to this request. */
|
|
124
|
+
export declare function getGoogleVerifiedUserId(req: express.Request): string | undefined;
|
|
79
125
|
/**
|
|
80
126
|
* Derive the canonical `@authority` the AAuth profile requires (AAuth §10.3.1).
|
|
81
127
|
* We trust explicit configuration first, then fall back to the host the
|
|
@@ -111,6 +157,18 @@ export declare function routeAllowsLocalIssueWriteFallback(req: Pick<express.Req
|
|
|
111
157
|
export declare function routeConsumesGuestWriteBudget(req: Pick<express.Request, "method" | "path">): boolean;
|
|
112
158
|
/** Exported for unit tests: guest-rate-limit key precedence is thumbprint -> guest token -> IP. */
|
|
113
159
|
export declare function guestWriteRateLimitKey(req: express.Request): string;
|
|
160
|
+
/**
|
|
161
|
+
* Issue *submission* is the one route a third party may reach with no identity
|
|
162
|
+
* at all: the guest access token is minted BY the submit, so requiring one to
|
|
163
|
+
* perform it is a chicken-and-egg that closes the inbox to first contact.
|
|
164
|
+
*
|
|
165
|
+
* Deliberately submit-only. `add_message` stays authenticated: by the time a
|
|
166
|
+
* reporter has a thread to append to, they hold the token returned by their
|
|
167
|
+
* submit, so anonymity buys nothing there — and letting an unidentified caller
|
|
168
|
+
* write into an existing thread is a strictly wider blast radius than letting
|
|
169
|
+
* them open a new one. The token is what proves "I opened this thread".
|
|
170
|
+
*/
|
|
171
|
+
export declare function routeAcceptsAnonymousIssueSubmit(req: Pick<express.Request, "method" | "path">): boolean;
|
|
114
172
|
/**
|
|
115
173
|
* Derive a userId for a guest principal from the validated token grant.
|
|
116
174
|
*
|
|
@@ -180,6 +238,7 @@ export declare function storeStructuredForApi(params: {
|
|
|
180
238
|
observation_index: number;
|
|
181
239
|
entity_type: string;
|
|
182
240
|
entity_id: string;
|
|
241
|
+
details?: Record<string, unknown>;
|
|
183
242
|
}[] | undefined;
|
|
184
243
|
warnings?: {
|
|
185
244
|
code: string;
|
package/dist/actions.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"actions.d.ts","sourceRoot":"","sources":["../src/actions.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"actions.d.ts","sourceRoot":"","sources":["../src/actions.ts"],"names":[],"mappings":"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"}
|
package/dist/actions.js
CHANGED
|
@@ -22,6 +22,7 @@ import { aauthAdmission, getAAuthAdmissionFromRequest } from "./middleware/aauth
|
|
|
22
22
|
import { buildSessionInfo, normalizeSessionOrigin } from "./services/session_info.js";
|
|
23
23
|
import { AttributionPolicyError, enforceAttributionPolicy } from "./services/attribution_policy.js";
|
|
24
24
|
import { OverridePolicyViolationError } from "./services/override_validation.js";
|
|
25
|
+
import { CursorError } from "./services/entity_cursor.js";
|
|
25
26
|
import { AgentCapabilityError, contextFromAgentIdentity, enforceAgentCapability, } from "./services/agent_capabilities.js";
|
|
26
27
|
import { assertGuestWriteAllowed, AccessPolicyError, } from "./services/access_policy.js";
|
|
27
28
|
import { hashGuestAccessToken } from "./services/guest_access_token.js";
|
|
@@ -53,6 +54,7 @@ import { resolveSandboxReportTransport } from "./services/sandbox/transport.js";
|
|
|
53
54
|
import { getSqliteDb } from "./repositories/sqlite/sqlite_client.js";
|
|
54
55
|
import { getMcpAuthToken } from "./crypto/mcp_auth_token.js";
|
|
55
56
|
import { isOauthKeyCredentialValid, normalizeOauthNextPath, OAuthKeySessionStore, } from "./services/oauth_key_gate.js";
|
|
57
|
+
import { buildGoogleAuthorizeUrl, createGoogleSigninNonce, consumeGoogleSigninNonce, exchangeGoogleAuthorizationCode, getSharedGraphUserId, GoogleIdTokenError, isGoogleSigninEnabled, verifyGoogleIdToken, } from "./services/google_oidc.js";
|
|
56
58
|
import { AnalyzeSchemaCandidatesRequestSchema, AuditUndeclaredFragmentsRequestSchema, CorrectEntityRequestSchema, CreateInterpretationRequestSchema, CreateRelationshipsRequestSchema, CreateRelationshipRequestSchema, DeleteEntityRequestSchema, DeleteRelationshipRequestSchema, IssuesBulkEntityIdsRequestSchema, IssuesAddMessageRequestSchema, IssuesGetStatusRequestSchema, IssuesSubmitRequestSchema, IssuesSyncRequestSchema, EntitiesQueryRequestSchema, EntitySnapshotRequestSchema, FieldProvenanceRequestSchema, GetSchemaRecommendationsRequestSchema, ListObservationsRequestSchema, ListRelationshipsRequestSchema, MergeEntitiesRequestSchema, SplitEntityRequestSchema, ObservationsQueryRequestSchema, RegisterSchemaRequestSchema, RelationshipSnapshotRequestSchema, RestoreEntityRequestSchema, RestoreRelationshipRequestSchema, RetrieveEntityByIdentifierSchema, RetrieveGraphNeighborhoodSchema, RetrieveRelatedEntitiesSchema, RELATIONSHIP_ENTITY_ID_FORMAT_HINT, RELATIONSHIP_ENTITY_ID_FORMAT_ISSUE_CODE, StoreRequestSchema, UpdateSchemaIncrementalRequestSchema, } from "./shared/action_schemas.js";
|
|
57
59
|
import { getMimeTypeFromExtension } from "./services/file_text_extraction.js";
|
|
58
60
|
import { queryEntitiesWithCount } from "./shared/action_handlers/entity_handlers.js";
|
|
@@ -1095,7 +1097,47 @@ export function isLocalRequest(req) {
|
|
|
1095
1097
|
return !isProductionEnvironment();
|
|
1096
1098
|
}
|
|
1097
1099
|
const OAUTH_KEY_SESSION_COOKIE = "neotoma_oauth_key_session";
|
|
1098
|
-
|
|
1100
|
+
/** Exported for tests asserting the cookie maxAge and the server-side session
|
|
1101
|
+
* expiry agree — the invariant setOAuthKeySessionCookie's docstring names. */
|
|
1102
|
+
export const oauthKeySessions = new OAuthKeySessionStore();
|
|
1103
|
+
/**
|
|
1104
|
+
* Lifetime of a SIGN-IN session (Google). Distinct from the key-entry gate's
|
|
1105
|
+
* short default: a user who signs in expects to stay signed in, whereas the
|
|
1106
|
+
* key-entry gate deliberately closes fast. Reusing the 15-minute gate TTL for
|
|
1107
|
+
* sign-in is what logged hosted-instance users out every quarter hour (#2005).
|
|
1108
|
+
*
|
|
1109
|
+
* Override with NEOTOMA_SIGN_IN_SESSION_TTL_MIN (minutes). Default 7 days.
|
|
1110
|
+
*/
|
|
1111
|
+
export const DEFAULT_SIGN_IN_SESSION_TTL_MIN = 7 * 24 * 60;
|
|
1112
|
+
/** Upper bound for the sign-in TTL override: 365 days in minutes. A value past
|
|
1113
|
+
* this is a typo (an extra zero), not an intent — without a ceiling,
|
|
1114
|
+
* "9999999999999" yields a ~19-million-year session. Mirrors the low-end
|
|
1115
|
+
* guard rather than guarding only one side (#2007 qa lens). */
|
|
1116
|
+
export const MAX_SIGN_IN_SESSION_TTL_MIN = 365 * 24 * 60;
|
|
1117
|
+
/**
|
|
1118
|
+
* Parse NEOTOMA_SIGN_IN_SESSION_TTL_MIN into milliseconds. Exported (and pure)
|
|
1119
|
+
* so the override's precedence and malformed-input behavior are directly
|
|
1120
|
+
* testable — a silently-wrong TTL here reintroduces exactly the desync class
|
|
1121
|
+
* this change fixes.
|
|
1122
|
+
*
|
|
1123
|
+
* Precedence: an integer within (0, MAX_SIGN_IN_SESSION_TTL_MIN] wins.
|
|
1124
|
+
* Everything else — unset, non-numeric, zero, negative, or above the ceiling —
|
|
1125
|
+
* falls back to the 7-day default rather than producing a degenerate session.
|
|
1126
|
+
*
|
|
1127
|
+
* Note on parseInt semantics, deliberately kept: "7.5" truncates to 7 minutes
|
|
1128
|
+
* and "1e10" parses as 1 minute (parseInt stops at the 'e'). Both are accepted
|
|
1129
|
+
* as-is because they are in-range integers after parsing; the fallback exists
|
|
1130
|
+
* for values that are absent, unparseable, or out of range, not to second-guess
|
|
1131
|
+
* a caller who wrote a decimal. Asserted in tests so the behavior is documented
|
|
1132
|
+
* rather than incidental.
|
|
1133
|
+
*/
|
|
1134
|
+
export function resolveSignInSessionTtlMs(raw) {
|
|
1135
|
+
const parsed = Number.parseInt(raw || "", 10);
|
|
1136
|
+
const inRange = Number.isFinite(parsed) && parsed > 0 && parsed <= MAX_SIGN_IN_SESSION_TTL_MIN;
|
|
1137
|
+
const minutes = inRange ? parsed : DEFAULT_SIGN_IN_SESSION_TTL_MIN;
|
|
1138
|
+
return minutes * 60 * 1000;
|
|
1139
|
+
}
|
|
1140
|
+
const SIGN_IN_SESSION_TTL_MS = resolveSignInSessionTtlMs(process.env.NEOTOMA_SIGN_IN_SESSION_TTL_MIN);
|
|
1099
1141
|
function readCookie(req, name) {
|
|
1100
1142
|
const header = (req.headers["cookie"] || req.headers["Cookie"] || "");
|
|
1101
1143
|
if (!header)
|
|
@@ -1321,8 +1363,15 @@ function hasValidOAuthKeySession(req) {
|
|
|
1321
1363
|
const token = readCookie(req, OAUTH_KEY_SESSION_COOKIE);
|
|
1322
1364
|
return oauthKeySessions.isValid(token);
|
|
1323
1365
|
}
|
|
1324
|
-
|
|
1325
|
-
|
|
1366
|
+
/**
|
|
1367
|
+
* Issue an OAuth key-session cookie. `ttlMs` covers BOTH the server-side
|
|
1368
|
+
* session and the cookie's own maxAge — they must agree, or the browser keeps
|
|
1369
|
+
* presenting a cookie the server has already forgotten (or vice versa).
|
|
1370
|
+
* Omit it for the short key-entry gate; pass SIGN_IN_SESSION_TTL_MS for
|
|
1371
|
+
* sign-in (#2005).
|
|
1372
|
+
*/
|
|
1373
|
+
export function setOAuthKeySessionCookie(req, res, ttlMs) {
|
|
1374
|
+
const token = oauthKeySessions.create(Date.now(), ttlMs);
|
|
1326
1375
|
const forwardedProto = (req.headers["x-forwarded-proto"] || req.headers["X-Forwarded-Proto"])
|
|
1327
1376
|
?.split(",")[0]
|
|
1328
1377
|
?.trim()
|
|
@@ -1333,8 +1382,16 @@ function setOAuthKeySessionCookie(req, res) {
|
|
|
1333
1382
|
sameSite: "lax",
|
|
1334
1383
|
secure,
|
|
1335
1384
|
path: "/mcp/oauth",
|
|
1336
|
-
maxAge: 15 * 60 * 1000,
|
|
1385
|
+
maxAge: ttlMs != null && ttlMs > 0 ? ttlMs : 15 * 60 * 1000,
|
|
1337
1386
|
});
|
|
1387
|
+
return token;
|
|
1388
|
+
}
|
|
1389
|
+
/** Resolve the local-auth user_id a Google-verified session (if any) mapped to this request. */
|
|
1390
|
+
export function getGoogleVerifiedUserId(req) {
|
|
1391
|
+
const token = readCookie(req, OAUTH_KEY_SESSION_COOKIE);
|
|
1392
|
+
// getBoundUser enforces session validity itself, so an expired session can
|
|
1393
|
+
// never resolve a user even if a binding is still in memory.
|
|
1394
|
+
return oauthKeySessions.getBoundUser(token);
|
|
1338
1395
|
}
|
|
1339
1396
|
/**
|
|
1340
1397
|
* Derive the canonical `@authority` the AAuth profile requires (AAuth §10.3.1).
|
|
@@ -1631,11 +1688,11 @@ app.all("/mcp", async (req, res) => {
|
|
|
1631
1688
|
const hadSessionHeader = typeof sessionId === "string" && sessionId.length > 0;
|
|
1632
1689
|
if (hadSessionHeader) {
|
|
1633
1690
|
logger.warn(`[MCP HTTP] Unknown or expired Mcp-Session-Id (first 8 chars): ${sessionId.slice(0, 8)}... often wrong replica behind a load balancer, API restart, or stale client state`);
|
|
1634
|
-
return res.status(
|
|
1691
|
+
return res.status(404).json({
|
|
1635
1692
|
jsonrpc: "2.0",
|
|
1636
1693
|
error: {
|
|
1637
1694
|
code: -32001,
|
|
1638
|
-
message: "
|
|
1695
|
+
message: "Not Found: MCP session is unknown or expired on this API instance. The client should re-initialize by sending a new InitializeRequest without a session ID. If you run multiple replicas, enable sticky sessions for POST /mcp (or route /mcp to a single instance).",
|
|
1639
1696
|
},
|
|
1640
1697
|
id: rpcId,
|
|
1641
1698
|
});
|
|
@@ -1886,9 +1943,32 @@ function sendStoreValidationError(res, issues) {
|
|
|
1886
1943
|
}
|
|
1887
1944
|
return sendValidationError(res, issues);
|
|
1888
1945
|
}
|
|
1946
|
+
/**
|
|
1947
|
+
* Lift a structured `hint` off the first Zod issue that carries one.
|
|
1948
|
+
*
|
|
1949
|
+
* Zod nests custom issue metadata under `params`, so a `ctx.addIssue({ params:
|
|
1950
|
+
* { hint } })` lands at `details.issues[].params.hint`. Clients (and the
|
|
1951
|
+
* legacy-payload replay harness, per docs/subsystems/errors.md § Tightening-change
|
|
1952
|
+
* hint obligation) read the migration hint from the flat `details.hint` slot that
|
|
1953
|
+
* every other standard-envelope error uses. Without this lift, a tightening can
|
|
1954
|
+
* populate `hint` exactly as the guardrail requires and still be invisible to the
|
|
1955
|
+
* gate meant to catch it.
|
|
1956
|
+
*/
|
|
1957
|
+
function firstIssueHint(issues) {
|
|
1958
|
+
if (!Array.isArray(issues))
|
|
1959
|
+
return undefined;
|
|
1960
|
+
for (const issue of issues) {
|
|
1961
|
+
const hint = issue?.params?.hint;
|
|
1962
|
+
if (typeof hint === "string" && hint.length > 0)
|
|
1963
|
+
return hint;
|
|
1964
|
+
}
|
|
1965
|
+
return undefined;
|
|
1966
|
+
}
|
|
1889
1967
|
function sendValidationError(res, issues) {
|
|
1968
|
+
const hint = firstIssueHint(issues);
|
|
1890
1969
|
return res.status(400).json(buildErrorEnvelope("VALIDATION_INVALID_FORMAT", "Invalid request payload.", {
|
|
1891
1970
|
issues,
|
|
1971
|
+
...(hint ? { hint } : {}),
|
|
1892
1972
|
}));
|
|
1893
1973
|
}
|
|
1894
1974
|
// Public health endpoint (no auth)
|
|
@@ -2104,10 +2184,19 @@ app.get("/mcp/oauth/key-auth", async (req, res) => {
|
|
|
2104
2184
|
const nextPathEscaped = escapeHtml(nextPath);
|
|
2105
2185
|
const hasBearerFallback = Boolean((process.env.NEOTOMA_BEARER_TOKEN || "").trim());
|
|
2106
2186
|
const defaultCredentialMode = hasBearerFallback ? "bearer" : "private_key";
|
|
2187
|
+
const googleEnabled = isGoogleSigninEnabled();
|
|
2188
|
+
const googleStartHref = `/mcp/oauth/google/start?next=${encodeURIComponent(nextPath)}`;
|
|
2107
2189
|
return res.send(renderOauthPage({
|
|
2108
2190
|
title: "Authenticate OAuth preflight",
|
|
2109
2191
|
subtitle: "Choose one credential path below, then continue to complete OAuth authorization.",
|
|
2110
2192
|
contentHtml: `
|
|
2193
|
+
${googleEnabled
|
|
2194
|
+
? `<div class="actions" style="margin-bottom: 1rem;">
|
|
2195
|
+
<a class="btn-link" href="${escapeHtml(googleStartHref)}">Sign in with Google</a>
|
|
2196
|
+
</div>
|
|
2197
|
+
<div class="notice">Signs you in as your own Neotoma user if your Google account's verified email is on this server's approved list. Doesn't require a private key.</div>
|
|
2198
|
+
<hr style="margin: 1.5rem 0; border: none; border-top: 1px solid hsl(var(--border));" />`
|
|
2199
|
+
: ""}
|
|
2111
2200
|
<form method="post" action="/mcp/oauth/key-auth" id="oauth-key-auth-form">
|
|
2112
2201
|
<input type="hidden" name="next" value="${nextPathEscaped}" />
|
|
2113
2202
|
<input type="hidden" name="credential_mode" id="credential_mode" value="${defaultCredentialMode}" />
|
|
@@ -2213,6 +2302,109 @@ app.post("/mcp/oauth/key-auth", express.urlencoded({ extended: true }), async (r
|
|
|
2213
2302
|
setOAuthKeySessionCookie(req, res);
|
|
2214
2303
|
return res.redirect(nextPath);
|
|
2215
2304
|
});
|
|
2305
|
+
/**
|
|
2306
|
+
* "Sign in with Google" entry point. Additive to, and independent of, the
|
|
2307
|
+
* key/mnemonic/bearer credential form above — feature-flagged off unless
|
|
2308
|
+
* NEOTOMA_GOOGLE_CLIENT_ID + NEOTOMA_APPROVED_EMAILS are both configured.
|
|
2309
|
+
* AUTH-LAYER ONLY: this establishes who the person is (their own user_id
|
|
2310
|
+
* via the existing per-email primitive); it does not touch data-sharing or
|
|
2311
|
+
* read-scoping.
|
|
2312
|
+
*/
|
|
2313
|
+
app.get("/mcp/oauth/google/start", async (req, res) => {
|
|
2314
|
+
if (!isGoogleSigninEnabled()) {
|
|
2315
|
+
return res.status(404).send("Not found");
|
|
2316
|
+
}
|
|
2317
|
+
const nextPath = normalizeOauthNextPath(req.query.next || undefined);
|
|
2318
|
+
const nonce = createGoogleSigninNonce(nextPath);
|
|
2319
|
+
const redirectUri = `${req.protocol}://${req.get("host") || ""}/mcp/oauth/google/callback`;
|
|
2320
|
+
try {
|
|
2321
|
+
const authorizeUrl = buildGoogleAuthorizeUrl({ redirectUri, nonce });
|
|
2322
|
+
return res.redirect(authorizeUrl);
|
|
2323
|
+
}
|
|
2324
|
+
catch (error) {
|
|
2325
|
+
logError("GoogleOAuthStart", req, error);
|
|
2326
|
+
return res.status(500).send(error?.message ?? "Failed to start Google sign-in");
|
|
2327
|
+
}
|
|
2328
|
+
});
|
|
2329
|
+
app.get("/mcp/oauth/google/callback", async (req, res) => {
|
|
2330
|
+
if (!isGoogleSigninEnabled()) {
|
|
2331
|
+
return res.status(404).send("Not found");
|
|
2332
|
+
}
|
|
2333
|
+
const errorParam = req.query.error?.trim();
|
|
2334
|
+
const code = req.query.code?.trim();
|
|
2335
|
+
const nonceParam = req.query.state?.trim();
|
|
2336
|
+
const failWith = (subtitle) => {
|
|
2337
|
+
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
2338
|
+
return res.status(401).send(renderOauthPage({
|
|
2339
|
+
title: "Google sign-in failed",
|
|
2340
|
+
subtitle,
|
|
2341
|
+
contentHtml: `<div class="actions">
|
|
2342
|
+
<a class="btn-link secondary" href="/mcp/oauth/key-auth">Back to authentication</a>
|
|
2343
|
+
</div>`,
|
|
2344
|
+
}));
|
|
2345
|
+
};
|
|
2346
|
+
if (errorParam) {
|
|
2347
|
+
logger.warn("[Google OAuth] Callback returned an error", { error: errorParam });
|
|
2348
|
+
return failWith("Google sign-in was cancelled or denied.");
|
|
2349
|
+
}
|
|
2350
|
+
if (!code || !nonceParam) {
|
|
2351
|
+
return failWith("Missing code or state from Google callback.");
|
|
2352
|
+
}
|
|
2353
|
+
const nonceEntry = consumeGoogleSigninNonce(nonceParam);
|
|
2354
|
+
if (!nonceEntry) {
|
|
2355
|
+
return failWith("This sign-in link has expired or was already used. Please try again.");
|
|
2356
|
+
}
|
|
2357
|
+
// Same nextPath contract as the key/mnemonic/bearer path above: normalized
|
|
2358
|
+
// back to a /mcp/oauth/* path (typically /mcp/oauth/authorize?...), never
|
|
2359
|
+
// an external URL.
|
|
2360
|
+
const nextPath = normalizeOauthNextPath(nonceEntry.next);
|
|
2361
|
+
const redirectUri = `${req.protocol}://${req.get("host") || ""}/mcp/oauth/google/callback`;
|
|
2362
|
+
try {
|
|
2363
|
+
const { idToken } = await exchangeGoogleAuthorizationCode({ code, redirectUri });
|
|
2364
|
+
const { email } = await verifyGoogleIdToken(idToken);
|
|
2365
|
+
// Resolve the verified email to a stable user_id via the EXISTING per-email
|
|
2366
|
+
// primitive (local_auth.ts) — the same primitive createLocalAuthUser /
|
|
2367
|
+
// hashEmailToUserId already uses elsewhere. By default this person is
|
|
2368
|
+
// admitted as their own isolated user_id, same as any other local-auth user.
|
|
2369
|
+
const { createLocalAuthUser } = await import("./services/local_auth.js");
|
|
2370
|
+
const { randomBytes } = await import("node:crypto");
|
|
2371
|
+
// Password is unused for Google-authenticated users (they never log in
|
|
2372
|
+
// with it) — generate an unguessable throwaway so createLocalAuthUser's
|
|
2373
|
+
// shared code path is satisfied without weakening any other account.
|
|
2374
|
+
const throwawayPassword = randomBytes(32).toString("hex");
|
|
2375
|
+
const perEmailUser = createLocalAuthUser(email, throwawayPassword);
|
|
2376
|
+
// Shared-graph opt-in: when NEOTOMA_SHARED_GRAPH_USER_ID is set, an
|
|
2377
|
+
// approved teammate (the email already passed the allowlist inside
|
|
2378
|
+
// verifyGoogleIdToken) is bound to the shared graph's user_id instead of
|
|
2379
|
+
// their isolated per-email one — so the whole allowlisted team, plus the
|
|
2380
|
+
// bearer/MCP identity that owns the data, share one populated graph. Unset
|
|
2381
|
+
// → isolated per-email behavior (unchanged). See getSharedGraphUserId.
|
|
2382
|
+
const resolvedUserId = getSharedGraphUserId() ?? perEmailUser.id;
|
|
2383
|
+
// Mark this browser session as Google-verified and bound to the resolved
|
|
2384
|
+
// user_id, exactly the same OAuth key-session cookie the private-key/
|
|
2385
|
+
// mnemonic/bearer path sets. `/mcp/oauth/authorize` -> `/mcp/oauth/
|
|
2386
|
+
// local-login` then runs completely unmodified, except local-login now
|
|
2387
|
+
// resolves the user via this session instead of always ensureLocalDevUser().
|
|
2388
|
+
// Sign-in gets the long session TTL, not the key-entry gate's 15 minutes,
|
|
2389
|
+
// and the user binding lives in the same store so it expires with it (#2005).
|
|
2390
|
+
const sessionToken = setOAuthKeySessionCookie(req, res, SIGN_IN_SESSION_TTL_MS);
|
|
2391
|
+
oauthKeySessions.bindUser(sessionToken, resolvedUserId);
|
|
2392
|
+
return res.redirect(nextPath);
|
|
2393
|
+
}
|
|
2394
|
+
catch (error) {
|
|
2395
|
+
logError("GoogleOAuthCallback", req, error);
|
|
2396
|
+
if (error instanceof GoogleIdTokenError) {
|
|
2397
|
+
if (error.code === "EMAIL_NOT_APPROVED") {
|
|
2398
|
+
return failWith("Your Google account is not on the approved list for this server.");
|
|
2399
|
+
}
|
|
2400
|
+
if (error.code === "EMAIL_NOT_VERIFIED") {
|
|
2401
|
+
return failWith("Your Google account's email is not verified.");
|
|
2402
|
+
}
|
|
2403
|
+
return failWith("Google sign-in could not be verified.");
|
|
2404
|
+
}
|
|
2405
|
+
return failWith(error?.message ?? "Google sign-in failed.");
|
|
2406
|
+
}
|
|
2407
|
+
});
|
|
2216
2408
|
// RFC 8414 authorization endpoint (GET) for Cursor and other OAuth clients
|
|
2217
2409
|
app.get("/mcp/oauth/authorize", async (req, res) => {
|
|
2218
2410
|
try {
|
|
@@ -2263,7 +2455,10 @@ app.get("/mcp/oauth/authorize", async (req, res) => {
|
|
|
2263
2455
|
// When reached via tunnel, only allow redirect_uri to localhost, app schemes, or OpenAI Custom GPT
|
|
2264
2456
|
if (!isLocalRequest(req)) {
|
|
2265
2457
|
const { isRedirectUriAllowedForTunnel } = await import("./services/mcp_oauth.js");
|
|
2266
|
-
|
|
2458
|
+
// Public host as seen from outside the tunnel (x-forwarded-host set by the
|
|
2459
|
+
// proxy) so an Inspector callback on this instance's own origin is allowed.
|
|
2460
|
+
const selfHost = req.header("x-forwarded-host")?.split(",")[0]?.trim() || req.get("host");
|
|
2461
|
+
if (!isRedirectUriAllowedForTunnel(redirect_uri, selfHost)) {
|
|
2267
2462
|
logger.warn("[MCP OAuth] Authorize rejected: redirect_uri not allowed for tunnel", {
|
|
2268
2463
|
redirect_uri: sanitizeRedirectUriForLog(redirect_uri),
|
|
2269
2464
|
});
|
|
@@ -2378,9 +2573,15 @@ app.get("/mcp/oauth/local-login", async (req, res) => {
|
|
|
2378
2573
|
}));
|
|
2379
2574
|
}
|
|
2380
2575
|
try {
|
|
2381
|
-
|
|
2576
|
+
// If this browser session was admitted via Google sign-in (see
|
|
2577
|
+
// /mcp/oauth/google/callback), complete authorization as THAT verified
|
|
2578
|
+
// user's own user_id instead of the shared dev user. Every other path
|
|
2579
|
+
// (no Google session on this cookie — the default, and the only
|
|
2580
|
+
// possibility when the Google feature flag is off) is unchanged.
|
|
2581
|
+
const googleUserId = getGoogleVerifiedUserId(req);
|
|
2582
|
+
const resolvedUserId = googleUserId ?? ensureLocalDevUser().id;
|
|
2382
2583
|
const { completeLocalAuthorization } = await import("./services/mcp_oauth.js");
|
|
2383
|
-
const { connectionId, redirectUri, clientState } = await completeLocalAuthorization(state,
|
|
2584
|
+
const { connectionId, redirectUri, clientState } = await completeLocalAuthorization(state, resolvedUserId);
|
|
2384
2585
|
const frontendBase = process.env.NEOTOMA_FRONTEND_URL || process.env.FRONTEND_URL || "http://localhost:5195";
|
|
2385
2586
|
const frontendOauth = `${frontendBase}/oauth`;
|
|
2386
2587
|
if (redirectUri) {
|
|
@@ -2734,6 +2935,23 @@ function guestWriteRateLimit(req, res, next) {
|
|
|
2734
2935
|
return next();
|
|
2735
2936
|
void guestWriteRateLimitMiddleware(req, res, next);
|
|
2736
2937
|
}
|
|
2938
|
+
/**
|
|
2939
|
+
* Issue *submission* is the one route a third party may reach with no identity
|
|
2940
|
+
* at all: the guest access token is minted BY the submit, so requiring one to
|
|
2941
|
+
* perform it is a chicken-and-egg that closes the inbox to first contact.
|
|
2942
|
+
*
|
|
2943
|
+
* Deliberately submit-only. `add_message` stays authenticated: by the time a
|
|
2944
|
+
* reporter has a thread to append to, they hold the token returned by their
|
|
2945
|
+
* submit, so anonymity buys nothing there — and letting an unidentified caller
|
|
2946
|
+
* write into an existing thread is a strictly wider blast radius than letting
|
|
2947
|
+
* them open a new one. The token is what proves "I opened this thread".
|
|
2948
|
+
*/
|
|
2949
|
+
export function routeAcceptsAnonymousIssueSubmit(req) {
|
|
2950
|
+
return (req.method === "POST" && (req.path === "/issues/submit" || req.path === "/api/issues/submit"));
|
|
2951
|
+
}
|
|
2952
|
+
function buildAnonymousGuestPrincipal() {
|
|
2953
|
+
return { kind: "guest", guestId: {}, agentIdentity: null, accessToken: undefined };
|
|
2954
|
+
}
|
|
2737
2955
|
function buildGuestPrincipalFromRequest(req) {
|
|
2738
2956
|
const agentIdentity = getCurrentAgentIdentity();
|
|
2739
2957
|
const accessToken = guestAccessTokenFromRequest(req);
|
|
@@ -2757,8 +2975,21 @@ async function maybeStampGuestPrincipal(req) {
|
|
|
2757
2975
|
if (!routeAcceptsGuestPrincipal(req))
|
|
2758
2976
|
return false;
|
|
2759
2977
|
const guestPrincipal = buildGuestPrincipalFromRequest(req);
|
|
2760
|
-
if (!guestPrincipal)
|
|
2761
|
-
|
|
2978
|
+
if (!guestPrincipal) {
|
|
2979
|
+
// No AAuth signature and no guest token: for issue submission this is the
|
|
2980
|
+
// NORMAL first contact from a third party's agent, since the token is an
|
|
2981
|
+
// OUTPUT of the submit rather than a precondition for it (#1953). Remains
|
|
2982
|
+
// the operator's switch — NEOTOMA_ACCESS_POLICY_ISSUE=closed shuts it.
|
|
2983
|
+
if (!routeAcceptsAnonymousIssueSubmit(req))
|
|
2984
|
+
return false;
|
|
2985
|
+
const { resolveAccessPolicy } = await import("./services/access_policy.js");
|
|
2986
|
+
const mode = await resolveAccessPolicy("issue");
|
|
2987
|
+
if (mode !== "submit_only" && mode !== "submitter_scoped" && mode !== "open") {
|
|
2988
|
+
return false;
|
|
2989
|
+
}
|
|
2990
|
+
stampGuestPrincipal(req, buildAnonymousGuestPrincipal());
|
|
2991
|
+
return true;
|
|
2992
|
+
}
|
|
2762
2993
|
const headerAuth = (req.headers.authorization || req.headers.Authorization || "");
|
|
2763
2994
|
if (headerAuth.startsWith("Bearer ") && guestPrincipal.accessToken) {
|
|
2764
2995
|
const { validateGuestAccessToken } = await import("./services/guest_access_token.js");
|
|
@@ -3321,6 +3552,12 @@ function handleApiError(req, res, error, defaultMessage, errorCode = "DB_QUERY_F
|
|
|
3321
3552
|
.status(403)
|
|
3322
3553
|
.json(buildErrorEnvelope(error.code, error.message, error.toErrorEnvelope()));
|
|
3323
3554
|
}
|
|
3555
|
+
if (error instanceof CursorError) {
|
|
3556
|
+
logWarn(logContext || "CursorRejection", req, error.toErrorEnvelope());
|
|
3557
|
+
return res
|
|
3558
|
+
.status(error.statusCode)
|
|
3559
|
+
.json(buildErrorEnvelope(error.code, error.message, error.toErrorEnvelope()));
|
|
3560
|
+
}
|
|
3324
3561
|
if (error instanceof IssueValidationError) {
|
|
3325
3562
|
logWarn(logContext || "IssueValidationError", req, {
|
|
3326
3563
|
code: error.code,
|
|
@@ -3371,8 +3608,8 @@ async function runEntitiesQuery(req, res, rawInput) {
|
|
|
3371
3608
|
// scoping only via getAuthenticatedUserId; it is never read directly off
|
|
3372
3609
|
// the request for access control.
|
|
3373
3610
|
const userId = await getAuthenticatedUserId(req, parsed.data.user_id);
|
|
3374
|
-
const { entity_type, entity_types, search, limit, offset, sort_by, sort_order, published, published_after, published_before, include_snapshots, include_merged, updated_since, created_since, identity_basis, snapshot_filters, exclude_bookkeeping, } = parsed.data;
|
|
3375
|
-
const { entities, total, applied_search_strategies, search_mode } = await queryEntitiesWithCount({
|
|
3611
|
+
const { entity_type, entity_types, search, limit, offset, cursor, sort_by, sort_order, published, published_after, published_before, include_snapshots, include_merged, updated_since, created_since, identity_basis, snapshot_filters, exclude_bookkeeping, } = parsed.data;
|
|
3612
|
+
const { entities, total, applied_search_strategies, search_mode, next_cursor } = await queryEntitiesWithCount({
|
|
3376
3613
|
userId,
|
|
3377
3614
|
entityType: entity_type,
|
|
3378
3615
|
entityTypes: entity_types,
|
|
@@ -3386,6 +3623,7 @@ async function runEntitiesQuery(req, res, rawInput) {
|
|
|
3386
3623
|
search,
|
|
3387
3624
|
limit,
|
|
3388
3625
|
offset,
|
|
3626
|
+
cursor,
|
|
3389
3627
|
updatedSince: updated_since,
|
|
3390
3628
|
createdSince: created_since,
|
|
3391
3629
|
identityBasis: identity_basis,
|
|
@@ -3399,6 +3637,7 @@ async function runEntitiesQuery(req, res, rawInput) {
|
|
|
3399
3637
|
offset,
|
|
3400
3638
|
...(applied_search_strategies ? { applied_search_strategies } : {}),
|
|
3401
3639
|
search_mode,
|
|
3640
|
+
...(next_cursor ? { next_cursor } : {}),
|
|
3402
3641
|
});
|
|
3403
3642
|
}
|
|
3404
3643
|
catch (error) {
|
|
@@ -3439,6 +3678,7 @@ function coerceEntitiesQueryParams(query) {
|
|
|
3439
3678
|
"updated_since",
|
|
3440
3679
|
"created_since",
|
|
3441
3680
|
"identity_basis",
|
|
3681
|
+
"cursor",
|
|
3442
3682
|
]) {
|
|
3443
3683
|
const value = readString(key);
|
|
3444
3684
|
if (value !== undefined)
|
|
@@ -5834,6 +6074,12 @@ export async function storeStructuredForApi(params) {
|
|
|
5834
6074
|
// entity_id.
|
|
5835
6075
|
const priorSnapshotByEntityId = new Map();
|
|
5836
6076
|
const newSnapshotByEntityId = new Map();
|
|
6077
|
+
// #1963: auto-link may retract a stale reference-field edge (e.g. a contact's
|
|
6078
|
+
// organization moved companies). A retraction *failure* must not be silent —
|
|
6079
|
+
// that would reproduce the very silent-drift bug this fix exists to close, one
|
|
6080
|
+
// layer up. Accumulate per-observation retraction outcomes here so the
|
|
6081
|
+
// schemaStoreWarnings pass below can surface them on the store response.
|
|
6082
|
+
const autoLinkRetractionByObsIndex = new Map();
|
|
5837
6083
|
for (const r of resolved) {
|
|
5838
6084
|
let observation_id = null;
|
|
5839
6085
|
let snapshotAfter = null;
|
|
@@ -5917,14 +6163,25 @@ export async function storeStructuredForApi(params) {
|
|
|
5917
6163
|
const schemaEntry = await schemaRegistry.loadActiveSchema(r.entity_type, userId);
|
|
5918
6164
|
if (schemaEntry?.schema_definition?.reference_fields?.length) {
|
|
5919
6165
|
const { autoLinkReferenceFields } = await import("./services/schema_reference_linking.js");
|
|
5920
|
-
await autoLinkReferenceFields({
|
|
6166
|
+
const autoLinkResult = await autoLinkReferenceFields({
|
|
5921
6167
|
entityId: r.entity_id,
|
|
5922
6168
|
entityType: r.entity_type,
|
|
5923
6169
|
fields: r.fields,
|
|
5924
6170
|
schema: schemaEntry.schema_definition,
|
|
5925
6171
|
userId,
|
|
5926
6172
|
sourceId: observationSourceId,
|
|
6173
|
+
commit,
|
|
5927
6174
|
});
|
|
6175
|
+
if (autoLinkResult.retracted > 0 || autoLinkResult.retraction_failures > 0) {
|
|
6176
|
+
autoLinkRetractionByObsIndex.set(r.observation_index, {
|
|
6177
|
+
entity_type: r.entity_type,
|
|
6178
|
+
entity_id: r.entity_id,
|
|
6179
|
+
retracted: autoLinkResult.retracted,
|
|
6180
|
+
retraction_failures: autoLinkResult.retraction_failures,
|
|
6181
|
+
failed_target_entity_ids: autoLinkResult.failed_retraction_target_entity_ids,
|
|
6182
|
+
failed_retractions: autoLinkResult.failed_retractions,
|
|
6183
|
+
});
|
|
6184
|
+
}
|
|
5928
6185
|
}
|
|
5929
6186
|
}
|
|
5930
6187
|
catch (linkErr) {
|
|
@@ -6254,6 +6511,46 @@ export async function storeStructuredForApi(params) {
|
|
|
6254
6511
|
}
|
|
6255
6512
|
}
|
|
6256
6513
|
}
|
|
6514
|
+
// #1963: surface auto-link edge retractions on the store response so a
|
|
6515
|
+
// retraction — and especially a retraction *failure* — is never silent. A
|
|
6516
|
+
// failure means a stale reference-field edge may still be live despite the
|
|
6517
|
+
// store reporting success, so it is emitted as its own actionable warning.
|
|
6518
|
+
for (const [obsIndex, rec] of autoLinkRetractionByObsIndex) {
|
|
6519
|
+
if (rec.retraction_failures > 0) {
|
|
6520
|
+
schemaStoreWarnings.push({
|
|
6521
|
+
code: "AUTO_LINK_RETRACTION_FAILED",
|
|
6522
|
+
message: `Auto-link retraction failed for ${rec.retraction_failures} stale ` +
|
|
6523
|
+
`reference-field edge(s) on ${rec.entity_type}/${rec.entity_id}. ` +
|
|
6524
|
+
`A superseded edge (e.g. a prior works_at) may still be live even though ` +
|
|
6525
|
+
`the store succeeded. To clean up, call delete_relationship for each ` +
|
|
6526
|
+
`stale edge in details.stale_edges, or re-run the store.`,
|
|
6527
|
+
observation_index: obsIndex,
|
|
6528
|
+
entity_type: rec.entity_type,
|
|
6529
|
+
entity_id: rec.entity_id,
|
|
6530
|
+
// Structured remediation: each entry is a ready-to-use delete_relationship
|
|
6531
|
+
// argument set, so the caller needs no knowledge of its own schema.
|
|
6532
|
+
details: {
|
|
6533
|
+
stale_edges: rec.failed_retractions.map((f) => ({
|
|
6534
|
+
source_entity_id: rec.entity_id,
|
|
6535
|
+
target_entity_id: f.target_entity_id,
|
|
6536
|
+
relationship_type: f.relationship_type,
|
|
6537
|
+
field_name: f.field,
|
|
6538
|
+
})),
|
|
6539
|
+
},
|
|
6540
|
+
});
|
|
6541
|
+
}
|
|
6542
|
+
else if (rec.retracted > 0) {
|
|
6543
|
+
schemaStoreWarnings.push({
|
|
6544
|
+
code: "AUTO_LINK_EDGE_RETRACTED",
|
|
6545
|
+
message: `Retracted ${rec.retracted} stale auto-linked reference-field edge(s) on ` +
|
|
6546
|
+
`${rec.entity_type}/${rec.entity_id} because the field's resolved target ` +
|
|
6547
|
+
`changed (#1963).`,
|
|
6548
|
+
observation_index: obsIndex,
|
|
6549
|
+
entity_type: rec.entity_type,
|
|
6550
|
+
entity_id: rec.entity_id,
|
|
6551
|
+
});
|
|
6552
|
+
}
|
|
6553
|
+
}
|
|
6257
6554
|
const unknownFieldNames = Array.from(unknownFieldNamesSet).sort();
|
|
6258
6555
|
// Conditional hint (#1549): update_schema_incremental requires the schema to
|
|
6259
6556
|
// declare canonical_name_fields, else it dead-ends on
|
|
@@ -7879,6 +8176,10 @@ const handleIssuesSubmitHttp = async (req, res) => {
|
|
|
7879
8176
|
author: parsed.data.author,
|
|
7880
8177
|
local_issue_id: parsed.data.local_issue_id,
|
|
7881
8178
|
submission_timestamp: parsed.data.submission_timestamp,
|
|
8179
|
+
reporter_git_sha: parsed.data.reporter_git_sha,
|
|
8180
|
+
reporter_git_ref: parsed.data.reporter_git_ref,
|
|
8181
|
+
reporter_channel: parsed.data.reporter_channel,
|
|
8182
|
+
reporter_app_version: parsed.data.reporter_app_version,
|
|
7882
8183
|
});
|
|
7883
8184
|
return {
|
|
7884
8185
|
issue_number: 0,
|
|
@@ -8261,7 +8562,7 @@ app.post("/update_schema_incremental", async (req, res) => {
|
|
|
8261
8562
|
}
|
|
8262
8563
|
try {
|
|
8263
8564
|
const userId = await getAuthenticatedUserId(req, parsed.data.user_id);
|
|
8264
|
-
const { entity_type, fields_to_add, fields_to_remove, schema_version, user_specific = false, activate = true, migrate_existing = false, force = false, } = parsed.data;
|
|
8565
|
+
const { entity_type, fields_to_add, fields_to_remove, canonical_name_fields, schema_version, user_specific = false, activate = true, migrate_existing = false, force = false, } = parsed.data;
|
|
8265
8566
|
const { schemaRegistry } = await import("./services/schema_registry.js");
|
|
8266
8567
|
let newSchema;
|
|
8267
8568
|
try {
|
|
@@ -8269,6 +8570,11 @@ app.post("/update_schema_incremental", async (req, res) => {
|
|
|
8269
8570
|
entity_type,
|
|
8270
8571
|
fields_to_add: (fields_to_add || []),
|
|
8271
8572
|
fields_to_remove: fields_to_remove || [],
|
|
8573
|
+
// #2018: thread the identity-rule change through the HTTP/offline path.
|
|
8574
|
+
// Omitted here originally, so CLI/HTTP callers silently no-op'd the
|
|
8575
|
+
// re-key (the MCP server handler had it, but this Express route — which
|
|
8576
|
+
// the CLI offline transport uses — did not).
|
|
8577
|
+
canonical_name_fields,
|
|
8272
8578
|
schema_version,
|
|
8273
8579
|
user_specific,
|
|
8274
8580
|
user_id: userId,
|
|
@@ -8295,6 +8601,10 @@ app.post("/update_schema_incremental", async (req, res) => {
|
|
|
8295
8601
|
schema: newSchema,
|
|
8296
8602
|
schema_version: newSchema.schema_version,
|
|
8297
8603
|
fields_removed: fields_to_remove || [],
|
|
8604
|
+
// Echo the resolved identity rule so CLI/HTTP callers can confirm the
|
|
8605
|
+
// re-key without a second describe_entity_type round trip (#2020 ux).
|
|
8606
|
+
canonical_name_fields: newSchema.schema_definition
|
|
8607
|
+
.canonical_name_fields ?? null,
|
|
8298
8608
|
});
|
|
8299
8609
|
}
|
|
8300
8610
|
catch (error) {
|